React Native
Le package @wajub/react-native, un hook qui présente une interface de paiement native, et aucune WebView.
@wajub/react-native encaisse les paiements dans votre application React Native. present() ouvre
une interface native qui mène tout le paiement, approbation Mobile Money, cartes, étapes de
confirmation et pages des prestataires comprises, et se résout une fois, à la fermeture de
l'interface. Le payeur ne voit jamais de WebView.
À partir de 1.5.0, le package suit le contrat mobile commun : les mêmes options, callbacks, états, résultats, erreurs et objet d'apparence que les SDKs Flutter, Android et iOS, et que Wajub.js 1.8. Ce qu'ils partagent est décrit dans SDKs mobiles ; cette page couvre le côté React Native.
@wajub/react-native
Stable · GAnpm
- Version
- 1.1.1
- Runtime
- React 18+, React Native 0.74+
- Frameworks
- Android and iOS
Couvre
- Paiements
- Mobile Money
- Cartes
Installer
npm install @wajub/react-native @react-native-community/netinfo @react-native-async-storage/async-storagenpm install @stripe/stripe-react-nativecd ios && pod installLe package est du TypeScript simple, sans module natif propre, et pusher-js est sa seule
dépendance. Le code natif vient de ses dépendances homologues, que votre application installe.
| Dépendance homologue | Plage | Utilité |
|---|---|---|
react | >=18 | Toujours |
react-native | >=0.74 | Toujours |
@react-native-community/netinfo | >=11.0.0 | Toujours : le client temps réel en a besoin |
@react-native-async-storage/async-storage | >=1.17.0 | Toujours : elle conserve d'un lancement à l'autre l'identifiant d'installation derrière l'empreinte de l'appareil, voir Confidentialité |
@stripe/stripe-react-native | >=0.38.0 | Facultative, uniquement pour les cartes que Stripe porte. Sans elle, l'interface ne propose pas de carte Stripe |
@stripe/stripe-react-native impose ses propres versions minimales d'iOS et d'Android, qui
deviennent les vôtres dès que vous l'installez.
Sans AsyncStorage, un paiement aboutit quand même
Le package charge AsyncStorage dans un try. S'il manque ou n'est pas lié, les paiements passent
toujours, mais l'identifiant d'installation ne vit que le temps du runtime JavaScript : l'empreinte
change à chaque lancement, ce que Wajub lit comme un nouvel appareil. Une version de
développement le signale. Pour conserver l'identifiant ailleurs, passez dans storage tout
stockage doté de getItem et setItem asynchrones, à present() ou avec
createSession(token, undefined, { storage }).
WajubProvider à la racine de votre application est facultatif. Sans lui, usePayment() crée les
sessions avec la fabrique par défaut : le hook fonctionne partout.
Stripe sans StripeProvider
Pour une carte que Stripe porte, l'interface initialise @stripe/stripe-react-native avec la clé
publiable du paiement lui-même, issue de GET /pay/sdk-config, tant que le formulaire de carte est
affiché : seul le compte Stripe qui crée un pm_… peut le débiter. L'interface n'a besoin d'aucun
StripeProvider.
Stripe n'a qu'une configuration par application. Si votre application encaisse aussi ses propres
paiements Stripe, passez votre clé dans stripe: { appPublishableKey } : l'interface la remet en
place à la fermeture de son formulaire de carte. Sans elle, Stripe reste initialisé avec la clé du
paiement, et une version de développement le signale.
Démarrage rapide
- Votre serveur crée le paiement avec sa clé secrète et transmet
authorization_tokenà l'application. Rien dans ce package n'appelle/paymentsni ne détient de clé. - L'application appelle
present()avec ce jeton. Affichez l'élémentPaymentSheetque le hook renvoie dans le même composant. - Votre serveur confirme le paiement, avec le
webhook
payment.succeededouGET /payments/{id}, avant de livrer. Le résultat dans l'application est ce que le payeur a vu, pas une preuve de paiement.
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() se résout exactement une fois, avec la valeur que reçoit onComplete. Il se règle aussi
quand le composant qui a appelé le hook est démonté : cancelled avec host_gone avant toute
tentative, processing dès qu'une tentative a commencé, complete après un succès. Appelé après le démontage
de ce composant, parce que votre code attendait autre chose, il se résout aussitôt en cancelled
avec host_gone, sans aucune requête. Un appel pendant qu'une interface est ouverte est rejeté
aussitôt avec une WajubError de code sheet_already_presented, sans toucher à l'interface
ouverte.
PaymentSheet de usePayment est un élément, affichez-le
usePayment() renvoie { present, PaymentSheet }, où PaymentSheet est un élément ou null.
Placez {PaymentSheet} dans votre JSX : <PaymentSheet /> lève une erreur, car ce n'est pas un
composant. Le package exporte aussi un PaymentSheet de premier niveau qui est un composant,
celui de 1.4, déprécié : voir Migrer depuis 1.4.
Options
Tout se passe dans l'unique objet que prend present(), callbacks de la section suivante compris.
| Option | Type | Par défaut | Rôle |
|---|---|---|---|
sessionToken | string | Obligatoire | Le authorization_token du paiement |
locale | string | Celle du paiement, sinon celle de l'appareil | 'fr' ou 'en'. Toute autre valeur donne l'anglais |
appearance | Appearance | Aucune | L'apparence de l'interface, voir Apparence |
loader | 'skeleton' ou 'spinner' | 'skeleton' | Ce qui s'affiche pendant le chargement du paiement |
readyTimeout | number, en ms | 15000 | Passé ce délai, une interface encore en chargement affiche une erreur avec Réessayer, et onLoadError reçoit ready_timeout. Le paiement s'affiche quand même s'il arrive plus tard. 0 désactive le délai |
returnUrl | string | Aucune | Un lien universel ou d'application https, envoyé comme return_url aux pages des prestataires qui s'ouvrent dans le navigateur. Sans lui, rien n'est envoyé et le payeur revient dans l'application par lui-même |
mobileMoney.country | string, ISO-2 | Le pays du payeur s'il est proposé, sinon le premier | Le pays sur lequel le formulaire s'ouvre |
mobileMoney.countryLocked | boolean | false | Masque le sélecteur de pays, quand country est proposé |
mobileMoney.operatorMode | 'choose' ou 'detect' | 'choose' | 'detect' masque la liste des opérateurs tant que le numéro suffit à désigner l'opérateur |
realtime | { enabled, host, port, tls }, tous facultatifs | Le serveur du paiement | Un autre hôte pour un Reverb ou un Soketi auto-hébergé, ou enabled: false pour le seul polling |
cancelOnDismiss | boolean | true | Fermer l'interface avant toute tentative annule le paiement chez Wajub, tant qu'il est pending. false le laisse ouvert, pour rappeler present() |
autoCloseSuccess | number, en ms | 3000 | La durée d'affichage de l'écran de succès. 0 le garde jusqu'à ce que le payeur le ferme |
storage | WajubStorage | AsyncStorage | Où l'identifiant d'installation est conservé : tout objet doté de getItem et setItem asynchrones |
stripe.appPublishableKey | string | Aucune | La clé Stripe de votre application, remise en place après le formulaire de carte de l'interface |
Callbacks et états
Tous facultatifs, tous appelés sur le thread JavaScript, dans cet ordre pour un paiement :
onReady, puis onStateChange à mesure que le payeur avance, onPaymentStarted et onRedirect
quand ils s'appliquent, onSuccess, onError ou onCancel, et onComplete en dernier, une seule
fois. Un callback qui lève une erreur est journalisé avec console.warn, et l'interface continue.
| Callback | Reçoit | Quand |
|---|---|---|
onReady | { sessionId, environment, amount, currency, methods } | Le premier écran est affiché, y compris pour un paiement déjà payé ou terminé. environment vaut 'sandbox' ou 'live', methods liste les slugs des canaux affichés |
onStateChange | { state, method } | Après onReady, puis à chaque changement d'état. method vaut mobile_money, card, wallet ou unknown |
onLoadError | WajubError | L'interface ne peut proposer aucun paiement : session_not_found, session_terminal_expired, session_terminal_cancelled, session_terminal_failed, load_error, ready_timeout ou no_payment_method |
onPaymentStarted | { method, channel } | La première réponse acceptée d'une tentative. Dès lors, fermer l'interface donne processing |
onRedirect | { url, method: 'GET' } | Juste avant l'ouverture d'une page de prestataire dans le navigateur du système |
onSuccess | PaymentSuccess | Le paiement a réussi. method vaut unknown s'il était payé avant l'ouverture de l'interface |
onError | WajubError | Chaque échec que voit le payeur, y compris ceux qui permettent un nouvel essai, hors validation des champs |
onCancel | { reason, redirectUrl } | L'interface se ferme en cancelled |
onComplete | CheckoutResult | L'interface se ferme : la valeur avec laquelle present() se résout |
Chargement. Un paiement déjà payé, terminé ou en cours est affiché à partir de la seule session,
aussitôt : l'écran de succès, l'avis « ce paiement est terminé », ou l'écran d'approbation ou
d'attente où il en était, suivi. Il n'attend pas sdk-config et n'échoue pas quand sdk-config échoue.
Un paiement encore à payer avec Mobile Money attend sdk-config jusqu'à 3 secondes, puis s'affiche,
la carte et les portefeuilles le rejoignant à son arrivée. Celui dont les seuls moyens sont la carte
ou des portefeuilles l'attend, et un sdk-config en échec affiche load_error avec Réessayer,
jamais no_payment_method.
onStateChange emploie les noms CheckoutState de Wajub.js : INITIATED (liste des moyens),
COLLECTING_DETAILS (le formulaire d'un moyen), PROCESSING (Payer pressé), APPROVAL_REQUIRED,
USSD_REQUIRED, OTP_REQUIRED, PIN_REQUIRED, PHONE_REQUIRED, BIRTHDAY_REQUIRED,
ADDRESS_REQUIRED, 3DS_REQUIRED, VERIFYING, PENDING, SUCCESS, FAILED, EXPIRED et
CANCELLED. L'écran derrière chacun est décrit dans SDKs mobiles.
Résultats
present() se résout avec un PresentResult, en pratique un CheckoutResult. Faites un switch
sur status.
status | Quand | Contenu |
|---|---|---|
complete | L'interface s'est fermée après l'écran de succès, y compris pour un paiement payé avant son ouverture | payment (PaymentSuccess), transaction |
processing | L'interface s'est fermée pendant une tentative, ou après 15 minutes de suivi sans issue, comptées sur l'horloge monotone du téléphone depuis la tentative, jamais d'après l'expires_at du paiement. De l'argent peut encore bouger | transaction, et instruction quand le payeur en avait une |
failed | L'interface s'est fermée sur un échec, un paiement expiré, une erreur de chargement, ou un paiement annulé ailleurs (payment_cancelled) | error, transaction ou null |
cancelled | Annuler le paiement, une fermeture avant toute tentative, ou le démontage de l'hôte d'abord | reason (payer_cancelled, dismissed ou host_gone), redirectUrl |
PaymentSuccess est celui de Wajub.js : id (l'identifiant trx_… de Wajub, celui que prend
GET /payments/{id}), reference (la vôtre), amount (ce que le payeur a payé, frais compris, en
unité principale), currency, method, status: 'success', timestamp, et receipt_url quand il
existe un reçu.
Une application tuée ne peut résoudre aucune promesse. Au lancement suivant, interrogez votre
serveur, ou lisez le paiement avec createSession(token).status().
Annuler et fermer
L'interface est un Modal React Native, sans glissement vers le bas. Le payeur la ferme avec ✕
dans son en-tête, le bouton retour d'Android, ou Fermer sur un écran final. L'interface
n'appelle POST /pay/cancel que si le dernier statut connu du paiement est pending et qu'aucune
tentative n'est indécise, de l'appui sur Payer à une réponse définitive ; toute réponse
acceptée, même une que l'interface ne sait pas traiter, laisse la tentative indécise. Wajub
annulerait aussi un paiement que le payeur peut encore approuver sur son téléphone. Annuler le
paiement s'affiche donc sous la liste des moyens et sous les formulaires, jamais pendant que le
payeur approuve, confirme une étape ou attend. Un refus définitif (un refus de paiement, celui du
prestataire au retour de sa page compris, une approbation refusée, une étape ou un code 3-D Secure
en échec) remet le paiement en pending, et il peut de nouveau être annulé.
| Le payeur ferme l'interface | Résultat | Appel à Wajub |
|---|---|---|
| Pendant le chargement du paiement | cancelled, dismissed. Une fois le paiement lu, son statut décide aussitôt | Aucun |
| Sur la liste des moyens ou un formulaire, avant toute tentative ou après Réessayer | cancelled, dismissed, avec le redirectUrl renvoyé par l'annulation | POST /pay/cancel, sauf si cancelOnDismiss vaut false |
| De l'appui sur Payer jusqu'à une issue, y compris pendant que Stripe lit la carte ou que le défi Altcha est résolu | processing | Aucun : une approbation peut encore arriver |
| Sur l'écran d'échec | failed, avec l'erreur que le payeur a vue. onCancel n'est pas appelé | POST /pay/cancel quand le paiement est de nouveau pending, pour libérer la commande, sauf si cancelOnDismiss vaut false |
| Sur l'écran de succès | complete | Aucun |
| Avec Annuler le paiement | cancelled, payer_cancelled | POST /pay/cancel |
Pendant une annulation, Payer, le bouton de fermeture et Annuler le paiement sont
désactivés. Une annulation que Wajub refuse avec un 400 est tranchée en lisant le statut du
paiement, et un statut illisible donne processing. Un composant démonté alors que l'interface est
ouverte la règle en host_gone, ou en processing dès qu'une tentative a commencé, sans jamais
rien annuler. La règle complète est dans
SDKs mobiles.
Apparence
appearance est l'objet d'apparence de Wajub.js : les Components web et l'interface native peuvent
partager le même.
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',
},
});- Priorité, jeton par jeton :
variables, puis les clés de premier niveau, puis le préréglage, puis l'image de marque du marchand dans le Dashboard (couleurs, arrondi, graisses, logo, nom commercial), puis la palette claire ou sombre par défaut du checkout web. Une couleur principale du marchand sous 3:1 face au fond revient à celle par défaut, et un marchand en marque blanche masque « Sécurisé par Wajub ». - Mode de couleur :
colorScheme, ou le mode que nommetheme, puis le mode de l'application quandinheritvauttrue, puis le réglage du marchand dans le Dashboard, sinonauto, qui suit le téléphone pendant que l'interface est ouverte. Sur React Native,inheritne lit que le mode, avecuseColorScheme(): il n'existe aucun objet de thème où prendre des couleurs. - Polices :
fontFamilydoit être une police que votre application embarque. Une pile système (-apple-system,Roboto,sans-serif…) désigne la police de la plateforme. La police du marchand dans le Dashboard n'est pas utilisée, le SDK ne pouvant savoir si votre application l'embarque. - Valeurs : couleurs
#rgb,#rrggbb,#rrggbbaa,rgb()ourgba(); longueurs'8px','8'ou'0.5rem';borderRadiusest plafonné à 22. Les valeurs illisibles, les clés inconnues etrulessont ignorées, jamais en erreur. - Libellés :
labels: 'above', la valeur par défaut, place chaque libellé au-dessus de son champ. Aveclabels: 'floating', comme sur le web, le libellé repose dans le champ vide et monte sur sa bordure quand le champ a le focus ou une valeur. - Mouvement :
disableAnimations: true, ou le réglage de réduction des animations du téléphone, arrête la pulsation du squelette et les fondus.
DEFAULT_PALETTES et THEME_PRESETS sont exportés avec les valeurs qu'emploie l'interface. Chaque
clé et chaque préréglage sont décrits une seule fois, pour le web et les applications, dans
Apparence, et ce qu'en fait l'interface mobile dans
SDKs mobiles.
Mobile Money
Un seul formulaire couvre tous les opérateurs Mobile Money du paiement : un sélecteur de pays, une
ligne par opérateur avec son logo (un badge de marque quand le logo manque), et le numéro avec son
indicatif, mis en forme pendant la saisie. Un canal agrégé comme cm.mobile affiche chacun de ses
opérateurs.
- Le préfixe du numéro présélectionne son opérateur tant que le payeur n'en a pas choisi un à la main, et un choix manuel n'est jamais remplacé.
- Un numéro d'un autre opérateur proposé est refusé avant tout envoi, dans la langue du payeur. Jamais pour Wave et les autres portefeuilles qui acceptent n'importe quel numéro, et jamais en sandbox, dont les numéros de test ne suivent pas les plans de numérotation réels.
- Le numéro part au format E.164,
+237677123456, la Côte d'Ivoire gardant son 0 initial, avecoperatorsur un canal agrégé quand le payeur a désigné l'opérateur. - Après Payer, « Validez le paiement sur votre téléphone » affiche le numéro et l'instruction de l'opérateur, un code USSD à composer par exemple.
mobileMoney.country ouvre le formulaire sur un pays, countryLocked masque le sélecteur, et
operatorMode: 'detect' garde la liste des opérateurs masquée tant que le numéro désigne un
opérateur proposé. Les règles de détection et de validation sont dans
SDKs mobiles.
Cartes
Le prestataire de la carte décide de l'écran, par l'entrée du canal carte dans
GET /pay/sdk-config.
| Canal carte | Dans l'interface |
|---|---|
client_session, Paystack, Flutterwave ou Adyen | Les champs de contact qui manquent encore au prestataire, puis son checkout hébergé dans le navigateur du système, vérifié au retour du payeur |
stripe_elements | Le CardField de Stripe, avec la clé publiable du paiement. Requiert @stripe/stripe-react-native ; sans lui, la carte n'est pas proposée |
hosted_redirect, Kkiapay, CinetPay, Mollie ou PayPal par exemple | Exactement les champs listés dans required_fields, puis la page du prestataire dans le navigateur du système |
form, en sandbox uniquement | Le formulaire de carte de test de Wajub |
Tout autre canal carte est masqué, et aucun numéro de carte n'est jamais envoyé à Wajub en live.
Les pages des prestataires s'ouvrent avec Linking, dans le navigateur du système, jamais dans une
WebView. Pendant qu'une page est ouverte, l'interface affiche « Paiement en cours » avec
Continuer le paiement, et le statut est lu dès que l'application revient au premier plan.
Votre propre écran de carte peut utiliser StripeCardSection, le composant qu'affiche l'interface.
Avec publishableKey, il initialise Stripe avec cette clé tant qu'il est affiché ; sans elle,
initialisez Stripe vous-même.
import { useEffect, useMemo, useState } from 'react';
import { buildStripeCardRequest, createSession, StripeCardSection } from '@wajub/react-native';
function CardCheckout({ token }: { token: string }) {
const session = useMemo(() => createSession(token), [token]);
const [card, setCard] = useState<{ slug: string; key: string } | null>(null);
const [name, setName] = useState('');
useEffect(() => {
void (async () => {
await session.loadSession();
const slug = session.cardChannelSlug();
const entry = slug ? (await session.sdkConfig()).channels[slug] : undefined;
if (slug && entry?.sdk === 'stripe_elements' && entry.publishable_key) {
setCard({ slug, key: entry.publishable_key });
}
})();
}, [session]);
if (!card) return null;
return (
<StripeCardSection
publishableKey={card.key}
cardholderName={name}
onCardholderNameChange={setName}
submitting={false}
error={null}
onPay={async (paymentMethodId) => {
// A WajubError thrown here shows above the card field.
const outcome = await session.submit(card.slug, buildStripeCardRequest(paymentMethodId, name));
handleOutcome(outcome); // complete, processing, or requires_action with a 3-D Secure url
}}
/>
);
}Pour une carte que le prestataire collecte lui-même, session.payCardHosted('card', { email })
démarre la session client, ouvre la page du prestataire, attend le retour du payeur et la vérifie,
en un seul appel. Pour lancer plutôt le SDK natif du prestataire, appelez
startClientSession('card', { email }), lisez client_session dans son résultat
requires_action (Paystack : public_key et access_code ; Flutterwave : public_key,
encryption_key, et reference comme tx_ref), puis appelez
completeClientSession(client_session.id) une fois le payeur revenu. Ces SDKs ne sont pas fournis.
Étapes de confirmation
Quand un prestataire de carte en demande davantage après Payer, l'interface affiche un écran natif pour cela : un code à usage unique, le code PIN de la carte, un numéro de téléphone, une date de naissance ou une adresse de facturation. Chacun indique les essais restants après une mauvaise réponse et propose Renvoyer 60 secondes après son ouverture. Une carte de sandbox qui demande 3-D Secure obtient l'écran du code envoyé par e-mail, avec son compte à rebours. Les étapes, leurs états et leurs erreurs sont dans SDKs mobiles.
Erreurs
Chaque échec est une WajubError, une sous-classe d'Error. onLoadError, onError et un
résultat failed en transmettent une, typée WajubErrorShape, et les appels de session en lèvent
une. Ses champs gardent le snake_case du format réseau.
| Champ | Utilité |
|---|---|
type | api_error, authentication_error, invalid_request_error, payment_error ou rate_limit_error |
code | Le code du catalogue, ou le code propre de l'API transmis tel quel |
message | Pour le payeur, dans la langue de l'interface |
decline_code | Sur un 402, le motif du prestataire |
retryable | Indique si un nouvel essai peut aboutir |
param | Le champ en cause, data.phone par exemple |
http_status | Le statut HTTP, quand il y en a un |
correlation_id | Le X-Request-Id de la réponse : à communiquer au support |
retry_after_seconds | L'attente sur rate_limited |
details | Les erreurs de champ de l'API, un message par champ |
block_reference | La référence d'un refus shield_blocked |
api_message | Le message anglais propre de l'API, pour vos journaux |
toJSON() donne la forme simple, et localized('fr') la même erreur avec son message en français.
WajubError.fromResponse() traduit une réponse de l'API comme le fait l'interface,
WajubError.fromStepFailure() une étape de confirmation en échec, et WajubError.create(code)
construit une erreur du catalogue.
import { WajubError } from '@wajub/react-native';
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);
}
try {
await session.submit('cm.mobile', data);
} catch (error) {
if (error instanceof WajubError && error.type === 'rate_limit_error') {
showRetryIn(error.retry_after_seconds ?? 60);
}
}Une requête de paiement sans réponse exploitable, une erreur réseau, un délai dépassé, un 500, ou
un 502 à 504 après les nouvelles tentatives, n'est jamais affichée comme un échec : l'interface
suit le statut, et ne revient au formulaire avec une erreur de connexion que si le paiement n'a
toujours pas commencé une minute après le dernier essai, le Payer suivant réutilisant la même
Idempotency-Key. Un 400 amène l'interface à lire le statut : une issue est affichée, un statut
processing ou illisible est suivi, et un paiement encore pending donne not_processable. La
règle est dans SDKs mobiles.
L'interface et les appels de 1.5 (session(), submit(), les appels d'étape…) utilisent le
catalogue commun. Les méthodes 1.4 de WajubSession gardent les codes de 1.4 et le texte de l'API.
Chaque code, avec son type, ses nouveaux essais possibles et ses messages en anglais et en
français, est dans SDKs mobiles.
Votre propre interface
createSession(token) renvoie le client sur lequel repose l'interface. Il envoie les mêmes
en-têtes, signaux d'appareil, délais et nouvelles tentatives, et les nouvelles tentatives
automatiques d'un paiement renvoient la même Idempotency-Key. La machine à états de l'interface
n'est pas exportée : vos propres écrans s'appuient sur ces appels.
import { createSession, openHostedRedirect, WajubError } from '@wajub/react-native';
const session = createSession(authorizationToken);
const payment = await session.session(); // GET /pay/session
const config = await session.sdkConfig(); // keyed by raw slug: cm.mtn, never cm.mobile
const outcome = await session.submit('cm.mobile', { phone: '+237677123456', country: 'CM', operator: 'mtn' });
if (outcome.status === 'processing') {
// kind: 'approval' (show outcome.instruction), 'ussd' or 'pending'. Watch the status.
} else if (outcome.status === 'requires_action' && outcome.action === 'confirm_otp' && outcome.stepId) {
const answer = await session.verifyOtp(outcome.stepId, code);
if (answer.status === 'failed') showError(WajubError.fromStepFailure(answer).message);
} else if (outcome.status === 'requires_action' && outcome.url) {
await openHostedRedirect(outcome.url); // a provider page, in the system browser
}
const stop = session.watchStatus((update) => {
if (update.status === 'complete' || update.status === 'failed') stop();
});submit() se résout avec un ProcessOutcome : complete ; processing avec son kind et son
instruction ; requires_action avec une url pour une page de prestataire, ou le stepId et le
hint d'une étape de confirmation ; ou failed, une action que le SDK ne sait pas mener donnant
unsupported_action. Toute autre réponse est levée en WajubError. Une network_error, un
timeout ou une server_error levés ne signifient pas que le paiement a échoué : lisez status(),
et renvoyez avec la même idempotencyKey seulement si le paiement est encore pending.
mapProcessOutcome(body, methodType) lit un corps de /pay/process que vous détenez déjà.
| Depuis 1.5, catalogue commun | Rôle |
|---|---|
session() | GET /pay/session, sans cache |
sdkConfig() | GET /pay/sdk-config, indexé par slug brut |
status() | GET /pay/status. Sa reference est l'identifiant de paiement de Wajub, pas la vôtre |
submit(channel, data, { idempotencyKey }) | POST /pay/process, en ProcessOutcome. Une nouvelle clé par appel, sauf si vous en passez une |
verifyOtp(otpId, code), verifyPin(pinId, pin), verifyPhone(phoneId, phone), verifyBirthday(birthdayId, birthday) | La réponse à une étape, birthday au format YYYY-MM-DD |
verifyAddress(addressId, { address, city, state, zip_code }) | L'étape de l'adresse de facturation, tous les champs obligatoires |
resend(kind, stepId) | Redemande l'étape |
verify3ds(threeDsId, code), send3ds(threeDsId) | Le code 3-D Secure de sandbox envoyé par e-mail |
| Conservé de 1.4, avec les résultats et les erreurs de 1.4 | Rôle |
|---|---|
loadSession(forceRefresh), getSdkConfig() | La session, mise en cache, et la sdk-config |
payMobileMoney({ channel_slug, phone, country }) | Envoie le numéro tel que vous le donnez |
payCard(channelSlug, paymentMethodId, cardholderName) | Un pm_… de Stripe |
payCardHosted(channel, options) | Une session client en un appel : démarrer, ouvrir, attendre, vérifier |
startClientSession(channel, options), completeClientSession(id) | La même chose, en deux moitiés |
payCardHostedRedirect(channel, billing), payWallet(channel) | Une carte sur la page du prestataire, un portefeuille en un geste |
process(channel, data, { idempotencyKey }) | POST /pay/process, en PaymentResult de 1.4 |
handleRedirectAction(result) | Ouvre la page d'un résultat dans le navigateur du système |
watchStatus(onUpdate, intervalMs) | Lit la session aussitôt, puis la suit jusqu'à la fin. Renvoie la fonction d'arrêt |
cancel() | POST /pay/cancel, renvoie le redirect_url |
cardChannelSlug() | Le canal carte de la session chargée |
Confidentialité
Chaque tentative de paiement transmet, pour les contrôles anti-fraude de Wajub, des signaux de
l'appareil dans data._client : le fuseau horaire, la première langue de l'appareil, la taille de
l'écran en pixels, une profondeur de couleur et une empreinte d'installation. Chaque appel envoie
aussi X-Wajub-Client, wajub-react-native/1.5.0 (ios 18.1) par exemple, un X-Request-Id neuf
et les langues de l'appareil dans Accept-Language.
L'empreinte est le SHA-256 d'un identifiant aléatoire que le SDK crée à la première utilisation et
conserve dans AsyncStorage sous la clé @wajub/install-id, ou dans le storage que vous passez. Ce
n'est jamais un identifiant matériel ou publicitaire. Un identifiant illisible donne une empreinte
valable pour ce lancement seulement, et l'identifiant stocké n'est jamais écrasé. Ce qu'il faut
déclarer dans les formulaires de l'App Store et de Google Play est dans
SDKs mobiles.
Migrer depuis 1.4
Le code écrit pour 1.4 compile et fonctionne sans changement : chaque nom, type et union de 1.4 est toujours là. Ce qui change, c'est tout ce que l'interface fait avant de se résoudre.
// 1.4: the sheet came back after the first answer, and you finished the job.
const result = await present({ sessionToken: token });
if ('cancelled' in result) return;
if (result.status === 'requires_action' && result.action === 'push_approval') {
showApproval(result.transaction.processing_context?.payer_instruction);
session.watchStatus(onUpdate);
}
// 1.5: the sheet runs the payment to its end, and resolves once.
const outcome = await present({ sessionToken: token });
if (outcome.status === 'complete') showReceipt(outcome.payment.id);
if (outcome.status === 'processing') showPending();present()se résout à la fermeture de l'interface, aveccomplete,processing,failedoucancelled. La valeur reste compatible avecPaymentResult | { cancelled: true }de 1.4 :'cancelled' in resultfonctionne toujours, etprocessingporte toujours le paiement. Le type liste encorerequires_actionde 1.4, qui n'arrive plus : approbations, étapes de confirmation et pages des prestataires se passent dans l'interface, qui suit elle-même le statut. Votre proprewatchStatus()aprèspresent()n'est donc plus nécessaire. Le nouveau code litPresentResultouCheckoutResult.- Le composant
PaymentSheetde premier niveau est déprécié au profit deusePayment(). Il garde ses props,visible,session,locale,onSubmit,onDismissetonResult, et affiche la nouvelle interface :onSubmitquand le payeur paie,onResultavec l'issue,processingavec le paiement et son instruction quand l'interface se ferme pendant une tentative, etonDismissquand elle se ferme avant. Il n'annule jamais le paiement à la fermeture. Rendu visible pendant qu'une autre interface est ouverte, il s'ouvre dès qu'elle se ferme. - Les numéros Mobile Money quittent l'interface au format E.164, avec
operatorsur un canal agrégé quand le payeur a désigné l'opérateur.buildMobileMoneyRequest()etpayMobileMoney()envoient toujours ce que vous leur donnez. - Les méthodes 1.4 de
WajubSession,mapProcessResponse(),mapStatusToResult()etWajubError.fromHttp()gardent les résultats, codes et messages de 1.4. Les lectures de 1.5 ont de nouveaux noms :submit(),mapProcessOutcome(),WajubError.fromResponse()et les appels d'étape.WajubError.fromHttp()est déprécié. watchStatus()etsubscribeStatus()gardent les résultats de 1.4, leur première lecture immédiate et leurs appels, à chaque interrogation sans canal temps réel et une fois par changement avec lui, sans limite de durée. Ils ralentissent désormais après deux minutes sans changement, espacent leurs essais en cas d'erreur, et s'arrêtent sur une session terminée (401,403,404,410) avec un résultatfailed, là où 1.4 l'interrogeait indéfiniment.SessionData.brandingreste l'objet de l'API. Sa lecture typée est le nouveauparsedBranding. Les objets des méthodes 1.4 gardent chaque clé de la réponse de l'API, comme 1.4 transmettait le JSON.- Chaque requête porte
X-Wajub-Client,X-Request-IdetAccept-Language, et chaque tentative de paiementdata._client.@react-native-async-storage/async-storageest désormais une dépendance homologue, pour que l'empreinte survive aux redémarrages : installez-la.
Tests
Il n'y a aucune URL de base à changer : chaque appel part vers https://api.wajub.com. Un paiement
que votre serveur crée avec une clé de sandbox donne un jeton de sandbox, et l'interface affiche un
badge « MODE TEST ».
- Mobile Money : le préfixe de l'opérateur et sept chiffres, les six derniers choisissant
l'issue :
000000réussit,000001donne des fonds insuffisants,000002échoue,000003expire,000004est annulé.670000000réussit au Cameroun. - Cartes : le formulaire de sandbox accepte les cartes de test de Wajub.
4000056655665556demande le code 3-D Secure envoyé par e-mail.
Chaque numéro et chaque carte figurent dans
Scénarios de test. Pour un test unitaire,
createSession(token, client) prend un PayClient en second argument : c'est le point à
remplacer par un double.
Pages associées
- SDKs mobilesLe contrat commun aux quatre SDKs.
- Créer un paiementL'appel serveur qui génère le jeton.
- WebhooksLa confirmation qui déclenche la livraison de la commande.
- Recette React NativeMobile Money dans une application React Native, de bout en bout.
- FlutterLa même interface, en Dart.
- AndroidLa même interface, en Kotlin et Compose.
- iOSLa même interface, en Swift et SwiftUI.