Aller au contenu

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 · GA

Maven 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

build.gradle.kts
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.

ArtefactTypeContenu
com.wajub:wajub-mobile-coreJar Kotlin/JVM simpleWajubMobile, WajubSession, modèles et client en temps réel
com.wajub:wajub-mobile-composeAAR AndroidComposable PaymentSheet, Stripe et Custom Tabs
PrérequisValeur
minSdk24, so Android 7.0
compileSdk35
Cible Java et Kotlin17
ComposeBOM 2024.12.01, Material 3
Également installésstripe-android 21.2.0, androidx.browser 1.8.0, OkHttp, Moshi, Pusher

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.

Toute l'intégration
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.

BrancheRésultat
CompletePaiement réglé. transaction contient la référence
ProcessingEnvoyé à l'opérateur. instruction contient le texte à afficher au payeur
RequiresAction3DS ou redirection bancaire déjà ouverte dans Custom Tabs
Failederror 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.

Lire la session, puis payer
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é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(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

Suivre le résultat

watchStatus est un Flow
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.

Stripe piloté par la session
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)
}

Erreurs

WajubError étend Exception. Les fonctions suspendues la génèrent et elle constitue aussi le payload de PaymentResult.Failed.

Le même type à deux endroits
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é
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
httpStatusStatut à 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.

Que pensez-vous de ce contenu ?