Flutter
Le package wajub_mobile, une interface de paiement native qui mène tout le paiement, et aucune WebView.
wajub_mobile encaisse les paiements dans votre application Flutter. Un seul appel présente une
interface de paiement native qui charge le paiement, accepte le Mobile Money, les cartes et les
portefeuilles en un geste, suit le paiement jusqu'à son issue et se résout une fois, à sa
fermeture. Le payeur ne voit jamais de WebView.
À partir de 1.6.0, le package implémente le contrat mobile partagé, la spec v2 : les mêmes options, callbacks, états, résultats, erreurs et objet d'apparence que les SDKs Android, React Native et iOS, et que Wajub.js 1.8.
wajub_mobile
Stable · GApub.dev
- Version
- 1.1.1
- Runtime
- Flutter 3.24+ (Dart 3.5+)
- Frameworks
- Android and iOS
Couvre
- Payments
- Mobile Money
- Cards
Installer
flutter pub add wajub_mobileOu fixez-la dans pubspec.yaml avec wajub_mobile: ^1.6.0.
| Prérequis | Version |
|---|---|
| Flutter | 3.24 ou version ultérieure (environment: flutter: '>=3.24.0') |
| Dart | ^3.5.0 |
flutter_stripe | ^11.4.0, une dépendance du package : ses propres prérequis deviennent les vôtres |
| Build Android | Java 17, compileSdk 35 ou ultérieur, minSdk 24 |
| iOS | Cible de déploiement 13.0 |
Le package installe aussi url_launcher (les pages des prestataires dans le navigateur du
système), web_socket_channel (les mises à jour en temps réel), flutter_svg (les logos des
opérateurs), ainsi que shared_preferences, device_info_plus et flutter_timezone (les
signaux de l'appareil décrits dans Confidentialité). Ils ne demandent aucun code. Les
minimums Android et iOS ci-dessus sont ceux qu'ils exigent dans les versions que pub résout sur un
Flutter récent ; sur un Flutter plus ancien, pub résout des versions plus anciennes de ces
plugins, aux minimums plus bas. pusher_channels_flutter n'est plus une dépendance.
Flutter 3.24 à 3.26 : fixez flutter_stripe à 11.4
flutter_stripe 11.5 appelle des API de Flutter 3.27 alors qu'il accepte tout Flutter 3, et pub
le choisit. Sur Flutter 3.24, 3.25 ou 3.26, fixez les packages Stripe à 11.4.0.
dependency_overrides:
flutter_stripe: 11.4.0
stripe_android: 11.4.0
stripe_ios: 11.4.0
stripe_platform_interface: 11.4.0Android nécessite FlutterFragmentActivity
Le champ de carte Stripe s'affiche comme un fragment, une exigence de flutter_stripe. Si
MainActivity continue d'étendre FlutterActivity, le formulaire de carte échoue dès son
ouverture tandis que le Mobile Money continue de fonctionner. Le problème ressemble alors à un bug
de carte au lieu d'une erreur de configuration. Le thème de l'application doit aussi descendre de
Theme.AppCompat.
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterFragmentActivity()Démarrage rapide
Trois étapes, et seule celle du milieu s'exécute dans l'application.
1. Votre serveur crée le paiement. POST /payments avec votre clé secrète, puis transmettez
à l'application l'authorization_token de la réponse. Aucun élément de ce package n'appelle
/payments ni ne détient de clé. Créer un paiement
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. L'application présente l'interface de paiement avec ce jeton et lit le résultat.
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. Votre serveur confirme. Le résultat est ce que le payeur a vu, pas une preuve de paiement.
Livrez sur le webhook payment.succeeded, ou après
GET /payments/{id} avec payment.id.
presentWajubCheckout se résout exactement une fois, à la fermeture de l'interface, et
Wajub.present(context, options) est le même appel. CheckoutResult est une classe scellée : le
switch ci-dessus est donc exhaustif. Présenter une seconde interface pendant qu'une autre est
ouverte termine aussitôt le second appel avec une WajubError dont le code est
sheet_already_presented, sans toucher à l'interface ouverte. Une nouvelle interface peut s'ouvrir
dès que la précédente s'est réglée, y compris depuis son onComplete.
Options
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,
),
realtime: const RealtimeOptions(host: 'ws.shop.example', port: 443, tls: true),
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)),
)| Champ | Type, défaut | Rôle |
|---|---|---|
sessionToken | String, obligatoire | L'authorization_token du paiement |
locale | String? | 'fr' ou 'en'. Sans lui, la langue du paiement, sinon celle de l'appareil. Tout ce qui n'est pas du français donne l'anglais |
appearance | Appearance? | L'apparence de l'interface, voir Apparence |
loader | WajubLoader, skeleton | WajubLoader.skeleton, des blocs de substitution aux couleurs de l'interface, ou WajubLoader.spinner |
readyTimeout | Duration, 15 s | Au-delà, une interface encore en chargement affiche « Impossible de charger le paiement » avec Réessayer, et transmet ready_timeout à onLoadError. Si les données arrivent ensuite, l'interface les affiche. Duration.zero attend indéfiniment |
returnUrl | String? | Un lien universel ou App Link https qui revient dans votre application, envoyé comme return_url aux pages des prestataires ouvertes dans le navigateur. Sans lui, le payeur revient de lui-même |
mobileMoney | MobileMoneyOptions | country (le pays ISO-2 de départ), countryLocked (masquer le sélecteur, false par défaut), operatorMode (OperatorMode.choose par défaut, ou OperatorMode.detect) |
realtime | RealtimeOptions | host, port, tls pour un Reverb ou un Soketi auto-hébergé derrière un autre nom d'hôte, ou enabled: false pour le polling seul. Par défaut, l'interface se connecte là où la session l'indique |
cancelOnDismiss | bool, true | Une fermeture avant tout paiement l'annule chez Wajub. false le laisse ouvert pour que vous puissiez le présenter à nouveau |
autoCloseSuccess | Duration, 3 s | La durée d'affichage de l'écran de succès. Duration.zero le garde jusqu'à ce que le payeur le ferme |
stripe | StripeOptions | appPublishableKey, la clé Stripe propre à votre application, remise en place quand l'interface se ferme après son formulaire de carte. Voir Cartes |
callbacks | CheckoutCallbacks | Voir ci-dessous |
Quel que soit returnUrl, l'interface lit le statut dès que l'application revient au premier
plan.
Callbacks et états
Chaque callback est facultatif et s'exécute sur le thread de l'interface, dans cet ordre pour un
paiement : onReady, puis onStateChange à chaque changement, onPaymentStarted et
onRedirect, puis onSuccess, onError ou onCancel, et onComplete exactement une fois. Un
callback qui lève une exception ne casse pas l'interface. L'interface est libérée avant les
callbacks : onComplete peut donc présenter la suivante.
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
)| Callback | Déclenchement |
|---|---|
onReady | Une fois, quand le premier vrai écran est dessiné, y compris pour un paiement déjà payé ou terminé. environment vaut sandbox ou live, methods liste les slugs des canaux affichés |
onStateChange | Après onReady, puis à chaque changement. method vaut mobile_money, card, wallet, ou unknown avant tout choix |
onLoadError | Le paiement ne peut pas être proposé : session_not_found, session_terminal_expired, session_terminal_cancelled, session_terminal_failed, load_error, ready_timeout, no_payment_method |
onPaymentStarted | La première réponse acceptée d'une tentative. Dès lors, fermer l'interface donne processing |
onRedirect | Juste avant qu'une page de prestataire s'ouvre dans le navigateur |
onSuccess | Paiement réussi. method vaut unknown quand le paiement était déjà payé à l'ouverture |
onError | Chaque échec que voit le payeur, sauf la validation des champs |
onCancel | L'interface se ferme comme cancelled |
onComplete | L'interface se ferme |
Un paiement déjà payé, terminé ou encore en cours s'ouvre aussitôt sur son écran, à partir de la
seule session, sans attendre la configuration SDK. Quand cette configuration ne peut pas être lue,
le Mobile Money s'affiche quand même ; un paiement qui ne propose que des cartes ou des
portefeuilles affiche « Impossible de charger le paiement » avec Réessayer et signale
load_error, jamais no_payment_method.
CheckoutState porte les 17 noms de Wajub.js, et state.wireName donne la chaîne exacte :
INITIATED, COLLECTING_DETAILS, PROCESSING, APPROVAL_REQUIRED, USSD_REQUIRED,
OTP_REQUIRED, PIN_REQUIRED, PHONE_REQUIRED, BIRTHDAY_REQUIRED, ADDRESS_REQUIRED,
3DS_REQUIRED, VERIFYING, PENDING, SUCCESS, FAILED, EXPIRED, CANCELLED. Les cas Dart
sont en camel case : CheckoutState.collectingDetails, CheckoutState.threeDsRequired. L'écran
qui correspond à chacun figure sur SDKs mobiles.
Résultats
CheckoutResult | Quand | Contenu |
|---|---|---|
CheckoutComplete | L'interface s'est fermée après le succès, y compris pour un paiement déjà payé à l'ouverture | payment (PaymentSuccess), transaction |
CheckoutProcessing | Fermée pendant un paiement en cours, ou 15 minutes après l'appui sur Payer sans issue, mesurées sur l'horloge monotone du téléphone | transaction, éventuellement null |
CheckoutFailed | Fermée sur un échec, un paiement terminé ou expiré, une erreur de chargement, ou un paiement annulé ailleurs (payment_cancelled) | error (WajubError), transaction |
CheckoutCancelled | « Annuler le paiement », une fermeture avant tout paiement, ou la disparition préalable de la page qui l'a présentée | reason (CancelReason), redirectUrl |
Chaque résultat porte aussi status : complete, processing, failed ou cancelled.
CancelReason vaut payerCancelled, dismissed ou hostGone, et reason.wireName donne
payer_cancelled, dismissed ou host_gone.
PaymentSuccess est celui de Wajub.js : id (l'identifiant Wajub du paiement, 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 (toujours success), timestamp (ISO 8601)
et receiptUrl. toMap() le donne avec les clés de Wajub.js.
Quand la page qui a présenté l'interface disparaît, parce que votre application la retire ou la
remplace, l'interface se règle d'elle-même : cancelled avec hostGone avant tout paiement,
processing une fois un paiement commencé, complete après un succès. Elle n'annule jamais le
paiement dans ce cas. Un presentWajubCheckout dont le context n'est plus monté se résout
aussitôt avec hostGone. Si le processus de l'application meurt, lisez l'issue sur votre serveur,
ou avec WajubSession.status(), au lancement suivant.
Annuler et fermer
Seules les sorties du payeur comptent comme une fermeture : le bouton de fermeture, un glissement vers le bas, le bouton retour et un appui sur le fond. Ce que fait la fermeture dépend de l'état du paiement.
| Fermeture… | Résultat | Appel à Wajub |
|---|---|---|
| Pendant le chargement du paiement | CheckoutCancelled, dismissed | Aucun |
Avant toute tentative, le paiement pending | CheckoutCancelled, dismissed, avec redirectUrl | POST /pay/cancel, sauf si cancelOnDismiss vaut false |
| Pendant que le payeur approuve, compose un code, confirme une étape ou attend | CheckoutProcessing | Aucun : l'argent peut encore circuler |
| Sur l'écran d'échec d'un paiement refusé, son bouton Fermer compris | CheckoutFailed, avec l'erreur que le payeur a vue | POST /pay/cancel aussi, sauf si cancelOnDismiss vaut false. onCancel ne se déclenche pas |
| Après le succès | CheckoutComplete | Aucun |
Dès que la session a répondu, son statut décide : un paiement trouvé processing au chargement
donne processing. Une tentative acceptée par l'API reste non tranchée jusqu'à une réponse
définitive, un refus, un refus signalé par le statut, une étape échouée pour de bon : un échec que
l'interface ne peut pas suivre ensuite, comme unsupported_action, redirect_unavailable ou
3ds_timeout, n'annule jamais le paiement. Après un refus définitif, le paiement est de nouveau
pending et peut être annulé.
« Annuler le paiement » est proposé sur la liste des moyens, sur un formulaire et sur un échec que
le payeur peut retenter, jamais pendant une tentative non tranchée. Il annule le paiement
définitivement et donne CheckoutCancelled avec payerCancelled. Pendant une annulation, Payer,
Fermer, le bouton retour et le glissement vers le bas l'attendent. Une annulation que l'API refuse
affiche ce que dit le statut, ou donne processing quand le statut ne peut pas être lu. La règle
complète figure sur SDKs mobiles.
Apparence
Appearance reprend l'objet d'apparence de Wajub.js, clé pour clé : vos Components web et
l'interface native partagent un seul style. Les clés, les préréglages et les formats de valeur
sont ceux d'Apparence.
const appearance = Appearance(
theme: 'stripe', // presets: stripe, night, flat, none; or light, dark, auto
colorScheme: 'auto', // light, dark or auto, which follows the phone, live
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', // Wajub.js's top-level keys work too
disableAnimations: false,
);
// Or the very object your web checkout uses:
final shared = Appearance.fromMap(jsonDecode(appearanceJson) as Map<String, Object?>);Pour chaque couleur, taille et police, l'interface prend la première valeur définie : vos
variables, vos clés de premier niveau, votre préréglage, le thème de votre application quand
inherit vaut true (seules les couleurs qui restent lisibles sont gardées), l'identité visuelle
du marchand définie dans le Dashboard, puis la palette claire ou sombre par défaut de Wajub.
Appearance.fromMap ignore les clés inconnues, rules et les valeurs du mauvais type, et
l'interface ignore les valeurs illisibles. La pulsation du chargement et les fondus s'arrêtent avec
disableAnimations ou quand le téléphone demande moins d'animations. Le choix du mode clair ou
sombre et ce que complète l'identité visuelle du marchand figurent sur
SDKs mobiles.
Mobile Money
Tous les canaux Mobile Money du paiement forment un seul formulaire, comme sur le checkout web.
- Un sélecteur de pays, qui part de
mobileMoney.country, sinon du pays du payeur quand un canal le couvre.countryLocked: truele masque. - Les opérateurs, une ligne chacun, avec leur logo, ou un badge de marque quand le logo manque ou
ne se charge pas. Les opérateurs d'un agrégat comme
cm.mobilesont listés un par un. - Le numéro avec l'indicatif du pays, mis en forme pendant la saisie. Son préfixe présélectionne l'opérateur tant que le payeur n'en a pas choisi un lui-même, et un choix manuel n'est jamais remplacé.
- Un numéro qui appartient à un autre opérateur proposé est refusé avant tout appel (« Ce numéro est un numéro… »), sauf pour les portefeuilles qui acceptent n'importe quel numéro, comme Wave, et en sandbox, dont les numéros de test ne suivent pas les vrais préfixes.
- Le numéro part au format E.164,
+237677123456. La Côte d'Ivoire garde son 0 initial. Le champoperatorn'est envoyé que pour un agrégat, et seulement quand le payeur a désigné l'opérateur.
Avec operatorMode: OperatorMode.detect, la liste des opérateurs reste masquée et l'opérateur
détecté s'affiche dans le champ du numéro, jusqu'à ce que le numéro ne suffise plus ou que le
payeur ouvre la liste. Après Payer, l'interface affiche « Validez le paiement sur votre
téléphone » avec le numéro et l'instruction du prestataire, ou un code USSD à composer, et suit le
paiement. Les règles communes figurent sur SDKs mobiles.
Cartes
L'entrée du canal carte dans GET /pay/sdk-config décide de la façon dont l'interface prend la
carte. Aucun numéro de carte n'atteint jamais Wajub en live.
| sdk-config | Dans l'interface |
|---|---|
client_session vaut true : Paystack, Flutterwave, Adyen | Les champs de contact qui manquent encore au prestataire, l'e-mail du client prérempli, puis le checkout du prestataire dans le navigateur du système. Vérifié auprès de Wajub au retour du payeur |
stripe_elements | Le CardField de flutter_stripe dans l'interface, avec la clé publiable du paiement. Non proposé quand sdk-config n'envoie pas de clé |
hosted_redirect : CinetPay, Kkiapay, Mollie… | Exactement les champs de contact de required_fields, puis la page du prestataire dans le navigateur du système |
form, en sandbox | Le formulaire de carte de test de Wajub |
| Autre chose | Carte non proposée |
Les pages des prestataires s'ouvrent avec url_launcher comme une application externe, le
navigateur du système, jamais une WebView. Revenir dans l'application lit aussitôt le statut et
termine une session client ouverte ; tant que la page est ouverte, « Continuer le paiement » la
rouvre.
La clé Stripe vient du paiement, car seul le compte Stripe qui a émis un pm_… peut le débiter.
Le formulaire de carte règle la clé globale Stripe.publishableKey de flutter_stripe sur cette
clé. Si votre application encaisse aussi des paiements Stripe avec sa propre clé, passez-la dans
stripe: StripeOptions(appPublishableKey: …) : l'interface la remet en place, et applique de
nouveau les réglages de Stripe, quand elle se ferme après avoir utilisé son formulaire de carte.
Sinon, redéfinissez votre clé vous-même avant votre prochain appel à Stripe.
Une session client peut aussi passer par le SDK natif du prestataire au lieu du navigateur, voir Votre propre interface. Ces SDKs de prestataires ne sont pas inclus.
Étapes de confirmation
Quand un prestataire de carte demande davantage au payeur, 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, et en sandbox le code 3-D Secure envoyé par e-mail. Chaque écran affiche les essais restants et propose « Renvoyer le code » 60 secondes après son ouverture. Une mauvaise réponse reste sur l'écran ; une étape expirée ou trop d'essais mènent à l'écran d'échec, avec « Réessayer ». Les étapes sont listées sur SDKs mobiles.
Erreurs
Chaque échec est une WajubError, une Exception qui porte les champs de Wajub.js et ceux du
mobile. L'interface la transmet à onLoadError ou onError et la place dans CheckoutFailed.
processPayment la renvoie dans un PaymentFailed ; les autres appels de WajubSession la
lèvent.
| Champ | Utilité |
|---|---|
type | Un WajubErrorType : apiError, authenticationError, invalidRequestError, paymentError, rateLimitError. type.wireName donne api_error… |
code | Un code du catalogue, ou le code propre à l'API transmis tel quel |
message | Pour le payeur, dans la langue de l'interface |
declineCode | Sur un 402, le code du prestataire |
retryable | Si une nouvelle tentative peut aboutir |
param | Le champ en cause, quand le refus en désigne exactement un |
httpStatus, correlationId | Le statut HTTP et le X-Request-Id de la réponse : communiquez-le au support |
retryAfterSeconds | Sur rate_limited |
details | Champ, puis premier message, d'après les errors de l'API |
blockReference | Sur shield_blocked |
apiMessage | Le texte anglais de l'API, pour vos journaux |
attemptsRemaining | Une étape de confirmation refusée : les essais restants |
toMap() donne la forme JSON de Wajub.js, avec ses clés en snake_case, localized('fr') la même
erreur avec son message dans une autre langue, et WajubError.fromStep(answer) l'erreur d'une
étape que verifyStep() a refusée.
| Codes | Quand |
|---|---|
session_not_found, session_terminal_expired, session_terminal_cancelled, session_terminal_failed, load_error, ready_timeout, no_payment_method | onLoadError : rien ne peut être payé |
network_error, timeout, server_error, rate_limited, request_failed | Transport |
payment_declined, insufficient_funds et les codes de refus propres au prestataire, merchant_not_verified, shield_blocked, payment_failed, payment_attempt_ended, payment_cancelled, session_expired, unsupported_action, redirect_unavailable… | Le paiement |
invalid_otp, otp_expired, invalid_pin, max_attempts_exceeded, invalid_code, 3ds_timeout… | Étapes de confirmation |
client_session_unavailable, client_session_expired | Cartes hébergées par le prestataire |
sheet_already_presented | Un second presentWajubCheckout pendant qu'une interface est ouverte |
Le catalogue complet, avec le type de chaque code, s'il permet une nouvelle tentative et son message en anglais et en français, figure sur SDKs mobiles.
Votre propre interface
WajubSession donne les appels /pay/* à une application qui dessine ses propres écrans. Elle
envoie les mêmes en-têtes, signaux de l'appareil, délais et nouvelles tentatives que l'interface
de paiement. Le package n'exporte aucun contrôleur sans interface : la machine à états de
l'interface est interne, et vos écrans pilotent eux-mêmes les appels de session.
final session = Wajub.createSession(authorizationToken);
final data = await session.loadSession(); // amount, channels, merchantBranding…
final config = await session.getSdkConfig();
final result = await session.processPayment(
'cm.mobile',
{'phone': '+237677123456', 'country': 'CM', 'operator': 'mtn'},
idempotencyKey: payPressKey, // yours, one per press of Pay
);
switch (result) {
case PaymentRequiresAction(:final step?, :final stepId?):
// confirm_otp, confirm_pin, confirm_phone, confirm_birthday, confirm_address
final answer = await session.verifyStep(step, stepId, {'code': '123456'});
if (!answer.success) showError(WajubError.fromStep(answer).message);
case PaymentRequiresAction(action: ActionKind.pushApproval):
showApproveOnYourPhone();
case PaymentRequiresAction():
await session.handleRedirectAction(result); // a provider page, in the system browser
case PaymentProcessing(kind: ProcessingKind.ussd, :final instruction):
showDialThisCode(instruction);
case PaymentProcessing():
// Pending, or no answer at all (network, timeout, 5xx, payment_in_progress):
// the charge may be running. Follow the status.
showWaiting();
case PaymentComplete():
showPaid();
case PaymentFailed(:final error):
showError(error.message);
}
final subscription = session.watchStatus().listen(
(update) {}, // PaymentProcessing on each change, then PaymentComplete or PaymentFailed
onError: (Object error) {}, // a WajubError when the session is gone: 401, 403, 404, 410
);
// subscription.cancel() in dispose()processPayment() lit la réponse comme le fait l'interface et renvoie chaque réponse comme un
PaymentResult ; seule une session qui ne se charge pas lève une exception. Un refus est un
PaymentFailed avec sa WajubError. Un paiement sans réponse, après une erreur réseau, un délai
dépassé ou un 5xx, ou déjà en cours (409 payment_in_progress), est un PaymentProcessing avec
la transaction de la session : le débit peut être en cours, suivez donc status() ou
watchStatus(). Si le statut indique toujours un simple pending 60 secondes plus tard, appelez
de nouveau processPayment avec la même idempotencyKey : une requête arrivée malgré tout est
rejouée plutôt que débitée deux fois. Donnez une clé par appui sur Payer ; sans clé, chaque appel en
reçoit une nouvelle.
Pour envoyer les appels par votre propre http.Client, un proxy, un épinglage de certificat ou un
test, passez un PayClient :
Wajub.createSession(authorizationToken, client: PayClient(httpClient: myClient)).
| Méthode | Rôle |
|---|---|
loadSession({forceRefresh}) | GET /pay/session, mis en cache après le premier appel |
getSdkConfig() | GET /pay/sdk-config, indexé par slug de canal : sdk, publishableKey, requiredFields, clientSession |
status() | GET /pay/status, un PaymentStatusUpdate. Son reference est l'identifiant Wajub du paiement, pas le vôtre |
processPayment(channel, data, {idempotencyKey}) | Nouveau en 1.6. POST /pay/process, chaque réponse étant une issue : une étape de confirmation est un PaymentRequiresAction avec ActionKind.confirm, step, stepId et hint, un refus un PaymentFailed, une absence de réponse un PaymentProcessing |
verifyStep(kind, stepId, fields), resendStep(kind, stepId) | Nouveau en 1.6. Répondre à une étape avec la clé code, pin, phone ou birthday (YYYY-MM-DD), ou address, city, state et zip_code. Renvoie un StepVerifyResult |
verifyThreeDs(threeDsId, code), sendThreeDs(threeDsId) | Nouveau en 1.6. Le code 3-D Secure de la sandbox, envoyé par e-mail |
startClientSession({channelSlug, email, name, returnUrl, restart, billing}), completeClientSession(id) | Une carte sur le checkout du prestataire |
payCardHostedRedirect({channelSlug, billing, returnUrl}) | Une carte sur la page du prestataire, billing portant chaque champ de requiredFields |
payCard(...), payCardWithStripeElements(...) | Une carte Stripe, depuis un pm_… ou depuis un CardField affiché |
payMobileMoney(input), payWallet(channelSlug:), process(channel, data) | Les appels de 1.5, avec leur lecture de 1.5 : une étape de confirmation y est unsupported_action |
handleRedirectAction(result) | Ouvre la page du résultat dans le navigateur du système |
watchStatus({interval, realtime}) | Un flux de résultats jusqu'à l'issue |
cancel() | POST /pay/cancel, renvoie l'URL de redirection du marchand |
cardChannelSlug() | Le canal carte de la session chargée, s'il existe |
Réglez session.language = 'fr' pour des messages d'erreur en français.
Wajub.initialize(publicKey:) existe toujours sans être nécessaire, et WajubMobile est un alias
obsolète de Wajub.
await session.loadSession();
final slug = session.cardChannelSlug();
final card = slug == null ? null : (await session.getSdkConfig()).channels[slug];
if (card?.clientSession == true) {
final started = await session.startClientSession(channelSlug: slug!, email: 'payer@example.com');
if (started case PaymentRequiresAction(action: ActionKind.clientSession, :final clientSession?)) {
await session.handleRedirectAction(started); // opens clientSession.hostedUrl
// ...once the app is back in the foreground:
final result = await session.completeClientSession(clientSession.id);
}
} else if (card?.sdk == SdkFlavor.hostedRedirect) {
final result = await session.payCardHostedRedirect(
channelSlug: slug!,
billing: {'email': 'payer@example.com'}, // every field of card.requiredFields
);
await session.handleRedirectAction(result);
}Ouvrir la page n'attend pas le payeur : appelez completeClientSession une fois votre application
revenue au premier plan. Il renvoie PaymentComplete, ou PaymentProcessing tant que le
prestataire n'a pas confirmé. Pour utiliser le SDK natif du prestataire au lieu du navigateur,
lancez-le avec les champs de ClientSession, puis terminez de la même façon : Paystack prend
publicKey et accessCode ; Flutterwave prend publicKey, encryptionKey, reference comme
tx_ref, amount et currency.
Confidentialité
Pour évaluer la fraude comme le fait le checkout web, chaque tentative de paiement envoie à Wajub quelques signaux de l'appareil, et chaque appel nomme le SDK :
- le fuseau horaire de l'appareil, sa langue préférée et ses langues, la taille de l'écran principal en pixels et sa profondeur de couleur ;
- le nom et la version du SDK, et le nom et la version du système, dans
X-Wajub-Clientet à la fin duUser-Agent; - une empreinte d'installation : le SHA-256 d'un identifiant aléatoire que le SDK crée à la
première utilisation et conserve avec
shared_preferences, sous la cléwajub_mobile.install_id. Elle ne dérive d'aucun identifiant matériel ou publicitaire, et disparaît quand l'application est désinstallée ou ses données effacées.
Les signaux sont lus en arrière-plan : aucun appel n'attend ces plugins, et les appels envoyés avant leur réponse portent ce que l'application sait sans eux.
Le numéro, l'e-mail et les autres champs saisis par le payeur ne vont à Wajub que pour effectuer
le paiement, et les signaux de l'appareil et l'empreinte d'installation ne vont qu'à
api.wajub.com. En plus de Wajub, l'interface se connecte à :
- Stripe, avec le formulaire de carte Stripe : la carte va du champ de Stripe à Stripe, qui renvoie un jeton que l'interface transmet à Wajub. Le SDK de Stripe collecte ce que Stripe documente pour ses contrôles antifraude.
- L'hôte temps réel indiqué par la session, Pusher ou le Reverb ou Soketi du marchand. Il reçoit la connexion, donc l'adresse IP de l'appareil, et le nom du canal, et envoie le statut du paiement.
- Les hôtes des logos des opérateurs, les URL que donne la session : de simples requêtes d'images.
- Les pages de paiement des prestataires, ouvertes dans le navigateur du système quand le paiement en a besoin.
Ce qu'il faut déclarer dans les stores :
| Formulaire | À déclarer |
|---|---|
| Étiquettes de confidentialité de l'App Store | Identifiers, Device ID pour l'empreinte d'installation, et pour le paiement Contact Info (numéro de téléphone, e-mail) et Financial Info (informations de paiement). Le tout Used for: App Functionality, lié à l'utilisateur quand le paiement l'est, non utilisé pour le suivi |
| Sécurité des données de Google Play | Device or other IDs pour l'empreinte d'installation, Personal info (numéro de téléphone, e-mail) et Financial info (historique d'achats). Collectés pour Fraud prevention, security, and compliance et App functionality, partagés avec Wajub en tant que prestataire de paiement, chiffrés en transit |
C'est ce qu'ajoute ce package ; votre application peut collecter davantage. Les signaux eux-mêmes figurent sur SDKs mobiles.
Migrer depuis 1.5
Le code écrit pour 1.5 compile toujours : aucun type n'a perdu ni gagné de valeur, les points d'entrée obsolètes gardent leur signature, et les appels de 1.5 gardent leur sens. Ce qui change :
showWajubPaymentSheetetPaymentSheetsont obsolètes au profit depresentWajubCheckout. Ils fonctionnent toujours et dessinent désormais la nouvelle interface, qui gère elle-même redirections, approbations et étapes de confirmation.onResultarrive une fois, à la fermeture, avecPaymentComplete,PaymentFailed, ouPaymentProcessingquand elle s'est fermée pendant un paiement, et jamais avecPaymentRequiresAction.onCancel, etPaymentSheet.onDismiss, se déclenchent comme avant quand le payeur a fermé ou annulé, ou que le paiement n'a pas pu se charger.PaymentSheet.onPaymentStartedse déclenche quand le payeur envoie, avant toute réponse. Un paiement déjà payé donnePaymentCompleteau lieu d'afficher un formulaire. Un paiement encore en cours donnePaymentProcessingavec la dernière transaction connue. Comme en 1.5,showWajubPaymentSheetn'envoie jamais/pay/cancelune fois qu'un paiement a été envoyé, et une seconde interface s'ouvre par-dessus celle qui est ouverte au lieu d'être refusée.- Étapes de confirmation. Les appels de 1.5 renvoient toujours
PaymentFailedavecunsupported_actionpour elles.processPayment()les renvoie commePaymentRequiresActionavecActionKind.confirm, sansactionUrl, et les nouveauxstep,stepIdethint;verifyStep()y répond. WajubError. Les erreurs construites à partir des réponses de l'API suivent le catalogue de Wajub.js :messagedans la langue de l'interface ou de l'appareil, le texte de l'API dansapiMessage, un410typéinvalidRequestErrorlà où il étaitauthenticationError, et un code de refus inconnu du catalogue lu comme « Ce paiement n'a pas abouti… ».WajubError.fromHttpgarde sa correspondance de 1.5.SessionTransaction.statusvautunknownquand une réponse ne porte aucun statut ; 1.5 le lisait commepending.MobileMoneyInput.phoneest envoyé tel que vous le donnez : donnez-le au format E.164,+237677123456. L'interface le construit à partir de la saisie du payeur.watchStatus()garde ses résultats,PaymentProcessingà chaque changement, puisPaymentComplete, ouPaymentFaileddont le code est le statut (failed,cancelled,expired). Il lit désormaisGET /pay/statusplutôt que toute la session, suit un Reverb auto-hébergé comme Pusher, se termine avec laWajubErrorsur401,403,404et410, et signale une tentative revenue àpendingsans débit commePaymentFailedavec le code du refus,insufficient_fundspar exemple, au lieu d'attendre indéfiniment.- Dépendances.
pusher_channels_flutterdisparaît. Les nouveaux plugins exigent Java 17,compileSdk35, AndroidminSdk24 et iOS 13 avec les versions que résout un Flutter récent, et les signaux de l'appareil se déclarent dans vos fiches de store, voir Confidentialité.
// 1.5
await showWajubPaymentSheet(context: context, session: session, onResult: handle, onCancel: cancelled);
// 1.6
final result = await presentWajubCheckout(context, PresentOptions(sessionToken: token));Tester
L'origine de l'API est une constante : il n'y a pas d'URL de base à changer. Créez le paiement avec une clé de sandbox et son jeton place tout le parcours en sandbox, interface comprise.
- Les numéros Mobile Money de test sont un préfixe d'opérateur suivi de sept chiffres qui se
terminent par le scénario :
000000succès,000001fonds insuffisants,000002échec,000003délai dépassé,000004annulé, par exemple+237670000000. L'interface ne refuse jamais un numéro de test comme appartenant à un autre opérateur. - Les cartes passent par le formulaire de carte de test.
4000056655665556demande le code 3-D Secure envoyé par e-mail.
Tous les numéros et toutes les cartes figurent sur Scénarios de test.
Pages associées
- SDKs mobilesLe contrat que partagent les quatre.
- Créer un paiementL'appel serveur qui produit le jeton.
- WebhooksLa confirmation qui déclenche la livraison.
- ApparenceL'objet d'apparence que partagent le web et l'interface native.
- React NativeLa même interface, en TypeScript.
- AndroidLa même interface, en Kotlin et Compose.
- iOSLa même interface, en Swift et SwiftUI.