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 · GAMaven 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
dependencies {
implementation("com.wajub:wajub-mobile-compose:1.3.0")
}| Artefact | Usage |
|---|---|
com.wajub:wajub-mobile-compose | Le composable PaymentSheet. Il apporte wajub-mobile-core avec lui |
com.wajub:wajub-mobile-core | Le même parcours de paiement sans interface, pour une application qui dessine ses propres écrans, voir Votre propre interface |
| Prérequis | Valeur |
|---|---|
minSdk | 24, soit Android 7.0 |
compileSdk | 35. La bibliothèque ne fixe aucun targetSdk : votre application garde le sien |
| Cible Java et Kotlin | 17 |
| Compose | Material 3 1.3 (BOM 2024.12.01) ou 1.4 (BOM 2025.09.01) |
| Apporte | stripe-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.
- Votre serveur crée le paiement avec
POST /paymentset sa clé secrète, puis transmetauthorization_tokenà l'application. La clé secrète n'entre jamais dans l'application. - L'application affiche
PaymentSheetavec ce jeton.onCompletereçoit le résultat une seule fois, à la fermeture de l'interface. - Votre serveur confirme le paiement, avec le webhook
payment.succeededouGET /payments/{id}, avant de livrer. Le résultat dans l'application est ce que le payeur a vu, pas une preuve de paiement.
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.
| Option | Type, défaut | Effet |
|---|---|---|
sessionToken | String | L'authorization_token de votre serveur |
locale | String?, null | "fr" ou "en". Sans elle, la langue du paiement, sinon celle de l'appareil |
appearance | Appearance?, null | L'apparence de l'interface, voir Apparence |
loader | LoaderStyle, Skeleton | Skeleton, des blocs de substitution animés, ou Spinner |
readyTimeout | Long en millisecondes, 15_000 | Une interface encore en chargement passé ce délai affiche une erreur avec Réessayer et signale ready_timeout à onLoadError. 0 le désactive |
returnUrl | String?, null | Un 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 |
mobileMoney | MobileMoneyOptions | Le formulaire Mobile Money, voir Mobile Money |
cancelOnDismiss | Boolean, true | Fermer l'interface avant toute tentative de paiement annule le paiement. false le laisse ouvert pour afficher l'interface à nouveau, voir Annuler et fermer |
autoCloseSuccess | Long en millisecondes, 3_000 | L'écran de succès se ferme seul passé ce délai. 0 le garde jusqu'à ce que le payeur touche Terminé |
callbacks | CheckoutCallbacks | Voir 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.
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
),
)| Callback | Déclenchement |
|---|---|
onReady | Une 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 |
onStateChange | Après onReady, puis à chaque changement d'état. method vaut mobile_money, card, wallet ou unknown |
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 requête de paiement est acceptée. À partir de là, fermer l'interface donne Processing |
onRedirect | Juste avant l'ouverture de la page d'un prestataire de paiement |
onSuccess | Le paiement a réussi. method vaut unknown quand il était payé avant l'ouverture de l'interface |
onError | Chaque échec que voit le payeur, y compris ceux qu'il peut retenter, hors erreurs de champ |
onCancel | L'interface se ferme sur Cancelled |
onComplete | Une 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, PROCESSING | La 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, VERIFYING | Une é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, CANCELLED | Les issues, avec Réessayer sur un échec qui le permet |
Ce que voit le payeur dans chaque état est sur SDKs mobiles.
Résultats
CheckoutResult | Quand | Contenu |
|---|---|---|
Complete | L'interface s'est fermée après un succès, y compris quand le paiement était déjà payé à l'ouverture | payment: PaymentSuccess, transaction |
Processing | L'interface s'est fermée pendant un paiement en cours. L'argent peut encore bouger : confirmez sur votre serveur | transaction, peut être null |
Failed | L'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 |
Cancelled | Annuler le paiement, une fermeture avant toute tentative de paiement, ou votre écran qui disparaît avant | reason: 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 ferme | Résultat | Le paiement |
|---|---|---|
| Pendant le chargement du paiement | Cancelled, Dismissed | Inchangé |
| Avant toute tentative de paiement, ou de retour sur le formulaire après un refus ou Réessayer | Cancelled, Dismissed | Annulé, sauf avec cancelOnDismiss = false |
| Pendant un paiement en cours : approbation, code USSD, étape de confirmation, « Paiement en cours » | Processing | Continue |
| Sur l'écran d'échec | Failed, 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.
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()ourgba(); les longueurs"8px","8"ou"0.5rem". Une valeur illisible est ignorée. inherit = trueprend dans leMaterialThemede votre application ce que vous laissez vide.- Une
fontFamilyn'atteint l'interface qu'une fois enregistrée par votre application avecWajubFonts.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 = truecoupe 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.
MobileMoneyOptions | Défaut | Effet |
|---|---|---|
country | null | Le pays de départ, ISO-2 |
countryLocked | false | Masque le sélecteur de pays quand country est fourni |
operatorMode | OperatorMode.Choose | Choose 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 |
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-mail | OTP_REQUIRED |
| Le code PIN de la carte | PIN_REQUIRED |
| Un numéro de téléphone | PHONE_REQUIRED |
| Une date de naissance | BIRTHDAY_REQUIRED |
| Une adresse de facturation | ADDRESS_REQUIRED |
| En sandbox, le code 3-D Secure envoyé par e-mail | THREE_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 |
|---|---|
type | WajubErrorType : ApiError, AuthenticationError, InvalidRequestError, PaymentError, RateLimitError |
code | Ce qui n'a pas marché, insufficient_funds par exemple |
message | Pour le payeur, dans la langue de l'interface |
declineCode | Sur un refus, le motif donné |
retryable | Si Réessayer peut aboutir |
param | Le champ en cause |
correlationId | L'identifiant de requête à communiquer au support Wajub |
retryAfterSeconds | L'attente sur rate_limited |
blockReference | Sur 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.
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,closeetdismiss. - Appelez
onReturn()quand votre application reprend la main, ethostGone()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?.flavord'un formulaire est unCardFlavor.Stripe, affichez leCardInputWidgetde Stripe et payez aveccontroller.payStripe { StripeTokenizer.createPaymentMethod(context, stripe.publishableKey, card) }, oùstripeest ceCardFlavor.StripeetcardlepaymentMethodCarddu widget. Ajoutezimplementation("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.
Pages associées
- SDKs mobilesCe que voit le payeur, les résultats et les codes d’erreur.
- JavaLe SDK de votre serveur Java.
- Créer un paiementL'appel serveur qui produit le jeton.
- WebhooksLa confirmation qui déclenche la livraison.
- FlutterLe SDK pour les applications Flutter.
- React NativeLe SDK pour les applications React Native.
- iOSLe SDK pour les applications iOS.