Flutter
The wajub_mobile package and its ready-made payment sheet for Flutter apps.
The Flutter SDK lets you take Mobile Money, card and wallet payments in your app, with a ready-made payment sheet.
wajub_mobile
Stable · GApub.dev
- Version
- 1.1.1
- Runtime
- Flutter 3.24+ (Dart 3.5+)
- Frameworks
- Android and iOS
Covers
- Payments
- Mobile Money
- Cards
Install
flutter pub add wajub_mobileOr pin it in pubspec.yaml with wajub_mobile: ^1.6.0.
| Requirement | Version |
|---|---|
| Flutter | 3.24 or later (environment: flutter: '>=3.24.0') |
| Dart | ^3.5.0 |
flutter_stripe | ^11.4.0, a dependency of the package: its own requirements become yours |
| Android build | Java 17, compileSdk 35 or later, minSdk 24 |
| iOS | Deployment target 13.0 |
The package's other dependencies need no code of yours.
Flutter 3.24 to 3.26: pin flutter_stripe 11.4
flutter_stripe 11.5 calls Flutter 3.27 APIs although it accepts any Flutter 3, and pub picks
it. On Flutter 3.24, 3.25 or 3.26, pin the Stripe packages to 11.4.0.
dependency_overrides:
flutter_stripe: 11.4.0
stripe_android: 11.4.0
stripe_ios: 11.4.0
stripe_platform_interface: 11.4.0Android needs FlutterFragmentActivity
Stripe's card field requires it. Leave MainActivity extending FlutterActivity and the card
form fails the moment it opens, while Mobile Money keeps working, which makes it look like a card
bug rather than a configuration one. The app's theme must also descend from Theme.AppCompat.
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterFragmentActivity()Quick start
Three steps, and only the middle one runs in the app.
1. Your server creates the payment. POST /payments with your secret key, then hand the
app the authorization_token of the answer. The package never holds a secret key.
Create a payment
final response = await http.post(
Uri.parse('https://api.yourshop.com/checkout'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'cart_id': cart.id}),
);
final authorizationToken = jsonDecode(response.body)['authorization_token'] as String;2. The app presents the sheet with that token and reads the result.
import 'package:wajub_mobile/wajub_mobile.dart';
Future<void> pay(BuildContext context, String authorizationToken) async {
final result = await presentWajubCheckout(
context,
PresentOptions(sessionToken: authorizationToken),
);
switch (result) {
case CheckoutComplete(:final payment):
// Paid: payment.id is Wajub's payment id. Confirm it on your server first.
showReceipt(payment.id);
case CheckoutProcessing():
// Closed while a payment was running: money may still move.
showPending();
case CheckoutFailed(:final error):
// A failure, an ended or expired payment, or a load error.
showError(error.message);
case CheckoutCancelled(:final reason):
// "Cancel payment", or closed before any payment.
debugPrint('cancelled: ${reason.wireName}');
}
}3. Your server confirms. The result is what the payer saw, not proof of payment. Deliver on
the payment.succeeded webhook, or after
GET /payments/{id} with payment.id.
presentWajubCheckout resolves exactly once, when the sheet closes, and Wajub.present(context, options) is the same call. CheckoutResult is a sealed class, so the switch above is exhaustive.
Presenting a second sheet while one is open throws a WajubError whose code is
sheet_already_presented, and leaves the open sheet alone. A new sheet can open as soon as the last
one has closed, from its onComplete too.
Options
PresentOptions(
sessionToken: authorizationToken,
locale: 'fr',
appearance: const Appearance(theme: 'night'),
loader: WajubLoader.skeleton,
readyTimeout: const Duration(seconds: 15),
returnUrl: 'https://shop.example.com/app/return',
mobileMoney: const MobileMoneyOptions(
country: 'CI',
countryLocked: true,
operatorMode: OperatorMode.detect,
),
cancelOnDismiss: true,
autoCloseSuccess: const Duration(seconds: 3),
stripe: const StripeOptions(appPublishableKey: 'pk_live_…'), // your app's own Stripe key, if it has one
callbacks: CheckoutCallbacks(onStateChange: (event) => debugPrint(event.state.wireName)),
)| Field | Type, default | What it does |
|---|---|---|
sessionToken | String, required | The payment's authorization_token |
locale | String? | 'fr' or 'en'. Without it the payment's language, else the device's. Anything not French reads English |
appearance | Appearance? | The sheet's look, see Appearance |
loader | WajubLoader, skeleton | WajubLoader.skeleton, placeholder blocks in the sheet's colours, or WajubLoader.spinner |
readyTimeout | Duration, 15 s | Past it, a sheet still loading shows "We couldn't load the payment" with Retry and reports ready_timeout to onLoadError. Duration.zero waits forever |
returnUrl | String? | An https universal link or App Link back into your app, for the pages that open in the browser. Without it the payer comes back to the app by hand |
mobileMoney | MobileMoneyOptions | See Mobile Money |
cancelOnDismiss | bool, true | Closing the sheet before any payment cancels the payment. false leaves it open so you can present it again |
autoCloseSuccess | Duration, 3 s | How long the success screen stays. Duration.zero keeps it until the payer closes it |
stripe | StripeOptions | appPublishableKey, your app's own Stripe key, see Cards |
callbacks | CheckoutCallbacks | See below |
Callbacks and states
Every callback is optional and runs on the UI thread, in this order for one payment: onReady,
then onStateChange on each change, onPaymentStarted and onRedirect, then onSuccess,
onError or onCancel, and onComplete exactly once. A callback that throws does not break the
sheet.
CheckoutCallbacks(
onReady: (ReadyEvent e) {}, // sessionId, environment, amount, currency, methods
onStateChange: (StateChangeEvent e) {}, // state (CheckoutState), method
onLoadError: (WajubError error) {}, // the sheet could not offer a payment
onPaymentStarted: (PaymentStartedEvent e) {}, // method, channel
onRedirect: (RedirectEvent e) {}, // e.url, right before the system browser opens
onSuccess: (PaymentSuccess payment) {},
onError: (WajubError error) {}, // every failure the payer sees, retryable ones included
onCancel: (CancelEvent e) {}, // reason (CancelReason), redirectUrl
onComplete: (CheckoutResult result) {}, // what presentWajubCheckout resolves with
)| Callback | Fires |
|---|---|
onReady | Once, when the first screen is drawn, also for an already paid or ended payment. environment is sandbox or live, methods the channel slugs shown |
onStateChange | After onReady, then on every change. method is mobile_money, card, wallet, or unknown before one is chosen |
onLoadError | The payment cannot be offered: session_not_found, session_terminal_expired, session_terminal_cancelled, session_terminal_failed, load_error, ready_timeout, no_payment_method |
onPaymentStarted | The payment request is accepted. From now on, closing the sheet resolves processing |
onRedirect | Right before a provider page opens in the browser |
onSuccess | Paid. method is unknown when the payment was already paid as the sheet opened |
onError | Every failure the payer sees, except field validation |
onCancel | The sheet closes as cancelled |
onComplete | The sheet closes |
state.wireName is the state's name: INITIATED, COLLECTING_DETAILS, PROCESSING,
APPROVAL_REQUIRED, USSD_REQUIRED, PAYER_OTP_REQUIRED, OTP_REQUIRED, PIN_REQUIRED,
PHONE_REQUIRED, BIRTHDAY_REQUIRED, ADDRESS_REQUIRED, 3DS_REQUIRED, VERIFYING, PENDING,
SUCCESS, FAILED, EXPIRED, CANCELLED. The Dart cases are in camel case:
CheckoutState.collectingDetails, CheckoutState.payerOtpRequired,
CheckoutState.threeDsRequired. What the payer sees in each one is on Mobile
SDKs.
Results
CheckoutResult | When | Carries |
|---|---|---|
CheckoutComplete | The sheet closed after the success, also for a payment already paid as it opened | payment (PaymentSuccess), transaction |
CheckoutProcessing | The sheet closed while a payment was under way: money may still move, confirm on your server | transaction, possibly null |
CheckoutFailed | Closed on a failure, an ended or expired payment, a load error, or a payment cancelled elsewhere (payment_cancelled) | error (WajubError), transaction |
CheckoutCancelled | "Cancel payment", a close before any payment, or the page that presented it going away first | reason (CancelReason), redirectUrl |
Every result also has status: complete, processing, failed or cancelled.
CancelReason is payerCancelled, dismissed or hostGone, and reason.wireName gives
payer_cancelled, dismissed or host_gone.
PaymentSuccess carries id (Wajub's payment id, what GET /payments/{id} takes), reference
(yours), amount (what the payer paid, fee included, in major units), currency, method,
status (always success), timestamp (ISO 8601) and receiptUrl.
The sheet never shows a failure when the outcome is unknown: it shows "Payment in progress" and
keeps waiting, and closing it then resolves processing. When the page that presented the sheet
goes away, the sheet resolves cancelled with hostGone before any payment, processing once one
has started, complete after a success, and never cancels the payment. If the app's process dies,
read the outcome on your server, or with WajubSession.status(), at the next launch.
Cancelling and closing
The payer closes the sheet with its close button, a swipe down, the back button or a tap outside it.
| Closing… | Result |
|---|---|
| While the payment still loads | CheckoutCancelled, dismissed |
| Before any payment | CheckoutCancelled, dismissed. The payment is cancelled, unless cancelOnDismiss is false |
| While the payer approves, dials a code, confirms a step or waits | CheckoutProcessing: the payment keeps running |
| On the failure screen | CheckoutFailed, with the error the payer saw. The payment is cancelled when it still can be, unless cancelOnDismiss is false. onCancel does not fire |
| After the success | CheckoutComplete |
"Cancel payment" in the sheet cancels the payment for good and resolves CheckoutCancelled with
payerCancelled. It is not offered while a payment is under way. See
Mobile SDKs.
Appearance
The appearance option sets the sheet's look. The keys, presets and variables are on
Appearance.
const appearance = Appearance(
theme: 'stripe', // presets: stripe, night, flat, none; or light, dark, auto
colorScheme: 'auto', // light, dark or auto, which follows the phone
inherit: true, // read Theme.of(context) where you set nothing
variables: AppearanceVariables(
colorPrimary: '#0F766E',
colorBackground: '#FFFFFF',
colorText: '#171717',
colorDanger: '#B91C1C',
fontFamily: 'Inter', // used when your app ships it
borderRadius: '12px',
spacingUnit: '4px',
),
buttonTextColor: '#FFFFFF',
disableAnimations: false,
);
// Or from a map, such as decoded JSON:
final shared = Appearance.fromMap(jsonDecode(appearanceJson) as Map<String, Object?>);What you leave unset comes from the merchant's branding in the Dashboard, then Wajub's default light or dark palette. See Mobile SDKs.
Mobile Money
One form covers every Mobile Money operator of the payment: the country, the operators, and the number with the country's dial code, formatted as it is typed. The number preselects its operator until the payer picks one by hand. After Pay, the sheet shows "Approve the payment on your phone", or a USSD code to dial.
MobileMoneyOptions | Default | What it does |
|---|---|---|
country | null | The country to start on, ISO-2. Without it, the payer's country when a Mobile Money channel covers it |
countryLocked | false | Hides the country selector |
operatorMode | OperatorMode.choose | OperatorMode.choose lists the operators. OperatorMode.detect shows the operator the number belongs to in the number field, and the list only when the number does not tell or the payer opens it |
See Mobile SDKs.
Cards
Stripe's card form needs FlutterFragmentActivity on Android, see Install. If your app
also takes its own Stripe payments with flutter_stripe, pass your app's key in
stripe: StripeOptions(appPublishableKey: …) so your own Stripe payments keep working.
Other cards open a secure page of the payment provider in the system browser, and in sandbox the sheet shows a test card form. See Mobile SDKs.
Confirmation steps
When a payment needs one more check, the sheet shows it on its own screen: a one-time code, the card PIN, a phone number, a date of birth or a billing address. A wrong answer stays on the screen with the attempts left; an expired step or too many attempts end on the failure screen, with "Try again". See Mobile SDKs.
Errors
Every failure is a WajubError, an Exception. The sheet hands it to onLoadError or onError
and puts it in CheckoutFailed.
| Field | What it is for |
|---|---|
type | A WajubErrorType: apiError, authenticationError, invalidRequestError, paymentError, rateLimitError |
code | The code to branch on |
message | For the payer, in the sheet's language |
declineCode | On a decline, the reason's code |
retryable | Whether another attempt can work |
param | The field at fault, when there is one |
correlationId | The request id to quote to Wajub support |
retryAfterSeconds | On rate_limited, how long to wait |
blockReference | On shield_blocked, the reference to quote to Wajub support |
Codes worth branching on: insufficient_funds, payment_declined, payment_attempt_ended
(refused, nothing was charged), payment_failed, payment_cancelled, session_expired,
shield_blocked, merchant_restricted, rate_limited, session_terminal_expired,
unsupported_action, redirect_unavailable, and sheet_already_presented from
presentWajubCheckout. The full list is on Mobile SDKs.
Your own UI
The sheet is the simplest integration. An app that draws its own payment screens uses the
lower-level WajubSession, created with Wajub.createSession(authorizationToken) and described in
the package's API reference on pub.dev.
Privacy
To make the payment, the SDK sends Wajub what the payer types. For fraud prevention, it also sends basic device information and a random install identifier, which is not the advertising identifier. What to declare in the App Store privacy labels and in Google Play's data safety form, category by category, is on Mobile SDKs.
Testing
Create the payment with a sandbox key: the whole flow runs in sandbox, sheet included, and the sheet shows a "Test mode" badge. Test numbers and cards are on Test scenarios.
Related pages
- Mobile SDKsWhat the payer sees, results and error codes.
- Create a paymentThe server call that produces the token.
- WebhooksThe confirmation that ships the order.
- AppearanceThe keys, presets and values of the appearance object.
- React NativeThe SDK for React Native apps.
- AndroidThe SDK for Kotlin and Compose apps.
- iOSThe SDK for Swift and SwiftUI apps.