React
Wajub Components for React and Next.js — complete examples with server routes, hosted checkout, and payment fields.
20 min read
Install:
npm install @wajub/react @wajub/jsArchitecture (Next.js App Router)
- Server route creates the payment session with
sk_…. - Client component receives
sessionIdand rendersCheckoutEmbed. - Webhook confirms payment — not
onSuccessalone.
Full Next.js example
Server route
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const { cartId } = await req.json();
const res = await fetch('https://api.wajub.com/payments', {
method: 'POST',
headers: {
Authorization: process.env.WAJUB_SECRET_KEY!,
'Content-Type': 'application/json',
'Idempotency-Key': `cart-${cartId}`,
},
body: JSON.stringify({
amount: 25000,
currency: 'XAF',
customer: { email: 'buyer@example.com' },
description: `Cart ${cartId}`,
callback: `${process.env.NEXT_PUBLIC_APP_URL}/order/return`,
metadata: { cart_id: cartId },
}),
});
if (!res.ok) {
return NextResponse.json({ error: 'Payment init failed' }, { status: 502 });
}
const { authorization_token } = await res.json();
return NextResponse.json({ sessionId: authorization_token });
}Checkout page
import { CheckoutClient } from './CheckoutClient';
export default function CheckoutPage() {
return <CheckoutClient />;
}'use client';
import { useEffect, useState } from 'react';
import { WajubProvider, CheckoutEmbed } from '@wajub/react';
export function CheckoutClient() {
const [sessionId, setSessionId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('/api/checkout/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cartId: 'cart_123' }),
})
.then((r) => r.json())
.then((d) => setSessionId(d.sessionId))
.catch(() => setError('Could not start checkout'));
}, []);
if (error) return <p>{error}</p>;
if (!sessionId) return <p>Loading checkout…</p>;
return (
<WajubProvider>
<CheckoutEmbed
sessionId={sessionId}
layout="tabs"
appearance={{ primaryColor: '#6366f1' }}
onBreakdown={(b) => {
const el = document.getElementById('cart-total');
if (el) el.textContent = b.total.toLocaleString() + ' ' + b.currency;
}}
onSuccess={() => { window.location.href = '/order/complete'; }}
onError={(e) => setError(e.message)}
/>
</WajubProvider>
);
}Environment variables
Never prefix Wajub keys with NEXT_PUBLIC_. Keep WAJUB_SECRET_KEY
server-only.
Hosted checkout — minimal
'use client';
import { WajubProvider, CheckoutEmbed } from '@wajub/react';
export function PayPage({ sessionId }: { sessionId: string }) {
return (
<WajubProvider>
<CheckoutEmbed
sessionId={sessionId}
onSuccess={() => (window.location.href = '/thanks')}
onError={(e) => console.error(e)}
/>
</WajubProvider>
);
}CheckoutEmbed props
All EmbeddedConfig options
are supported as props: locale, layout, appearance, onReady, onSuccess, onError,
onCancel, onBreakdown, onLoadError, etc.
Additional React-only wrapper props:
| Prop | Default | Description |
|---|---|---|
className | — | CSS class on the container |
style | — | Inline styles on the container |
minHeight | 480 | Min height in px while loading |
Live appearance updates
CheckoutEmbed remounts when sessionId changes. For live
appearance / locale updates, capture the instance in
onReady and call instance.update({ appearance }).
Payment field components (CardComponent, etc.) call update()
automatically when those props change.
const checkoutRef = useRef<CheckoutInstance | null>(null);
<CheckoutEmbed
sessionId={sessionId}
onReady={(inst) => { checkoutRef.current = inst; }}
appearance={{ colorScheme: dark ? 'dark' : 'light' }}
/>
// Or imperatively:
checkoutRef.current?.update({ appearance: { primaryColor: '#0f172a' } });Payment fields — card + pay button
'use client';
import { useRef, useState } from 'react';
import type { ComponentInstance } from '@wajub/js';
import {
WajubProvider,
PaymentComponent,
AddressComponent,
useConfirmPayment,
} from '@wajub/react';
export function CustomCheckout({ sessionId }: { sessionId: string }) {
const confirm = useConfirmPayment(sessionId);
const paymentRef = useRef<ComponentInstance | null>(null);
const [paying, setPaying] = useState(false);
async function handlePay() {
if (!paymentRef.current) return;
setPaying(true);
try {
await confirm(paymentRef.current);
} finally {
setPaying(false);
}
}
return (
<WajubProvider>
<AddressComponent sessionId={sessionId} addressMode="shipping" />
<PaymentComponent
sessionId={sessionId}
collectAddress="shipping"
collectName
layout="tabs"
appearance={{ theme: 'stripe', primaryColor: '#0f172a' }}
onInstance={(c) => { paymentRef.current = c; }}
/>
<button onClick={handlePay} disabled={paying}>
{paying ? 'Processing…' : 'Pay now'}
</button>
</WajubProvider>
);
}Overlay modal
'use client';
import { useWajub } from '@wajub/react';
function UpgradeButton({ sessionId }: { sessionId: string }) {
const { wajub } = useWajub();
return (
<button onClick={() => wajub.open({
sessionId,
onSuccess: () => alert('Upgraded!'),
})}>
Upgrade plan
</button>
);
}
// Wrap UpgradeButton in WajubProvider at page levelAPI reference
| Export | Description |
|---|---|
WajubProvider | Loads SDK once; wrap app or checkout page |
CheckoutEmbed | Full hosted checkout inline |
CardComponent | Card fields only |
PaymentComponent | Method selector + form (+ optional address) |
AddressComponent | Shipping / billing address |
MobileMoneyComponent | Mobile Money form |
WalletComponent | Apple Pay / Google Pay |
useWajub() | { wajub, Wajub } — throws while loading |
useWajubOptional() | Same, or null during SSR / before load |
useLoadWajub() | Manually trigger SDK load |
useConfirmPayment(sessionId?) | Returns (component, opts?) => Promise |
Next.js tips
- Mark all embed components
"use client". - Put
WajubProviderat layout or page level — one load for the whole checkout flow. - Pass
sessionIdfrom a Server Component prop or client fetch to your API route. - For redirect flow instead of embed, see Integrate with React.