SpareSpare Docs
GuidesAPI Reference
Sdk

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

AreaBeta (0.1.0)Stable (1.0.1)
Packagespare_link: ^0.1.0spare_link: ^1.0.1
Backend mintPOST /link/token/payment returns a linkTokenPOST /payment-requests returns a reference
Request field-channel: 'sdk_flutter' is required
Start a paymentstart(linkToken: ...)start(reference: ...)
Confirm a resultPaymentSuccess.exchangeCode to POST /link/exchangePaymentSuccess.reference to GET /payment-requests/{reference}
RecoveryresumeIfNeeded(navigator: ...)resumeIfNeeded() returns a sealed result
Tenantuae, ksa, bahuae only
Android build-flutter_inappwebview override for Android Gradle Plugin 9

Update the package version

dependencies:
  spare_link: ^1.0.1

Run 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

StepBetaStable
CreatePOST /link/token/payment returns linkTokenPOST /payment-requests returns reference
ConfirmPOST /link/exchange with the exchangeCodeGET /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

VariantBeta fieldsStable fields
PaymentSuccessexchangeCode (nullable)reference
PaymentFailederrorCodeerrorCode, message
PaymentAbandonednonereference
PaymentCancellednonenone

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

Remove the override when version 6.2.0 of flutter_inappwebview becomes stable.

Removed

  • linkToken and start(linkToken: ...).
  • PaymentSuccess.exchangeCode and the POST /link/exchange step.
  • The POST /link/token/payment mint endpoint.
  • The navigator argument on resumeIfNeeded().

Checklist

  • Your backend mints with POST /payment-requests and returns the reference
  • Each request sends channel: 'sdk_flutter'
  • start() takes reference: instead of linkToken:
  • Result handling reads PaymentSuccess.reference and confirms with GET /payment-requests/{reference}
  • resumeIfNeeded() handles SessionRecovered and NoRecoveryNeeded
  • pubspec.yaml requests spare_link: ^1.0.1 with the flutter_inappwebview override
  • The SDK is constructed with tenant: SpareApiTenant.uae

On this page