iOS (Swift)
Le package Swift WajubMobile, une interface de paiement SwiftUI et aucune WebView.
Le SDK iOS encaisse les paiements dans votre application native. Il présente les paiements Mobile Money, par carte et par portefeuille dans une interface de paiement SwiftUI, sans WebView pour les écrans de Wajub, et ouvre dans Safari la page carte d'un prestataire ou une vérification 3-D Secure. Il implémente le contrat mobile commun, la spec v2 : les mêmes options, callbacks, états, résultats, erreurs et le même objet d'apparence que les SDKs Flutter, Android et React Native et que Wajub.js 1.8.
WajubMobileUI
Stable · GASwift Package Manager
- Version
- 1.0.0
- Runtime
- iOS 15+, Swift 5.9+ (Xcode 15+)
- Frameworks
- SwiftUI sheet, UIKit presenter
Couvre
- Payments
- Mobile Money
- Cards
Installer
Le package s'ajoute avec Swift Package Manager. Dans Xcode, File › Add Package Dependencies…
avec https://github.com/wajubhq/wajub-ios, ou dans votre propre Package.swift :
dependencies: [
.package(url: "https://github.com/wajubhq/wajub-ios.git", from: "1.0.0"),
],
targets: [
.target(name: "App", dependencies: [
.product(name: "WajubMobileUI", package: "wajub-ios"),
]),
]| Prérequis | Valeur |
|---|---|
| iOS | 15.0 ou version ultérieure |
| Swift | 5.9 ou version ultérieure, donc Xcode 15 ou version ultérieure |
| Dépendances | Aucune. Stripe est facultatif et vit dans votre application, voir Cartes |
| Cible | Une application. L'interface se présente depuis les fenêtres de l'application (UIApplication.shared), donc pas depuis une extension d'application |
Le package propose deux produits, et WajubMobileUI installe WajubMobile avec lui.
| Produit | Contenu |
|---|---|
WajubMobile | Foundation uniquement : les modèles, le client de l'API (délais, nouvelles tentatives, Idempotency-Key), les erreurs, les tables et règles Mobile Money, le suivi du statut (polling et connexion au protocole Pusher), et CheckoutController, la machine à états de l'interface sans aucune UI |
WajubMobileUI | L'interface de paiement SwiftUI et son présentateur UIKit, WajubCheckout.present(_:from:) |
Importez WajubMobile seul quand vous dessinez vos propres écrans de paiement.
Démarrage rapide
Votre serveur crée le paiement, l'application présente l'interface avec son jeton, et votre serveur
confirme le résultat. Aucun élément du package n'appelle /payments ni ne détient de clé secrète.
1. Créez le paiement sur votre serveur avec POST /payments et votre
clé secrète, puis transmettez son authorization_token à l'application par votre propre endpoint.
struct CheckoutResponse: Decodable {
let authorizationToken: String
}
func fetchToken(cartId: String) async throws -> String {
var request = URLRequest(url: URL(string: "https://api.yourshop.com/checkout")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(["cart_id": cartId])
let (data, _) = try await URLSession.shared.data(for: request)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(CheckoutResponse.self, from: data).authorizationToken
}2. Présentez l'interface depuis un view controller. present se résout une seule fois, quand
l'interface a disparu, avec la valeur que reçoit onComplete : un autre present peut suivre
aussitôt. Appelé depuis onComplete, ou juste après la disparition de l'écran de présentation, il
attend que l'interface réglée ait disparu ; seule une interface encore ouverte lui fait lever
sheet_already_presented.
import UIKit
import WajubMobile
import WajubMobileUI
@MainActor
func pay(token: String, from viewController: UIViewController) async {
var options = PresentOptions(sessionToken: token)
options.returnURL = "https://shop.example.com/app/return" // optional, a universal link into your app
options.callbacks.onStateChange = { event in print("checkout:", event.state.rawValue) }
do {
switch try await WajubCheckout.present(options, from: viewController) {
case .complete(let payment, _):
// Paid. Confirm on your server before delivering.
await confirmOnServer(paymentId: payment.id)
case .processing:
// Closed while a payment was running: money may still move.
showPending()
case .failed(let error, _):
showError(error.message)
case .cancelled(let reason, _):
print("cancelled:", reason.rawValue)
}
} catch let error as WajubError {
// Only sheet_already_presented: a sheet is already open.
print(error.code)
} catch {}
}from: est facultatif. Sans lui, l'interface se présente depuis le view controller le plus haut de
la fenêtre principale. Il n'existe ni vue ni modificateur SwiftUI pour l'interface : depuis un écran
SwiftUI, appelez WajubCheckout.present sans from:, ou avec le view controller qui héberge votre
vue.
3. Confirmez sur votre serveur avant toute livraison, avec GET /payments/{id} ou le
webhook payment.succeeded. Le résultat dans l'application
est ce que le payeur a vu, pas une preuve de paiement : une approbation Mobile Money peut arriver
après la fermeture de l'interface.
Options
PresentOptions est une structure de propriétés var. Son initialiseur les prend toutes en
arguments nommés, avec les valeurs par défaut ci-dessous : réglez-les là, ou sur la valeur ensuite.
| Option | Type | Par défaut | Rôle |
|---|---|---|---|
sessionToken | String | Obligatoire | Le authorization_token du paiement |
locale | String? | nil | "fr" ou "en". Sans elle, la langue de la session, sinon celle de l'appareil. Une balise qui commence par fr donne le français, toute autre l'anglais |
appearance | Appearance? | nil | L'apparence de l'interface, voir Apparence |
loader | LoaderStyle | .skeleton | .skeleton, des blocs de substitution qui pulsent, ou .spinner |
readyTimeout | Int, millisecondes | 15000 | Passé ce délai, une interface encore en chargement affiche « Impossible de charger le paiement » avec Réessayer et transmet ready_timeout à onLoadError. 0 le désactive |
returnURL | String? | nil | Un lien universel https vers votre application, envoyé comme return_url aux pages des prestataires. Sans lui, le payeur revient dans l'application par lui-même |
mobileMoney | MobileMoneyOptions | MobileMoneyOptions() | country pour le pays de départ (ISO-2), countryLocked pour masquer le sélecteur de pays, operatorMode .choose (la liste des opérateurs, par défaut) ou .detect |
realtime | RealtimeOptions? | nil | RealtimeOptions(enabled:host:port:tls:) : un autre hôte au protocole Pusher, un Reverb ou un Soketi auto-hébergé, ou enabled: false pour le polling seul |
cancelOnDismiss | Bool | true | Fermer l'interface avant toute tentative annule le paiement. false le laisse ouvert pour le présenter à nouveau |
autoCloseSuccess | Int, millisecondes | 3000 | Le délai avant que l'écran de succès se ferme seul. 0 le garde |
callbacks | CheckoutCallbacks | CheckoutCallbacks() | Voir Callbacks et états |
cardCollector | CardCollector? | nil | Votre collecteur de carte Stripe. Sans lui, une carte routée vers Stripe n'est pas proposée |
let options = PresentOptions(
sessionToken: token,
locale: "fr",
loader: .spinner,
returnURL: "https://shop.example.com/app/return",
mobileMoney: MobileMoneyOptions(country: "CI", countryLocked: true, operatorMode: .detect),
realtime: RealtimeOptions(host: "ws.shop.example", port: 443, tls: true),
cancelOnDismiss: false
)LoaderStyle(wajubJS:) lit la valeur loader d'une configuration Wajub.js : "skeleton" donne
.skeleton, toute autre valeur, "none" comprise, donne .spinner.
Callbacks et états
Chaque callback est facultatif et appelé sur le thread principal, dans cet ordre : onReady, puis
onStateChange au fil des écrans, onPaymentStarted et onRedirect, puis onSuccess, onError
ou onCancel, et onComplete une seule fois. Chacun est typé
(@MainActor (Payload) throws -> Void)? : un callback qui lève une erreur est intercepté et
journalisé, et l'interface continue.
| Callback | Contenu | Quand |
|---|---|---|
onReady | ReadyEvent : sessionId, environment ("sandbox" ou "live"), amount, currency, methods (les slugs des canaux affichés) | Le premier écran est affiché, y compris pour un paiement déjà payé ou terminé |
onStateChange | StateChangeEvent : state, method (mobile_money, card, wallet ou unknown) | Après onReady, puis à chaque changement d'état |
onLoadError | WajubError | L'interface ne peut proposer aucun paiement |
onPaymentStarted | PaymentStartedEvent : method, channel | Le premier 2xx d'une tentative. Dès lors, fermer l'interface donne .processing |
onRedirect | RedirectEvent : url, method (toujours "GET") | Juste avant l'ouverture de la page d'un prestataire |
onSuccess | PaymentSuccess | Le paiement a réussi, avec method à "unknown" s'il était déjà payé au chargement |
onError | WajubError | Chaque échec que voit le payeur, y compris ceux qui permettent un nouvel essai. La validation des champs n'est pas transmise |
onCancel | CancelEvent : reason, redirectURL | L'interface se ferme en .cancelled |
onComplete | CheckoutResult | L'interface se ferme, avec la valeur que renvoie present |
options.callbacks = CheckoutCallbacks(
onReady: { event in print("ready:", event.environment, event.methods) },
onPaymentStarted: { event in print("paying with", event.channel) },
onError: { error in print(error.code, error.correlationId ?? "-") }
)CheckoutState est une énumération String dont les valeurs brutes sont les 17 noms de
Wajub.js : .initiated (INITIATED), .collectingDetails, .processing, .approvalRequired,
.ussdRequired, .otpRequired, .pinRequired, .phoneRequired, .birthdayRequired,
.addressRequired, .threeDSRequired (3DS_REQUIRED), .verifying, .pending, .success,
.failed, .expired et .cancelled. Comparez sur le cas, journalisez rawValue. Ce que montre
chaque état est décrit sur SDKs mobiles.
Résultats
CheckoutResult est une énumération à quatre cas, et status donne son nom sous forme de chaîne,
complete, processing, failed ou cancelled.
| Cas | Quand |
|---|---|
.complete(payment:transaction:) | L'interface s'est fermée après l'écran de succès, y compris pour un paiement déjà payé à son ouverture |
.processing(transaction:) | L'interface s'est fermée pendant une tentative, ou après les 15 minutes de suivi : de l'argent peut encore bouger |
.failed(error:transaction:) | L'interface s'est fermée sur un échec, une expiration, une erreur de chargement, ou un paiement annulé ailleurs (payment_cancelled). Fermer sur un échec libère aussi un paiement encore annulable, voir Annuler et fermer |
.cancelled(reason:redirectURL:) | Annuler le paiement (.payerCancelled), une fermeture avant toute tentative (.dismissed), ou l'écran de présentation disparu avant (.hostGone) |
PaymentSuccess est celui de Wajub.js : 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.
present se résout quoi qu'il arrive à l'écran depuis lequel vous l'avez présentée. Quand cet écran
est fermé ou désalloué pendant que l'interface est ouverte, elle se résout en .cancelled avec
.hostGone avant toute tentative, en .processing une fois une tentative lancée et en .complete
après un succès, sans jamais annuler le paiement. Sans fenêtre depuis laquelle se présenter, ou si
UIKit refuse la présentation, elle se résout aussitôt en .hostGone.
Si le processus de l'application meurt, rien ne peut se résoudre. Au lancement suivant, lisez le
résultat sur votre serveur, ou avec WajubSession(sessionToken:).status().
Ouverture et paiements sans réponse
- Un paiement qui n'est pas
pendingà l'ouverture est dessiné aussitôt à partir de la session, sans attendre la configuration des moyens de paiement (GET /pay/sdk-config) : l'écran de succès s'il est payé, un avis « Ce paiement est terminé » aveconLoadErrors'il a échoué, a été annulé ou a expiré, et l'écran d'approbation ou d'attente où il en était, suivi, s'il est encoreprocessing. - Quand la configuration des moyens de paiement ne peut pas être lue, le Mobile Money reste
proposé. Si rien ne peut l'être sans elle, une carte ou un portefeuille seulement, l'interface
signale
load_error, qu'on peut réessayer, plutôt queno_payment_method. - Une requête de paiement restée sans réponse, un
5xxcompris, n'est jamais affichée comme un échec : l'interface affiche « Paiement en cours » et suit le paiement. Un refus plus récent que la tentative s'affiche aussitôt. Si le paiement n'a toujours pas commencé 60 secondes après le dernier essai, l'interface revient au formulaire avec une erreur de connexion, et le Payer suivant envoie la mêmeIdempotency-Key. - Un
400fait lire le statut : une issue s'affiche, un paiementprocessingou un statut illisible est suivi, et un paiement encorependingéchoue avecnot_processable. - Le suivi s'arrête 15 minutes après l'appui sur Payer, mesurées sur l'horloge monotone du téléphone, quoi que disent son heure ou l'expiration du paiement.
Les règles complètes sont sur SDKs mobiles.
Annuler et fermer
Le payeur ferme l'interface avec son bouton de fermeture ou d'un glissement vers le bas. Pendant une requête (un paiement, une vérification, une annulation, votre collecteur de carte), l'interface ne peut pas être glissée vers le bas.
| Le payeur ferme | Résultat | Appel à Wajub |
|---|---|---|
| Pendant le chargement du paiement, avant la réponse de la session | .cancelled, .dismissed | Aucun. Une fois la session arrivée, son statut décide aussitôt : processing donne .processing |
| Avant toute tentative | .cancelled, .dismissed, avec le redirectURL de l'annulation | POST /pay/cancel, sauf si cancelOnDismiss vaut false |
| Pendant une tentative, ou pendant que le payeur approuve, compose un code ou confirme une étape | .processing | Aucun |
| Sur l'écran d'échec | .failed avec l'erreur affichée, sans appel à onCancel | POST /pay/cancel si le paiement peut encore être annulé et que cancelOnDismiss est actif. Pas après unsupported_action ou redirect_unavailable, quand le paiement peut encore avancer |
| Après l'écran de succès | .complete | Aucun |
L'interface n'annule qu'un paiement chargé, dont le dernier statut connu est pending, sans
tentative non tranchée. Une tentative n'est pas tranchée de l'appui sur Payer jusqu'à une
réponse définitive : un refus, une étape ou un 3-D Secure échoués pour de bon, une session carte
refusée, ou un statut pending avec un nouveau code de refus. Après un tel refus, le paiement est
de nouveau pending et peut être annulé.
Annuler le paiement est proposé sur la liste des moyens, sur un formulaire et sur un échec que le
payeur peut réessayer, jamais pendant une tentative non tranchée. Il annule le paiement
définitivement et donne .cancelled avec .payerCancelled. Une annulation que l'API refuse lit le
statut du paiement et se résout avec ce qu'il indique. Les règles complètes sont sur
SDKs mobiles.
Apparence
PresentOptions.appearance prend l'objet d'apparence de Wajub.js 1.8, clé pour clé : vos
Components web et l'interface native partagent un même style.
options.appearance = Appearance(
theme: .stripe, // presets: .stripe, .night, .flat, .noPreset
colorScheme: .auto, // .light, .dark, .auto (follows the phone, live)
variables: AppearanceVariables(
colorPrimary: "#0F766E",
fontFamily: "Poppins", // used when your app ships it
borderRadius: "12px"
),
buttonTextColor: "#FFFFFF"
)- Les arguments suivent l'ordre de déclaration des initialiseurs, comme Swift l'exige : dans
AppearanceVariables,fontFamilyvient avantborderRadius. - Le
"none"de Wajub.js s'écrit.noPresetpourthemeet.noShadowpourshadow: aucun des deux ne peut se lire comme lenonedu type optionnel. - Les clés sont celles de Wajub.js :
theme,colorScheme,inherit,variables(seize jetons de design, decolorPrimaryàspacingGridColumn) et les clés de premier niveauprimaryColor,secondaryColor,backgroundColor,fontFamily,borderRadius,buttonTextColor,inputBackgroundColor,inputBorderColor,textMutedColor,successColor,errorColor,shadow,labelsetdisableAnimations. Les valeurs sont des chaînes, comme sur le web, et une valeur illisible est ignorée. - L'objet que votre checkout web utilise déjà se lit tel quel :
Appearance(json: try JSONValue.parse(jsonString)). Les clés inconnues,rulescompris, sont ignorées. inherit: truelit l'écran de présentation : satintColorcomme couleur principale,systemBackground,label,secondaryLabeletseparatorrésolues pour son apparence, et son style clair ou sombre. Une couleur n'est gardée que si elle contraste assez avec le fond.- Pour chaque jeton, la première valeur définie l'emporte :
variables, les clés de premier niveau, le préréglage, la valeur héritée, l'image de marque du marchand dans le Dashboard, puis la palette par défaut de Wajub. - L'image de marque du marchand complète ce que vous laissez vide : les couleurs, l'arrondi, la police (utilisée seulement si votre application l'embarque), et le logo et le nom commercial dans l'en-tête. Un marchand en marque blanche masque « Sécurisé par Wajub ».
disableAnimations: true, ou Réduire les animations sur le téléphone, arrête la pulsation du squelette et les fondus.
Les règles du schéma de couleurs et les palettes par défaut sont sur SDKs mobiles, les clés elles-mêmes sur Apparence.
Mobile Money
Un seul formulaire couvre tous les opérateurs : le pays, le numéro avec son indicatif, mis en forme
pendant la saisie, et les opérateurs de la session, un agrégat comme cm.mobile présentant un
opérateur par ligne. L'opérateur auquel appartient le numéro est présélectionné tant que le payeur
n'en a pas choisi un lui-même, et un numéro d'un autre opérateur proposé est refusé avant tout
appel. Wave et les autres portefeuilles qui acceptent n'importe quel numéro ne sont jamais refusés.
Le numéro part au format E.164, et la Côte d'Ivoire garde son 0 initial.
Après Payer, l'interface affiche « Validez le paiement sur votre téléphone » avec le numéro et l'instruction de l'opérateur, puis suit le paiement jusqu'à son issue.
MobileMoneyOptions | Par défaut | Rôle |
|---|---|---|
country | nil | Le pays de départ, ISO-2. Sans lui, le pays du payeur quand un canal le couvre |
countryLocked | false | Masque le sélecteur de pays, quand country est proposé |
operatorMode | .choose | .choose liste les opérateurs. .detect affiche l'opérateur détecté en badge dans le champ du numéro, et la liste seulement quand le numéro ne tranche pas ou que le payeur l'ouvre |
Les logos des opérateurs se chargent depuis leur URL https. Un logo absent ou qui ne se charge pas
comme une image, un SVG par exemple, est remplacé par le badge de la marque de l'opérateur.
Pour vos propres écrans, MobileMoneyData.shared expose les mêmes règles :
nationalDigits(_:country:), isValid(_:country:), detectOperator(_:country:),
e164(_:country:), formatAsYouType(_:) et badge(operatorId:fallbackName:). Le comportement
commun à tous les SDKs est sur SDKs mobiles.
Cartes
Le prestataire derrière le canal carte décide de l'écran. L'interface le lit dans la configuration SDK de la session.
| Carte routée vers | Dans l'interface |
|---|---|
| Paystack, Flutterwave ou Adyen (une session client) | Les champs de contact dont le prestataire a encore besoin, puis sa page dans SFSafariViewController, vérifiée au retour du payeur |
| La page d'un prestataire (CinetPay, Kkiapay, Mollie…) | Les required_fields du prestataire, puis sa page dans SFSafariViewController |
| Stripe | Payer par carte par votre CardCollector. Sans lui, la carte n'est pas proposée |
| Sandbox | Le formulaire de carte de test. Les numéros de carte ne quittent jamais l'appareil en live |
Un portefeuille en un geste comme Djamo, et une page Mobile Money comme celle de Wave, est confié à
iOS avec UIApplication.open, qui ouvre l'application du portefeuille. Au retour du payeur, Safari
fermé ou l'application revenue au premier plan, l'interface lit aussitôt le statut du paiement.
Tant qu'une page est ouverte ailleurs, « Continuer le paiement » la rouvre.
Stripe par votre propre collecteur
Seul le SDK de Stripe peut créer un pm_… Stripe, et le package ne dépend pas de stripe-ios.
Implémentez CardCollector dans votre application : l'interface l'appelle quand le payeur touche
Payer par carte, puis envoie le pm_… qu'il renvoie.
import StripePaymentsUI
import WajubMobile
final class StripeCardCollector: CardCollector {
@MainActor
func collectPaymentMethod(_ request: CardCollectionRequest) async throws -> String? {
STPAPIClient.shared.publishableKey = request.publishableKey
// CardEntryScreen is your own screen with an STPPaymentCardTextField, presented over the sheet.
guard let card = await CardEntryScreen.present(amount: request.amount, currency: request.currency) else {
return nil // the payer backed out
}
let paymentMethod = try await STPAPIClient.shared.createPaymentMethod(with: card.paymentMethodParams)
return paymentMethod.stripeId
}
}
options.cardCollector = StripeCardCollector()CardCollectionRequest contient publishableKey (la clé Stripe propre au paiement, issue de la
session), amount, currency, customerEmail, customerName et language, celle de
l'interface. Renvoyez nil quand le payeur renonce : l'interface reste sur le formulaire carte et
rien n'est débité. Une erreur levée s'affiche sur le formulaire carte : une WajubError telle
quelle, toute autre erreur comme validation_error, avec la description propre à l'erreur quand
elle en a une (celles de Stripe en ont), sinon « Vérifiez les informations saisies. », et son texte
conservé dans apiMessage. Dès le toucher
sur Payer par carte, une fermeture donne .processing, et rien n'est envoyé une fois
l'interface fermée.
Étapes de confirmation
Certains prestataires de carte demandent davantage au payeur après le premier envoi. Chaque étape a son écran, avec les essais restants dans l'erreur après une mauvaise réponse et « Renvoyer le code » proposé 60 secondes après l'ouverture de l'écran.
| État | Écran |
|---|---|
OTP_REQUIRED | Le code reçu par le payeur |
PIN_REQUIRED | Le code PIN de la carte |
PHONE_REQUIRED | Un numéro de téléphone à confirmer |
BIRTHDAY_REQUIRED | Une date de naissance |
ADDRESS_REQUIRED | Une adresse de facturation |
3DS_REQUIRED | En sandbox, le code 3-D Secure envoyé par e-mail, avec le compte à rebours de son expiration. En live, le 3-D Secure dans Safari |
Un mauvais code garde l'écran. Une étape expirée ou à court d'essais se termine sur l'écran d'échec, et Réessayer ramène au choix d'un moyen de paiement. Les codes sont sur SDKs mobiles.
Erreurs
Chaque échec est une WajubError, une structure conforme à Error et LocalizedError, dont
errorDescription est message.
| Champ | Utilité |
|---|---|
type | WajubErrorType : .apiError, .authenticationError, .invalidRequestError, .paymentError, .rateLimitError, avec les noms de Wajub.js comme valeurs brutes |
code | Un code du catalogue, listé en constantes dans WajubError.Code, ou le code propre de l'API transmis tel quel |
message | Pour le payeur, dans la langue de l'interface |
declineCode | Sur un refus (402), le code du prestataire |
retryable | Si Réessayer peut aboutir |
param | Le champ en cause, data.phone par exemple |
httpStatus, correlationId | Le statut HTTP et le X-Request-Id de la réponse. Communiquez le second au support |
retryAfterSeconds | Sur rate_limited |
details | Les erreurs de champ de l'API, un message par champ |
blockReference | Sur shield_blocked |
apiMessage | Le texte anglais de l'API, pour vos journaux |
toJSON() donne la forme du contrat, avec des clés en snake_case.
case .failed(let error, _):
switch error.code {
case WajubError.Code.insufficientFunds:
showError(error.message) // retryable: another number or method
case WajubError.Code.shieldBlocked:
showError(error.message) // carries error.blockReference
default:
showError(error.message)
}Les codes que vous rencontrerez : insufficient_funds, payment_declined, payment_attempt_ended
(refusé, rien n'a été débité), payment_failed, shield_blocked, merchant_restricted,
rate_limited, session_terminal_expired, unsupported_action, redirect_unavailable,
security_check_failed. Le catalogue complet, avec ses messages anglais et français, est sur
SDKs mobiles.
Votre propre interface
WajubMobile vous donne le client de l'API et la machine à états de l'interface, pour une
application qui dessine ses propres écrans de paiement.
WajubSession(sessionToken:language:) est le client bas niveau. Il envoie les en-têtes, les
signaux de l'appareil, les délais, les nouvelles tentatives et la Idempotency-Key du contrat, et
renvoie les formes de l'API.
| Méthode | Appel |
|---|---|
session() | GET /pay/session : montant, canaux, image de marque, langue |
sdkConfig() | GET /pay/sdk-config : le prestataire et la variante de chaque canal |
status() | GET /pay/status. Son reference est l'identifiant Wajub du paiement, pas le vôtre |
process(channel:data:idempotencyKey:) | POST /pay/process, avec data._client ajouté. Renvoie un ProcessOutcome |
startClientSession(fields:returnURL:restart:altcha:) | POST /pay/client-session, une carte sur la page du prestataire |
completeClientSession(id:) | POST /pay/client-session/complete, au retour du payeur |
cancel() | POST /pay/cancel : le paiement est annulé définitivement |
verify(_:id:fields:), resend(_:id:) | Une étape de confirmation, ConfirmStepKind .otp, .pin, .phone, .birthday ou .address |
send3DSCode(threeDSId:), verify3DSCode(threeDSId:code:) | Le code 3-D Secure de la sandbox, envoyé par e-mail |
broadcastingAuth(socketId:channelName:) | La signature du canal temps réel privé du paiement |
import WajubMobile
func payHeadless(token: String) async throws {
let session = WajubSession(sessionToken: token)
let payment = try await session.session()
print(payment.transaction.amountTotal, payment.channels.map(\.slug))
let outcome = await session.process(
channel: "cm.mobile",
data: ["phone": "+237677123456", "country": "CM", "operator": "mtn"]
)
switch outcome {
case .complete(let transaction):
print("paid", transaction.id)
case .processing(_, let instruction, let kind):
// .approval, .ussd or .pending: follow GET /pay/status, never show a failure.
print(kind, instruction ?? "")
case .requiresAction(let required):
if required.action == .confirmOTP, let stepId = required.stepId {
let answer = try await session.verify(.otp, id: stepId, fields: ["code": "123456"])
print(answer)
}
case .failed(let error, _):
print(error.code, error.message)
}
}process génère une nouvelle Idempotency-Key sauf si vous en passez une, et ses nouvelles
tentatives automatiques la réutilisent. Une réponse jamais arrivée, ou un 5xx, revient en
.processing de type .pending : suivez le statut, n'affichez jamais un échec.
Checkout.present(_:presenter:browser:) exécute tout le checkout sous votre propre interface, avec
les mêmes règles que WajubCheckout.present : une seule interface à la fois, et un résultat une fois
votre interface disparue. Il crée le CheckoutController, l'automate de l'interface, à partir de
vos options, le confie à CheckoutPresenting.present(_:), le démarre, puis, une fois le checkout
réglé, appelle votre dismiss(_:completion:) et se résout avec le CheckoutResult.
- Votre présentateur dessine le
screendu contrôleur (unCheckoutScreen), avecstate,methods,mobileMoney,cardForm,fieldErrors,banneret le reste, redessine suronChange, et relie vos boutons à ses actions :selectMethod,setPhone,pickOperator,submitMobileMoney,setCardField,submitCard,payWallet,submitStep,submitThreeDSCode,continuePayment,tryAgain,cancelPayment,dismiss,appWillEnterForeground,hostGoneet les autres. Si votre interface ne peut pas s'afficher, appelezcontroller.hostGone(). Appelez lacompletiondedismissune fois votre interface disparue. - Votre
BrowserOpeningouvre les pages des prestataires :.inApp(pages carte, 3-D Secure, sessions client) dans unSFSafariViewController, en appelantonCloseà sa fermeture, et.external(la page d'un portefeuille) avecUIApplication.open. Il renvoiefalsequand rien ne peut ouvrir la page. completionetonClosesont tous deux@MainActor @Sendable: ils peuvent passer dans les gestionnaires de fin du système dans une application Swift 6.
import Foundation
import WajubMobile
@MainActor
final class MySheet: CheckoutPresenting {
private var controller: CheckoutController?
func present(_ controller: CheckoutController) {
self.controller = controller
controller.onChange = { [weak self] in self?.render() }
// Show your sheet; its buttons call the controller's actions.
}
func dismiss(_ controller: CheckoutController, completion: @escaping @MainActor @Sendable () -> Void) {
// Hide your sheet, then:
self.controller = nil
completion()
}
private func render() {
guard let controller else { return }
print(controller.state?.rawValue ?? "loading") // draw controller.screen
}
}
@MainActor
final class MyBrowser: BrowserOpening {
func open(_ url: URL, mode: BrowserMode, onClose: @escaping @MainActor @Sendable () -> Void) async -> Bool {
// .inApp: present an SFSafariViewController and call onClose() once it is closed.
// .external: return await UIApplication.shared.open(url).
false // nothing could open the page
}
}
@MainActor
func payWithMySheet(_ options: PresentOptions) async throws -> CheckoutResult {
try await Checkout.present(options, presenter: MySheet(), browser: MyBrowser())
}Sans Checkout, vous pouvez aussi créer vous-même CheckoutController(options:browser:), définir
son onChange et son onFinish, qui reçoit le CheckoutResult une seule fois, quand votre
interface doit se fermer, et appeler start().
Confidentialité
Pour évaluer la fraude comme le fait le checkout web, chaque POST /pay/process envoie à Wajub
quelques signaux de l'appareil, et chaque appel nomme le SDK.
- Le fuseau horaire, la première langue préférée et les langues (
Accept-Language), la taille de l'écran principal en pixels et une profondeur de couleur de 24. - Le SDK et le système,
wajub-ios/1.0.0 (ios 18.1), dansX-Wajub-Clientet à la fin duUser-Agent, après le nom et la version de votre application. - Une empreinte d'installation : le SHA-256 d'un identifiant aléatoire que le SDK crée à la première
utilisation. Elle ne dérive d'aucun identifiant matériel ou publicitaire. Elle est conservée dans
un fichier du dossier Application Support de l'application,
com.wajub.mobile/install-id, exclu des sauvegardes, et disparaît avec l'application quand elle est désinstallée.
WajubMobile fournit un manifeste de confidentialité, PrivacyInfo.xcprivacy, qu'Xcode ajoute au
rapport de confidentialité de votre application (Product › Archive, puis Generate Privacy
Report dans l'Organizer). Il déclare :
- Aucun suivi, et aucun domaine de suivi.
- Une seule API à raison requise, le temps depuis le démarrage du système
(
ProcessInfo.systemUptime), avec la raison35F9.1: elle mesure les attentes du SDK sur le téléphone et ne le quitte jamais. AucunUserDefaults: l'identifiant d'installation est le fichier ci-dessus. - Les données collectées, toutes liées à l'utilisateur, non utilisées pour le suivi, pour le fonctionnement de l'app (App Functionality) : nom, adresse e-mail, numéro de téléphone, adresse postale, informations de paiement, identifiant de l'appareil et autres données.
Le manifeste ne remplit pas App Store Connect à votre place. Déclarez ce que le SDK envoie à Wajub, votre prestataire de paiement, sous App Privacy, comme des données que collecte votre application, liées à l'identité de l'utilisateur, non utilisées pour le suivi, pour App Functionality, avec ce que votre application collecte elle-même.
| App Store Connect | Ce qu'envoie le SDK |
|---|---|
| Contact Info, Name | Prénom et nom, ou le titulaire de la carte, quand le moyen de paiement les demande |
| Contact Info, Email Address | Quand le prestataire de carte la demande |
| Contact Info, Phone Number | Le numéro Mobile Money, ou un numéro de téléphone demandé par un prestataire de carte |
| Contact Info, Physical Address | Adresse, ville, code postal et pays, quand le prestataire de carte les demande |
| Financial Info, Payment Info | Le compte Mobile Money, le pm_… Stripe que renvoie votre collecteur, un code PIN de carte demandé par un prestataire, et les cartes de test en sandbox. Les numéros de carte ne quittent jamais l'appareil en live |
| Identifiers, Device ID | Le SHA-256 de l'identifiant d'installation aléatoire, ni l'identifiant publicitaire ni identifierForVendor, pour la prévention de la fraude |
| Other Data | Une date de naissance quand un prestataire la demande, et le fuseau horaire, la langue et la taille d'écran de l'appareil, pour la prévention de la fraude |
Un numéro de carte saisi sur la page d'un prestataire dans Safari va à ce prestataire, sans passer par le SDK. Les signaux qu'envoient tous les SDKs sont sur SDKs mobiles.
Avant 1.0
Il n'existe aucune version antérieure depuis laquelle migrer. Jusqu'à ce package, une application iOS encaissait un paiement Wajub par le checkout hébergé dans Safari, ou par le SDK Flutter ou React Native.
Passer une application native à l'interface de paiement ne change rien sur votre serveur.
POST /payments renvoie le authorization_token que prend l'interface à côté du
authorization_url qu'ouvre le checkout hébergé, et le même webhook payment.succeeded confirme le
paiement.
Tests
L'origine de l'API est une constante, il n'y a donc aucune URL de base à rediriger. Un paiement que votre serveur crée avec une clé sandbox place tout le parcours en sandbox, interface comprise, et l'interface affiche « Mode test ».
- Les numéros Mobile Money de test sont un préfixe d'opérateur et sept chiffres finissant par le
scénario :
000000succès,000001fonds insuffisants,000002échec,000003délai dépassé,000004annulé,+237670000000par exemple. Les préfixes de la sandbox ne suivent pas les plans de numérotation réels : l'interface s'y contente d'avertir d'un opérateur qui ne correspond pas. - Le formulaire carte accepte les cartes de test de Wajub, et
4000056655665556demande le code 3-D Secure envoyé par e-mail.
Tous les numéros et toutes les cartes sont sur Scénarios de test.
Pages associées
- SDKs mobilesLe contrat commun aux quatre SDKs.
- Créer un paiementL'appel serveur qui génère le jeton.
- WebhooksLa confirmation qui déclenche la livraison de la commande.
- ApparenceL'objet d'apparence que l'interface partage avec Wajub.js.
- FlutterLe même contrat en Dart.
- React NativeLe même contrat en TypeScript.
- AndroidLe même contrat en Kotlin et Compose.