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
| Platform | Package | Version | Needs at least | Open the sheet with |
|---|---|---|---|---|
| Flutter | wajub_mobile, on pub.dev | 1.1.1 | Flutter 3.24 | presentWajubCheckout(context, options) |
| React Native | @wajub/react-native, on npm | 1.1.1 | React Native 0.74, React 18 | usePayment().present(options) |
| Android | com.wajub:wajub-mobile-compose, on Maven Central | 1.2.1 | Android 7.0, API 24 | PaymentSheet(options) { result -> } |
| iOS | WajubMobileUI, with Swift Package Manager | 1.0.0 | iOS 15 | WajubCheckout.present(options, from:) |
Related pages
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
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 orderThe 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.
| State | What the payer sees |
|---|---|
INITIATED | The list of payment methods |
COLLECTING_DETAILS | The form of the chosen method |
PROCESSING | The 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_REQUIRED | A confirmation step |
3DS_REQUIRED | 3-D Secure in the browser, or in sandbox a code sent by email |
VERIFYING | The step's screen while its answer is checked |
PENDING | "Payment in progress", with Continue payment when a payment page can be reopened |
SUCCESS | The success screen: amount, merchant, reference and receipt link |
FAILED | The 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.
| Result | When | What to do |
|---|---|---|
complete | The sheet closed after the success screen, which closes itself after 3 seconds unless autoCloseSuccess says otherwise | Confirm on your server, then deliver |
processing | The sheet closed while a payment was under way. Money may still move | Show a waiting screen: your server learns the outcome by webhook |
failed | The sheet closed on a failure, an expired payment, a load error, or a payment cancelled elsewhere | Show the error's message |
cancelled | The 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 payer | Result | The payment |
|---|---|---|
| Taps Cancel payment | cancelled, payer_cancelled | Cancelled |
| Closes the sheet before paying, on the payment code screen, or back on the form after a refusal | cancelled, dismissed | Cancelled, unless cancelOnDismiss is false |
| Closes it on the failure screen | failed, with the error the payer saw | Cancelled 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 waiting | processing | Keeps running: confirm on your server |
| Closes it after the success screen | complete | Paid |
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 option | What it does |
|---|---|
country | The country to start on, ISO-2. Without it, the payer's country when it is offered |
countryLocked | Hides the country selector |
operatorMode | choose 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-nativepackage, and on iOS your app provides aCardCollectorbuilt with Stripe's SDK. Each platform page shows what to add. - A secure page of the payment provider, opened in the browser. Set
returnUrlto anhttpsuniversal 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.
| State | The payer enters |
|---|---|
OTP_REQUIRED | The code they received |
PIN_REQUIRED | The card PIN |
PHONE_REQUIRED | A phone number |
BIRTHDAY_REQUIRED | A date of birth |
ADDRESS_REQUIRED | A billing address |
3DS_REQUIRED | 3-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,darkorauto, which follows the phone. - Presets.
stripe,night,flatandnone. - Your app's theme.
inherit: truepicks 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
fontFamilyis used only if your app ships the font. - Loading.
loaderis 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.
| Field | What it is for |
|---|---|
type | api_error, authentication_error, invalid_request_error, payment_error or rate_limit_error |
code | One of the codes below |
message | For the payer, in the sheet's language. Show it as is |
declineCode | On a decline, the bank's or the operator's code |
retryable | Whether trying again can work |
param | The field at fault |
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 |
React Native writes these fields in snake_case: decline_code, correlation_id,
retry_after_seconds, block_reference.
| Group | Codes | Reaches |
|---|---|---|
| Load | session_not_found, session_terminal_expired, session_terminal_cancelled, session_terminal_failed, load_error and ready_timeout (retryable), no_payment_method | onLoadError, then the result failed |
| Connection | network_error, timeout, server_error, rate_limited, request_failed | onError |
| Payment | payment_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_unavailable | onError |
| Confirmation steps | invalid_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_timeout | The step's screen, or onError |
| Your code | sheet_already_presented | The 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.
Payment attempts are rate limited
On rate_limited, the sheet counts down retryAfterSeconds before Try again. Do not wrap the
SDK in a retry loop of your own.
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 Connect | What the SDK sends |
|---|---|
| Contact Info, Name | First and last name, or the card holder, when the payment method asks for them |
| Contact Info, Email Address | When the card payment asks for it |
| Contact Info, Phone Number | The Mobile Money number, or a phone number a card payment asks for |
| Contact Info, Physical Address | Address, city, ZIP code and country, when the card payment asks for them |
| Financial Info, Payment Info | The Mobile Money account, the Stripe card token, a card PIN when one is asked, and test cards in sandbox |
| Identifiers, Device ID | The random install identifier, for fraud prevention |
| Other Data | A 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 safety | What the SDK sends |
|---|---|
| Personal info, Name | First and last name, or the card holder, when the payment method asks for them |
| Personal info, Email address | When the card payment asks for it |
| Personal info, Phone number | The Mobile Money number, or a phone number a card payment asks for |
| Personal info, Address | Address, city, ZIP code and country, when the card payment asks for them |
| Personal info, Other info | A date of birth, when one is asked |
| Financial info, User payment info | The Mobile Money account, the Stripe card token, a card PIN when one is asked, and test cards in sandbox |
| Device or other IDs | The 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.
Related pages
- Create a paymentThe server call that produces the token.
- Sessions & securityWhat a session token may and may not do.
- WebhooksThe confirmation that ships the order.
- AppearanceThe object that styles the sheet.
- Payment methodsEvery operator, by country.
- Choose your SDKRuntime floors and the constraints that rule one out.