Flutter
Le package wajub_mobile et son interface de paiement prête à l'emploi pour les applications Flutter.
Le SDK Flutter vous permet d'encaisser des paiements Mobile Money, par carte et par portefeuille dans votre application, avec une interface de paiement prête à l'emploi.
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 |
Les autres dépendances du package ne demandent aucun code de votre part.
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 de Stripe l'exige. 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. Le package ne détient jamais de clé secrète.
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 lève 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 fermé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,
),
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. Duration.zero attend indéfiniment |
returnUrl | String? | Un lien universel ou App Link https qui revient dans votre application, pour les pages qui s'ouvrent dans le navigateur. Sans lui, le payeur revient de lui-même dans l'application |
mobileMoney | MobileMoneyOptions | Voir Mobile Money |
cancelOnDismiss | bool, true | Fermer l'interface avant tout paiement annule le paiement. 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, voir Cartes |
callbacks | CheckoutCallbacks | Voir ci-dessous |
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.
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 é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 demande de paiement est acceptée. 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 |
state.wireName donne le nom de l'état : INITIATED, COLLECTING_DETAILS, PROCESSING,
APPROVAL_REQUIRED, USSD_REQUIRED, PAYER_OTP_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.payerOtpRequired,
CheckoutState.threeDsRequired. Ce que voit le payeur dans 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 | L'interface s'est fermée pendant un paiement en cours : l'argent peut encore circuler, confirmez sur votre serveur | 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 porte 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.
L'interface n'affiche jamais d'échec quand l'issue est inconnue : elle affiche « Paiement en
cours » et continue d'attendre, et la fermer à ce moment donne processing. Quand la page qui a
présenté l'interface disparaît, l'interface donne cancelled avec hostGone avant tout paiement,
processing une fois un paiement commencé, complete après un succès, et n'annule jamais le
paiement. Si le processus de l'application meurt, lisez l'issue sur votre serveur, ou avec
WajubSession.status(), au lancement suivant.
Annuler et fermer
Le payeur ferme l'interface avec son bouton de fermeture, un glissement vers le bas, le bouton retour ou un appui en dehors d'elle.
| Fermeture… | Résultat |
|---|---|
| Pendant le chargement du paiement | CheckoutCancelled, dismissed |
| Avant tout paiement | CheckoutCancelled, dismissed. Le paiement est annulé, sauf si cancelOnDismiss vaut false |
| Pendant que le payeur approuve, compose un code, confirme une étape ou attend | CheckoutProcessing : le paiement continue |
| Sur l'écran d'échec | CheckoutFailed, avec l'erreur que le payeur a vue. Le paiement est annulé s'il peut encore l'être, sauf si cancelOnDismiss vaut false. onCancel ne se déclenche pas |
| Après le succès | CheckoutComplete |
« Annuler le paiement » dans l'interface annule le paiement définitivement et donne
CheckoutCancelled avec payerCancelled. Il n'est pas proposé pendant un paiement en cours. Voir
SDKs mobiles.
Apparence
L'option appearance règle l'apparence de l'interface. Les clés, les préréglages et les variables
figurent sur 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
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',
disableAnimations: false,
);
// Or from a map, such as decoded JSON:
final shared = Appearance.fromMap(jsonDecode(appearanceJson) as Map<String, Object?>);Ce que vous laissez vide vient de l'identité visuelle du marchand définie dans le Dashboard, puis de la palette claire ou sombre par défaut de Wajub. Voir SDKs mobiles.
Mobile Money
Un seul formulaire couvre tous les opérateurs Mobile Money du paiement : le pays, les opérateurs, et le numéro avec l'indicatif du pays, mis en forme pendant la saisie. Le numéro présélectionne son opérateur tant que le payeur n'en a pas choisi un lui-même. Après Payer, l'interface affiche « Validez le paiement sur votre téléphone », ou un code USSD à composer.
MobileMoneyOptions | Défaut | Rôle |
|---|---|---|
country | null | Le pays de départ, ISO-2. Sans lui, le pays du payeur quand un canal Mobile Money le couvre |
countryLocked | false | Masque le sélecteur de pays |
operatorMode | OperatorMode.choose | OperatorMode.choose liste les opérateurs. OperatorMode.detect affiche l'opérateur du numéro dans le champ du numéro, et la liste seulement quand le numéro ne suffit pas ou que le payeur l'ouvre |
Voir SDKs mobiles.
Cartes
Le formulaire de carte de Stripe nécessite FlutterFragmentActivity sur Android, voir
Installer. Si votre application encaisse aussi ses propres paiements Stripe avec
flutter_stripe, passez la clé de votre application dans
stripe: StripeOptions(appPublishableKey: …) pour que vos propres paiements Stripe continuent de
fonctionner.
Les autres cartes ouvrent une page sécurisée du prestataire de paiement dans le navigateur du système, et en sandbox l'interface affiche un formulaire de carte de test. Voir SDKs mobiles.
Étapes de confirmation
Quand un paiement demande une vérification de plus, l'interface l'affiche sur son propre écran : 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. Une mauvaise réponse reste sur l'écran avec les essais restants ; une étape expirée ou trop d'essais mènent à l'écran d'échec, avec « Réessayer ». Voir SDKs mobiles.
Erreurs
Chaque échec est une WajubError, une Exception. L'interface la transmet à onLoadError ou
onError et la place dans CheckoutFailed.
| Champ | Utilité |
|---|---|
type | Un WajubErrorType : apiError, authenticationError, invalidRequestError, paymentError, rateLimitError |
code | Le code sur lequel brancher votre logique |
message | Pour le payeur, dans la langue de l'interface |
declineCode | Sur un refus, le code de son motif |
retryable | Si une nouvelle tentative peut aboutir |
param | Le champ en cause, quand il y en a un |
correlationId | L'identifiant de requête à communiquer au support de Wajub |
retryAfterSeconds | Sur rate_limited, le temps d'attente |
blockReference | Sur shield_blocked, la référence à communiquer au support de Wajub |
Les codes utiles à traiter : insufficient_funds, payment_declined, payment_attempt_ended
(refusé, rien n'a été débité), payment_failed, payment_cancelled, session_expired,
shield_blocked, merchant_restricted, rate_limited, session_terminal_expired,
unsupported_action, redirect_unavailable, et sheet_already_presented pour
presentWajubCheckout. La liste complète figure sur SDKs mobiles.
Votre propre interface
L'interface de paiement est l'intégration la plus simple. Une application qui dessine ses propres
écrans de paiement utilise WajubSession, le client de plus bas niveau, créée avec
Wajub.createSession(authorizationToken) et décrite dans la référence API du package sur
pub.dev.
Confidentialité
Pour effectuer le paiement, le SDK envoie à Wajub ce que saisit le payeur. Pour la prévention de la fraude, il envoie aussi des informations de base sur l'appareil et un identifiant d'installation aléatoire, qui n'est pas l'identifiant publicitaire. Ce qu'il faut déclarer dans les étiquettes de confidentialité de l'App Store et dans le formulaire de sécurité des données de Google Play, catégorie par catégorie, est sur SDKs mobiles.
Tester
Créez le paiement avec une clé de sandbox : tout le parcours se déroule en sandbox, interface comprise, et l'interface affiche un badge « Mode test ». Les numéros et les cartes de test figurent sur Scénarios de test.
Pages associées
- SDKs mobilesCe que voit le payeur, les résultats et les codes d'erreur.
- Créer un paiementL'appel serveur qui produit le jeton.
- WebhooksLa confirmation qui déclenche la livraison.
- ApparenceLes clés, préréglages et valeurs de l'objet d'apparence.
- React NativeLe SDK pour les applications React Native.
- AndroidLe SDK pour les applications Kotlin et Compose.
- iOSLe SDK pour les applications Swift et SwiftUI.