Champs de paiement
Votre mise en page et votre bouton de paiement, Wajub affiche uniquement les champs sécurisés.
Les champs de paiement placent les champs sécurisés de Wajub dans votre propre checkout : vous gardez la page, le récapitulatif de commande et le bouton de paiement, et vous envoyez les champs quand vous êtes prêt. S'il vous faut vos propres champs de checkout personnalisés dans la page de paiement, utilisez plutôt le checkout complet.
Les étapes qu'un paiement peut ajouter après le premier envoi restent dans le composant : un code
reçu par SMS (OTP), le code PIN d'un portefeuille, un numéro à confirmer, une date de naissance,
une adresse, une validation sur le téléphone, un code USSD à composer et le 3D Secure. Vous n'avez
rien à brancher pour elles, et confirmPayment() attend qu'elles soient terminées.
Les sept composants
| Type | Affichage | Déclenchement du paiement |
|---|---|---|
card | Des champs de carte, ou un bouton qui ouvre la fenêtre de paiement sécurisée du prestataire | Vous, avec confirmPayment() |
mobileMoney | Opérateur et numéro de téléphone | Vous, avec confirmPayment() |
bankTransfer | Le compte bancaire du payeur | Vous, avec confirmPayment() |
crypto | L'actif avec lequel payer | Vous, avec confirmPayment() |
payment | Sélecteur de moyen de paiement et formulaire correspondant | Vous, avec confirmPayment() |
address | Adresse de livraison ou de facturation | Personne, lisez-la avec getValue() |
wallet | Apple Pay ou Google Pay | Bouton natif dans le composant |
Structure générale
Une fabrique conserve la session et les options partagées. Chaque appel create() crée un composant,
puis mount() le place sur la page.
import { components, confirmPayment } from '@wajub/js';
const factory = await components(sessionId, {
locale: 'fr',
appearance: { primaryColor: '#2563eb', labels: 'floating' },
});
const card = factory
.create('card')
.on('change', (event) => (payButton.disabled = !event.complete))
.mount('#card');
payButton.onclick = () => confirmPayment({ sessionId, components: card });Les options transmises à la fabrique deviennent les valeurs par défaut de tous ses composants. Les
options transmises à create() sont prioritaires pour le composant concerné.
Les composants se dimensionnent automatiquement
Vous n'avez rien à mesurer. Par défaut, la page bouge une fois, quand le composant apparaît ;
loader: 'skeleton' tient la place dès mount(), voir
Charger sans décaler la page. Les écrans qui suivent un envoi,
un code OTP ou PIN, une validation sur le téléphone, un code USSD, le 3D Secure, s'affichent en
entier dans le composant.
Configuration
| Option | Type | Composants concernés | Fonction |
|---|---|---|---|
sessionId | string | Tous | Obligatoire, défini une fois sur la fabrique |
locale | string | Tous | fr, en, es, pt, ar |
appearance | AppearanceConfig | Tous | Apparence |
layout | CheckoutLayout | payment | Disposition du sélecteur de moyen de paiement |
collectAddress | shipping ou billing | payment | Ajoute un bloc d'adresse dans le formulaire |
addressMode | shipping ou billing | address | Ensemble de libellés à utiliser |
collectName | boolean | payment, address | Ajoute le champ de nom du payeur |
fields.phone | always, auto, never | payment, address | Visibilité du champ de téléphone |
fields.customer | auto ou external | Tous sauf address | external ne demande plus le contact, le nom ni l'adresse du payeur, votre serveur les envoie |
fields.country | ISO 3166 alpha-2 | mobileMoney, payment | Démarre sur ce pays, et masque le sélecteur de pays quand le moyen le couvre |
fields.operator | choose ou detect | mobileMoney, payment | detect lit l'opérateur dans le numéro au lieu de lister les opérateurs, et ne les liste que si le numéro ne permet pas de reconnaître l'opérateur |
variant | default ou fields | Tous sauf address | fields retire la marge intérieure et le fond, pour votre propre bloc de moyen de paiement |
onBeforeSubmit | () => boolean | Promise<boolean> | Tous sauf address | S'exécute avant le débit. false ou une exception l'arrête |
frameTitle | string | Tous | Le nom du composant pour les lecteurs d'écran, dans la langue de votre page. Par défaut Wajub <type> component, par exemple Wajub mobileMoney component |
loader | none ou skeleton | Tous | Ce que votre page affiche pendant le chargement du composant. none par défaut. Voir Charger sans décaler la page |
readyTimeout | number, en millisecondes | Tous | Durée d'attente de ready avant que onLoadError reçoive ready_timeout. 0 ou absent : aucune limite |
factory
.create('address', { addressMode: 'shipping', collectName: true, fields: { phone: 'auto' } })
.mount('#address');
factory
.create('payment', { collectAddress: 'shipping', collectName: true, layout: 'tabs' })
.mount('#payment');Charger sans décaler la page
Par défaut, avec loader: 'none', votre page bouge une fois, quand le composant prend sa hauteur.
loader: 'skeleton' tient la place dès mount().
const momo = factory
.create('mobileMoney', {
variant: 'fields',
fields: { country: 'CM', operator: 'detect' },
loader: 'skeleton',
readyTimeout: 10000,
})
.mount('#momo-fields');
momo.focus(); // appelé pendant le chargement : le focus entre quand le formulaire est prêt| Moment | Ce que montre votre page |
|---|---|
Dès mount() | Le conteneur garde la hauteur du formulaire, et un squelette du formulaire, dessiné dans le style de vos champs, pulse légèrement. Le conteneur porte aria-busy="true" |
À ready | Le formulaire apparaît en fondu pendant que le squelette s'efface |
focus() pendant le chargement | Le focus entre dans le formulaire une fois celui-ci prêt, sauf si le payeur a tapé au clavier, cliqué ou déplacé le focus entre-temps |
unmount(), destroy() | Le conteneur retrouve ses propres styles, et un composant peut y être monté de nouveau |
Pour tenir la même place dans votre propre mise en page avant que Wajub.js soit chargé, dans un
rendu serveur et son hydratation par exemple, utilisez componentEstimatedHeight(type, variant, fields),
exporté par @wajub/js : il renvoie la même hauteur sur le serveur et dans le navigateur.
Avec une Content Security Policy stricte, la pulsation a besoin du nonce des scripts ou des styles
en ligne de votre page ; sans lui, le squelette reste simplement immobile. Il l'est aussi pour un
payeur qui demande moins d'animations, ou avec appearance.disableAnimations.
@wajub/js utilise loader: 'none' par défaut. Les composants de champs
React, Vue,
Svelte et Angular utilisent
skeleton par défaut.
Quand le formulaire n'arrive pas
readyTimeout, en millisecondes, borne l'attente de ready, comptée depuis chaque mount().
Au-delà, onLoadError et l'événement loaderror reçoivent une WajubError de code
ready_timeout, marquée retryable, et un confirmPayment() qui attend le composant rejette avec
la même erreur. Sans readyTimeout, vous n'êtes pas prévenu : donnez-le.
Un cadre bloqué, une origine que votre compte n'autorise pas ou une panne réseau en sont les causes habituelles. Proposez au payeur un autre moyen de payer plutôt qu'un bloc vide : retirez les champs et envoyez le payeur sur la page hébergée, comme le fait Mobile Money et carte dans votre propre checkout.
onLoadError et loaderror disent pourquoi un composant n'a jamais démarré. Rien n'a été payé.
| Code | Ce qui s'est passé | Que faire |
|---|---|---|
session_not_found | Le jeton est inconnu, ou la session n'a pas pu être lue | Vérifiez le jeton que votre serveur a remis à la page |
session_terminal_expired, session_terminal_cancelled, session_terminal_failed | La session est déjà terminée sans paiement : elle a expiré, a été annulée, ou s'est terminée sur un paiement échoué. Le composant affiche un message à la place du formulaire | Créez un nouveau paiement et montez les champs sur son jeton |
component_origin_missing | Le composant n'a pas été monté avec Wajub.js ou un paquet framework | Montez-le avec l'un d'eux |
invalid_component | Un type de composant qui n'existe pas | Utilisez l'un des sept composants |
method_unavailable | La session ne propose aucun moyen que ce composant accepte | Lisez fetchSession().payment_methods avant le montage, et montez un composant avec lequel la session peut être payée |
load_error | Le composant n'a pas pu se charger | Vérifiez l'onglet réseau et votre Content Security Policy |
ready_timeout | Pas de ready dans le délai de readyTimeout | Proposez un autre moyen de payer, comme ci-dessus |
Un composant dont la session est déjà payée affiche son message et envoie ready, pas une erreur :
le paiement est déjà fait, traitez donc la commande depuis votre webhook.
Événements
Abonnez-vous avec .on(event, handler), qui renvoie le composant et permet de chaîner les appels.
| Événement | Payload | Déclenchement |
|---|---|---|
ready | ComponentInstance | Formulaire affiché et interactif, ou aussitôt quand le composant n'a aucun formulaire à afficher |
change | { complete, empty, error, value } | Modification d'un champ ou de sa validité |
focus | Rien | Prise de focus par un champ |
blur | Rien | Perte de focus par un champ |
success | Transaction | Réussite du paiement |
error | WajubError | Échec du paiement, ou refus d'un envoi que vous avez demandé |
loaderror | WajubError | Le composant n'a pas pu démarrer : sa session est terminée, il n'a pas pu se charger, ou pas de ready dans le délai readyTimeout. Les codes |
resize | { height } | Agrandissement ou réduction du composant, ou écran de suite d'un envoi qui le recouvre |
statechange | { state, method } | Passage du checkout à un autre état |
methodchange | { methodId } | Choix d'un autre moyen de paiement par le payeur |
redirect | { url, method } | Départ imminent de votre page vers la page du prestataire |
Chaque événement a aussi un callback dans ComponentConfig, appelé juste avant lui : onReady,
onChange et les autres. Le onResize d'un composant reçoit { height }, alors que celui du
checkout hébergé reçoit la hauteur elle-même.
on() et off() typent la donnée de chaque événement connu avec ComponentEventMap :
component.on('statechange', (event) => event.state) passe la vérification des types.
component.on('change', (event) => {
payButton.disabled = !event.complete;
errorLabel.textContent = event.error ? event.error.message : '';
});complete signifie valide, pas payé
change.complete indique que les champs réussiraient la validation. L'argent est transféré lors
de success, mais seul votre webhook le confirme.
Verrouiller votre checkout pendant le paiement
Une fois le paiement activé par votre serveur, son montant est fixé. Pendant le paiement, un payeur
qui change de moyen, de livraison ou de code promo sur votre page lui laisserait un total qui ne
correspond plus. statechange indique quand le paiement est en cours : gardez votre propre checkout
verrouillé tant que l'état est PROCESSING, VERIFYING, PENDING, OTP_REQUIRED,
PIN_REQUIRED, PHONE_REQUIRED, BIRTHDAY_REQUIRED, ADDRESS_REQUIRED, 3DS_REQUIRED,
USSD_REQUIRED ou APPROVAL_REQUIRED. APPROVAL_REQUIRED correspond au payeur qui valide sur son
téléphone.
const RUNNING = [
'PROCESSING', 'VERIFYING', 'PENDING', 'OTP_REQUIRED', 'PIN_REQUIRED', 'PHONE_REQUIRED',
'BIRTHDAY_REQUIRED', 'ADDRESS_REQUIRED', '3DS_REQUIRED', 'USSD_REQUIRED', 'APPROVAL_REQUIRED',
];
const momo = factory.create('mobileMoney', {
onStateChange: ({ state }) => {
const locked = RUNNING.includes(state);
checkoutOptions.disabled = locked; // un <fieldset> : moyens, livraison, code promo
payButton.disabled = locked;
hint.textContent = state === 'APPROVAL_REQUIRED' ? 'Validez le paiement sur votre téléphone.' : '';
},
});Verrouillez vos propres contrôles, jamais le composant : laissez son conteneur visible et accessible, car les étapes auxquelles répond le payeur s'y trouvent.
Envoyer le paiement
confirmPayment reçoit la session et un composant monté, l'envoie et attend le résultat.
import type { WajubError } from '@wajub/js';
try {
const { status, transaction } = await confirmPayment({ sessionId, components: card });
showReceipt(status, transaction);
} catch (error) {
// A caught value is `unknown` in strict mode: confirmPayment rejects with a WajubError.
const { code, message } = error as WajubError;
showRetry(code, message);
}| Option | Valeur par défaut | Fonction |
|---|---|---|
sessionId | Obligatoire | Session utilisée pour créer le composant |
components | Obligatoire | Un composant card, mobileMoney, bankTransfer, crypto ou payment monté |
timeout | 60000 | Millisecondes d'attente du résultat avant le rejet avec confirmation_timeout. 0 attend indéfiniment. Le temps pendant lequel le paiement attend le payeur ne compte pas |
Trois rejets se produisent avant tout envoi. Ils signalent donc des erreurs d'intégration, pas des échecs de paiement.
| Code | Cause |
|---|---|
missing_component | Aucun composant transmis ou objet incapable d'envoyer le paiement |
wallet_not_supported | Composant wallet, dont le bouton natif déclenche le paiement |
address_not_payment | Composant address, à lire avec getValue() |
Une fois l'envoi fait, c'est le paiement qui met fin à l'attente, ou le délai.
| Code | Cause |
|---|---|
confirmation_timeout | Aucun résultat reçu avant timeout. Le paiement continue et peut encore réussir |
checkout_validation | onBeforeSubmit a renvoyé false, a levé une exception ou a mis plus de 30 secondes |
submission_failed | Le composant a refusé votre envoi avant tout débit : un champ qui n'est pas prêt, un numéro de carte refusé, ou une fenêtre de paiement fermée ou qui n'a pas pu s'ouvrir |
payment_error | L'API a refusé le débit. Sur un paiement préparé, il n'était pas encore activé |
payment_cancelled | Le payeur a annulé. Peut être retenté |
session_expired | La session a expiré avant la fin du paiement |
component_unmounted | Le composant a été démonté, détruit ou retiré de la page pendant l'attente |
confirmation_timeout n'annule rien
Il met seulement fin à votre attente. Le payeur peut encore valider sur son téléphone, et le
paiement peut réussir après le rejet. Avant de proposer de payer à nouveau, écoutez success sur
le composant ou attendez le webhook payment.succeeded.
timeout ne court pas pendant que le paiement attend le payeur, OTP_REQUIRED, PIN_REQUIRED,
PHONE_REQUIRED, BIRTHDAY_REQUIRED, ADDRESS_REQUIRED, APPROVAL_REQUIRED, USSD_REQUIRED,
3DS_REQUIRED ou PENDING, ni une fois que votre page est partie chez le prestataire
(redirect). Un composant remonté repart de INITIATED.
Un refus porte son code de refus, et un composant qui ne se charge pas rejette avec son erreur de
chargement, ready_timeout compris quand vous définissez readyTimeout.
submission_failed répond à un envoi que vous avez demandé, avec confirmPayment() ou submit(),
et que le composant a refusé avant tout débit : un numéro Mobile Money vide ou invalide, aucun
opérateur choisi, une carte laissée incomplète, des champs encore en chargement, un virement sans
IBAN ni numéro de compte, un paiement en crypto sans actif choisi, un numéro de carte refusé,
l'e-mail du payeur manquant. Son message indique pourquoi, dans la langue du payeur. Affichez-le
et réactivez votre bouton : le payeur corrige le champ et vous envoyez de nouveau. Si le payeur
appuie sur Entrée dans un formulaire qui n'est pas prêt, le composant lui montre la raison et vous
ne recevez rien, puisque vous n'avez rien envoyé.
Pendant le paiement, un nouvel envoi ne fait rien : votre bouton, ou Entrée dans un champ, ne
débite pas une seconde fois et ne rappelle pas onBeforeSubmit, et c'est le paiement en cours qui
vous donne son résultat.
Certaines cartes se paient dans la fenêtre sécurisée du prestataire, qui s'ouvre au-dessus de votre
page une fois la commande acceptée par onBeforeSubmit. Quand le navigateur la bloque, error
apporte submission_failed avec un message qui le dit, et le composant affiche un bouton qui ouvre
la fenêtre pour le même paiement, sans rappeler onBeforeSubmit.
Quand le paiement se poursuit sur la page du prestataire
Certains paiements se terminent sur la page du prestataire, un paiement en crypto par exemple.
Toute votre page s'y rend, et le composant émet redirect avec { url, method } juste avant son
départ.
component.on('redirect', () => {
// La page part dès le retour de ce handler, une tâche asynchrone peut ne pas se terminer.
sessionStorage.setItem('pending-order', orderId);
});Le callback onRedirect de create() reçoit le même { url, method }, juste avant l'événement.
Votre page part dans tous les cas, même quand le callback ou un écouteur lève une erreur.
Votre page est partie, et le composant avec elle : success n'y est pas émis, et
confirmPayment() ne se termine pas. Selon le moyen de paiement, le payeur revient soit par la page
Wajub de la session, qui affiche le résultat puis l'envoie sur le callback du paiement avec
status, reference et trxref, comme après un checkout avec redirection, soit directement sur
votre callback, sans ces paramètres. Sans callback, le résultat reste affiché sur la page Wajub.
La page callback | Pourquoi |
|---|---|
| Existe, en HTTPS en live | Définissez-la sur POST /payments, ou à l'activation d'un paiement préparé. Sans elle, le payeur n'est pas ramené sur votre site. Dans la sandbox, http://localhost et http://127.0.0.1 sont aussi acceptés |
| Lit le résultat sur votre serveur | Le payeur peut revenir sans status, reference ni trxref : faites porter la commande par l'URL, /orders/4172/return par exemple. Dans tous les cas, lisez GET /payments/{id}, ou attendez le webhook payment.succeeded |
| Affiche un paiement encore en attente | Un paiement en crypto peut encore être en cours de confirmation sur la blockchain au retour du payeur. Indiquez qu'il est en route, et laissez le webhook finaliser la commande |
Avant que la commande existe
Un checkout affiche ses moyens de paiement avant que la commande soit passée : l'acheteur choisit
encore une livraison, saisit un code promo, change de pays. Créez la session à ce moment-là avec
deferred: true : le paiement est seulement préparé. Il reste hors de vos paiements et n'envoie
aucun webhook tant que votre serveur ne l'a pas activé, une fois la commande passée. Un acheteur
qui s'en va ne laisse aucune trace, tant que vous n'envoyez aucun client avant l'activation : un
client envoyé à la préparation est créé, avec son customer.created.
| Étape | Où | Appel |
|---|---|---|
| Le checkout s'ouvre | Votre serveur | POST /payments avec deferred: true, le montant et la devise |
| Le total change | Votre serveur | PATCH /payments/{id} avec le nouveau amount et la currency |
| L'acheteur paie | Votre serveur, depuis onBeforeSubmit | Passer la commande, puis POST /payments/{id}/activate |
| Le débit | Le composant | S'exécute dès que onBeforeSubmit a renvoyé true |
curl https://api.wajub.com/payments \
-H "Authorization: sk_test.xxxxxxxxxxxxxxxx…" \
-H "Content-Type: application/json" \
-d '{ "amount": 35000, "currency": "XOF", "deferred": true, "expires": { "in": 30 } }'Aucun client n'est encore nécessaire. La réponse a la forme d'un paiement créé : envoyez
authorization_token au navigateur comme sessionId, et gardez transaction.id avec le panier.
Quand le total change, modifiez ce même paiement au lieu d'en préparer un autre. Les composants
montés gardent leur session.
curl -X PATCH https://api.wajub.com/payments/trx_test_CSUGajfv9xh0XQ5wu2lx \
-H "Authorization: sk_test.xxxxxxxxxxxxxxxx…" \
-H "Content-Type: application/json" \
-d '{ "amount": 37500, "currency": "XOF" }'PATCH soumet le nouveau total aux règles de POST /payments. Un montant hors des limites de la
devise répond 422 avec errors.amount. Sur un paiement préparé avec accept, un total qu'aucun
des moyens conservés ne peut prendre répond 422 avec errors.accept. Dans les deux cas, le
paiement garde son total précédent.
PATCH accepte aussi expires, { "in": 120 } par exemple : une nouvelle expiration comptée à
partir de maintenant, de 5 à 43 200 minutes, que le paiement garde une fois activé. Envoyez-la
quand le checkout reste ouvert plus longtemps que prévu. Sans elle, l'expiration ne change pas. Une
fois le paiement expiré, annulé ou activé, PATCH répond 409 : préparez-en alors un nouveau.
Dans le navigateur, c'est dans onBeforeSubmit que la commande est passée. Le composant l'appelle
une fois les champs remplis par le payeur, avant tout débit.
const factory = await components(sessionId, { locale: 'fr' });
const momo = factory
.create('mobileMoney', {
fields: { customer: 'external' },
onBeforeSubmit: async () => {
// Votre serveur vérifie le panier, passe la commande, puis active le paiement.
const response = await fetch('/checkout/place-order', { method: 'POST' });
return response.ok;
},
})
.mount('#momo');
payButton.onclick = () => confirmPayment({ sessionId, components: momo });curl https://api.wajub.com/payments/trx_test_CSUGajfv9xh0XQ5wu2lx/activate \
-H "Authorization: sk_test.xxxxxxxxxxxxxxxx…" \
-H "Content-Type: application/json" \
-d '{
"amount": 37500,
"currency": "XOF",
"reference": "order-4172",
"description": "Order #4172",
"customer": { "email": "buyer@example.com", "name": "Amina Diallo" },
"metadata": { "order_id": "4172" }
}'Le paiement existe désormais, avec l'id que vous avez gardé, et l'événement payment.created
est envoyé. Le jeton
de session du navigateur reste le même : le composant débite sans être remonté.
reference et description nomment le paiement d'après la commande, qui n'existait pas encore à
la préparation : la référence sur laquelle vos webhooks font la correspondance, et le libellé que
lit le payeur. Elles remplacent celles données à la préparation. Absentes, celles de la
préparation restent.
| Règle | Pourquoi |
|---|---|
| L'activation reprend le total préparé | Un autre amount ou une autre currency répond 422. Faites d'abord un PATCH |
| L'activation peut être rejouée | Le même corps répond de nouveau 200. Un autre corps, y compris une autre reference ou description, répond 409 |
| L'activation demande la clé secrète | Une clé publique répond 406 |
onBeforeSubmit doit répondre en moins de 30 secondes | Une attente plus longue arrête le paiement avec checkout_validation |
fields.customer: 'external' | Le payeur ne ressaisit pas ses coordonnées : l'activation apporte le client. Un prestataire de carte qui redirige peut encore demander un e-mail |
DELETE /payments/{id} avant l'activation | Met fin à la session, sans webhook |
callback à l'activation | L'adresse où arrive le payeur après un paiement poursuivi sur la page du prestataire. Voir Quand le paiement se poursuit sur la page du prestataire |
onBeforeSubmit est votre parcours de commande, pas une protection
Tant qu'il n'est pas activé, un paiement préparé refuse tout débit, quoi que le navigateur envoie. N'activez que depuis votre serveur, après vos propres vérifications, avec le montant calculé par votre serveur.
Dans votre propre bloc de moyen de paiement
Quand votre checkout liste lui-même les moyens de paiement, un bloc par moyen avec un bouton radio,
montez le composant dans le bloc sélectionné avec variant: 'fields'. Il retire sa marge
intérieure et son fond : les champs s'insèrent dans votre bloc, qui apporte le titre, la bordure et
le logo. Le Mobile Money remplace aussi son panneau d'instructions par une indication d'une ligne.
Les autres types gardent leur panneau. Le composant prend la hauteur de ses champs.
Les composants de champs React, Vue, Svelte et Angular utilisent loader: 'skeleton' par défaut :
votre bloc finit à la hauteur des champs. Avec loader: 'none', ils tiennent une hauteur minimale
à eux : passez 0, minHeight={0}, :min-height="0" ou [minHeight]="0", pour que votre bloc
épouse les champs. Un minHeight que vous passez, ou en React, Vue et Angular un min-height dans
style, est gardé pendant toute la vie du composant.
factory
.create('mobileMoney', {
variant: 'fields',
fields: { customer: 'external', country: 'CM', operator: 'detect' },
frameTitle: 'Paiement Mobile Money',
onBeforeSubmit: placeOrder,
})
.mount('#method-mobile-money .fields');| Option | Effet sur le Mobile Money |
|---|---|
fields.country: 'CM' | Démarre sur ce pays et masque le sélecteur quand le moyen le couvre. Passez le pays que votre formulaire a déjà demandé |
fields.operator: 'detect' | Pas de liste d'opérateurs. L'opérateur est lu dans le numéro et affiché dans le champ de téléphone. La liste n'apparaît que si le numéro saisi ne correspond à aucun opérateur accepté par ce paiement |
Mobile Money et carte dans votre propre checkout
Votre checkout garde son propre formulaire, ses propres blocs de moyen de paiement et son propre bouton « Payer ». Chaque bloc n'affiche que les champs de son moyen, dans le style de votre page, et la commande est passée quand le payeur paie. Cette recette assemble les sections ci-dessus.
- Votre serveur prépare la session à l'ouverture du checkout.
POST /paymentsavecdeferred: true, le montant, la devise et une expiration, avec votre clé secrète. Envoyez à la pageauthorization_token, sonsessionId, etauthorization_url, la page hébergée de secours. Gardeztransaction.idavec le panier, et faites unPATCHdu même paiement quand le total change. Voir Avant que la commande existe. - La page monte les champs du moyen choisi par le payeur, dans son bloc :
variant: 'fields',appearance: { inherit: true },loader: 'skeleton',readyTimeoutetfields: { customer: 'external' }, aveccountryetoperator: 'detect'pour le Mobile Money.inheritlit un champ texte de votre formulaire, son libellé, son bouton d'envoi et le fond derrière le bloc : affichez donc vos propres champs d'abord. Changer de moyen détruit un composant et monte l'autre, sur la même session. onBeforeSubmitpasse la commande. Votre serveur vérifie le panier, crée la commande et active le paiement avecPOST /payments/{id}/activate: le total final, le client, votrereferenceet lecallbackoù revient le payeur. Renvoyezfalsepour arrêter : rien n'est débité.- Votre bouton « Payer » envoie le paiement, avec
confirmPayment(), ou avecsubmit()et les callbacksonSuccessetonError. Letimeoutpar défaut suffit : il ne court pas pendant que le payeur valide. onStateChangegarde votre checkout verrouillé pendant le paiement, et demande au payeur de valider sur son téléphone pendantAPPROVAL_REQUIRED. Voir Verrouiller votre checkout pendant le paiement.onLoadErroravecready_timeoutse replie sur la page hébergée. Retirez les champs et faites de votre bouton « Payer » un passage vers la page hébergée. Un paiement préparé ne peut pas être débité avant son activation : ce bouton passe donc d'abord la commande, commeonBeforeSubmit, puis envoie le navigateur surauthorization_url.- Après
success, emmenez le payeur sur votre page de confirmation, et livrez sur le webhookpayment.succeededouGET /payments/{id}, jamais sur le seul navigateur. Un refus s'affiche dans le composant, et le payeur peut réessayer dans le même formulaire.
import { components, confirmPayment } from '@wajub/js';
// Préparé par votre serveur à l'ouverture du checkout (POST /payments, deferred: true).
const { sessionId, hostedUrl } = await fetch('/checkout/payment', { method: 'POST' }).then((r) => r.json());
const RUNNING = [
'PROCESSING', 'VERIFYING', 'PENDING', 'OTP_REQUIRED', 'PIN_REQUIRED', 'PHONE_REQUIRED',
'BIRTHDAY_REQUIRED', 'ADDRESS_REQUIRED', '3DS_REQUIRED', 'USSD_REQUIRED', 'APPROVAL_REQUIRED',
];
const factory = await components(sessionId, { locale: 'fr' });
const state = { complete: false, running: false, fallback: false };
let fields = null;
function render() {
checkoutOptions.disabled = state.running; // un <fieldset> : moyens, livraison, code promo
payButton.disabled = state.running || !(state.complete || state.fallback);
}
// Votre serveur vérifie le panier, crée la commande et active le paiement.
async function placeOrder() {
const response = await fetch('/checkout/place-order', { method: 'POST', body: new FormData(checkoutForm) });
return response.ok; // false : rien n'est débité
}
function mountFields(method) {
fields?.destroy();
state.complete = false;
render();
fields = factory
.create(method, {
variant: 'fields',
appearance: { inherit: true },
loader: 'skeleton',
readyTimeout: 10000,
fields: method === 'mobileMoney'
? { customer: 'external', country: shippingCountry.value, operator: 'detect' }
: { customer: 'external' },
frameTitle: method === 'mobileMoney' ? 'Paiement Mobile Money' : 'Paiement par carte',
onChange: ({ complete }) => { state.complete = Boolean(complete); render(); },
onBeforeSubmit: placeOrder,
onStateChange: ({ state: step }) => {
state.running = RUNNING.includes(step);
hint.textContent = step === 'APPROVAL_REQUIRED' ? 'Validez le paiement sur votre téléphone.' : '';
render();
},
onLoadError: (error) => {
if (error.code !== 'ready_timeout') return;
fields.destroy();
fields = null;
state.fallback = true;
payButton.textContent = 'Continuer sur la page de paiement sécurisée';
render();
},
})
.mount(`#method-${method} .fields`);
}
for (const radio of methodRadios) {
radio.addEventListener('change', () => state.fallback || mountFields(radio.value));
}
mountFields('mobileMoney');
payButton.addEventListener('click', async () => {
if (state.fallback) {
// Un paiement préparé n'est débité qu'une fois activé : passez d'abord la commande.
if (await placeOrder()) window.location.assign(hostedUrl);
return;
}
try {
await confirmPayment({ sessionId, components: fields });
window.location.assign('/order/complete');
} catch {
// Le composant affiche la raison. Le payeur corrige et paie de nouveau.
}
});onBeforeSubmit peut s'exécuter de nouveau quand le payeur réessaie après un refus. Faites
répondre votre endpoint de commande avec la même commande : une activation avec le même corps
répond de nouveau 200. La même recette en
React,
Vue,
Svelte et
Angular utilise les
composants de champs de chaque paquet.
Piloter un composant
| Méthode | Fonction |
|---|---|
mount(container) | S'attache à un sélecteur ou un élément |
update({ locale, appearance, layout, frameTitle }) | Applique la modification en direct sans nouveau montage |
focus(), blur() | Donne ou retire le focus au premier champ. Avec loader: 'skeleton', focus() appelé pendant le chargement attend le formulaire |
submit() | Envoie sans confirmPayment, vous gérez les événements |
selectMethod(id) | Change de moyen de paiement sur un composant payment |
isComplete() | Indique si les champs réussiraient la validation |
getState() | CheckoutState actuel, INITIATED avant toute action |
getError() | Dernière WajubError, ou null |
getValue() | Valeurs de l'adresse, { name, phone, address, mode } |
unmount(), destroy() | Retire le composant du DOM, en conservant ou non l'instance |
update({ appearance }) change les clés que vous donnez et garde les autres. Une clé donnée à undefined ou null est effacée, et appearance: undefined ou null efface
toute l'apparence : le composant reprend le style de votre Dashboard Wajub, et son prochain
mount() part sans elle. Avec appearance.inherit, la page est relue à chaque appel, et retirer
inherit retire aussi ce qui avait été lu. update({ appearance: {} }) ne change aucune clé.
component.update({ appearance: { borderRadius: '16px' } }); // les autres clés restent
component.update({ appearance: { primaryColor: undefined } }); // efface cette clé
component.update({ appearance: undefined }); // retour au style de votre Dashboard WajubgetValue() renvoie null jusqu'à ce que le composant d'adresse signale une valeur lors de son
premier change. Lisez-la dans le handler ou lors de l'envoi par le payeur, pas au montage.
Avec un framework
En React, Vue et Svelte, chaque type a son propre composant, de CardComponent à
CryptoComponent, et ComponentEmbed prend le type en prop. En Angular, chacun est un composant
standalone, de <wajub-card> à <wajub-crypto>, et <wajub-component> prend son type. Dans
chaque framework :
- Chaque option est une prop,
loader,readyTimeout,onStateChange,onResizeetonRedirectcompris. En Angular, les options sont des inputs et les callbacks des outputs,(stateChange),(resize),(redirect)et les autres, etonBeforeSubmitest l'inputbeforeSubmit, car un output ne peut pas répondre.loadervautskeletonpar défaut. - Avec
skeleton, le conteneur tient la place du formulaire dès le premier rendu, rendu serveur compris.minHeightest décrit dans Dans votre propre bloc de moyen de paiement. - Un changement d'
appearance, delocale, delayoutou deframeTitles'applique au composant monté sans rechargement : un objet écrit en ligne convient donc. - Une clé de premier niveau d'
appearanceretirée depuis le dernier rendu est effacée, et uneappearancedevenueundefinedou{}revient à votre image de marque. - Un nouveau
sessionId,type,variant,fields,loader,readyTimeout,collectAddress,addressModeoucollectNamemonte un nouveau composant dans le même conteneur : le précédent est d'abord détruit, et le rappel d'instance reçoitnullentre les deux. - Un nouveau handler prend le relais sans recharger le formulaire.
useConfirmPayment,injectConfirmPayment()en Angular, lit la session au moment de confirmer : unconfirmgardé d'un rendu précédent paie la session actuelle, et unsessionIdpassé àconfirm()l'emporte, et le composant confirmé est toujours celui que vous passez.- En Angular, un objet
appearanceoufieldsmodifié sur place compte, comme un nouvel objet.
const { confirm, isConfirming } = useConfirmPayment(sessionId);
const field = useRef<ComponentInstance | null>(null);
<PaymentComponent sessionId={sessionId} onInstance={(i) => (field.current = i)} />;
<button disabled={isConfirming} onClick={() => field.current && confirm(field.current)}>
Pay now
</button>;useConfirmPayment protège contre les doubles envois. Un second clic lorsque isConfirming vaut
true est ignoré. Il réussit avec { status } et rejette avec une WajubError, dont
confirmation_timeout.
<MobileMoneyComponent
sessionId={sessionId}
frameTitle="Paiement Mobile Money"
// La page part juste après : rien d'asynchrone n'est sûr de finir.
onRedirect={rememberPendingOrder}
/>Le provider, les tableaux de props et l'intégration côté serveur figurent dans React, Vue, Svelte et Angular, chacun avec Mobile Money et carte dans votre propre checkout écrit pour ce framework.
Fonctionnalités abandonnées
| Fonctionnalité | Checkout hébergé | Champs de paiement |
|---|---|---|
| Moyens de paiement enregistrés | Oui | Oui |
| 3DS | Oui | Oui |
| Portefeuilles | Oui | Oui, avec le bouton natif |
| Coupons | Non | Non |
| Champs de checkout personnalisés | Oui | Non |
| Flux de livraison complet | Non | Bloc d'adresse uniquement |
| OTP, PIN, USSD et les autres étapes de suite | Oui | Oui |
Pages associées
- ApparenceStylisez les champs comme l'intégration.
- Afficher le checkoutLa page clé en main lorsque ce travail est trop important.
- Référence du SDKToutes les méthodes, tous les types et tous les événements au même endroit.
- Dépannage de Wajub ComponentsPourquoi rien ne s'affiche et pourquoi confirm rejette l'appel.