Flutter Integration
The Payment SDK ships as native Android and iOS libraries — there is no Dart package. In a Flutter app you integrate it with a small platform bridge:
Your Flutter code (Dart)
│
▼
PaymentSdkFlutter Dart wrapper class (you write this — full code below)
│ MethodChannel('payment_sdk_flutter')
▼
┌─────┴──────────────────────┐
▼ ▼
MainActivity.kt AppDelegate.swift
(Android bridge) (iOS bridge)
▼ ▼
payment-2.0.4.aar payment_sdk.xcframework
The SDK draws its own native payment UI (method chooser, card form, SuperQi QR screen, result screen), so your Flutter side only initializes it, starts payments, and reacts to the result — you never build payment screens yourself.
This page gives you the complete v2.0.4 bridge: project setup, a copy-pasteable Dart wrapper, the native bridge code for both platforms, and the gotchas that cost integrators the most time.
1. Project setup
- Android
- iOS
1. Add the SDK binaries to android/app/libs/:
payment-2.0.4.aaremv-3ds-sdk-1.1.6.aar
2. Update android/app/build.gradle:
android {
compileOptions {
// Required by the 2.0.4 AAR
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
implementation(name: 'payment-2.0.4', ext: 'aar')
implementation(name: 'emv-3ds-sdk-1.1.6', ext: 'aar')
// Required for core library desugaring
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
// Provides the LanguageCode enum used by the localization API
implementation 'com.neovisionaries:nv-i18n:1.29'
}
3. Toolchain floor: Gradle wrapper 8.14, Android Gradle Plugin 8.7.3, Java 17. With recent
Flutter versions also add to android/gradle.properties:
android.builtInKotlin=false
android.newDsl=false
The AAR also expects the host app to pin its transitive dependencies (Jackson, Retrofit, OkHttp, BouncyCastle, AndroidX Lifecycle/Room, Lottie, Glide, …) — take the dependency block from the integration example project you received with the SDK.
1. Embed the frameworks in ios/ and link them in Runner.xcodeproj (General → Frameworks,
Libraries, and Embedded Content → Embed & Sign):
payment_sdk.xcframeworkTdsSdkIos.xcframework
No CocoaPods entry is needed — the frameworks are vendored directly.
2. Register a URL scheme (needed for the SuperQi deep-link return). In ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>None</string>
<key>CFBundleURLName</key>
<string>your.bundle.identifier</string>
<key>CFBundleURLSchemes</key>
<array>
<string>finon</string>
</array>
</dict>
</array>
The same scheme goes into Merchant.finishPaymentUri (e.g. finon://payment) in the bridge code
below.
2. The Dart wrapper
One class talks to both platforms over a single MethodChannel named payment_sdk_flutter.
Three small enums model the v2.x payment-method configuration:
import 'package:flutter/services.dart';
/// Payment methods the SDK can offer.
/// ALIPAY is the SDK's internal name for "Pay with SuperQi".
enum PaymentMethodOption { CARD, ALIPAY, PAYMENT_TOKEN }
/// Who renders the "choose payment method" screen:
/// ON_SDK = the SDK shows its own chooser; ON_APP = your app decides.
enum PaymentMethodChoice { ON_SDK, ON_APP }
/// Which SuperQi flow is shown first: a scannable QR, or a deep link
/// that opens the SuperQi app on the same device.
enum AliPayShowFirst { QR, LINK }
class PaymentSdkFlutter {
static const MethodChannel _channel = MethodChannel('payment_sdk_flutter');
/// Initialize the SDK once, at app start or before the first payment.
static Future<bool> initializeSDK({
required String baseUrl,
required String publicKey,
required String terminalId,
required String authUsername,
required String authPassword,
String language = 'english', // 'english' | 'arabic' | 'kurdish' | 'auto'
String theme = 'light', // 'light' | 'dark' | 'system'
String writingDirection = 'ltr', // 'ltr' | 'rtl'
List<PaymentMethodOption> availablePaymentMethods = const [
PaymentMethodOption.CARD,
PaymentMethodOption.ALIPAY,
PaymentMethodOption.PAYMENT_TOKEN,
],
PaymentMethodChoice paymentMethodChoice = PaymentMethodChoice.ON_SDK,
AliPayShowFirst aliPayShowFirst = AliPayShowFirst.QR,
String? merchantName,
String? merchantLogoUrlLight,
String? merchantLogoUrlDark,
}) async {
final result = await _channel.invokeMethod<bool>('initializeSDK', {
'baseUrl': baseUrl,
'publicKey': publicKey,
'terminalId': terminalId,
'authUsername': authUsername,
'authPassword': authPassword,
'language': language,
'theme': theme,
'writingDirection': writingDirection,
// Enums are sent as plain strings — the native side maps them back.
'availablePaymentMethods':
availablePaymentMethods.map((m) => m.name).toList(),
'paymentMethodChoice': paymentMethodChoice.name,
'aliPayShowFirst': aliPayShowFirst.name,
'merchantName': merchantName,
'merchantLogoUrlLight': merchantLogoUrlLight,
'merchantLogoUrlDark': merchantLogoUrlDark,
});
return result ?? false;
}
/// Start a payment. The SDK presents its own native UI; the Future
/// completes when the payment flow finishes (success or failure).
static Future<Map<String, dynamic>> processPayment({
required String paymentId,
required String requestId,
required double amount,
required String currency,
required String accountId, // must NOT be null/empty in v2.0.4
String paymentType = 'CARD', // 'CARD' | 'PAYMENT_TOKEN' | 'ALIPAY' | 'AQSATI'
String? paymentToken, // required when paymentType == 'PAYMENT_TOKEN'
bool needPaymentToken = false,
String tokenType = 'AUTH', // 'AUTH' | 'NON_RECUR' | 'UNAUTH'
String? additionalInfo,
}) async {
final result =
await _channel.invokeMethod<Map<Object?, Object?>>('processPayment', {
'paymentId': paymentId,
'requestId': requestId,
'amount': amount,
'currency': currency,
'accountId': accountId,
'paymentType': paymentType,
'paymentToken': paymentToken,
'needPaymentToken': needPaymentToken,
'tokenType': tokenType,
'additionalInfo': additionalInfo,
});
return Map<String, dynamic>.from(result ?? {});
}
/// Retrieve the saved payment tokens for an account.
static Future<List<dynamic>> getPaymentTokens(String accountId) async {
final result = await _channel
.invokeMethod<List<Object?>>('getPaymentTokens', {'accountId': accountId});
return result ?? const [];
}
/// Reconfigure a RUNNING SDK without re-initializing (new in v2.0.4):
/// change the offered methods, who draws the chooser, and the SuperQi
/// presentation, all in one call.
static Future<bool> updatePaymentMethods({
required List<PaymentMethodOption> methods,
required PaymentMethodChoice choice,
AliPayShowFirst aliPayShowFirst = AliPayShowFirst.QR,
}) async {
final result = await _channel.invokeMethod<bool>('updatePaymentMethods', {
'availablePaymentMethods': methods.map((m) => m.name).toList(),
'paymentMethodChoice': choice.name,
'aliPayShowFirst': aliPayShowFirst.name,
});
return result ?? false;
}
}
ALIPAY and not SUPER_QIALIPAY is the SDK's internal identifier for Pay with SuperQi on both platforms. Keeping the
Dart enum name identical means the string sent over the channel maps 1:1 to the native
AvailablePaymentMethods.ALIPAY — no translation table needed. Put the friendly name
("Pay with SuperQi") in your UI labels, never in the channel contract.
Pass language: 'kurdish' (or 'auto' to follow the device). The native bridges map the string
to a language code (LanguageCode.ku on Android, "ku" on iOS) and feed it into the new
PaymentSDKLocalization API — the old per-platform language enums were removed in v2.x.
3. The native bridges
Each platform registers a handler for the payment_sdk_flutter channel, parses the string
arguments back into SDK enums, and builds the v2 configuration. Because the SDK shows its own UI,
processPayment is asynchronous: stash the Flutter result callback and invoke it from the SDK's
success/error handler.
- Android (Kotlin)
- iOS (Swift)
android/app/src/main/kotlin/.../MainActivity.kt:
class MainActivity : FlutterActivity() {
private var activePaymentResult: MethodChannel.Result? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "payment_sdk_flutter")
.setMethodCallHandler { call, result ->
when (call.method) {
"initializeSDK" -> handleInitializeSDK(call, result)
"processPayment" -> handleProcessPayment(call, result)
"getPaymentTokens" -> handleGetPaymentTokens(call, result)
"updatePaymentMethods" -> handleUpdatePaymentMethods(call, result)
else -> result.notImplemented()
}
}
}
// --- string → SDK enum parse helpers -------------------------------
private fun parsePaymentMethods(names: List<String>?): Set<AvailablePaymentMethods> {
val mapped = names.orEmpty().mapNotNull {
when (it.uppercase()) {
"CARD" -> AvailablePaymentMethods.CARD
"ALIPAY" -> AvailablePaymentMethods.ALIPAY // Pay with SuperQi
"PAYMENT_TOKEN" -> AvailablePaymentMethods.PAYMENT_TOKEN
else -> null
}
}.toSet()
// Never leave the SDK with zero methods; fall back to card.
return mapped.ifEmpty { setOf(AvailablePaymentMethods.CARD) }
}
private fun parsePaymentMethodChoice(value: String?) = when (value?.uppercase()) {
"ON_APP" -> PaymentMethodChoice.ON_APP
else -> PaymentMethodChoice.ON_SDK
}
private fun parseAliPayShowFirst(value: String?) = when (value?.uppercase()) {
"LINK" -> PaymentTypeAliPay.LINK
else -> PaymentTypeAliPay.QR
}
private fun parseLanguageCode(value: String?): LanguageCode? = when (value?.lowercase()) {
"english" -> LanguageCode.en
"arabic" -> LanguageCode.ar
"kurdish" -> LanguageCode.ku
else -> null // "auto" → follow the system language
}
// --- initialization -------------------------------------------------
private fun handleInitializeSDK(call: MethodCall, result: MethodChannel.Result) {
val localization = PaymentSDKLocalization(
availableLanguages = setOf(
SDKLanguage(LanguageCode.en, "English"),
SDKLanguage(LanguageCode.ar, "العربية"),
SDKLanguage(LanguageCode.ku, "کوردی"),
SDKLanguage(null, "System")
),
selectedLanguageCode = parseLanguageCode(call.argument("language")),
writingDirection = WritingDirection.LEFT_TO_RIGHT
)
// Merchant is REQUIRED on Android — build() throws without it
val merchant = Merchant(
call.argument<String>("merchantName") ?: "QiCard",
call.argument<String>("merchantLogoUrlLight") ?: "",
call.argument<String>("merchantLogoUrlDark") ?: ""
)
val aliPaySettings = AliPaySettings(
parseAliPayShowFirst(call.argument("aliPayShowFirst")),
true, // qrToDeepLinkFallback
true // deepLinkToQrFallback
)
val config = PaymentSDKConfiguration.Builder() // tech.finon.payment.config.v2
.setConnectionSettings(connectionSettings) // built from baseUrl/publicKey/auth args
.setMerchant(merchant)
.setLocalization(localization)
.setTheme(Theme.LIGHT)
.setAvailablePaymentMethods(parsePaymentMethods(call.argument("availablePaymentMethods")))
.setPaymentMethodChoice(parsePaymentMethodChoice(call.argument("paymentMethodChoice")))
.setAliPaySettings(aliPaySettings)
.build()
PaymentSDK.initialize(this, config, sdkExitCallback)
result.success(true)
}
// --- runtime reconfiguration ----------------------------------------
private fun handleUpdatePaymentMethods(call: MethodCall, result: MethodChannel.Result) {
try {
PaymentSDK.updateConfiguration(parsePaymentMethods(call.argument("availablePaymentMethods")))
PaymentSDK.updateConfiguration(parsePaymentMethodChoice(call.argument("paymentMethodChoice")))
PaymentSDK.updateConfiguration(
AliPaySettings(parseAliPayShowFirst(call.argument("aliPayShowFirst")), true, true)
)
result.success(true)
} catch (e: Exception) {
result.error("UPDATE_ERROR", e.message, null)
}
}
// --- payment ----------------------------------------------------------
private fun handleProcessPayment(call: MethodCall, result: MethodChannel.Result) {
// v2.0.4 rejects a null accountId — always pass the real user identifier
val customerInfo = CustomerInfo(accountId = call.argument<String>("accountId")!!)
val paymentDetails = PaymentDetails(
paymentId = call.argument<String>("paymentId")!!,
requestId = call.argument<String>("requestId")!!,
customerInfo = customerInfo,
amount = call.argument<Double>("amount"),
currency = call.argument<String>("currency"),
/* paymentMethod, needPaymentToken, tokenType, additionalInfo ... */
)
activePaymentResult = result // completed later from the SDK callbacks
PaymentSDK.processPayment(
paymentDetails,
onSuccess = { activePaymentResult?.success(mapOf("status" to "success")); activePaymentResult = null },
onError = { message -> activePaymentResult?.error("PAYMENT_ERROR", message, null); activePaymentResult = null }
)
}
}
ios/Runner/AppDelegate.swift:
@main
class AppDelegate: FlutterAppDelegate {
private var paymentSDK: PaymentSDK?
private var activePaymentResult: FlutterResult?
override func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: "payment_sdk_flutter",
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { [weak self] call, result in
switch call.method {
case "initializeSDK": self?.handleInitializeSDK(call: call, result: result)
case "processPayment": self?.handleProcessPayment(call: call, result: result)
case "getPaymentTokens": self?.handleGetPaymentTokens(call: call, result: result)
case "updatePaymentMethods": self?.handleUpdatePaymentMethods(call: call, result: result)
default: result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// --- string → SDK enum parse helpers -------------------------------
private static func parsePaymentMethods(_ names: [String]?) -> Set<AvailablePaymentMethods> {
let mapped: [AvailablePaymentMethods] = (names ?? []).compactMap {
switch $0.uppercased() {
case "CARD": return .CARD
case "ALIPAY": return .ALIPAY // Pay with SuperQi
case "PAYMENT_TOKEN": return .PAYMENT_TOKEN
default: return nil
}
}
// Never leave the SDK with zero methods; fall back to card.
return mapped.isEmpty ? [.CARD] : Set(mapped)
}
private static func parsePaymentMethodChoice(_ value: String?) -> PaymentMethodChoice {
value?.uppercased() == "ON_APP" ? .onApp : .onSdk
}
private static func parseAliPayShowFirst(_ value: String?) -> PaymentTypeAliPay {
value?.uppercased() == "LINK" ? .LINK : .QR
}
private static func parseLanguageCode(_ value: String?) -> String? {
switch value?.lowercased() {
case "english": return "en"
case "arabic": return "ar"
case "kurdish": return "ku"
default: return nil // "auto" → follow the system language
}
}
// --- initialization -------------------------------------------------
private func handleInitializeSDK(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: nil, details: nil)); return
}
let localization = PaymentSDKLocalization(
availableLanguages: [
SdkLanguage(code: "en", name: "English"),
SdkLanguage(code: "ar", name: "العربية"),
SdkLanguage(code: "ku", name: "کوردی"),
SdkLanguage(code: nil, name: "System")
],
selectedLanguageCode: Self.parseLanguageCode(args["language"] as? String),
writingDirection: .leftToRight
)
let merchant = Merchant(
name: args["merchantName"] as? String ?? "QiCard",
logoUrlLight: args["merchantLogoUrlLight"] as? String ?? "",
logoUrlDark: args["merchantLogoUrlDark"] as? String ?? "",
finishPaymentUri: "finon://payment" // must match the Info.plist URL scheme
)
let config = PaymentSDKConfiguration(
localization: localization,
theme: .light,
skipResultScreen: false,
connectionSettings: connectionSettings, // built from baseUrl/publicKey/auth args
tdsSettings: TDSSettings(authFirst: .SDK, authFallback: true,
tdssUICustomization: UiCustomization()),
paymentMethodChoice: Self.parsePaymentMethodChoice(args["paymentMethodChoice"] as? String),
availablePaymentMethods: Self.parsePaymentMethods(args["availablePaymentMethods"] as? [String]),
merchant: merchant,
aliPaySettings: AliPaySettings(
showFirst: Self.parseAliPayShowFirst(args["aliPayShowFirst"] as? String),
qrToDeepLinkFallback: true,
deepLinkToQrFallback: true)
)
paymentSDK = PaymentSDK(with: config)
result(true)
}
// --- runtime reconfiguration ----------------------------------------
private func handleUpdatePaymentMethods(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let sdk = paymentSDK else {
result(FlutterError(code: "SDK_NOT_INITIALIZED", message: nil, details: nil)); return
}
guard let args = call.arguments as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: nil, details: nil)); return
}
sdk.updatePaymentSDKConfiguration(
availablePaymentMethods: Self.parsePaymentMethods(args["availablePaymentMethods"] as? [String]))
sdk.updatePaymentSDKConfiguration(
paymentMethodChoice: Self.parsePaymentMethodChoice(args["paymentMethodChoice"] as? String))
sdk.updatePaymentSDKConfiguration(
aliPaySettings: AliPaySettings(
showFirst: Self.parseAliPayShowFirst(args["aliPayShowFirst"] as? String),
qrToDeepLinkFallback: true, deepLinkToQrFallback: true))
result(true)
}
// processPayment: call sdk.setCustomerInfo(...) first (required on iOS),
// stash `result` in activePaymentResult, then run sdk.processPayment(...)
// inside Task { @MainActor } and complete the stashed result from the
// SDK's success/error handlers — same pattern as Android above.
}
A method missing from the when / switch block silently falls through to notImplemented() /
FlutterMethodNotImplemented, which surfaces in Dart as a MissingPluginException. When you add
a channel method, add it to both bridges.
4. Using it: end-to-end payment
// 1. Initialize once (e.g. at app start)
final initialized = await PaymentSdkFlutter.initializeSDK(
baseUrl: 'https://uat-sandbox-3ds-api.qi.iq', // UAT sandbox
publicKey: '<payment-gateway-public-key>',
terminalId: '<your-terminal-id>',
authUsername: '<basic-auth-username>',
authPassword: '<basic-auth-password>',
language: 'kurdish', // 'english' | 'arabic' | 'kurdish' | 'auto'
availablePaymentMethods: [
PaymentMethodOption.CARD,
PaymentMethodOption.ALIPAY, // offer "Pay with SuperQi"
PaymentMethodOption.PAYMENT_TOKEN,
],
paymentMethodChoice: PaymentMethodChoice.ON_SDK, // SDK shows the chooser
aliPayShowFirst: AliPayShowFirst.QR,
merchantName: 'My Store',
);
// 2. Create the payment on your backend first (with appChannel: true),
// then hand the ids to the SDK:
try {
final result = await PaymentSdkFlutter.processPayment(
paymentId: paymentIdFromBackend,
requestId: requestIdFromBackend,
amount: 25000,
currency: 'IQD',
accountId: currentUser.id, // must be a real, non-empty identifier
);
// The SDK already showed its result screen; update your own UI/order state:
debugPrint('Payment finished: $result');
} on PlatformException catch (e) {
switch (e.code) {
case 'SDK_NOT_INITIALIZED':
// initializeSDK was never called (or failed) — initialize and retry
break;
case 'PAYMENT_ERROR':
// payment failed or was cancelled — e.message has the reason
break;
}
}
With paymentMethodChoice: ON_SDK the SDK opens its method chooser ("Pay by Card" /
"Pay with SuperQi" / saved cards) and handles everything from there. With ON_APP, your app
must pass the chosen method explicitly — e.g. paymentType: 'ALIPAY' to go straight to the
SuperQi screen.
appChannel: trueThe create-payment request your backend sends to the Payment Gateway must include
appChannel: true, or the SDK payment fails with payment is not in app channel. See the
SDK overview.
5. Changing the configuration at runtime
New in v2.0.4: you can reconfigure a running SDK without re-initializing. A robust pattern is store, then apply — keep the chosen configuration in your service class so it survives re-initialization, and push it live only when the SDK is already up:
class PaymentService {
bool _isInitialized = false;
List<PaymentMethodOption> _methods = const [
PaymentMethodOption.CARD,
PaymentMethodOption.ALIPAY,
PaymentMethodOption.PAYMENT_TOKEN,
];
PaymentMethodChoice _choice = PaymentMethodChoice.ON_SDK;
AliPayShowFirst _aliPayShowFirst = AliPayShowFirst.QR;
/// Store the new configuration and apply it to the running SDK if possible.
Future<bool> applyPaymentMethods({
required List<PaymentMethodOption> methods,
required PaymentMethodChoice choice,
required AliPayShowFirst aliPayShowFirst,
}) async {
_methods = methods;
_choice = choice;
_aliPayShowFirst = aliPayShowFirst;
if (!_isInitialized) return true; // will be used at the next initializeSDK
try {
return await PaymentSdkFlutter.updatePaymentMethods(
methods: methods,
choice: choice,
aliPayShowFirst: aliPayShowFirst,
);
} on PlatformException {
return false;
}
}
}
6. Channel error codes
| Code | Thrown by | Meaning |
|---|---|---|
SDK_NOT_INITIALIZED | both platforms | A method was called before initializeSDK succeeded |
SDK_ALREADY_INITIALIZED | both platforms | initializeSDK was called twice |
PAYMENT_ERROR | both platforms | The payment failed or was cancelled; message has the details |
UPDATE_ERROR | Android | updatePaymentMethods failed while applying the new config |
INVALID_ARGUMENTS | iOS | The argument map sent over the channel was malformed |
7. Gotchas
accountId must never be nullv2.0.4 rejects a CustomerInfo without an accountId — the old "empty customer info" pattern
crashes the payment. Always pass your real user/account identifier from Dart, and guard against
empty strings in the bridge.
- Base URL slashes differ per platform. Android wants a trailing
/, iOS wants none. Pass the raw URL over the channel and let each native side normalize it — don't "fix" it in Dart. - iOS needs
setCustomerInfobeforeprocessPaymentto initialize the SDK's local storage. Android has no such requirement. - Never configure an empty payment-method set. The parse helpers above fall back to
{CARD}for exactly this reason — keep that guard. - The back/close button is handled natively, not from Dart. The SDK reports the user tapping
its back button via an exit callback passed to
PaymentSDK.initialize(Android) and thefinon_pay_sdk_on_back_clicknotification (iOS) — show your "close without finishing?" confirmation dialog there, in native code. A Dart-sidecancelchannel method won't be reached while the SDK's own UI is in the foreground. - The SuperQi deep-link return needs the URL scheme registered in
Info.plistand matchingMerchant.finishPaymentUri(iOS); Android declares its scheme inAndroidManifest.xml.
Going further
- Pay with SuperQi in depth — flows, fallbacks, presets, and a full trace of this bridge: Pay with SuperQi section, especially the Flutter walkthrough.
- Every configuration attribute and native method: SDK API Reference.
- Native details referenced by the bridge code: Android SDK · iOS SDK.
Integration Support
After reading this entire page, if you've faced any issues or any questions in mind regarding the integration with the Payment SDK then please contact the Payment Gateway Integration Support via Payment Gateway Team