Skip to content

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 · GA

pub.dev

Version
1.1.1
Runtime
Flutter 3.24+ (Dart 3.5+)
Frameworks
Android and iOS

Covers

  • Payments
  • Mobile Money
  • Cards

Install

Add the dependency
flutter pub add wajub_mobile

Or pin it in pubspec.yaml with wajub_mobile: ^1.6.0.

RequirementVersion
Flutter3.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 buildJava 17, compileSdk 35 or later, minSdk 24
iOSDeployment target 13.0

The package's other dependencies need no code of yours.

pubspec.yaml, on Flutter 3.24 to 3.26 only
dependency_overrides:
  flutter_stripe: 11.4.0
  stripe_android: 11.4.0
  stripe_ios: 11.4.0
  stripe_platform_interface: 11.4.0
android/app/src/main/kotlin/.../MainActivity.kt
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

Your own endpoint, your own client
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.

The whole integration
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, every field
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)),
)
FieldType, defaultWhat it does
sessionTokenString, requiredThe payment's authorization_token
localeString?'fr' or 'en'. Without it the payment's language, else the device's. Anything not French reads English
appearanceAppearance?The sheet's look, see Appearance
loaderWajubLoader, skeletonWajubLoader.skeleton, placeholder blocks in the sheet's colours, or WajubLoader.spinner
readyTimeoutDuration, 15 sPast it, a sheet still loading shows "We couldn't load the payment" with Retry and reports ready_timeout to onLoadError. Duration.zero waits forever
returnUrlString?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
mobileMoneyMobileMoneyOptionsSee Mobile Money
cancelOnDismissbool, trueClosing the sheet before any payment cancels the payment. false leaves it open so you can present it again
autoCloseSuccessDuration, 3 sHow long the success screen stays. Duration.zero keeps it until the payer closes it
stripeStripeOptionsappPublishableKey, your app's own Stripe key, see Cards
callbacksCheckoutCallbacksSee 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
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
)
CallbackFires
onReadyOnce, when the first screen is drawn, also for an already paid or ended payment. environment is sandbox or live, methods the channel slugs shown
onStateChangeAfter onReady, then on every change. method is mobile_money, card, wallet, or unknown before one is chosen
onLoadErrorThe payment cannot be offered: session_not_found, session_terminal_expired, session_terminal_cancelled, session_terminal_failed, load_error, ready_timeout, no_payment_method
onPaymentStartedThe payment request is accepted. From now on, closing the sheet resolves processing
onRedirectRight before a provider page opens in the browser
onSuccessPaid. method is unknown when the payment was already paid as the sheet opened
onErrorEvery failure the payer sees, except field validation
onCancelThe sheet closes as cancelled
onCompleteThe 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

CheckoutResultWhenCarries
CheckoutCompleteThe sheet closed after the success, also for a payment already paid as it openedpayment (PaymentSuccess), transaction
CheckoutProcessingThe sheet closed while a payment was under way: money may still move, confirm on your servertransaction, possibly null
CheckoutFailedClosed 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 firstreason (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 loadsCheckoutCancelled, dismissed
Before any paymentCheckoutCancelled, dismissed. The payment is cancelled, unless cancelOnDismiss is false
While the payer approves, dials a code, confirms a step or waitsCheckoutProcessing: the payment keeps running
On the failure screenCheckoutFailed, 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 successCheckoutComplete

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

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.

MobileMoneyOptionsDefaultWhat it does
countrynullThe country to start on, ISO-2. Without it, the payer's country when a Mobile Money channel covers it
countryLockedfalseHides the country selector
operatorModeOperatorMode.chooseOperatorMode.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.

FieldWhat it is for
typeA WajubErrorType: apiError, authenticationError, invalidRequestError, paymentError, rateLimitError
codeThe code to branch on
messageFor the payer, in the sheet's language
declineCodeOn a decline, the reason's code
retryableWhether another attempt can work
paramThe field at fault, when there is one
correlationIdThe request id to quote to Wajub support
retryAfterSecondsOn rate_limited, how long to wait
blockReferenceOn 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.

What did you think of this content?