Champs de paiement
Votre mise en page et votre bouton de paiement, Wajub affiche uniquement les champs sécurisés.
Les champs de paiement inversent le principe du checkout hébergé. Vous conservez la page, le récapitulatif de commande et le bouton de paiement. Wajub affiche dans des iframes séparées les champs qui ne doivent pas toucher à votre DOM. Vous les envoyez lorsque vous êtes prêt.
Ce compromis vous donne le contrôle de la mise en page, mais retire les parties de la page hébergée qui nécessitent une page complète : vos propres champs de checkout personnalisés. Si vous en avez besoin, utilisez plutôt le checkout hébergé.
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 | Les champs de carte du fournisseur (Stripe), le bouton carte de PayPal, ou un bouton qui ouvre la fenêtre de paiement du fournisseur (Paystack, Flutterwave, FedaPay, CinetPay, Kkiapay) | 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
Chaque composant démarre avec min-height: 120px. Le SDK définit la hauteur de l'iframe depuis le
checkout à chaque modification du contenu. Vous n'avez rien à mesurer ni à réserver.
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 |
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 |
componentOrigin | string | Tous | Votre origine canonique pour les boutiques sur plusieurs domaines |
Les cinq lignes au-dessus de componentOrigin sont lues par l'environnement d'exécution servi
depuis js.wajub.com, quelle que soit la version du paquet installé. @wajub/js 1.5.0 apporte
leurs types. Avec un framework, React transmet toutes les options. @wajub/vue et @wajub/svelte
transmettent fields, onBeforeSubmit depuis la 1.5.0, et variant depuis la 1.5.1.
componentOrigin utilise par défaut l'origine de la page qui effectue le montage. Un domaine unique
ne nécessite donc aucune configuration.
factory
.create('address', { addressMode: 'shipping', collectName: true, fields: { phone: 'auto' } })
.mount('#address');
factory
.create('payment', { collectAddress: 'shipping', collectName: true, layout: 'tabs' })
.mount('#payment');Événements
Abonnez-vous avec .on(event, handler), qui renvoie le composant et permet de chaîner les appels.
Les mêmes handlers existent sous les noms onReady, onChange et autres dans ComponentConfig.
| Événement | Payload | Déclenchement |
|---|---|---|
ready | ComponentInstance | Composant monté et interactif |
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 |
loaderror | WajubError | Échec du chargement du composant |
resize | { height } | Agrandissement ou réduction du composant |
statechange | { state, method } | Passage du checkout à un autre état |
methodchange | { methodId } | Choix d'un autre moyen de paiement par le payeur |
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.
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 avant le rejet avec confirmation_timeout. 0 attend indéfiniment |
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 dépassé 30 secondes |
submission_failed | Un champ obligatoire manque, ou une carte payée dans la fenêtre du prestataire a échoué, a été fermée, ou son onBeforeSubmit a refusé |
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 quitté 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.
Un refus porte le code du prestataire, et un composant qui ne se charge pas rejette avec son erreur de chargement.
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 répond 409 une fois le paiement expiré ou activé. 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",
"customer": { "email": "[email protected]", "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é.
| 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 répond 409 |
| L'activation demande la clé secrète | Une clé publique répond 406 |
onBeforeSubmit dispose de 30 secondes | Au-delà, le paiement s'arrête 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 |
onBeforeSubmit est votre parcours de commande, pas une protection
Tant qu'il n'est pas activé, un paiement préparé refuse tout débit avec 409, 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.
factory
.create('mobileMoney', {
variant: 'fields',
fields: { customer: 'external', country: 'CM', operator: 'detect' },
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 |
Un moyen qui regroupe plusieurs réseaux sous un seul canal n'a besoin d'aucun opérateur, et n'en
affiche aucun. En Vue et en Svelte, variant demande @wajub/vue ou @wajub/svelte 1.5.1.
Piloter un composant
| Méthode | Fonction |
|---|---|
mount(container) | S'attache à un sélecteur ou un élément |
update({ locale, appearance, layout }) | Applique la modification en direct sans nouveau montage |
focus(), blur() | Donne ou retire le focus au premier champ |
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() fonctionne ici, contrairement à l'intégration hébergée
Un composant envoie locale, appearance et layout à son iframe, puis se redessine. Le checkout
hébergé applique tout sauf layout. Cette différence existe réellement.
getValue() 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
Chaque composant existe sous forme d'enveloppe prête à l'emploi, avec les mêmes options comme props,
variant compris depuis la 1.5.1 en Vue et en Svelte. bankTransfer et crypto n'ont pas d'enveloppe propre :
utilisez ComponentEmbed avec type="bankTransfer" ou type="crypto". L'enveloppe surveille
aussi appearance, locale et layout, puis appelle update() pour vous.
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>;Le provider, les tableaux de props et l'intégration côté serveur figurent dans React, Vue et Svelte.
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, désactivés aujourd'hui | Non |
| Champs de checkout personnalisés | Oui | Non |
| Flux de livraison complet | Non, désactivé aujourd'hui | 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.
- Checkout hébergéLa page clé en main lorsque ce travail est trop important.
- Référence APIToutes 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.