Aller au contenu

React

Provider, hooks, props d'intégration et configuration Next.js associée.

@wajub/react vous donne un provider, un CheckoutEmbed, sept composants de champs et quatre hooks. Le rôle de chaque option est présenté dans Afficher le checkout et Champs de paiement. Cette page traite des éléments propres à React.

@wajub/react

Stable · GA

npm

Version
1.8.1
Runtime
React 18+ · Next.js 13+

Couvre

  • Paiements
  • Liens de paiement

Absent de ce paquet : Facturation, Transferts, Sync, Shield, Taxes.

Installer

@wajub/js est une dépendance homologue. Installez-la donc sur la même ligne. Le package est uniquement ESM.

npm install @wajub/react @wajub/js

Provider

Un seul WajubProvider suffit autour du checkout : il couvre toute une sous-arborescence. Placez-le donc au niveau de la mise en page ou de la page, pas autour de chaque composant.

app/checkout/layout.tsx
'use client';

import { WajubProvider } from '@wajub/react';

export default function CheckoutLayout({ children }: { children: React.ReactNode }) {
  return <WajubProvider>{children}</WajubProvider>;
}

Hooks

La forme sûre
import { useWajubOptional } from '@wajub/react';

function UpgradeButton({ sessionId }: { sessionId: string }) {
  const runtime = useWajubOptional();

  return (
    <button
      disabled={!runtime}
      onClick={() => runtime?.wajub.open({ sessionId, onSuccess: onUpgraded })}
    >
      Upgrade plan
    </button>
  );
}
HookValeur renvoyéeRemarques
useWajub(){ wajub, Wajub, WajubError }Génère une erreur pendant le chargement, en cas d'échec ou sans environnement
useWajubOptional()La même valeur ou nullSûr pendant le SSR et pendant le chargement
useLoadWajub(){ runtime, loading, error, loadWajub }loading et error permettent d'afficher un indicateur de chargement ou une nouvelle tentative
useConfirmPayment(sessionId?){ confirm(instance, options?), isConfirming }Envoie un composant de champ monté

CheckoutEmbed

Le checkout hébergé dans votre propre mise en page. Chaque option de EmbeddedConfig est une prop, plus ces quatre.

app/checkout/CheckoutClient.tsx
'use client';

import { CheckoutEmbed } from '@wajub/react';

export function CheckoutClient({ sessionId }: { sessionId: string }) {
  return (
    <CheckoutEmbed
      sessionId={sessionId}
      layout="tabs"
      minHeight={480}
      onSuccess={() => (window.location.href = '/order/complete')}
      onError={(error) => console.error(error.code, error.message)}
    />
  );
}
PropTypeValeur par défautFonction
sessionIdstringObligatoireLa valeur authorization_token de POST /payments
classNamestringaucuneClasse de l'élément conteneur
styleCSSPropertiesaucunStyles en ligne du conteneur
minHeightnumber480Hauteur réservée en pixels pendant le chargement du checkout
onReady(instance) => voidaucunReçoit la CheckoutInstance à piloter ensuite

Les props peuvent changer après le montage, et un nouveau sessionId charge un nouveau checkout.

Champs de paiement

Les sept composants affichent chacun un groupe de champs : CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent, AddressComponent, BankTransferComponent et CryptoComponent. Chaque option et chaque callback de ComponentConfig est une prop, plus les props ci-dessous. ComponentEmbed est le même composant avec un type explicite lorsque le choix intervient pendant l'exécution.

Votre mise en page et votre bouton
'use client';

import { useRef } from 'react';
import type { ComponentInstance } from '@wajub/js';
import { PaymentComponent, useConfirmPayment } from '@wajub/react';

export function PayForm({ sessionId }: { sessionId: string }) {
  const { confirm, isConfirming } = useConfirmPayment(sessionId);
  const field = useRef<ComponentInstance | null>(null);

  return (
    <>
      <PaymentComponent
        sessionId={sessionId}
        collectAddress="shipping"
        collectName
        onInstance={(instance) => (field.current = instance)}
      />
      <button
        onClick={() => field.current && confirm(field.current)}
        disabled={isConfirming}
      >
        {isConfirming ? 'Processing…' : 'Pay now'}
      </button>
    </>
  );
}
PropTypeValeur par défautFonction
typeComponentTypeDéfinie par l'aliasUniquement sur ComponentEmbed
loader'skeleton' ou 'none''skeleton''skeleton' : le conteneur garde la hauteur du formulaire dès le premier rendu, avec un squelette dans le style de vos champs. 'none' : aucun squelette
minHeightnumberNon définieHauteur minimale du conteneur en pixels. Un minHeight dans style compte de la même façon
readyTimeoutnumberNon définieMillisecondes d'attente du formulaire. Au-delà, onLoadError reçoit une WajubError de code ready_timeout. Non définie, aucune limite
onInstance(instance | null) => voidaucuneSe déclenche au montage, puis avec null au démontage
onReady(instance) => voidaucuneSe déclenche lorsque le champ devient interactif
onStateChange({ state, method }) => voidaucuneLe paiement passe à une autre étape, par exemple APPROVAL_REQUIRED pendant que le payeur valide sur son téléphone
onResize({ height }) => voidaucuneLe champ prend une nouvelle hauteur, en pixels
onRedirect({ url, method }) => voidaucuneLe paiement continue sur la page du prestataire, et votre page va s'y rendre
onBeforeSubmit() => boolean | Promise<boolean>aucuneS'exécute avant le débit, pour passer votre commande : false, ou une erreur, ne débite rien. Définissez-le dès le premier rendu du champ

Mobile Money et carte dans votre propre checkout

Votre checkout garde son formulaire, son choix de moyen de paiement et son bouton Payer, et chaque moyen n'affiche que ses champs, dans le style de votre page. Les étapes, les appels serveur et la raison de chaque option sont dans Champs de paiement. Ici, votre serveur a préparé le paiement avec deferred: true à l'ouverture du checkout, et a transmis à la page son authorization_token et son authorization_url.

app/checkout/PaymentStep.tsx
'use client';

import { useRef, useState } from 'react';
import { CardComponent, MobileMoneyComponent, useConfirmPayment, type ComponentInstance } from '@wajub/react';

// Le paiement est en cours, ou attend le payeur : rien ne doit changer sur la page.
const RUNNING = [
  'PROCESSING', 'VERIFYING', 'PENDING', 'OTP_REQUIRED', 'PIN_REQUIRED', 'PHONE_REQUIRED',
  'BIRTHDAY_REQUIRED', 'ADDRESS_REQUIRED', '3DS_REQUIRED', 'USSD_REQUIRED', 'APPROVAL_REQUIRED',
];

// Votre serveur vérifie le panier, passe la commande et active le paiement.
async function placeOrder() {
  const response = await fetch('/checkout/place-order', { method: 'POST' });
  return response.ok; // false : rien n'est débité
}

// Rendu sous <WajubProvider>. sessionId et hostedUrl : l'authorization_token et
// l'authorization_url du paiement préparé, transmis par votre serveur.
export function PaymentStep({ sessionId, hostedUrl }: { sessionId: string; hostedUrl: string }) {
  const { confirm, isConfirming } = useConfirmPayment(sessionId);
  const component = useRef<ComponentInstance | null>(null);
  const [method, setMethod] = useState<'mobileMoney' | 'card'>('mobileMoney');
  const [complete, setComplete] = useState(false);
  const [step, setStep] = useState('INITIATED');
  const [fallback, setFallback] = useState(false);
  const locked = isConfirming || RUNNING.includes(step);
  const Fields = method === 'card' ? CardComponent : MobileMoneyComponent;

  async function pay() {
    if (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;
    }
    if (!component.current) return;
    try {
      await confirm(component.current);
      window.location.assign('/thanks');
    } catch {
      // Le formulaire affiche la raison, et le payeur peut y réessayer.
    }
  }

  return (
    <>
      <fieldset disabled={locked}>
        <label>
          <input type="radio" checked={method === 'mobileMoney'} onChange={() => setMethod('mobileMoney')} />
          Mobile Money
        </label>
        <label>
          <input type="radio" checked={method === 'card'} onChange={() => setMethod('card')} />
          Carte
        </label>
      </fieldset>

      {fallback ? (
        <p>Vous finirez sur la page de paiement sécurisée de Wajub.</p>
      ) : (
        <Fields
          sessionId={sessionId}
          variant="fields"
          appearance={{ inherit: true }}
          // loader="skeleton" par défaut : le bloc garde sa hauteur pendant le chargement.
          readyTimeout={10000}
          fields={method === 'card'
            ? { customer: 'external' }
            : { customer: 'external', country: 'CM', operator: 'detect' }}
          onBeforeSubmit={placeOrder}
          onInstance={(instance) => {
            component.current = instance;
            setComplete(false);
          }}
          onChange={(event) => setComplete(Boolean(event.complete))}
          onStateChange={({ state }) => setStep(state)}
          onLoadError={(error) => {
            if (error.code === 'ready_timeout') setFallback(true);
          }}
        />
      )}

      {step === 'APPROVAL_REQUIRED' && <p role="status">Validez le paiement sur votre téléphone.</p>}
      <button type="button" disabled={locked || (!fallback && !complete)} onClick={pay}>
        Payer
      </button>
    </>
  );
}

Les champs affichent eux-mêmes un paiement refusé, et le payeur y réessaie. S'ils ne s'affichent pas dans le délai readyTimeout, le bouton Payer passe la commande puis envoie le payeur sur la page de Wajub : un paiement préparé n'est débité qu'une fois activé. appearance.inherit lit un champ texte de votre page, son libellé, votre bouton principal et le fond derrière les champs. Affichez donc les champs de votre propre formulaire avant cette étape, ou nommez-les avec appearance={{ inherit: { input: '#email', button: '#pay' } }}. Marquez la commande payée sur votre serveur, à partir du webhook payment.succeeded, jamais depuis le navigateur.

Next.js

La session est créée sur votre serveur. Aucun élément de ce package ne nécessite ni ne doit recevoir de clé secrète.

app/api/checkout/session/route.ts
import { NextResponse } from 'next/server';
import { Wajub } from '@wajub/node';

const wajub = new Wajub({ secretKey: process.env.WAJUB_API_KEY! });

export async function POST(req: Request) {
  const { cartId } = await req.json();

  const payment = await wajub.payments.create(
    {
      amount: 25000,
      currency: 'XAF',
      description: `Cart ${cartId}`,
      customer: { email: 'buyer@example.com' },
      callback: `${process.env.NEXT_PUBLIC_APP_URL}/order/return`,
      metadata: { cart_id: cartId },
    },
    { idempotencyKey: `cart-${cartId}` },
  );

  return NextResponse.json({ sessionId: payment.authorization_token });
}

L'App Router impose trois autres règles :

  • Tous les composants de cette page sont des composants client. Marquez le fichier avec 'use client' ou importez-le depuis un fichier qui le fait.
  • Placez WajubProvider dans une mise en page, pour qu'il reste monté pendant que le payeur avance dans le flux de checkout.
  • Transmettez sessionId comme prop depuis un composant serveur ou récupérez-le depuis la route ci-dessus.

Éléments exportés par le package

Les exports sont listés dans la Référence du SDK, les types que le package ajoute dans TypeScript.

Que pensez-vous de ce contenu ?