React
Provider, hooks, embed props, and the Next.js wiring that goes with them.
@wajub/react gives you a provider, a CheckoutEmbed, seven field components and four hooks. What
every option does is documented on Display the checkout and
Payment fields. This page covers what is specific to React.
@wajub/react
Stable · GAnpm
- Version
- 1.8.1
- Runtime
- React 18+ · Next.js 13+
Covers
- Payments
- Payment Links
Not in this package: Billing, Transfers, Sync, Shield, Tax.
Install
@wajub/js is a peer dependency, so it goes on the same line. The package is ESM only.
npm install @wajub/react @wajub/jsThe provider
Wrap the checkout in one WajubProvider. It covers a whole subtree, so it belongs at the layout or
page level rather than around each component.
'use client';
import { WajubProvider } from '@wajub/react';
export default function CheckoutLayout({ children }: { children: React.ReactNode }) {
return <WajubProvider>{children}</WajubProvider>;
}Hooks
useWajub throws while Wajub.js is loading
It throws until Wajub.js has loaded, and if the load failed. A component that calls it directly
under the provider will crash on its first render. Use useWajubOptional(), which returns null
instead, unless you already know the runtime is there.
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>
);
}| Hook | Returns | Notes |
|---|---|---|
useWajub() | { wajub, Wajub, WajubError } | Throws while loading, on failure, and with no runtime |
useWajubOptional() | The same, or null | Safe during SSR and while loading |
useLoadWajub() | { runtime, loading, error, loadWajub } | loading and error let you render a spinner or a retry |
useConfirmPayment(sessionId?) | { confirm(instance, options?), isConfirming } | Submits a mounted field component |
CheckoutEmbed
The hosted checkout inside your own layout. Every EmbeddedConfig option is a prop, plus these four.
'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)}
/>
);
}| Prop | Type | Default | What it does |
|---|---|---|---|
sessionId | string | Required | The authorization_token from POST /payments |
className | string | none | Class on the container element |
style | CSSProperties | none | Inline styles on the container |
minHeight | number | 480 | Reserved height in pixels while the checkout loads |
onReady | (instance) => void | none | Receives the CheckoutInstance to drive afterwards |
Props can change after mount, and a new sessionId loads a new checkout.
Payment fields
Seven components render one field group each: CardComponent, MobileMoneyComponent,
WalletComponent, PaymentComponent, AddressComponent, BankTransferComponent and
CryptoComponent. Every option and callback of
ComponentConfig is a prop, plus the props below.
ComponentEmbed is the same component with an explicit type, when the choice is made at runtime.
'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>
</>
);
}| Prop | Type | Default | What it does |
|---|---|---|---|
type | ComponentType | Set by the alias | Only on ComponentEmbed |
loader | 'skeleton' or 'none' | 'skeleton' | 'skeleton': the container holds the form's height from the first render, with a placeholder in your inputs' style. 'none': no placeholder |
minHeight | number | Unset | The container's minimum height in pixels. A minHeight in style counts the same |
readyTimeout | number | Unset | Milliseconds to wait for the form. Past it, onLoadError receives a WajubError with the code ready_timeout. Unset, no limit |
onInstance | (instance | null) => void | none | Fires on mount and again with null on unmount |
onReady | (instance) => void | none | Fires once the field is interactive |
onStateChange | ({ state, method }) => void | none | The payment moved to another step, APPROVAL_REQUIRED while the payer approves on the phone for example |
onResize | ({ height }) => void | none | The field took a new height, in pixels |
onRedirect | ({ url, method }) => void | none | The payment continues on the provider's own page, and your page is about to go there |
onBeforeSubmit | () => boolean | Promise<boolean> | none | Runs before the charge, to place your order: false, or an error, charges nothing. Set it from the field's first render |
Mobile Money and Card in your own checkout
Your checkout keeps its own form, its method choice and its Pay button, and each method shows only
its inputs, in your page's style. The steps, the server calls and the reason for each option are
on Payment fields.
Here, your server has prepared the payment with deferred: true when the checkout opened, and
handed the page its authorization_token and authorization_url.
'use client';
import { useRef, useState } from 'react';
import { CardComponent, MobileMoneyComponent, useConfirmPayment, type ComponentInstance } from '@wajub/react';
// The payment is under way, or waits on the payer: nothing on the page may change.
const RUNNING = [
'PROCESSING', 'VERIFYING', 'PENDING', 'OTP_REQUIRED', 'PIN_REQUIRED', 'PHONE_REQUIRED',
'BIRTHDAY_REQUIRED', 'ADDRESS_REQUIRED', '3DS_REQUIRED', 'USSD_REQUIRED', 'APPROVAL_REQUIRED',
];
// Your server checks the cart, places the order and activates the payment.
async function placeOrder() {
const response = await fetch('/checkout/place-order', { method: 'POST' });
return response.ok; // false: nothing is charged
}
// Rendered inside <WajubProvider>. sessionId and hostedUrl: the prepared payment's
// authorization_token and authorization_url, from your server.
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) {
// A prepared payment is charged only once activated: place the order first.
if (await placeOrder()) window.location.assign(hostedUrl);
return;
}
if (!component.current) return;
try {
await confirm(component.current);
window.location.assign('/thanks');
} catch {
// The form shows the reason, and the payer can try again in it.
}
}
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')} />
Card
</label>
</fieldset>
{fallback ? (
<p>You will finish on Wajub's secure payment page.</p>
) : (
<Fields
sessionId={sessionId}
variant="fields"
appearance={{ inherit: true }}
// loader="skeleton" is the default: the box keeps its height while the form loads.
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">Approve the payment on your phone.</p>}
<button type="button" disabled={locked || (!fallback && !complete)} onClick={pay}>
Pay
</button>
</>
);
}The fields show a declined payment themselves, and the payer tries again in them. When they do not
come up within readyTimeout, the Pay button places the order and then sends the payer to Wajub's
page: a prepared payment is charged only once activated. appearance.inherit reads a text input
of your page, its label, your main button and the background behind the fields, so render your own
form's fields before this step, or name them with
appearance={{ inherit: { input: '#email', button: '#pay' } }}. Mark the order paid on your
server, from the payment.succeeded webhook, never from the browser.
Next.js
The session is created on your server. Nothing in this package needs a secret key, and nothing should ever receive one.
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 });
}NEXT_PUBLIC_ is for the publishable key only
A variable prefixed NEXT_PUBLIC_ is inlined into the browser bundle. pk. belongs there, sk.
never does. A secret key sent from a browser is answered 403 and its owner is emailed about it.
Three more things the App Router asks for:
- Every component on this page is a client component. Mark the file
'use client', or import it from one that is. - Put
WajubProviderin a layout, so that it stays mounted while the payer moves through the checkout flow. - Pass
sessionIddown as a prop from a server component, or fetch it from the route above.
What the package exports
The exports are listed in the SDK reference, the types the package adds in TypeScript.