Aller au contenu

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 · GA

pub.dev

Version
1.1.1
Runtime
Flutter 3.24+ (Dart 3.5+)
Frameworks
Android and iOS

Couvre

  • Payments
  • Mobile Money
  • Cards

Installer

Ajouter la dépendance
flutter pub add wajub_mobile

Ou fixez-la dans pubspec.yaml avec wajub_mobile: ^1.6.0.

PrérequisVersion
Flutter3.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 AndroidJava 17, compileSdk 35 ou ultérieur, minSdk 24
iOSCible 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.

pubspec.yaml, sur Flutter 3.24 à 3.26 uniquement
dependency_overrides:
  flutter_stripe: 11.4.0
  stripe_android: 11.4.0
  stripe_ios: 11.4.0
  stripe_platform_interface: 11.4.0
android/app/src/main/kotlin/.../MainActivity.kt
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

Votre endpoint et votre client
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.

Toute l'intégration
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, tous les champs
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)),
)
ChampType, défautRôle
sessionTokenString, obligatoireL'authorization_token du paiement
localeString?'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
appearanceAppearance?L'apparence de l'interface, voir Apparence
loaderWajubLoader, skeletonWajubLoader.skeleton, des blocs de substitution aux couleurs de l'interface, ou WajubLoader.spinner
readyTimeoutDuration, 15 sAu-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
returnUrlString?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
mobileMoneyMobileMoneyOptionscountry (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)
realtimeRealtimeOptionshost, 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
cancelOnDismissbool, trueUne fermeture avant tout paiement l'annule chez Wajub. false le laisse ouvert pour que vous puissiez le présenter à nouveau
autoCloseSuccessDuration, 3 sLa durée d'affichage de l'écran de succès. Duration.zero le garde jusqu'à ce que le payeur le ferme
stripeStripeOptionsappPublishableKey, la clé Stripe propre à votre application, remise en place quand l'interface se ferme après son formulaire de carte. Voir Cartes
callbacksCheckoutCallbacksVoir 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
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
)
CallbackDéclenchement
onReadyUne 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
onStateChangeAprès onReady, puis à chaque changement. method vaut mobile_money, card, wallet, ou unknown avant tout choix
onLoadErrorLe 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
onPaymentStartedLa première réponse acceptée d'une tentative. Dès lors, fermer l'interface donne processing
onRedirectJuste avant qu'une page de prestataire s'ouvre dans le navigateur
onSuccessPaiement réussi. method vaut unknown quand le paiement était déjà payé à l'ouverture
onErrorChaque échec que voit le payeur, sauf la validation des champs
onCancelL'interface se ferme comme cancelled
onCompleteL'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

CheckoutResultQuandContenu
CheckoutCompleteL'interface s'est fermée après le succès, y compris pour un paiement déjà payé à l'ouverturepayment (PaymentSuccess), transaction
CheckoutProcessingFermé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éphonetransaction, éventuellement null
CheckoutFailedFermé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éereason (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ésultatAppel à Wajub
Pendant le chargement du paiementCheckoutCancelled, dismissedAucun
Avant toute tentative, le paiement pendingCheckoutCancelled, dismissed, avec redirectUrlPOST /pay/cancel, sauf si cancelOnDismiss vaut false
Pendant que le payeur approuve, compose un code, confirme une étape ou attendCheckoutProcessingAucun : l'argent peut encore circuler
Sur l'écran d'échec d'un paiement refusé, son bouton Fermer comprisCheckoutFailed, avec l'erreur que le payeur a vuePOST /pay/cancel aussi, sauf si cancelOnDismiss vaut false. onCancel ne se déclenche pas
Après le succèsCheckoutCompleteAucun

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.

Appearance
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: true le 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.mobile sont 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 champ operator n'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-configDans l'interface
client_session vaut true : Paystack, Flutterwave, AdyenLes 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_elementsLe 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 sandboxLe formulaire de carte de test de Wajub
Autre choseCarte 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.

ChampUtilité
typeUn WajubErrorType : apiError, authenticationError, invalidRequestError, paymentError, rateLimitError. type.wireName donne api_error…
codeUn code du catalogue, ou le code propre à l'API transmis tel quel
messagePour le payeur, dans la langue de l'interface
declineCodeSur un 402, le code du prestataire
retryableSi une nouvelle tentative peut aboutir
paramLe champ en cause, quand le refus en désigne exactement un
httpStatus, correlationIdLe statut HTTP et le X-Request-Id de la réponse : communiquez-le au support
retryAfterSecondsSur rate_limited
detailsChamp, puis premier message, d'après les errors de l'API
blockReferenceSur shield_blocked
apiMessageLe texte anglais de l'API, pour vos journaux
attemptsRemainingUne é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.

CodesQuand
session_not_found, session_terminal_expired, session_terminal_cancelled, session_terminal_failed, load_error, ready_timeout, no_payment_methodonLoadError : rien ne peut être payé
network_error, timeout, server_error, rate_limited, request_failedTransport
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_expiredCartes hébergées par le prestataire
sheet_already_presentedUn 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.

Payer, répondre à une étape, suivre le statut
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éthodeRô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.

Une carte que le prestataire collecte lui-même
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-Client et à la fin du User-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 StoreIdentifiers, 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 PlayDevice 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 :

  • showWajubPaymentSheet et PaymentSheet sont obsolètes au profit de presentWajubCheckout. Ils fonctionnent toujours et dessinent désormais la nouvelle interface, qui gère elle-même redirections, approbations et étapes de confirmation. onResult arrive une fois, à la fermeture, avec PaymentComplete, PaymentFailed, ou PaymentProcessing quand elle s'est fermée pendant un paiement, et jamais avec PaymentRequiresAction. onCancel, et PaymentSheet.onDismiss, se déclenchent comme avant quand le payeur a fermé ou annulé, ou que le paiement n'a pas pu se charger. PaymentSheet.onPaymentStarted se déclenche quand le payeur envoie, avant toute réponse. Un paiement déjà payé donne PaymentComplete au lieu d'afficher un formulaire. Un paiement encore en cours donne PaymentProcessing avec la dernière transaction connue. Comme en 1.5, showWajubPaymentSheet n'envoie jamais /pay/cancel une 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 PaymentFailed avec unsupported_action pour elles. processPayment() les renvoie comme PaymentRequiresAction avec ActionKind.confirm, sans actionUrl, et les nouveaux step, stepId et hint ; verifyStep() y répond.
  • WajubError. Les erreurs construites à partir des réponses de l'API suivent le catalogue de Wajub.js : message dans la langue de l'interface ou de l'appareil, le texte de l'API dans apiMessage, un 410 typé invalidRequestError là où il était authenticationError, et un code de refus inconnu du catalogue lu comme « Ce paiement n'a pas abouti… ». WajubError.fromHttp garde sa correspondance de 1.5.
  • SessionTransaction.status vaut unknown quand une réponse ne porte aucun statut ; 1.5 le lisait comme pending.
  • MobileMoneyInput.phone est 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, puis PaymentComplete, ou PaymentFailed dont le code est le statut (failed, cancelled, expired). Il lit désormais GET /pay/status plutôt que toute la session, suit un Reverb auto-hébergé comme Pusher, se termine avec la WajubError sur 401, 403, 404 et 410, et signale une tentative revenue à pending sans débit comme PaymentFailed avec le code du refus, insufficient_funds par exemple, au lieu d'attendre indéfiniment.
  • Dépendances. pusher_channels_flutter disparaît. Les nouveaux plugins exigent Java 17, compileSdk 35, Android minSdk 24 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, puis 1.6
// 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 : 000000 succès, 000001 fonds insuffisants, 000002 échec, 000003 délai dépassé, 000004 annulé, 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. 4000056655665556 demande le code 3-D Secure envoyé par e-mail.

Tous les numéros et toutes les cartes figurent sur Scénarios de test.

Que pensez-vous de ce contenu ?