Skip to main content
Version: v2.0.4 latest

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 }
Why mirror the SDK's 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)
}
Defaults are "Super Qi on"

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 argumentType sentExampleNative enum it maps to
availablePaymentMethodsList<String>["CARD","ALIPAY"]AvailablePaymentMethods
paymentMethodChoiceString"ON_SDK"PaymentMethodChoice
aliPayShowFirstString"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,
);
This UI is a testing harness

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/.../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)
Register the channel case on both sides

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:

  1. Dart (payment_sdk.dart): add bool qrToDeepLinkFallback, bool deepLinkToQrFallback params and put them in the params map for both initializeSDK and updatePaymentMethods.
  2. Android (MainActivity.kt): read call.argument<Boolean>("qrToDeepLinkFallback") and pass into the AliPaySettings(...) constructor instead of the literal true.
  3. iOS (AppDelegate.swift): read args["qrToDeepLinkFallback"] as? Bool and pass into AliPaySettings(...).
  4. Plumb them through payment_service.dart and payment_screen.dart if 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.

Next: Testing & Troubleshooting →