Skip to content

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 · GA

npm

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/js

The 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.

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

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

export default function CheckoutLayout({ children }: { children: React.ReactNode }) {
  return <WajubProvider>{children}</WajubProvider>;
}
PropTypeDefaultWhat it does
loadOptions{ jsOrigin?, jsUrl? }CDN defaultsOverrides where the runtime is fetched from
deferbooleanfalseSkips 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

The safe form
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>
  );
}
HookReturnsNotes
useWajub(){ wajub, Wajub, WajubError }Throws while loading, on failure, and with no runtime
useWajubOptional()The same, or nullSafe 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.

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)}
    />
  );
}
PropTypeDefaultWhat it does
sessionIdstringRequiredThe authorization_token from POST /payments
classNamestringnoneClass on the container element
styleCSSPropertiesnoneInline styles on the container
minHeightnumber480Reserved height in pixels while the iframe loads
onReady(instance) => voidnoneReceives the CheckoutInstance to drive afterwards
Switching the theme after mount
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.

Your own layout, your own button
'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>
    </>
  );
}
PropTypeDefaultWhat it does
typeComponentTypeSet by the aliasOnly on ComponentEmbed
minHeightnumber200Reserved height while the field loads
onInstance(instance | null) => voidnoneFires on mount and again with null on unmount
onReady(instance) => voidnoneFires 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.

app/api/checkout/session/route.ts
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 });
}

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 WajubProvider in a layout so a navigation inside the checkout flow does not reload the runtime.
  • Pass sessionId down as a prop from a server component, or fetch it from the route above.

What the package exports

ExportWhat it is
WajubProviderLoads the runtime once for the subtree
CheckoutEmbedThe hosted checkout, inline
CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent, AddressComponentOne field group each
ComponentEmbedThe same, with the type given as a prop
useWajub, useWajubOptional, useLoadWajubReach the runtime
useConfirmPaymentSubmit a field component
WajubContext, useWajubContextThe raw context, for your own hook

Types come with the package: WajubProviderProps, CheckoutEmbedProps, ComponentEmbedProps and WajubContextValue, alongside everything @wajub/js exports.

What did you think of this content?