Flutter Integration
This page traces a single setting — "enable Super Qi" — all the way from the Dart UI down to the native SDK, so you can see exactly what to wire up in a Flutter app. Everything described here is implemented in the QiCard Flutter example app.
How the layers connect
The app talks to both native SDKs over one MethodChannel named payment_sdk_flutter. Because both platforms implement the same method names, the Dart layer is platform-agnostic.
payment_screen.dart UI: checkboxes + dropdowns (Debug options)
│ user toggles "Pay with Super Qi" + picks ON_SDK / QR
▼
payment_service.dart Holds the chosen config; validates; guards init state
│ initializeSDK(...) / applyPaymentMethods(...)
▼
payment_sdk.dart Channel wrapper: serializes enums → channel args
│ MethodChannel('payment_sdk_flutter').invokeMethod(...)
▼
┌─────────────┴──────────────┐
▼ ▼
MainActivity.kt AppDelegate.swift
(parse args → SDK config) (parse args → SDK config)
The Dart enums
Three enums model the Super Qi configuration. They live in lib/payment_sdk.dart and are the source of truth for the channel contract:
/// Payment methods the SDK can offer on its selection screen.
/// ALIPAY is the SDK's internal name for "Pay with Super Qi".
enum PaymentMethodOption { CARD, ALIPAY, PAYMENT_TOKEN }
/// Who selects the payment method:
/// [ON_SDK] = the SDK shows its own "Pay by Card / Pay with Super Qi" screen.
/// [ON_APP] = the host app decides the method and the SDK skips the chooser.
enum PaymentMethodChoice { ON_SDK, ON_APP }
/// Which Super Qi (AliPay) flow to show first.
enum AliPayShowFirst { QR, LINK }
ALIPAY name?Keeping the Dart enum value ALIPAY (rather than SUPER_QI) means the string sent over the channel matches the native AvailablePaymentMethods.ALIPAY exactly, so the parse helpers on each side are a trivial 1:1 map. The UI label is where the friendly name lives — see the debug UI.
The channel contract
The wrapper serializes the enums to plain strings (their .name) before sending them across the channel. This is the contract both native sides must honor.
initializeSDK arguments
PaymentSdkFlutter.initializeSDK(...) adds three Super Qi arguments (defaults shown):
static Future<bool> initializeSDK({
required String baseUrl,
required String publicKey,
required String terminalId,
// ...existing params...
List<PaymentMethodOption> availablePaymentMethods = const [
PaymentMethodOption.CARD,
PaymentMethodOption.ALIPAY,
PaymentMethodOption.PAYMENT_TOKEN,
],
PaymentMethodChoice paymentMethodChoice = PaymentMethodChoice.ON_SDK,
AliPayShowFirst aliPayShowFirst = AliPayShowFirst.QR,
}) async {
final params = <String, dynamic>{
// ...
'availablePaymentMethods':
availablePaymentMethods.map((m) => m.name).toList(), // ["CARD","ALIPAY",...]
'paymentMethodChoice': paymentMethodChoice.name, // "ON_SDK"
'aliPayShowFirst': aliPayShowFirst.name, // "QR"
};
// ...invokeMethod('initializeSDK', params)
}
Note the Dart defaults already include ALIPAY and use ON_SDK. So in this example app, Super Qi is on out of the box — unlike the bare SDK, whose defaults are card-only / ON_APP. If you copy this wrapper, you've effectively opted in.
updatePaymentMethods arguments
For changing the config on a running SDK:
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;
}
| Channel argument | Type sent | Example | Native enum it maps to |
|---|---|---|---|
availablePaymentMethods | List<String> | ["CARD","ALIPAY"] | AvailablePaymentMethods |
paymentMethodChoice | String | "ON_SDK" | PaymentMethodChoice |
aliPayShowFirst | String | "QR" | PaymentTypeAliPay (AliPaySettings.showFirst) |
Service layer
lib/payment_service.dart holds the chosen configuration as state and decides whether to apply it now (SDK already running) or defer it to the next initialize:
List<PaymentMethodOption> _methods = const [
PaymentMethodOption.CARD,
PaymentMethodOption.ALIPAY,
PaymentMethodOption.PAYMENT_TOKEN,
];
PaymentMethodChoice _choice = PaymentMethodChoice.ON_SDK;
AliPayShowFirst _aliPayShowFirst = AliPayShowFirst.QR;
/// Update the active payment-method configuration on a running SDK.
/// Stores the new config (so a later re-init keeps it) and pushes it to the
/// native SDK via the dynamic update channel.
Future<bool> applyPaymentMethods({
required List<PaymentMethodOption> methods,
required PaymentMethodChoice choice,
required AliPayShowFirst aliPayShowFirst,
}) async {
_methods = methods;
_choice = choice;
_aliPayShowFirst = aliPayShowFirst;
if (!_isInitialized) {
// Nothing to update yet; the stored config will be used at init time.
return true;
}
try {
return await PaymentSdkFlutter.updatePaymentMethods(
methods: methods, choice: choice, aliPayShowFirst: aliPayShowFirst,
);
} on PlatformException {
return false;
}
}
The stored _methods / _choice / _aliPayShowFirst are also passed into initializeSDK(...), so whatever the user last chose survives a re-initialization.
The debug UI
lib/payment_screen.dart renders an expandable "Debug options" card (_buildDebugOptions()): a checkbox per method, two dropdowns (method selection + Super Qi "show first"), and an Apply config button. This is the friendly face over the enums — note the user-facing labels:
CheckboxListTile(
title: const Text('Pay with Super Qi'), // ← what the user reads
subtitle: const Text('SDK method: ALIPAY'), // ← the internal name, for debugging
value: _enableSuperQi,
onChanged: enabled ? (v) => setState(() => _enableSuperQi = v ?? false) : null,
),
Tapping Apply config assembles the list and calls the service:
final methods = <PaymentMethodOption>[
if (_enableCard) PaymentMethodOption.CARD,
if (_enableSuperQi) PaymentMethodOption.ALIPAY,
if (_enableSavedTokens) PaymentMethodOption.PAYMENT_TOKEN,
];
if (methods.isEmpty) { /* show "select at least one" snackbar */ return; }
await _paymentService.applyPaymentMethods(
methods: methods, choice: _methodChoice, aliPayShowFirst: _aliPayShowFirst,
);
The Debug options panel exists so SDK integrators can flip configurations live. A production checkout would normally set the configuration once at init and not expose these toggles. Reuse the mechanism (enums → channel args → native config), not necessarily the UI.
The native bridges
Each native side receives the string arguments and maps them to its SDK enums with three small parse helpers, then either builds the config (init) or calls the dynamic-update API.
- Android (Kotlin)
- iOS (Swift)
android/.../MainActivity.kt
// 1:1 string → SDK enum mapping. ALIPAY == "Pay with Super Qi".
private fun parsePaymentMethods(names: List<String>?): Set<AvailablePaymentMethods> {
val mapped = names.orEmpty().mapNotNull { name ->
when (name.uppercase()) {
"CARD" -> AvailablePaymentMethods.CARD
"ALIPAY" -> AvailablePaymentMethods.ALIPAY
"PAYMENT_TOKEN" -> AvailablePaymentMethods.PAYMENT_TOKEN
else -> null
}
}.toSet()
// Never leave the SDK with zero methods; fall back to card.
return if (mapped.isEmpty()) setOf(AvailablePaymentMethods.CARD) else mapped
}
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
}
At init — feed them into the builder:
val aliPaySettings = AliPaySettings(
parseAliPayShowFirst(call.argument<String>("aliPayShowFirst")),
true, // qrToDeepLinkFallback (hardcoded)
true // deepLinkToQrFallback (hardcoded)
)
val config = PaymentSDKConfiguration.Builder()
.setConnectionSettings(connectionSettings)
.setMerchant(merchant)
.setLocalization(localization)
.setTheme(Theme.LIGHT)
.setAvailablePaymentMethods(parsePaymentMethods(call.argument("availablePaymentMethods")))
.setPaymentMethodChoice(parsePaymentMethodChoice(call.argument("paymentMethodChoice")))
.setAliPaySettings(aliPaySettings)
.build()
At runtime — the updatePaymentMethods channel case:
PaymentSDK.updateConfiguration(methods)
PaymentSDK.updateConfiguration(choice)
PaymentSDK.updateConfiguration(aliPaySettings)
ios/Runner/AppDelegate.swift
// 1:1 string → SDK enum mapping. .ALIPAY == "Pay with Super Qi".
private static func parsePaymentMethods(_ names: [String]?) -> Set<AvailablePaymentMethods> {
let mapped: [AvailablePaymentMethods] = (names ?? []).compactMap { name in
switch name.uppercased() {
case "CARD": return .CARD
case "ALIPAY": return .ALIPAY
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 {
switch value?.uppercased() { case "ON_APP": return .onApp; default: return .onSdk }
}
private static func parseAliPayShowFirst(_ value: String?) -> PaymentTypeAliPay {
switch value?.uppercased() { case "LINK": return .LINK; default: return .QR }
}
At init — pass them into the config initializer:
let aliPaySettings = AliPaySettings(
showFirst: Self.parseAliPayShowFirst(args["aliPayShowFirst"] as? String),
qrToDeepLinkFallback: true, // hardcoded
deepLinkToQrFallback: true // hardcoded
)
let config = PaymentSDKConfiguration(
localization: localization,
theme: theme,
skipResultScreen: false,
connectionSettings: connectionSettings,
paymentMethodChoice: Self.parsePaymentMethodChoice(args["paymentMethodChoice"] as? String),
availablePaymentMethods: Self.parsePaymentMethods(args["availablePaymentMethods"] as? [String]),
tdsSettings: tdsSettings,
aliPaySettings: aliPaySettings
)
At runtime — the handleUpdatePaymentMethods handler:
sdk.updatePaymentSDKConfiguration(availablePaymentMethods: methods)
sdk.updatePaymentSDKConfiguration(paymentMethodChoice: choice)
sdk.updatePaymentSDKConfiguration(aliPaySettings: aliPaySettings)
The runtime update only works because updatePaymentMethods is added to the channel switch in both MainActivity.kt (the when (call.method) block) and AppDelegate.swift (the switch call.method block). If you add new channel methods, register them on both platforms or the call silently falls through to notImplemented / FlutterMethodNotImplemented.
Extending the bridge
To expose the two fallback flags (qrToDeepLinkFallback, deepLinkToQrFallback) — currently hardcoded to true — from Dart:
- Dart (
payment_sdk.dart): addbool qrToDeepLinkFallback,bool deepLinkToQrFallbackparams and put them in theparamsmap for bothinitializeSDKandupdatePaymentMethods. - Android (
MainActivity.kt): readcall.argument<Boolean>("qrToDeepLinkFallback")and pass into theAliPaySettings(...)constructor instead of the literaltrue. - iOS (
AppDelegate.swift): readargs["qrToDeepLinkFallback"] as? Booland pass intoAliPaySettings(...). - Plumb them through
payment_service.dartandpayment_screen.dartif you want UI toggles.
The same four-step pattern applies to any future Super Qi setting.
Common gotcha: accountId must not be null
SDK 2.0.4 rejects a null accountId when building CustomerInfo. The Android bridge resolves the customer id from Flutter and falls back to a generated id so the SDK never receives null:
val resolvedAccountId =
if (customerId.isNotBlank()) customerId else generateFakeAccountId()
val customerInfo = CustomerInfo(accountId = resolvedAccountId, embossingName = embossingName)
This isn't Super-Qi-specific, but it's on the same processPayment path and is a frequent crash source after the 2.0.4 upgrade. More in Testing & Troubleshooting.