Skip to main content
Version: v2.0.4 latest

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

1. Add the SDK binaries to android/app/libs/:

  • payment-2.0.4.aar
  • emv-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.


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;
}
}
Why the enum is called ALIPAY and not SUPER_QI

ALIPAY 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.

Kurdish language

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/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 }
)
}
}
Register every channel method on both platforms

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.

Don't forget appChannel: true

The 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

CodeThrown byMeaning
SDK_NOT_INITIALIZEDboth platformsA method was called before initializeSDK succeeded
SDK_ALREADY_INITIALIZEDboth platformsinitializeSDK was called twice
PAYMENT_ERRORboth platformsThe payment failed or was cancelled; message has the details
UPDATE_ERRORAndroidupdatePaymentMethods failed while applying the new config
INVALID_ARGUMENTSiOSThe argument map sent over the channel was malformed

7. Gotchas

accountId must never be null

v2.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 setCustomerInfo before processPayment to 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 the finon_pay_sdk_on_back_click notification (iOS) — show your "close without finishing?" confirmation dialog there, in native code. A Dart-side cancel channel 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.plist and matching Merchant.finishPaymentUri (iOS); Android declares its scheme in AndroidManifest.xml.

Going further

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