spare_link 0.1.0-beta.3
spare_link: ^0.1.0-beta.3 copied to clipboard
Flutter SDK for Spare's redirect-based open-banking payment flows. Provides bank selection, consent orchestration, crash-safe session recovery, and sandbox/production support for KSA, UAE, and BAH.
spare_link #
A Flutter SDK for Spare's redirect-based open-banking payment flows. Handles bank selection, consent orchestration, deep-link return, and crash-safe session recovery — so your app only needs to launch the flow and handle the result.
Features #
- Full-screen SDK-owned payment flow launched with a single
await - Bank selection and consent details screens
- Redirect handoff to the bank and return via deep link
- Crash-safe session recovery via
SpareLink.resumeIfNeeded() - Sandbox and production environments
- KSA, UAE, and BAH tenant support
- Sealed
PaymentResulttype for exhaustive handling Stream<SpareLinkEvent>for analytics and lifecycle observabilitySpareLink.test()constructor for unit testing with injectable dependencies
Installation #
dependencies:
spare_link: ^0.1.0
Or via the command line:
flutter pub add spare_link
Platform setup #
Android #
Add an intent-filter in AndroidManifest.xml for your return URI scheme:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="payment-return" />
</intent-filter>
iOS #
Add your URL scheme to Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Forward incoming URIs to the SDK in both cases:
if (await spareLink.handleRedirect(incomingUri)) {
return true; // consumed — skip other deep-link handlers
}
Usage #
Create one SpareLink instance per environment and reuse it for every flow:
import 'package:spare_link/spare_link.dart';
final spareLink = SpareLink(
environment: PaymentEnvironment.sandbox,
tenant: SpareApiTenant.uae,
);
spareLink.events.listen((event) {
switch (event) {
case SpareLinkSessionLoaded(:final linkToken, :final linkSessionId):
debugPrint('loaded $linkToken → $linkSessionId');
case SpareLinkSucceeded(:final result):
debugPrint('succeeded: ${result.status.name}');
default:
break;
}
});
Start a payment flow #
Obtain a linkToken from your backend (via the Spare Link API), then launch the flow:
final session = await spareLink.start(
navigator: Navigator.of(context),
linkToken: linkToken,
);
debugPrint('session.linkToken=${session.linkToken}'); // available immediately
final result = await session.result;
switch (result) {
case PaymentSuccess(:final exchangeCode):
// Exchange the code with your backend to confirm the payment
case PaymentCancelled():
// User tapped back or dismissed the flow
case PaymentFailed(:final errorCode):
// Show error to user; errorCode and message are available
case PaymentAbandoned():
// Flow closed without completing (e.g. app was backgrounded)
}
The redirect URLs (successRedirectUrl, failureRedirectUrl) are set server-side when your backend creates the link token.
Handle deep-link return #
Forward every payment-return URI your platform delivers to:
if (await spareLink.handleRedirect(uri)) return true;
The recommended place is a WidgetsBindingObserver:
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
final uri = routeInformation.uri;
if (uri.scheme == 'myapp') {
if (await spareLink.handleRedirect(uri)) return true;
}
return super.didPushRouteInformation(routeInformation);
}
Crash-safe recovery on cold start #
Call this once during app initialisation (e.g. in initState of your root widget) to resume any payment session that was interrupted by a crash or forced quit:
final recovery = await spareLink.resumeIfNeeded(
navigator: navigatorKey.currentState,
);
switch (recovery) {
case SessionRecovered(:final result):
debugPrint('recovered: ${result.status.name}');
case NoRecoveryNeeded(:final reason, :final linkToken):
if (reason == NoRecoveryReason.navigatorRequired) {
// Retry with a navigator when UI is ready
debugPrint('pending link token: $linkToken');
}
}
Using with go_router #
spare_link works with MaterialApp.router and go_router without any SDK changes.
- Call
spareLink.start(navigator:, linkToken:)from any widget with aNavigatorState. - Forward return deep links to
spareLink.handleRedirect(uri). - Optionally call
spareLink.resumeIfNeeded(...)on app startup.
Universal links (optional, recommended) #
Spare's hosted Link return page (POST /link/authorize) tries a universal link on the Spare return host first, then falls back to your custom scheme URL from successRedirectUrl / failureRedirectUrl.
After bank authorisation, the hosted page:
- Navigates to
https://<spare-return-host>/link/app-return?exchange_code=…&fallback=… - If the OS does not open your app, redirects to your custom scheme URL with the same
exchange_code
Merchant app setup #
| Platform | Custom scheme | Universal link |
|---|---|---|
| Android | intent-filter for your scheme |
Verified App Link intent-filter for https://<spare-return-host>/link/app-return |
| iOS | CFBundleURLSchemes in Info.plist |
Associated Domains: applinks:<spare-return-host> |
| Both | Forward scheme URIs to spareLink.handleRedirect |
Forward https://…/link/app-return?… to spareLink.handleRedirect |
Verification files are served by the Link service:
GET /.well-known/apple-app-site-associationGET /.well-known/assetlinks.json
Your app bundle ID / package name must be listed in Spare's link app-return config for verification to succeed.
Identifier glossary #
| Dart field | Link API / redirect | Meaning |
|---|---|---|
linkToken |
link_token, header x-link-token |
JWS minted by POST /link/token/payment; passed to SpareLink.start() |
linkSessionId |
link_session_id |
Server-assigned session id from GET /link/status or PATCH /link |
exchangeCode |
exchange_code |
One-time code on the bank return redirect after /link/authorize |
merchantRef |
merchantRef |
Merchant order reference on the bank return redirect |
Bank return URIs use exchange_code + merchantRef on success (not session_id).
Deep-link requirements summary #
| Platform | Action required |
|---|---|
| Android | Add intent-filter in AndroidManifest.xml |
| iOS | Add URL scheme to Info.plist |
| Both | Call spareLink.handleRedirect(uri) in your URI handler |
Contributing #
Pull requests are welcome. The main branch requires:
- At least one approval from a code owner (other than the PR author)
- Passing CI (analyze, format, test)
- No direct pushes — all changes go through a PR
See CODEOWNERS for the approval team.
License #
BSD 3-Clause © Spare Technologies