Aller au contenu

Android (Kotlin)

Encaissez des paiements Mobile Money, par carte et par portefeuille dans votre application Android avec une interface de paiement Compose prête à l'emploi.

Le SDK Android 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 construite avec Jetpack Compose.

com.wajub:wajub-mobile-compose

Stable · GA

Maven Central

Version
1.2.1
Runtime
Android 7.0 (API 24)+, JVM target 17
Frameworks
Jetpack Compose with Material 3

Couvre

  • Paiements
  • Mobile Money
  • Cartes

Installer

build.gradle.kts
dependencies {
    implementation("com.wajub:wajub-mobile-compose:1.3.0")
}
ArtefactUsage
com.wajub:wajub-mobile-composeLe composable PaymentSheet. Il apporte wajub-mobile-core avec lui
com.wajub:wajub-mobile-coreLe même parcours de paiement sans interface, pour une application qui dessine ses propres écrans, voir Votre propre interface
PrérequisValeur
minSdk24, soit Android 7.0
compileSdk35. La bibliothèque ne fixe aucun targetSdk : votre application garde le sien
Cible Java et Kotlin17
ComposeMaterial 3 1.3 (BOM 2024.12.01) ou 1.4 (BOM 2025.09.01)
Apportestripe-android 21.2.0, OkHttp 4.12.0, coroutines Kotlin 1.9.0

Rien à ajouter à votre manifeste ni à vos règles R8 : les artefacts apportent les leurs.

Démarrage rapide

Trois étapes, dont seule la deuxième s'exécute dans l'application.

  1. Votre serveur crée le paiement avec POST /payments et sa clé secrète, puis transmet authorization_token à l'application. La clé secrète n'entre jamais dans l'application.
  2. L'application affiche PaymentSheet avec ce jeton. onComplete reçoit le résultat une seule fois, à la fermeture de l'interface.
  3. Votre serveur confirme le paiement, avec le webhook payment.succeeded ou GET /payments/{id}, avant de livrer. Le résultat dans l'application est ce que le payeur a vu, pas une preuve de paiement.
Toute l'intégration
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import com.wajub.mobile.checkout.CheckoutResult
import com.wajub.mobile.checkout.PresentOptions
import com.wajub.mobile.ui.PaymentSheet

@Composable
fun Checkout(authorizationToken: String) {
    var paying by rememberSaveable { mutableStateOf(true) }

    if (paying) {
        PaymentSheet(PresentOptions(sessionToken = authorizationToken)) { result ->
            paying = false
            when (result) {
                is CheckoutResult.Complete -> showReceipt(result.payment) // confirm on your server first
                is CheckoutResult.Processing -> showPending()              // money may still move
                is CheckoutResult.Failed -> showError(result.error.message)
                is CheckoutResult.Cancelled -> Unit                        // result.reason, result.redirectUrl
            }
        }
    }
}

CheckoutResult est une classe scellée : le when est exhaustif sans else. L'interface résiste aux changements de configuration, comme une rotation ou un changement de langue. Afficher une deuxième PaymentSheet pendant qu'une autre est ouverte règle la deuxième aussitôt avec Failed et le code sheet_already_presented, sans toucher à la première.

Options

PresentOptions est une data class de com.wajub.mobile.checkout. Seul sessionToken doit être fourni.

OptionType, défautEffet
sessionTokenStringL'authorization_token de votre serveur
localeString?, null"fr" ou "en". Sans elle, la langue du paiement, sinon celle de l'appareil
appearanceAppearance?, nullL'apparence de l'interface, voir Apparence
loaderLoaderStyle, SkeletonSkeleton, des blocs de substitution animés, ou Spinner
readyTimeoutLong en millisecondes, 15_000Une interface encore en chargement passé ce délai affiche une erreur avec Réessayer et signale ready_timeout à onLoadError. 0 le désactive
returnUrlString?, nullUn App Link https qui ramène dans votre application, où le payeur arrive après la page d'un prestataire de paiement. Sans lui, le payeur revient dans l'application par lui-même
mobileMoneyMobileMoneyOptionsLe formulaire Mobile Money, voir Mobile Money
cancelOnDismissBoolean, trueFermer l'interface avant toute tentative de paiement annule le paiement. false le laisse ouvert pour afficher l'interface à nouveau, voir Annuler et fermer
autoCloseSuccessLong en millisecondes, 3_000L'écran de succès se ferme seul passé ce délai. 0 le garde jusqu'à ce que le payeur touche Terminé
callbacksCheckoutCallbacksVoir Callbacks et états

Callbacks et états

CheckoutCallbacks prend neuf lambdas facultatives. Elles s'exécutent sur le thread principal, et un callback qui lève une exception est journalisé sans casser l'interface.

Tous les callbacks
PresentOptions(
    sessionToken = token,
    callbacks = CheckoutCallbacks(
        onReady = { event -> },          // ReadyEvent: sessionId, environment, amount, currency, methods
        onStateChange = { event -> },    // StateChangeEvent: state, method
        onLoadError = { error -> },      // WajubError: the sheet cannot offer a payment
        onPaymentStarted = { event -> }, // PaymentStartedEvent: method, channel
        onRedirect = { event -> },       // RedirectEvent: url, right before Custom Tabs opens
        onSuccess = { payment -> },      // PaymentSuccess
        onError = { error -> },          // WajubError: every failure the payer sees
        onCancel = { event -> },         // CancelEvent: reason, redirectUrl
        onComplete = { result -> },      // the CheckoutResult PaymentSheet's onComplete receives
    ),
)
CallbackDéclenchement
onReadyUne fois, quand le premier écran s'affiche, y compris pour un paiement déjà payé ou terminé. environment vaut "sandbox" ou "live", methods les moyens de paiement affichés
onStateChangeAprès onReady, puis à chaque changement d'état. method vaut mobile_money, card, wallet ou unknown
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 requête de paiement est acceptée. À partir de là, fermer l'interface donne Processing
onRedirectJuste avant l'ouverture de la page d'un prestataire de paiement
onSuccessLe paiement a réussi. method vaut unknown quand il était payé avant l'ouverture de l'interface
onErrorChaque échec que voit le payeur, y compris ceux qu'il peut retenter, hors erreurs de champ
onCancelL'interface se ferme sur Cancelled
onCompleteUne seule fois, à la fermeture de l'interface

StateChangeEvent.state est un CheckoutState. Sa propriété value donne le nom de l'état en chaîne, ce qui compte pour l'un d'eux : la constante est THREE_DS_REQUIRED et sa valeur 3DS_REQUIRED.

CheckoutStateÉcran
INITIATED, COLLECTING_DETAILS, PROCESSINGLa liste des moyens, le formulaire d'un moyen, le formulaire avec Payer occupé
APPROVAL_REQUIRED, USSD_REQUIRED« Validez le paiement sur votre téléphone », ou un code USSD à composer
PAYER_OTP_REQUIRED« Générez votre code de paiement » : comment obtenir le code que demande l'opérateur, puis un champ pour le saisir
OTP_REQUIRED, PIN_REQUIRED, PHONE_REQUIRED, BIRTHDAY_REQUIRED, ADDRESS_REQUIRED, THREE_DS_REQUIRED, VERIFYINGUne étape de confirmation, puis sa vérification
PENDING« Paiement en cours », avec Continuer le paiement quand la page d'un prestataire peut être rouverte
SUCCESS, FAILED, EXPIRED, CANCELLEDLes issues, avec Réessayer sur un échec qui le permet

Ce que voit le payeur dans chaque état est sur SDKs mobiles.

Résultats

CheckoutResultQuandContenu
CompleteL'interface s'est fermée après un succès, y compris quand le paiement était déjà payé à l'ouverturepayment: PaymentSuccess, transaction
ProcessingL'interface s'est fermée pendant un paiement en cours. L'argent peut encore bouger : confirmez sur votre serveurtransaction, peut être null
FailedL'interface s'est fermée sur un échec, un paiement expiré, une erreur de chargement, ou un paiement annulé ailleurs (payment_cancelled)error: WajubError, transaction
CancelledAnnuler le paiement, une fermeture avant toute tentative de paiement, ou votre écran qui disparaît avantreason: CancelReason, redirectUrl

Chaque résultat porte aussi status, son nom en chaîne : complete, processing, failed, cancelled. CancelReason vaut PayerCancelled, Dismissed ou HostGone, avec sa chaîne dans value.

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 tant que l'issue n'est pas connue : elle affiche « Paiement en cours » et continue d'attendre, et la fermer à ce moment donne Processing. Si le processus de votre application meurt, aucun résultat n'arrive : au lancement suivant, interrogez votre serveur.

Annuler et fermer

Le payeur ferme l'interface avec son bouton de fermeture, en la faisant glisser vers le bas, avec le bouton Retour, ou en touchant à l'extérieur.

Quand le payeur la fermeRésultatLe paiement
Pendant le chargement du paiementCancelled, DismissedInchangé
Avant toute tentative de paiement, ou de retour sur le formulaire après un refus ou RéessayerCancelled, DismissedAnnulé, sauf avec cancelOnDismiss = false
Pendant un paiement en cours : approbation, code USSD, étape de confirmation, « Paiement en cours »ProcessingContinue
Sur l'écran d'échecFailed, avec l'erreur affichée. onCancel n'est pas appeléAnnulé s'il peut encore l'être, sauf avec cancelOnDismiss = false

Annuler le paiement, proposé sur la liste des moyens et sur un formulaire, annule le paiement pour de bon et donne Cancelled avec PayerCancelled et le redirectUrl du marchand.

Votre application qui retire le composable, ou termine son activité, n'est pas le payeur qui ferme l'interface : elle se règle sur Cancelled avec HostGone avant toute tentative de paiement, sur Processing une fois une tentative lancée, sur Complete après un succès, et n'annule jamais le paiement. Voir SDKs mobiles.

Apparence

L'option appearance règle l'apparence de l'interface. Ses clés, ses presets et ses variables sont sur Apparence.

Apparence
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import com.wajub.mobile.checkout.Appearance
import com.wajub.mobile.checkout.AppearanceVariables
import com.wajub.mobile.ui.WajubFonts

// Once, before showing the sheet (in Application.onCreate, say): a font your app ships.
WajubFonts.register(
    "Poppins",
    FontFamily(Font(R.font.poppins_regular), Font(R.font.poppins_semibold, FontWeight.SemiBold)),
)

PresentOptions(
    sessionToken = token,
    appearance = Appearance(
        theme = "stripe",                 // a preset: stripe, night, flat, none, or a scheme: light, dark, auto
        colorScheme = "auto",             // follows the phone
        variables = AppearanceVariables(
            colorPrimary = "#0F766E",
            borderRadius = "12px",
            fontFamily = "Poppins",          // the name registered above
        ),
        buttonTextColor = "#FFFFFF",      // top-level keys work too
        inherit = true,                   // your MaterialTheme fills what you leave unset
    ),
)
  • Chaque valeur est une String. Les couleurs s'écrivent #rgb, #rrggbb, #rrggbbaa, rgb() ou rgba() ; les longueurs "8px", "8" ou "0.5rem". Une valeur illisible est ignorée.
  • inherit = true prend dans le MaterialTheme de votre application ce que vous laissez vide.
  • Une fontFamily n'atteint l'interface qu'une fois enregistrée par votre application avec WajubFonts.register(name, family) ; unregister(name) la retire.
  • Le logo, le nom commercial et les couleurs du marchand dans le Dashboard complètent ce que vous laissez vide, et un marchand en marque blanche masque « Sécurisé par Wajub ».
  • disableAnimations = true coupe les animations de l'interface.

Mobile Money

Le payeur choisit le pays et l'opérateur, et saisit le numéro avec l'indicatif du pays. Après Payer, l'interface affiche « Validez le paiement sur votre téléphone » avec l'instruction de l'opérateur jusqu'à l'issue du paiement.

MobileMoneyOptionsDéfautEffet
countrynullLe pays de départ, ISO-2
countryLockedfalseMasque le sélecteur de pays quand country est fourni
operatorModeOperatorMode.ChooseChoose liste les opérateurs. Detect affiche dans le champ l'opérateur déduit du numéro, et la liste seulement quand le numéro ne suffit pas
Démarrer sur la Côte d'Ivoire
PresentOptions(
    sessionToken = token,
    mobileMoney = MobileMoneyOptions(country = "CI", countryLocked = true, operatorMode = OperatorMode.Detect),
)

Pour votre propre formulaire, MobileMoney vérifie, met en forme et lit un numéro : isValid, formatAsYouType, detectOperator, et data() pour les pays et leurs indicatifs.

Cartes

Les cartes ne demandent aucune configuration dans votre application. Selon le paiement, le payeur saisit sa carte dans l'interface ou sur une page sécurisée du prestataire de paiement, ouverte dans Custom Tabs ; Continuer le paiement rouvre cette page si le payeur revient avant de payer. En sandbox, l'interface affiche un formulaire de carte de test.

Étapes de confirmation

Certains paiements demandent une information de plus au payeur après Payer. Chaque étape a son écran et affiche les essais restants après une réponse erronée.

ÉtapeÉtat
Un code envoyé par SMS ou par e-mailOTP_REQUIRED
Le code PIN de la cartePIN_REQUIRED
Un numéro de téléphonePHONE_REQUIRED
Une date de naissanceBIRTHDAY_REQUIRED
Une adresse de facturationADDRESS_REQUIRED
En sandbox, le code 3-D Secure envoyé par e-mailTHREE_DS_REQUIRED

Une réponse erronée avec des essais restants reste sur l'étape. Trop d'essais, ou une étape expirée, met fin à la tentative, et le payeur peut recommencer. Voir SDKs mobiles.

Erreurs

WajubError étend Exception. L'interface la transmet à onLoadError, à onError et à CheckoutResult.Failed.

PropriétéUsage
typeWajubErrorType : ApiError, AuthenticationError, InvalidRequestError, PaymentError, RateLimitError
codeCe qui n'a pas marché, insufficient_funds par exemple
messagePour le payeur, dans la langue de l'interface
declineCodeSur un refus, le motif donné
retryableSi Réessayer peut aboutir
paramLe champ en cause
correlationIdL'identifiant de requête à communiquer au support Wajub
retryAfterSecondsL'attente sur rate_limited
blockReferenceSur shield_blocked, la référence à communiquer au support

Codes que vous rencontrerez : 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, sheet_already_presented. ErrorCatalog.message(code, WajubLanguage.Fr, null) donne le message d'un code en anglais ou en français. Tous les codes sont sur SDKs mobiles.

Votre propre interface

CheckoutController, dans wajub-mobile-core, est toute l'interface de paiement sans son UI : il mène le paiement, appelle les mêmes callbacks et se termine par le même CheckoutResult, pendant que vous dessinez les écrans.

Vos écrans, le parcours de paiement du SDK
import com.wajub.mobile.WajubMobile
import com.wajub.mobile.checkout.CheckoutController
import com.wajub.mobile.checkout.PresentOptions
import com.wajub.mobile.checkout.SheetScreen
import com.wajub.mobile.ui.HostedRedirectLauncher

// In your ViewModel. token comes from your server, appContext is your Application.
val controller = CheckoutController(
    WajubMobile.createSession(token),
    PresentOptions(sessionToken = token),
    viewModelScope,
).apply {
    urlOpener = { url -> HostedRedirectLauncher.open(appContext, url) } // opens Custom Tabs
    onFinished = { result -> /* hide your screens, then handle the result */ }
    start()
}

// In your composable:
val screen by controller.screen.collectAsState()
when (val s = screen) {
    SheetScreen.Loading -> MyLoader()
    is SheetScreen.Methods -> MyMethodList(s.methods, onPick = controller::selectMethod)
    is SheetScreen.Form -> MyForm(s, onPhone = controller::setPhone, onPay = controller::payMobileMoney)
    is SheetScreen.Success -> MyReceipt(s.payment)
    else -> MyStatus(s) // approval, steps, "Payment in progress", failures…
}
  • Transmettez les actions du payeur : selectMethod, setCountry, setPhone, pickOperator, payMobileMoney, payWallet, setCardField, payCard, payStripe, setStepField, submitStep, resendStep, continuePayment, tryAgain, cancelPayment, close et dismiss.
  • Appelez onReturn() quand votre application reprend la main, et hostGone() quand votre écran disparaît pour de bon. Chaque appel se fait sur le thread principal, où s'exécutent les callbacks.
  • Quand le card?.flavor d'un formulaire est un CardFlavor.Stripe, affichez le CardInputWidget de Stripe et payez avec controller.payStripe { StripeTokenizer.createPaymentMethod(context, stripe.publishableKey, card) }, où stripe est ce CardFlavor.Stripe et card le paymentMethodCard du widget. Ajoutez implementation("com.stripe:stripe-android:21.2.0") à votre propre fichier de build : le module Compose n'expose pas Stripe à votre code.

WajubSession, le client de plus bas niveau d'un paiement, est documenté dans le README du paquet.

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 le formulaire de sécurité des données de Google Play, catégorie par catégorie, est sur SDKs mobiles.

Tests

Créez le paiement avec une clé sandbox, et tout le parcours se déroule en sandbox, interface comprise, qui affiche un badge « MODE TEST ». Rien à changer dans l'application. Les numéros et les cartes de test sont sur Scénarios de test.

wajub-mobile-core ne dépend pas d'Android : il tourne dans un simple test unitaire JVM, sans émulateur.

Que pensez-vous de ce contenu ?