Skip to content

React Native

The @wajub/react-native package, a ready-made payment sheet for your React Native app.

The React Native SDK lets you take Mobile Money, card and wallet payments in your app, with a ready-made payment sheet. present() opens it and resolves once, when it closes.

@wajub/react-native

Stable · GA

npm

Version
1.1.1
Runtime
React 18+, React Native 0.74+
Frameworks
Android and iOS

Covers

  • Payments
  • Mobile Money
  • Cards

Install

npm install @wajub/react-native @react-native-community/netinfo @react-native-async-storage/async-storage
npm install @stripe/stripe-react-native
iOS: link the native modules
cd ios && pod install
PeerRangeNeeded
react>=18Always
react-native>=0.74Always
@react-native-community/netinfo>=11.0.0Always
@react-native-async-storage/async-storage>=1.17.0Always
@stripe/stripe-react-native>=0.38.0Optional, for Stripe cards. Without it the sheet does not offer them, see Cards

@stripe/stripe-react-native sets its own minimum iOS and Android versions, which become yours when you install it. No provider is needed at the root of your app: usePayment() works in any component.

Quick start

  1. Your server creates the payment with its secret key and hands the app the authorization_token.
  2. The app calls present() with that token. Render the PaymentSheet element the hook returns in the same component.
  3. Your server confirms the payment, with the payment.succeeded webhook or GET /payments/{id}, before it delivers. The result in the app is what the payer saw, not proof of payment.
The whole integration
import { Button } from 'react-native';
import { usePayment } from '@wajub/react-native';

function CheckoutScreen({ authorizationToken }: { authorizationToken: string }) {
  const { present, PaymentSheet } = usePayment();

  const pay = async () => {
    const result = await present({
      sessionToken: authorizationToken,
      returnUrl: 'https://shop.example.com/app/payment-return', // optional, a universal/app link
    });

    switch (result.status) {
      case 'complete':
        // Paid on the device. Deliver once your server has confirmed it.
        return showReceipt(result.payment.id);
      case 'processing':
        // Closed while a payment was running: an approval may still land.
        return showPending();
      case 'failed':
        return showError(result.error.message);
      case 'cancelled':
        // result.reason: 'payer_cancelled', 'dismissed' or 'host_gone'
        return;
    }
  };

  return (
    <>
      <Button title="Pay" onPress={pay} />
      {PaymentSheet}
    </>
  );
}

present() resolves exactly once, with the value onComplete receives. If the component that called the hook unmounts while the sheet is open, it still resolves: cancelled with host_gone before any attempt, processing once one started, complete after a success. Calling it while a sheet is open rejects with a WajubError whose code is sheet_already_presented, and leaves the open sheet alone.

Options

Everything goes in the one object present() takes, the callbacks of the next section included.

OptionTypeDefaultWhat it does
sessionTokenstringRequiredThe payment's authorization_token
localestringThe payment's, else the device's'fr' or 'en'. Anything else reads English
appearanceAppearanceNoneThe sheet's look, see Appearance
loader'skeleton' or 'spinner''skeleton'What shows while the payment loads
readyTimeoutnumber, ms15000Past it, a sheet still loading shows an error with Retry and onLoadError gets ready_timeout. 0 disables it
returnUrlstringNoneAn https universal or app link the provider's page sends the payer back to. Without it, the payer comes back to the app by hand
mobileMoney.countrystring, ISO-2The payer's country when offered, else the first oneThe country the form starts on
mobileMoney.countryLockedbooleanfalseHides the country selector, when country is offered
mobileMoney.operatorMode'choose' or 'detect''choose''detect' hides the operator list while the number tells the operator
cancelOnDismissbooleantrueClosing the sheet before any attempt cancels the payment. false keeps it open, so you can present() it again
autoCloseSuccessnumber, ms3000How long the success screen stays. 0 keeps it until the payer closes it
stripe.appPublishableKeystringNoneYour app's own Stripe key, if your app also takes its own Stripe payments, see Cards

Callbacks and states

All optional, all called on the JavaScript thread, in this order for one payment: onReady, then onStateChange as the payer moves on, onPaymentStarted and onRedirect when they apply, onSuccess, onError or onCancel, and onComplete last, exactly once. A callback that throws is logged, and the sheet carries on.

CallbackReceivesWhen
onReady{ sessionId, environment, amount, currency, methods }The first screen is drawn, also for a payment already paid or ended. environment is 'sandbox' or 'live', methods the payment methods shown
onStateChange{ state, method }After onReady, then on every state change. method is mobile_money, card, wallet or unknown
onLoadErrorWajubErrorThe sheet cannot offer a payment: session_not_found, session_terminal_expired, session_terminal_cancelled, session_terminal_failed, load_error, ready_timeout or no_payment_method
onPaymentStarted{ method, channel }The payment request is accepted. From now on, closing the sheet resolves processing
onRedirect{ url, method: 'GET' }Right before a provider page opens in the system browser
onSuccessPaymentSuccessThe payment succeeded. method is unknown when it was paid before the sheet opened
onErrorWajubErrorEvery failure the payer sees, retryable ones included, field validation excluded
onCancel{ reason, redirectUrl }The sheet closes as cancelled
onCompleteCheckoutResultThe sheet closes: what present() resolves with

onStateChange reports one of these states: INITIATED (method list), COLLECTING_DETAILS (a method's form), PROCESSING (Pay pressed), APPROVAL_REQUIRED, USSD_REQUIRED, PAYER_OTP_REQUIRED (the code the payer generates), OTP_REQUIRED, PIN_REQUIRED, PHONE_REQUIRED, BIRTHDAY_REQUIRED, ADDRESS_REQUIRED, 3DS_REQUIRED, VERIFYING, PENDING, SUCCESS, FAILED, EXPIRED and CANCELLED. The screen behind each one is on Mobile SDKs.

Results

present() resolves with one of four results. Switch on status.

statusWhenCarries
completeThe sheet closed after the success screen, also for a payment paid before it openedpayment (PaymentSuccess), transaction
processingThe sheet closed while a payment was under way. The sheet never shows a failure it is not sure of: money may still move, so confirm on your servertransaction, and instruction when the payer had one
failedThe sheet closed on a failure, an expired payment, a load error, or a payment cancelled elsewhere (payment_cancelled)error, transaction or null
cancelledCancel payment, a close before any attempt, or the host unmounting firstreason (payer_cancelled, dismissed or host_gone), redirectUrl

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: 'success', timestamp, and receipt_url when there is a receipt.

A killed app cannot resolve a promise. At the next launch, ask your server for the payment's status.

Cancelling and closing

The payer closes the sheet with ✕ in its header, Android's back button, or Close on a final screen. There is no swipe-down.

The payer closes the sheetResult
While the payment loadscancelled, dismissed
On the method list or a form, before any attempt or after Try againcancelled, dismissed. The payment is cancelled, unless cancelOnDismiss is false
While a payment is under way: the payer approves, confirms a step or waitsprocessing. The payment keeps running: confirm it on your server
On the failure screenfailed, with the error the payer saw. The payment is cancelled when it still can be, unless cancelOnDismiss is false
On the success screencomplete
With Cancel paymentcancelled, payer_cancelled. The payment is cancelled for good

Cancel payment shows under the method list and the forms, never while a payment is under way. The rules are on Mobile SDKs.

Appearance

appearance sets the sheet's look: a preset, a colour scheme and the values you want to change.

Presets, variables and top-level keys
await present({
  sessionToken,
  appearance: {
    theme: 'stripe', // a preset: 'stripe', 'night', 'flat', 'none'; or 'light', 'dark', 'auto', 'system'
    colorScheme: 'auto', // follows the phone, live
    variables: { colorPrimary: '#0F766E', borderRadius: '12px', fontFamily: 'Inter' },
    buttonTextColor: '#FFFFFF',
  },
});
  • Colour scheme: colorScheme is light, dark or auto, which follows the phone. inherit: true follows your app's colour scheme instead.
  • Branding: what you leave unset comes from the merchant's branding in the Dashboard, and a white-label merchant hides "Secured by Wajub".
  • Fonts: fontFamily must be a font your app ships, else the platform's font is used.
  • Labels: labels: 'above', the default, draws each label above its field; 'floating' rests it inside the empty field.
  • Motion: disableAnimations: true, or the phone's reduce-motion setting, stops the animations.

Every key and preset is on Appearance.

Mobile Money

One form covers every Mobile Money operator of the payment: the payer picks the country and the operator and types the number, formatted as it is typed. The number's prefix preselects its operator, and a number of another offered operator is refused before anything is sent. After Pay, "Approve the payment on your phone" shows the number and the operator's instruction.

mobileMoney.country starts the form on a country, countryLocked hides the selector, and operatorMode: 'detect' keeps the operator list hidden while the number tells the operator. More on Mobile SDKs.

Cards

Stripe cards need the optional @stripe/stripe-react-native peer; no other Stripe setup is needed. If your app also takes its own Stripe payments, pass your app's key in stripe: { appPublishableKey } so your own Stripe payments keep working.

Other cards open a secure page of the payment provider in the system browser. In sandbox, the sheet shows a test card form.

Confirmation steps

When a payment needs one more step after Pay, the sheet shows it natively: a one-time code, the card's PIN, a phone number, a date of birth or a billing address, with the attempts left after a wrong answer. In sandbox, a card that asks for 3-D Secure gets a code sent by email. The steps and their states are on Mobile SDKs.

Errors

Every failure is a WajubError, a subclass of Error. onLoadError, onError and a failed result hand one over. Its fields are in snake_case.

FieldWhat it is for
typeapi_error, authentication_error, invalid_request_error, payment_error or rate_limit_error
codeWhat went wrong, to branch on
messageFor the payer, in the sheet's language
decline_codeOn a decline, its reason
retryableWhether another attempt can work
paramThe field at fault, when there is one
correlation_idThe request id to quote to Wajub support
retry_after_secondsThe wait on rate_limited
block_referenceOn shield_blocked, the reference to quote to Wajub support
Where errors reach you
import { WajubError } from '@wajub/react-native';

try {
  const result = await present({
    sessionToken,
    onError: (error) => log('wajub', error.code, error.correlation_id),
  });

  if (result.status === 'failed') {
    // insufficient_funds, payment_attempt_ended, session_terminal_expired, ready_timeout…
    report(result.error.code, result.error.retryable);
  }
} catch (error) {
  if (error instanceof WajubError && error.code === 'sheet_already_presented') {
    // A sheet is already open.
  }
}

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. Every code is on Mobile SDKs.

Your own UI

The sheet's screens are not exported. An app that draws its own payment screens uses the lower-level WajubSession, which createSession(token) returns and the package's README documents.

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

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

What did you think of this content?