Aller au contenu

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

TypeAffichageDéclenchement du paiement
cardLes 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()
mobileMoneyOpérateur et numéro de téléphoneVous, avec confirmPayment()
bankTransferLe compte bancaire du payeurVous, avec confirmPayment()
cryptoL'actif avec lequel payerVous, avec confirmPayment()
paymentSélecteur de moyen de paiement et formulaire correspondantVous, avec confirmPayment()
addressAdresse de livraison ou de facturationPersonne, lisez-la avec getValue()
walletApple Pay ou Google PayBouton 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.

Champs de carte et bouton personnalisé
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

OptionTypeComposants concernésFonction
sessionIdstringTousObligatoire, défini une fois sur la fabrique
localestringTousfr, en, es, pt, ar
appearanceAppearanceConfigTousApparence
layoutCheckoutLayoutpaymentDisposition du sélecteur de moyen de paiement
collectAddressshipping ou billingpaymentAjoute un bloc d'adresse dans le formulaire
addressModeshipping ou billingaddressEnsemble de libellés à utiliser
collectNamebooleanpayment, addressAjoute le champ de nom du payeur
fields.phonealways, auto, neverpayment, addressVisibilité du champ de téléphone
fields.customerauto ou externalTous sauf addressexternal ne demande plus le contact, le nom ni l'adresse du payeur, votre serveur les envoie
fields.countryISO 3166 alpha-2mobileMoney, paymentDémarre sur ce pays, et masque le sélecteur de pays quand le moyen le couvre
fields.operatorchoose ou detectmobileMoney, paymentdetect lit l'opérateur dans le numéro au lieu de lister les opérateurs
variantdefault ou fieldsTous sauf addressfields retire la marge intérieure et le fond, pour votre propre bloc de moyen de paiement
onBeforeSubmit() => boolean | Promise<boolean>Tous sauf addressS'exécute avant le débit. false ou une exception l'arrête
componentOriginstringTousVotre 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.

Une adresse seule et un formulaire qui en contient une
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énementPayloadDéclenchement
readyComponentInstanceComposant monté et interactif
change{ complete, empty, error, value }Modification d'un champ ou de sa validité
focusRienPrise de focus par un champ
blurRienPerte de focus par un champ
successTransactionRéussite du paiement
errorWajubErrorÉchec du paiement
loaderrorWajubErrorÉ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
Contrôler votre propre bouton
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.

Réussite ou rejet avec une WajubError
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);
}
OptionValeur par défautFonction
sessionIdObligatoireSession utilisée pour créer le composant
componentsObligatoireUn composant card, mobileMoney, bankTransfer, crypto ou payment monté
timeout60000Millisecondes 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.

CodeCause
missing_componentAucun composant transmis ou objet incapable d'envoyer le paiement
wallet_not_supportedComposant wallet, dont le bouton natif déclenche le paiement
address_not_paymentComposant address, à lire avec getValue()

Une fois l'envoi fait, c'est le paiement qui met fin à l'attente, ou le délai.

CodeCause
confirmation_timeoutAucun résultat reçu avant timeout. Le paiement continue et peut encore réussir
checkout_validationonBeforeSubmit a renvoyé false, a levé une exception ou a dépassé 30 secondes
submission_failedUn 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_errorL'API a refusé le débit. Sur un paiement préparé, il n'était pas encore activé
payment_cancelledLe payeur a annulé. Peut être retenté
session_expiredLa session a expiré avant la fin du paiement
component_unmountedLe composant a quitté la page pendant l'attente

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.

ÉtapeOùAppel
Le checkout s'ouvreVotre serveurPOST /payments avec deferred: true, le montant et la devise
Le total changeVotre serveurPATCH /payments/{id} avec le nouveau amount et la currency
L'acheteur paieVotre serveur, depuis onBeforeSubmitPasser la commande, puis POST /payments/{id}/activate
Le débitLe composantS'exécute dès que onBeforeSubmit a renvoyé true
Préparer, à l'ouverture du checkout
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.

L'acheteur a choisi une livraison
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.

Monter maintenant, passer la commande à l'envoi
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 });
Activer, depuis votre endpoint de commande
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èglePourquoi
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éeLe même corps répond de nouveau 200. Un autre corps répond 409
L'activation demande la clé secrèteUne clé publique répond 406
onBeforeSubmit dispose de 30 secondesAu-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'activationMet fin à la session, sans webhook

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 Mobile Money dans son bloc radio
factory
  .create('mobileMoney', {
    variant: 'fields',
    fields: { customer: 'external', country: 'CM', operator: 'detect' },
    onBeforeSubmit: placeOrder,
  })
  .mount('#method-mobile-money .fields');
OptionEffet 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éthodeFonction
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ésOuiOui
3DSOuiOui
PortefeuillesOuiOui, avec le bouton natif
CouponsNon, désactivés aujourd'huiNon
Champs de checkout personnalisésOuiNon
Flux de livraison completNon, désactivé aujourd'huiBloc d'adresse uniquement
OTP, PIN, USSD et les autres étapes de suiteOuiOui

Que pensez-vous de ce contenu ?