Svelte
Provider, stores, embed props, and the SvelteKit wiring that goes with them.
@wajub/svelte gives you a provider, a CheckoutEmbed, seven field components and four helpers
built on stores. What every option does is documented on
Display the checkout and
Payment fields. This page covers what is specific to Svelte.
@wajub/svelte
Stable · GAnpm
- Version
- 1.8.1
- Runtime
- Svelte 4 and 5 · SvelteKit
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 and works
with Svelte 4 and Svelte 5.
npm install @wajub/svelte @wajub/jsA Svelte 5 project that forces runes on every file stops its build at
Cannot use export let in runes mode, so force them on your own files only, as npx sv create
does.
compilerOptions: {
runes: ({ filename }) =>
filename.split(/[/\\]/).includes('node_modules') ? undefined : true,
},In a component written with runes, pass the handlers as props: the field components take
onReady, onInstance, onStateChange and every other handler as a prop. Their Svelte events,
on:ready, on:instance and the others, are there too, for markup written without runes.
The provider
Wrap the checkout in one WajubProvider. It covers a whole subtree, so it belongs at the layout
level rather than around each component.
<script lang="ts">
import { WajubProvider } from '@wajub/svelte';
</script>
<WajubProvider>
<slot />
</WajubProvider>Stores and helpers
Everything reactive here is a Svelte store, so read it with $ in the template.
useWajub throws while Wajub.js is loading
It throws outside a provider and until Wajub.js has loaded. Use useWajubOptional(), which
returns a store that holds null until the runtime has loaded, unless you already know it has.
<script lang="ts">
import { useWajubOptional } from '@wajub/svelte';
export let sessionId: string;
const runtime = useWajubOptional();
</script>
<button
disabled={!$runtime}
on:click={() => $runtime?.wajub.open({ sessionId, onSuccess: onUpgraded })}
>
Upgrade plan
</button>The store holds null when the component is created and fills once Wajub.js has loaded. Read it
with $, like every other store on this page.
| Helper | Returns | Notes |
|---|---|---|
useWajub() | { wajub, Wajub, WajubError } | Throws when the runtime is unavailable |
useWajubOptional() | A read-only store of the same, null until the runtime has loaded | Safe during SSR and while loading |
useLoadWajub() | { runtime, loading, error, loadWajub } | The first three are stores. loading and error let you render a spinner or a retry |
useConfirmPayment(sessionId) | { confirm, isConfirming } | isConfirming is a store. Submits a mounted field component. sessionId is a string, a store or a getter |
CheckoutEmbed
The hosted checkout inside your own layout. Every EmbeddedConfig option is a prop, and so is every callback.
<script lang="ts">
import { CheckoutEmbed } from '@wajub/svelte';
export let sessionId: string;
</script>
<CheckoutEmbed
{sessionId}
layout="tabs"
minHeight={480}
onSuccess={() => (window.location.href = '/order/complete')}
onError={(error) => console.error(error.code, error.message)}
/>Handlers are props, on the embed and on the fields
CheckoutEmbed takes its callbacks as props: onSuccess, onError, onReady and the rest, and
dispatches no Svelte event, so on:success on it does nothing. The field components take every
handler as a prop too, onInstance and onReady included, and also dispatch on:ready,
on:instance, on:statechange, on:resize and on:redirect.
Props can change after mount, and a new sessionId loads a new checkout, with no {#key sessionId}
needed.
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, and each
one is ComponentEmbed with its type already set. Use ComponentEmbed directly when the choice
is made at runtime.
<script lang="ts">
import type { ComponentInstance } from '@wajub/js';
import { AddressComponent, PaymentComponent, useConfirmPayment } from '@wajub/svelte';
export let sessionId: string;
const { confirm, isConfirming } = useConfirmPayment(() => sessionId);
let field: ComponentInstance | null = null;
</script>
<AddressComponent {sessionId} addressMode="shipping" />
<PaymentComponent
{sessionId}
collectAddress="shipping"
collectName
on:instance={(event) => (field = event.detail)}
/>
<button disabled={$isConfirming || !field} on:click={() => field && confirm(field)}>
{$isConfirming ? 'Processing…' : 'Pay now'}
</button>| Prop | Svelte event | Default | What it does |
|---|---|---|---|
type | Set by the alias | Only on ComponentEmbed | |
loader | 'skeleton' | 'skeleton': the container holds the form's height from the first render, with a placeholder in your inputs' style. 'none': no placeholder | |
minHeight | Unset | The container's minimum height in pixels | |
readyTimeout | Unset | Milliseconds to wait for the form. Past it, onLoadError receives a WajubError with the code ready_timeout. Unset, no limit | |
onInstance | on:instance | none | The instance once mounted, null once destroyed |
onReady | on:ready | none | The field is mounted and interactive |
onStateChange | on:statechange | none | { state, method }: the payment moved to another step, APPROVAL_REQUIRED while the payer approves on the phone for example |
onResize | on:resize | none | { height }: the field took a new height, in pixels |
onRedirect | on:redirect | none | { url, method }: the payment continues on the provider's own page, and your page is about to go there |
onBeforeSubmit | none | Runs before the charge, to place your order: false, or an error, charges nothing. Set it from the field's first render |
A Svelte event carries its payload in event.detail. The other handlers, onChange, onSuccess,
onError, onLoadError and the rest, are props only.
isConfirming is a store: read it as $isConfirming. useConfirmPayment takes the session as a
string, a store or a getter such as () => sessionId (the SessionIdSource type).
Types
Every helper and every component is typed, and so is everything @wajub/js exports, for your
editor and svelte-check in Svelte 4 and Svelte 5 alike: a misspelled prop, a missing sessionId
or a wrong value is an error. The field components take every prop of ComponentEmbed but type,
the TypedComponentProps type.
The package exports ComponentEmbedProps, TypedComponentProps and SessionIdSource, the session
useConfirmPayment() takes. It exports no CheckoutEmbedProps.
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.
<script lang="ts">
import {
CardComponent,
MobileMoneyComponent,
useConfirmPayment,
type ComponentInstance,
type ComponentStateChangeEvent,
} from '@wajub/svelte';
export let sessionId: string; // the prepared payment's authorization_token, from your server
export let hostedUrl: string; // its authorization_url, for the fallback
// 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',
];
let method: 'mobileMoney' | 'card' = 'mobileMoney';
let component: ComponentInstance | null = null;
let complete = false;
let step = 'INITIATED';
let fallback = false;
const { confirm, isConfirming } = useConfirmPayment(() => sessionId);
$: locked = $isConfirming || RUNNING.includes(step);
// 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
}
function onInstance(instance: ComponentInstance | null) {
component = instance;
complete = false;
}
function onStateChange(event: ComponentStateChangeEvent) {
step = event.state;
}
function onLoadError(error: { code?: unknown }) {
if (error.code === 'ready_timeout') fallback = true;
}
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) return;
try {
await confirm(component);
window.location.assign('/thanks');
} catch {
// The form shows the reason, and the payer can try again in it.
}
}
</script>
<fieldset disabled={locked}>
<label><input type="radio" bind:group={method} value="mobileMoney" /> Mobile Money</label>
<label><input type="radio" bind:group={method} value="card" /> Card</label>
</fieldset>
{#if fallback}
<p>You will finish on Wajub's secure payment page.</p>
{:else}
<!-- loader="skeleton" is the default: the box keeps its height while the form loads. -->
<svelte:component
this={method === 'card' ? CardComponent : MobileMoneyComponent}
{sessionId}
variant="fields"
appearance={{ inherit: true }}
readyTimeout={10000}
fields={method === 'card'
? { customer: 'external' }
: { customer: 'external', country: 'CM', operator: 'detect' }}
onBeforeSubmit={placeOrder}
onChange={(event) => (complete = Boolean(event.complete))}
{onInstance}
{onStateChange}
{onLoadError}
/>
{/if}
{#if step === 'APPROVAL_REQUIRED'}<p role="status">Approve the payment on your phone.</p>{/if}
<button type="button" disabled={locked || (!fallback && !complete)} on:click={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.
SvelteKit
The session is created on your server. Nothing in this package needs a secret key, and nothing should ever receive one.
import { json } from '@sveltejs/kit';
import { WAJUB_API_KEY } from '$env/static/private';
import { Wajub } from '@wajub/node';
import type { RequestHandler } from './$types';
const wajub = new Wajub({ secretKey: WAJUB_API_KEY });
export const POST: RequestHandler = async ({ request }) => {
const { cartId } = await request.json();
const payment = await wajub.payments.create(
{
amount: 25000,
currency: 'XAF',
description: `Cart ${cartId}`,
customer: { email: 'buyer@example.com' },
metadata: { cart_id: cartId },
},
{ idempotencyKey: `cart-${cartId}` },
);
return json({ sessionId: payment.authorization_token });
};$env/static/public is for the publishable key only
Anything imported from $env/static/public is inlined into the client bundle. pk. belongs
there, sk. never does, and $env/static/private refuses to be imported from client code for
exactly that reason. A secret key sent from a browser is answered 403 and its owner is emailed
about it.
Two more things SvelteKit asks for:
export const ssr = falseis not needed: the components render on the server, and the fields mount in the browser.- Put
WajubProviderin+layout.svelte, so that it stays mounted while the payer moves through the checkout flow.
What the package exports
The exports are listed in the SDK reference, the types the package adds in TypeScript.