Android (Kotlin)
Deux artefacts, une interface de paiement Compose et l'API de coroutines sous-jacente.
Le SDK Android encaisse les paiements dans votre application native. Les champs Mobile Money sont
des composables Material 3, les champs de carte proviennent de stripe-android et les vérifications
3DS s'ouvrent dans Custom Tabs plutôt que dans une WebView.
com.wajub:wajub-mobile-compose
Stable · GAMaven Central
- Version
- 1.1.0
- Runtime
- Android 7.0 (API 24)+, JVM target 17
- Frameworks
- Jetpack Compose with Material 3
Couvre
- Paiements
- Mobile Money
- Cartes
Deux artefacts, généralement nécessaires ensemble
dependencies {
implementation("com.wajub:wajub-mobile-compose:1.1.0")
}Cette seule ligne suffit, car le module Compose déclare le package principal comme dépendance api
et l'installe. Utilisez uniquement le package principal si vous créez vous-même l'interface.
| Artefact | Type | Contenu |
|---|---|---|
com.wajub:wajub-mobile-core | Jar Kotlin/JVM simple | WajubMobile, WajubSession, modèles et client en temps réel |
com.wajub:wajub-mobile-compose | AAR Android | Composable PaymentSheet, Stripe et Custom Tabs |
Le package principal ne contient aucun élément Android
wajub-mobile-core est publié comme kotlin("jvm"), pas comme bibliothèque Android. Il ne contient
ni manifeste, ni ressources, ni Context. Il fonctionne donc dans un simple test JVM. L'interface,
le lanceur de navigateur et le tokenizer Stripe se trouvent tous dans l'artefact Compose.
| Prérequis | Valeur |
|---|---|
minSdk | 24, so Android 7.0 |
compileSdk | 35 |
| Cible Java et Kotlin | 17 |
| Compose | BOM 2024.12.01, Material 3 |
| Également installés | stripe-android 21.2.0, androidx.browser 1.8.0, OkHttp, Moshi, Pusher |
Le groupe est com.wajub
co.wajub ne correspond à rien. Les deux artefacts se trouvent sous com.wajub, le même groupe
que wajub-java côté serveur.
Obtenir un jeton depuis votre serveur
Aucun de ces artefacts n'appelle /payments ni ne détient de clé secrète. Votre backend crée le
paiement et renvoie authorization_token.
Afficher l'interface de paiement
PaymentSheet est un composable, pas une classe dotée d'une méthode present.
import androidx.compose.runtime.*
import com.wajub.mobile.WajubMobile
import com.wajub.mobile.model.PaymentResult
import com.wajub.mobile.ui.PaymentSheet
@Composable
fun Checkout(token: String, onPaid: (String) -> Unit) {
var showSheet by remember { mutableStateOf(false) }
val session = remember(token) { WajubMobile.createSession(token) }
Button(onClick = { showSheet = true }) {
Text("Pay")
}
if (showSheet) {
PaymentSheet(
wajubSession = session,
onDismiss = { showSheet = false },
onResult = { result ->
showSheet = false
when (result) {
is PaymentResult.Complete -> onPaid(result.transaction.reference)
is PaymentResult.Processing -> showInstruction(result.instruction)
is PaymentResult.RequiresAction -> Unit // Custom Tabs already opened
is PaymentResult.Failed -> showError(result.error.message)
}
},
)
}
}PaymentResult est une classe scellée. Un when qui la traite est donc exhaustif sans else et
le compilateur signale toute branche manquante.
| Branche | Résultat |
|---|---|
Complete | Paiement réglé. transaction contient la référence |
Processing | Envoyé à l'opérateur. instruction contient le texte à afficher au payeur |
RequiresAction | 3DS ou redirection bancaire déjà ouverte dans Custom Tabs |
Failed | error contient code, declineCode et retryable |
Processing est le résultat normal pour Mobile Money
Un paiement Mobile Money renvoie rarement Complete depuis l'interface. Le payeur doit encore
approuver sur son téléphone. Vous recevez donc Processing avec une instruction, comme composer
un code USSD. Affichez ce texte, puis collectez watchStatus() ou attendez votre webhook.
Vos propres écrans à la place de l'interface
WajubSession représente toute l'API sous-jacente. Chaque méthode réseau est une suspend fun.
Les appels s'exécutent donc dans une coroutine et sont annulés avec elle.
val session = WajubMobile.createSession(token)
viewModelScope.launch {
val data = session.loadSession()
// data.transaction.amount, data.transaction.currency, data.branding, data.locale
val operators = data.channels.filter {
it.type.lowercase() in setOf("mobile_money", "mobile")
}
val result = session.payMobileMoney(
MobileMoneyInput(
channelSlug = operators.first().slug,
phone = "+237670000000",
country = "CM",
),
)
}| Méthode | Fonction |
|---|---|
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(slug, paymentMethodId, name) | Traite une PaymentMethod Stripe déjà créée |
process(channel, data) | Solution brute pour tous les canaux |
watchStatus(intervalMs) | Flow<PaymentResult> en temps réel si disponible |
watchStatusPolling(intervalMs) | Flow<PaymentResult> toujours obtenu par polling |
cancel() | Abandonne la session et renvoie l'URL de redirection |
cardChannelSlug() | Canal de carte de la session chargée, s'il existe |
WajubSession possède un constructeur interne
WajubSession(token) ne compile pas dans votre code. WajubMobile.createSession(token) constitue
le seul point d'entrée. Vous ne pouvez donc pas non plus créer une sous-classe pour un test.
Utilisez plutôt un stub à la limite du ViewModel.
Suivre le résultat
viewModelScope.launch {
session.watchStatus().collect { result ->
when (result) {
is PaymentResult.Complete -> _state.value = OrderState.Paid
is PaymentResult.Failed -> _state.value = OrderState.Failed
else -> Unit
}
}
}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. watchStatusPolling force le polling, utile
sur un appareil derrière un pare-feu qui bloque les WebSockets.
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. createPaymentMethod appelle lui-même ensureInitialized.
Aucune étape de configuration séparée n'est nécessaire.
import com.wajub.mobile.ui.PaymentActionHandler
import com.wajub.mobile.ui.StripeTokenizer
viewModelScope.launch {
session.loadSession()
val config = session.getSdkConfig()
val slug = session.cardChannelSlug() ?: return@launch
val publishableKey = config.channels[slug]?.publishableKey ?: return@launch
// card comes straight from Stripe's widget, never from your own state:
// cardInputWidget.paymentMethodCreateParams?.card
val paymentMethodId = StripeTokenizer.createPaymentMethod(
context = context,
publishableKey = publishableKey,
card = card,
cardholderName = "Amina Diallo",
)
val result = session.payCard(slug, paymentMethodId, cardholderName = "Amina Diallo")
PaymentActionHandler.handle(context, result)
}PaymentActionHandler ouvre Custom Tabs, pas une WebView
Il dirige Redirect, Confirm et Confirm3ds vers Custom Tabs de androidx.browser, puis ignore
PushApproval, qui ne nécessite aucun navigateur. De plus en plus de banques refusent les
vérifications 3DS dans une WebView. Ce parcours n'est donc pas configurable. Vérifiez que le
callback sur POST /payments pointe vers un lien profond géré par votre application.
Erreurs
WajubError étend Exception. Les fonctions suspendues la génèrent et elle constitue aussi le
payload de PaymentResult.Failed.
try {
when (val result = session.payMobileMoney(input)) {
is PaymentResult.Failed -> showRetry(result.error.message, result.error.retryable)
else -> Unit
}
} catch (error: WajubError) {
if (error.type == WajubErrorType.RateLimitError) {
showRetryIn(error.retryAfterSeconds ?: 60)
return
}
if (error.retryable) {
showRetryButton(error.message)
return
}
showFatal(error.message, error.correlationId)
}| Propriété | Utilité |
|---|---|
type | ApiError, AuthenticationError, InvalidRequestError, PaymentError ou RateLimitError |
code | Motif lisible par la machine |
declineCode | Présent sur une réponse 402, motif du refus du prestataire |
retryable | Indique si une nouvelle tentative doit être proposée |
param | Champ fautif lors d'une erreur de validation |
correlationId | Valeur à communiquer au support |
retryAfterSeconds | Attente imposée par une limite de requêtes |
httpStatus | Statut à l'origine de l'erreur, le cas échéant |
httpStatus existe ici, mais dans aucun autre SDK mobile. Le tri d'un rapport de plantage Android
est donc légèrement plus simple.
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.
Le package principal est un simple jar JVM. Il fonctionne donc dans un test unitaire sans émulateur, le bon endroit pour tester le mapping et la gestion des erreurs.
Pages associées