React
Provider, hooks, embed props, and the Next.js wiring that goes with them.
@wajub/react wraps @wajub/js in React syntax: a provider that loads the runtime once, five field
components, and four hooks. The payment API itself does not change, so the behaviour of every
option is documented on Hosted checkout and
Payment fields. This page covers what is specific to React.
@wajub/react
Stable · GAnpm
- Version
- 1.4.0
- 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, not a bundled one, so it goes on the same line. The package is ESM
only and declares sideEffects: false.
npm install @wajub/react @wajub/jsThe provider
WajubProvider calls loadWajub() from @wajub/js/pure and holds the runtime in context. One
provider 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>;
}| Prop | Type | Default | What it does |
|---|---|---|---|
loadOptions | { jsOrigin?, jsUrl? } | CDN defaults | Overrides where the runtime is fetched from |
defer | boolean | false | Skips the automatic load, leaving it to useLoadWajub() |
defer is for the case where a parent already loaded the runtime, or where you want the fetch to
wait for a user action. Without it, the provider starts loading as soon as it mounts.
Hooks
useWajub throws while the runtime is loading
It throws on every render until the script has arrived, and again 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 before the script lands |
useLoadWajub() | { runtime, loading, error, loadWajub } | Drives the load yourself, with defer |
useConfirmPayment(sessionId) | { confirm, isConfirming } | Submits a mounted field component |
loading and error come straight from the provider's context, so useLoadWajub() is also how you
render a spinner or a retry while the runtime is on its way.
CheckoutEmbed
The hosted checkout inside your own layout. Every EmbeddedConfig option is a prop, and four more belong to the wrapper.
'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 iframe loads |
onReady | (instance) => void | none | Receives the CheckoutInstance to drive afterwards |
Only sessionId remounts the embed
The component mounts once per session and never calls update() on its own. Changing
appearance, locale or layout after the first render does nothing. Keep the instance from
onReady and drive it yourself.
const checkout = useRef<CheckoutInstance | null>(null);
<CheckoutEmbed sessionId={sessionId} onReady={(instance) => (checkout.current = instance)} />;
useEffect(() => {
checkout.current?.update({ appearance: { colorScheme: dark ? 'dark' : 'light' } });
}, [dark]);Payment fields
Five components render one field group each: CardComponent, MobileMoneyComponent,
WalletComponent, PaymentComponent and AddressComponent. They share the props of
ComponentConfig plus the wrapper 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 |
minHeight | number | 200 | Reserved height while the field loads |
onInstance | (instance | null) => void | none | Fires on mount and again with null on unmount |
onReady | (instance) => void | none | Fires once the field is interactive |
useConfirmPayment guards against a double submission, so a second click while isConfirming is
true is ignored. It resolves with { status } and rejects with a WajubError, including
confirmation_timeout after 60 seconds.
Field components do update in place
Unlike the embed, they watch appearance, locale and layout and call update() with all
three when any of them changes. Changing sessionId or type remounts the field instead.
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_SECRET_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}`,
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 a navigation inside the checkout flow does not reload the runtime. - Pass
sessionIddown as a prop from a server component, or fetch it from the route above.
What the package exports
| Export | What it is |
|---|---|
WajubProvider | Loads the runtime once for the subtree |
CheckoutEmbed | The hosted checkout, inline |
CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent, AddressComponent | One field group each |
ComponentEmbed | The same, with the type given as a prop |
useWajub, useWajubOptional, useLoadWajub | Reach the runtime |
useConfirmPayment | Submit a field component |
WajubContext, useWajubContext | The raw context, for your own hook |
Types come with the package: WajubProviderProps, CheckoutEmbedProps, ComponentEmbedProps and
WajubContextValue, alongside everything @wajub/js exports.