Flutter SDK Migration (beta to v1)
Move a Flutter app from the spare_link 0.1.0 beta to the stable 1.0.1 release.
Who this is for
This page is for teams already shipping the spare_link 0.1.0 beta, which used
the link-token flow. It maps the beta API to the stable 1.0.1 release. New
integrations should start from the Flutter SDK guide and do
not need this page.
The stable release replaces the link-token flow with the payment-request (reference)
flow. Three things change: how your backend mints and confirms the payment, what
you pass to start(), and how you read the result. The payment UI, the deep-link
setup, and the event model keep the same shape.
At a glance
| Area | Beta (0.1.0) | Stable (1.0.1) |
|---|---|---|
| Package | spare_link: ^0.1.0 | spare_link: ^1.0.1 |
| Backend mint | POST /link/token/payment returns a linkToken | POST /payment-requests returns a reference |
| Request field | - | channel: 'sdk_flutter' is required |
| Start a payment | start(linkToken: ...) | start(reference: ...) |
| Confirm a result | PaymentSuccess.exchangeCode to POST /link/exchange | PaymentSuccess.reference to GET /payment-requests/{reference} |
| Recovery | resumeIfNeeded(navigator: ...) | resumeIfNeeded() returns a sealed result |
| Tenant | uae, ksa, bah | uae only |
| Android build | - | flutter_inappwebview override for Android Gradle Plugin 9 |
Update the package version
dependencies:
spare_link: ^1.0.1Run flutter pub upgrade spare_link. The signature stays an ES256 detached JWS
in the x-signature header, but the payload changed: sign the payment request
object directly, with no request wrapper. See
Signature of the payload.
Backend: mint and confirm
| Step | Beta | Stable |
|---|---|---|
| Create | POST /link/token/payment returns linkToken | POST /payment-requests returns reference |
| Confirm | POST /link/exchange with the exchangeCode | GET /payment-requests/{reference} |
| Request field | - | add channel: 'sdk_flutter' |
The reference from POST /payment-requests is what the app passes to start().
Your backend reads GET /payment-requests/{reference} with its own credentials
and fulfils only on a settled status (Processed or Scheduled). There is no
exchangeCode and no POST /link/exchange.
Start a payment
The token becomes a reference, and the success result no longer carries an
exchangeCode.
Before:
Future<void> pay(String linkToken) async {
final session = await spareLink.start(
navigator: rootNavigatorKey.currentState!,
linkToken: linkToken,
);
switch (await session.result) {
case PaymentSuccess(:final exchangeCode):
if (exchangeCode != null) {
await backend.exchange(exchangeCode);
} else {
await backend.pollOrWaitForWebhook();
}
case PaymentCancelled():
case PaymentFailed(:final errorCode):
case PaymentAbandoned():
}
}After:
Future<void> pay(String reference) async {
final session = await spareLink.start(
navigator: rootNavigatorKey.currentState!,
reference: reference,
);
switch (await session.result) {
case PaymentSuccess(:final reference):
// Confirm on your backend. Do not fulfil the order here.
final record = await backend.getPaymentRequest(reference);
if (record.status case 'Processed' || 'Scheduled') {
await fulfil(reference, record.paymentId);
}
case PaymentCancelled():
case PaymentFailed(:final errorCode, :final message):
case PaymentAbandoned(:final reference):
await backend.getPaymentRequest(reference);
}
}Read the result
| Variant | Beta fields | Stable fields |
|---|---|---|
PaymentSuccess | exchangeCode (nullable) | reference |
PaymentFailed | errorCode | errorCode, message |
PaymentAbandoned | none | reference |
PaymentCancelled | none | none |
Confirmation moves from the one-time exchangeCode to the payment reference.
The return deep link now also carries paymentId, mandateId, and a sig on the
https redirect. See The return deep link.
Recovery
resumeIfNeeded() no longer takes a navigator. It returns a sealed result you
handle.
Before:
WidgetsBinding.instance.addPostFrameCallback((_) {
spareLink.resumeIfNeeded(navigator: rootNavigatorKey.currentState);
});After:
final recovery = await spareLink.resumeIfNeeded();
switch (recovery) {
case SessionRecovered(:final result):
// The return arrived before the flow started. The SDK read the result from it.
case NoRecoveryNeeded(:final reason, :final reference):
if (reason == NoRecoveryReason.outcomeUnknown && reference != null) {
await backend.getPaymentRequest(reference);
}
}NoRecoveryReason has four values: noPendingSession, flowAlreadyActive,
persistenceUnavailable and outcomeUnknown. Only outcomeUnknown needs an
action from you.
Tenant
The beta accepted SpareApiTenant.uae, .ksa, and .bah. The stable payment
flow serves the UAE only. Any other tenant causes an error. Construct the SDK
with tenant: SpareApiTenant.uae.
Android Gradle Plugin 9
The SDK shows the payment page with flutter_inappwebview. Version 6.1.5 of that
package names proguard-android.txt, which Android Gradle Plugin 9 removed. Your
Android build fails on Android Gradle Plugin 9 unless you override the dependency:
dependency_overrides:
flutter_inappwebview: ^6.2.0-beta.3Remove the override when version 6.2.0 of flutter_inappwebview becomes stable.
Removed
linkTokenandstart(linkToken: ...).PaymentSuccess.exchangeCodeand thePOST /link/exchangestep.- The
POST /link/token/paymentmint endpoint. - The
navigatorargument onresumeIfNeeded().
Checklist
- Your backend mints with
POST /payment-requestsand returns thereference - Each request sends
channel: 'sdk_flutter' -
start()takesreference:instead oflinkToken: - Result handling reads
PaymentSuccess.referenceand confirms withGET /payment-requests/{reference} -
resumeIfNeeded()handlesSessionRecoveredandNoRecoveryNeeded -
pubspec.yamlrequestsspare_link: ^1.0.1with theflutter_inappwebviewoverride - The SDK is constructed with
tenant: SpareApiTenant.uae