Skip to content

Mobile SDKs

A native payment sheet for Flutter, React Native, Android and iOS apps.

Wajub's mobile SDKs let you take Mobile Money, card and wallet payments in your app, with a ready-made payment sheet. Pick the package for your platform below.

Pick your platform

PlatformPackageVersionNeeds at leastOpen the sheet with
Flutterwajub_mobile, on pub.dev1.1.1Flutter 3.24presentWajubCheckout(context, options)
React Native@wajub/react-native, on npm1.1.1React Native 0.74, React 18usePayment().present(options)
Androidcom.wajub:wajub-mobile-compose, on Maven Central1.2.1Android 7.0, API 24PaymentSheet(options) { result -> }
iOSWajubMobileUI, with Swift Package Manager1.0.0iOS 15WajubCheckout.present(options, from:)

The sheet offers Mobile Money, cards and one-tap wallets. Other payment methods, such as bank transfer or crypto, are not offered in it.

How a payment flows

Where each half runs
Your server                            Your app
───────────                            ────────
POST /payments with sk.
  returns authorization_token   ──▶    present({ sessionToken })
                                         │
                                         ├─ the payer picks a method and pays
                                         ├─ approval, confirmation step or payment page
                                         └─ the sheet waits for the outcome
                                             │
                                         complete · processing · failed · cancelled
webhook payment.succeeded
  fulfils the order

The token is the only credential the app ever holds. It is scoped to one payment, it expires, and it cannot list, refund or read anything else on your account. Your secret key stays on your server.

The webhook is still what fulfils the order

The result the app receives is what the payer saw, not proof of payment. A Mobile Money approval can land after the payer closed the app, and a phone can lose signal mid-approval. Use the result to pick the next screen, then confirm with GET /payments/{id} or the payment.succeeded webhook before you release the goods.

States

Each state is a screen of the sheet, and your onStateChange callback receives its name.

StateWhat the payer sees
INITIATEDThe list of payment methods
COLLECTING_DETAILSThe form of the chosen method
PROCESSINGThe form, its Pay button busy, or the payment code screen while the code is sent
APPROVAL_REQUIRED"Approve the payment on your phone"
USSD_REQUIRED"Dial this code"
PAYER_OTP_REQUIRED"Generate your payment code": the steps, the code to dial, the exact amount to enter and a field for the code. Flutter, React Native and Android
OTP_REQUIRED, PIN_REQUIRED, PHONE_REQUIRED, BIRTHDAY_REQUIRED, ADDRESS_REQUIREDA confirmation step
3DS_REQUIRED3-D Secure in the browser, or in sandbox a code sent by email
VERIFYINGThe step's screen while its answer is checked
PENDING"Payment in progress", with Continue payment when a payment page can be reopened
SUCCESSThe success screen: amount, merchant, reference and receipt link
FAILEDThe failure screen, with Try again when the error allows it
EXPIRED"This payment has expired"
CANCELLED"Payment cancelled"

SUCCESS, EXPIRED and CANCELLED are final. A payment that is already paid when the sheet opens shows the success screen, and one that has ended shows a notice and reports a load error.

Results

The call that presents the sheet resolves exactly once, when the sheet closes, with one of four results.

ResultWhenWhat to do
completeThe sheet closed after the success screen, which closes itself after 3 seconds unless autoCloseSuccess says otherwiseConfirm on your server, then deliver
processingThe sheet closed while a payment was under way. Money may still moveShow a waiting screen: your server learns the outcome by webhook
failedThe sheet closed on a failure, an expired payment, a load error, or a payment cancelled elsewhereShow the error's message
cancelledThe payer tapped Cancel payment (payer_cancelled), closed the sheet before paying (dismissed), or the screen that presented it went away first (host_gone)Create a new payment for the next try

The call never hangs. When the screen that presented the sheet goes away, it resolves cancelled before any payment, processing once one started and complete after a success, and it does not cancel the payment. Presenting a second sheet while one is open fails with sheet_already_presented. If the system kills your app, nothing resolves: at the next launch, read the outcome on your server.

Cancelling and closing

The payer closes the sheet with its close button or a swipe.

The payerResultThe payment
Taps Cancel paymentcancelled, payer_cancelledCancelled
Closes the sheet before paying, on the payment code screen, or back on the form after a refusalcancelled, dismissedCancelled, unless cancelOnDismiss is false
Closes it on the failure screenfailed, with the error the payer sawCancelled when it still can be, unless cancelOnDismiss is false
Closes it while a payment is under way: approving on the phone, dialling a code, confirming a step or waitingprocessingKeeps running: confirm on your server
Closes it after the success screencompletePaid

A cancelled payment is cancelled for good, and Wajub sends the payment.cancelled webhook. With cancelOnDismiss: false, closing before paying leaves the payment open, so you can present the sheet again with the same token.

The sheet never shows a failure when it cannot be sure of the outcome: it shows "Payment in progress" and keeps waiting. Closed then, it resolves processing.

Mobile Money

One form covers every operator. The payer picks the country and the operator and types the number, formatted as they type it. The number's prefix preselects its operator, and a number that belongs to another operator is flagged before anything is sent. After Pay, the sheet asks the payer to approve the payment on their phone, or to dial a code, and waits for the outcome.

Some operators, such as Orange Money in Burkina Faso, take a payment only with a code the payer generates in the operator's menu first. On Flutter, React Native and Android, the form says so when Wajub knows it in advance; either way, after Pay the sheet shows how to get the code, the code to dial and the exact amount to enter, then takes the code. A code the operator refuses can be entered again, with the tries left when they are known; after three refusals the payer is back on the form to pay another way.

Wherever the sheet shows an amount, on Pay and on each screen, it is what the payer pays: the payment's amount, plus the Wajub fee when the payer bears it (bearer: customer). On Flutter, React Native and Android, it follows the method or operator the payer picks.

mobileMoney optionWhat it does
countryThe country to start on, ISO-2. Without it, the payer's country when it is offered
countryLockedHides the country selector
operatorModechoose lists the operators (the default). detect shows the detected operator as a badge in the number field

Cards

Depending on the payment, the payer pays by card in one of three places.

  • Stripe's card form, inside the sheet. Built into the Flutter and Android SDKs. React Native needs the optional @stripe/stripe-react-native package, and on iOS your app provides a CardCollector built with Stripe's SDK. Each platform page shows what to add.
  • A secure page of the payment provider, opened in the browser. Set returnUrl to an https universal link or app link into your app to bring the payer back. Without it, the payer comes back to your app by hand, and Continue payment reopens the page.
  • Wajub's test card form, in sandbox.

In live, the card number is typed only into Stripe's form or the provider's page.

Confirmation steps

Some card payments ask the payer for one more thing after Pay. Each step has its own screen.

StateThe payer enters
OTP_REQUIREDThe code they received
PIN_REQUIREDThe card PIN
PHONE_REQUIREDA phone number
BIRTHDAY_REQUIREDA date of birth
ADDRESS_REQUIREDA billing address
3DS_REQUIRED3-D Secure in the browser, or in sandbox the code sent by email

A wrong answer keeps the screen and shows the attempts left. A step that expires or runs out of attempts ends on the failure screen.

Appearance

The appearance option sets the sheet's look. Its keys, presets and variables are on Appearance.

  • Scheme. light, dark or auto, which follows the phone.
  • Presets. stripe, night, flat and none.
  • Your app's theme. inherit: true picks up your app's colours; each platform page says what it reads.
  • Merchant branding. What you leave unset comes from the merchant's branding in the Dashboard: logo, business name and colours. A white-label merchant hides "Secured by Wajub".
  • Fonts. A fontFamily is used only if your app ships the font.
  • Loading. loader is a skeleton or a spinner. readyTimeout, 15 seconds by default, is how long the sheet may load before it shows an error with Retry.

Errors

Every failure is a WajubError.

FieldWhat it is for
typeapi_error, authentication_error, invalid_request_error, payment_error or rate_limit_error
codeOne of the codes below
messageFor the payer, in the sheet's language. Show it as is
declineCodeOn a decline, the bank's or the operator's code
retryableWhether trying again can work
paramThe field at fault
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

React Native writes these fields in snake_case: decline_code, correlation_id, retry_after_seconds, block_reference.

GroupCodesReaches
Loadsession_not_found, session_terminal_expired, session_terminal_cancelled, session_terminal_failed, load_error and ready_timeout (retryable), no_payment_methodonLoadError, then the result failed
Connectionnetwork_error, timeout, server_error, rate_limited, request_failedonError
Paymentpayment_declined, insufficient_funds and other decline codes (retryable), payment_attempt_ended (retryable, nothing was charged), payment_failed, payment_cancelled, session_expired, limit_exceeded, shield_blocked, merchant_not_verified, merchant_restricted, payment_method_restricted, country_restricted, currency_restricted, channel_not_accepted, unsupported_action, redirect_unavailableonError
Confirmation stepsinvalid_otp, invalid_pin, invalid_phone, invalid_birthday, invalid_address, invalid_code (the screen stays), otp_expired, pin_expired, phone_expired, birthday_expired, address_expired, max_attempts_exceeded, otp_attempts_exceeded (back to the form), 3ds_failed, 3ds_rejected, 3ds_cancelled, 3ds_timeoutThe step's screen, or onError
Your codesheet_already_presentedThe call that presents the sheet

A code not listed here, a bank's own decline code for instance, can reach you too: show message. Field errors, an invalid phone number for instance, stay on the form and never reach onError.

Privacy

The four SDKs, Flutter, React Native, Android and iOS, send the same data. When the payer pays, the SDK sends Wajub what the payment needs and, for fraud prevention, basic information about the device and a random install identifier, which is not the advertising identifier. When the payer uses Stripe's card form, Stripe's SDK also collects what Stripe documents for its own fraud checks.

Declare it next to what your app collects on its own.

For the App Store privacy labels, declare it linked to the user, not used for tracking, for App Functionality, which covers processing the payment and preventing fraud.

App Store ConnectWhat the SDK sends
Contact Info, NameFirst and last name, or the card holder, when the payment method asks for them
Contact Info, Email AddressWhen the card payment asks for it
Contact Info, Phone NumberThe Mobile Money number, or a phone number a card payment asks for
Contact Info, Physical AddressAddress, city, ZIP code and country, when the card payment asks for them
Financial Info, Payment InfoThe Mobile Money account, the Stripe card token, a card PIN when one is asked, and test cards in sandbox
Identifiers, Device IDThe random install identifier, for fraud prevention
Other DataA date of birth when one is asked, and basic device information, for fraud prevention

The iOS SDK ships a privacy manifest, which Xcode includes in your app's privacy report.

For Google Play data safety, declare it collected for App functionality and Fraud prevention, security, and compliance, shared with Wajub as your payment processor, encrypted in transit.

Google Play data safetyWhat the SDK sends
Personal info, NameFirst and last name, or the card holder, when the payment method asks for them
Personal info, Email addressWhen the card payment asks for it
Personal info, Phone numberThe Mobile Money number, or a phone number a card payment asks for
Personal info, AddressAddress, city, ZIP code and country, when the card payment asks for them
Personal info, Other infoA date of birth, when one is asked
Financial info, User payment infoThe Mobile Money account, the Stripe card token, a card PIN when one is asked, and test cards in sandbox
Device or other IDsThe random install identifier and basic device information, for fraud prevention

Testing

A payment your server creates with a sandbox key puts the whole flow in sandbox, sheet included, and the sheet shows a "Test mode" badge. Nothing changes in the app. The test numbers and cards are on Test scenarios.

What did you think of this content?