Aller au contenu

Flutter

Le package wajub_mobile, une interface de paiement native et aucune WebView.

wajub_mobile encaisse les paiements dans votre application Flutter. Les champs Mobile Money sont des widgets Flutter natifs, les champs de carte proviennent de flutter_stripe et le payeur ne voit jamais de WebView.

wajub_mobile

Stable · GA

pub.dev

Version
1.1.1
Runtime
Dart 3.5+, Flutter with Material
Frameworks
Android and iOS

Couvre

  • Payments
  • Mobile Money
  • Cards

Installer

Ajouter la dépendance
flutter pub add wajub_mobile

Il installe aussi flutter_stripe, pusher_channels_flutter, url_launcher, http et meta. La dépendance Stripe est obligatoire. Vous devez donc respecter ses prérequis de plateforme.

PlateformePrérequis de flutter_stripe 11.4
AndroidminSdkVersion 21, Kotlin 1.8 ou version ultérieure, Android Gradle Plugin 8 ou version ultérieure
AndroidMainActivity étend FlutterFragmentActivity avec un descendant de Theme.AppCompat
iOSCible de déploiement 13.0 ou version ultérieure
android/app/src/main/kotlin/.../MainActivity.kt
import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()

Obtenir un jeton depuis votre serveur

Aucun élément de ce package n'appelle /payments ni ne détient de clé secrète. Votre backend crée le paiement et renvoie authorization_token.

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 token = jsonDecode(response.body)['authorization_token'] as String;

Afficher l'interface de paiement

Deux lignes suffisent. L'interface charge la session, liste les opérateurs, collecte le numéro de téléphone ou la carte et traite le paiement.

Toute l'intégration
import 'package:wajub_mobile/wajub_mobile.dart';

Future<void> pay(BuildContext context, String token) async {
  final session = Wajub.createSession(token);

  await showWajubPaymentSheet(
    context: context,
    session: session,
    onResult: (result) {
      switch (result) {
        case PaymentComplete(:final transaction):
          context.go('/orders/${transaction.reference}');
        case PaymentProcessing(:final instruction):
          showDialog(context: context, builder: (_) => AwaitingApproval(instruction));
        case PaymentRequiresAction():
          // The sheet already opened the browser for you.
          break;
        case PaymentFailed(:final error):
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(error.message)),
          );
      }
    },
  );
}

PaymentResult est une classe scellée. Le switch ci-dessus est donc exhaustif et le compilateur signale tout cas manquant. Utilisez-le plutôt que de lire une chaîne de statut.

CasRésultat
PaymentCompletePaiement réglé. transaction contient la référence
PaymentProcessingEnvoyé à l'opérateur. instruction contient le texte à afficher au payeur
PaymentRequiresAction3DS ou redirection bancaire. L'interface a déjà ouvert le navigateur
PaymentFailederror contient code, declineCode et retryable

Processing est le résultat normal pour Mobile Money

Un paiement Mobile Money renvoie rarement PaymentComplete depuis l'interface. Le payeur doit encore approuver sur son téléphone. Vous recevez donc PaymentProcessing avec une instruction, comme composer un code USSD. Affichez ce texte, puis attendez watchStatus ou votre webhook.

Suivre le résultat

Un statut en direct pendant l'approbation du payeur
final subscription = session.watchStatus().listen((result) {
  if (result is PaymentComplete) {
    setState(() => _state = OrderState.paid);
  }
  if (result is PaymentFailed) {
    setState(() => _state = OrderState.failed);
  }
});

@override
void dispose() {
  subscription.cancel();
  super.dispose();
}

watchStatus s'abonne avec Pusher lorsque la session contient des paramètres de temps réel. Sinon, il se replie sur un polling toutes les cinq secondes. Vous n'avez rien à choisir, mais devez annuler l'abonnement dans dispose.

Vos propres écrans à la place de l'interface

WajubSession représente toute l'API sous l'interface de paiement. Utilisez-la si le paiement doit s'intégrer à un design existant.

Lire la session, puis payer
final session = Wajub.createSession(token);
final data = await session.loadSession();

// data.transaction.amount, data.transaction.currency, data.branding, data.locale
final operators = data.channels
    .where((c) => const {'mobile_money', 'mobile'}.contains(c.type.toLowerCase()))
    .toList();

final result = await session.payMobileMoney(
  MobileMoneyInput(
    channelSlug: operators.first.slug,
    phone: '+237670000000',
    country: 'CM',
  ),
);
MéthodeFonction
loadSession({forceRefresh})Montant, devise, canaux, image de marque et locale. Mis en cache après le premier appel
getSdkConfig()Paramètres du prestataire par canal, dont la clé publique Stripe
payMobileMoney(input)Envoie le push à l'opérateur
payCard(...)Traite une PaymentMethod Stripe déjà créée
payCardWithStripeElements(...)Initialise Stripe et crée le moyen de paiement en un appel
process(channel, data)Solution brute pour tous les canaux
handleRedirectAction(result)Ouvre une URL 3DS ou bancaire dans le navigateur système
watchStatus({interval})Flux de résultats en temps réel ou par polling
cancel()Abandonne la session et renvoie l'URL de redirection
cardChannelSlug()Canal de carte de la session chargée, s'il existe

Cartes sans l'interface de paiement

La clé publique provient de la session, pas de votre code, car elle diffère entre la sandbox et le mode live, ainsi qu'entre les marchands.

Stripe piloté par la session
final session = Wajub.createSession(token);
await session.loadSession();

final config = await session.getSdkConfig();
final slug = session.cardChannelSlug();
final publishableKey = config.channels[slug]?.publishableKey;

if (slug != null && publishableKey != null) {
  final result = await session.payCardWithStripeElements(
    channelSlug: slug,
    publishableKey: publishableKey,
    cardholderName: 'Amina Diallo',
  );

  await session.handleRedirectAction(result); // opens 3DS if needed
}

Erreurs

Chaque échec est une WajubError
try {
  await session.payMobileMoney(input);
} on WajubError catch (error) {
  if (error.type == WajubErrorType.rateLimitError) {
    showRetryIn(error.retryAfterSeconds ?? 60);
    return;
  }
  if (error.retryable) {
    showRetryButton(error.message);
    return;
  }
  showFatal(error.message, error.correlationId);
}
ChampUtilité
typeapiError, authenticationError, invalidRequestError, paymentError ou rateLimitError
codeMotif lisible par la machine
declineCodePrésent sur une réponse 402, motif du refus du prestataire
retryableIndique si une nouvelle tentative doit être proposée
paramChamp fautif lors d'une erreur de validation
correlationIdValeur à communiquer au support
retryAfterSecondsAttente imposée par une limite de requêtes

Deux noms à connaître

Wajub.initialize(publicKey:) enregistre une clé pk. pour les helpers Stripe. Cette opération est facultative et l'interface n'en a pas besoin, car la clé publique provient de getSdkConfig().

WajubMobile est un ancien alias

Les anciens exemples appellent WajubMobile.createSession(...). WajubMobile est maintenant un typedef de Wajub et compile encore, mais le nouveau code doit utiliser Wajub.

Tests

L'origine de l'API est une constante. Vous ne pouvez donc pas rediriger son URL de base. Configurez plutôt une clé de sandbox sur votre backend. Le jeton généré place tout le flux dans la sandbox, interface comprise. Les numéros qui produisent chaque résultat figurent dans Scénarios de test.

Que pensez-vous de ce contenu ?