Svelte
Provider, stores, embed props, and the SvelteKit wiring that goes with them.
@wajub/svelte wraps @wajub/js in Svelte syntax: a provider that loads the runtime once, five
field components, and four helpers built on stores. 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
Svelte.
@wajub/svelte
Stable · GAnpm
- Version
- 1.5.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, not a bundled one, so it goes on the same line. The package is ESM
only and ships its components as source, under a svelte export condition: your project's own
Svelte compiles them, so the same package serves Svelte 4 and Svelte 5.
npm install @wajub/svelte @wajub/jsInstall 1.6.0 or later
1.5.1 carries its own copy of the Svelte runtime next to your project's, and throws on the first render. 1.6.0 leaves Svelte to your project, version 4 or 5.
The components are written without runes, which both versions compile: Svelte 5 compiles them in
legacy mode, file by file. A 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, listen to the field components with on:ready and
on:instance as below: they dispatch Svelte events, not callback props.
The provider
WajubProvider calls loadWajub() from @wajub/js/pure and puts the runtime in Svelte context.
One provider 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>| Prop | Type | Default | What it does |
|---|---|---|---|
defer | boolean | false | Skips the automatic load. Leave it off: in 1.5.1 nothing loads the runtime into the provider afterwards |
loadOptions | { jsOrigin?, jsUrl? } | CDN defaults | Overrides where the runtime is fetched from |
Stores and helpers
Everything reactive here is a Svelte store, so read it with $ in the template.
useWajub throws when the runtime is not there yet
It throws outside a provider and while the script is still on its way, which is every render
before the load resolves. Use useWajubOptional(), which returns a store that holds null
until the runtime has landed, 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 is what makes this button work: it holds null when the component is created, because
the provider only starts loading once it is mounted, and it fills when the runtime arrives. Read
it with $, like every other store on this page.
A store from 1.6.0, a plain value before
In 1.5.1 and earlier useWajubOptional() returned the value read once when the component was
created, which is null before the load. 1.6.0 returns the store. Code written for the old form
that calls runtime.wajub still works once the runtime has loaded, but !runtime is no longer a
test, so a button written disabled={!runtime} is never disabled: move both to $runtime.
| 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 before the script lands |
useLoadWajub() | { runtime, loading, error, loadWajub } | The first three are stores. Its loadWajub() does not update the provider |
useConfirmPayment(sessionId) | { confirm, isConfirming } | isConfirming is a writable store. Submits a mounted field component |
useWajubContext() returns the same { runtime, loading, error } stores if you would rather build
your own helper on top. It is an alias of getWajubContext(), and setWajubContext() is what the
provider itself calls.
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>
{#key sessionId}
<CheckoutEmbed
{sessionId}
layout="tabs"
minHeight={480}
onSuccess={() => (window.location.href = '/order/complete')}
onError={(error) => console.error(error.code, error.message)}
/>
{/key}The embed and the fields do not use the same convention
CheckoutEmbed takes its callbacks as props: onSuccess, onError, onReady and the rest.
Field components dispatch Svelte events instead: on:ready and on:instance. Writing
on:success on the embed, or onInstance on a field, silently does nothing.
Wrapping in {#key sessionId} is what rebuilds the embed for a second session, and keeping the
instance from onReady is what lets you restyle it without rebuilding.
<script lang="ts">
import type { CheckoutInstance } from '@wajub/js';
import { CheckoutEmbed } from '@wajub/svelte';
export let sessionId: string;
export let dark = false;
let checkout: CheckoutInstance | null = null;
$: checkout?.update({ appearance: { colorScheme: dark ? 'dark' : 'light' } });
</script>
{#key sessionId}
<CheckoutEmbed {sessionId} onReady={(instance) => (checkout = instance)} />
{/key}Payment fields
Five components render one field group each: CardComponent, MobileMoneyComponent,
WalletComponent, PaymentComponent and AddressComponent. They share the props of
ComponentConfig, 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>| Event | Detail | When |
|---|---|---|
on:ready | ComponentInstance | The field is mounted and interactive |
on:instance | ComponentInstance or null | On mount, and again with null on unmount |
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.
Types
Every helper is fully typed, and so is everything @wajub/js exports. From 1.6.0 the components
are too: each one ships a declaration generated from its props, read by your editor and by
svelte-check in Svelte 4 and Svelte 5 alike.
| Component | What its types check |
|---|---|
CheckoutEmbed, ComponentEmbed | Every prop: a misspelled name, a missing sessionId or a wrong value is an error |
CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent, AddressComponent | sessionId, minHeight and the two events. Every other prop passes through to ComponentEmbed untyped |
1.5.1 ships no prop types
Its eight .d.ts files are a bare export { SvelteComponent as default } from 'svelte', so your
editor completes no prop and svelte-check catches no misspelled one. There, and for the props a
field component passes through, read the names off
Hosted checkout and
Payment fields, or import the config types from
@wajub/js and annotate your own variables. The package exports no CheckoutEmbedProps or
ComponentEmbedProps.
import type { AppearanceConfig, CheckoutLayout } from '@wajub/js';
const appearance: AppearanceConfig = { primaryColor: '#0f172a', labels: 'floating' };
const layout: CheckoutLayout = 'tabs';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: '[email protected]' },
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:
- Turn off server rendering on checkout routes with
export const ssr = falsein+page.ts, or mount the embed inside an{#if browser}block. The runtime needs awindow. - Put
WajubProviderin+layout.svelteso a navigation inside the checkout flow does not reload the runtime.
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 |
getWajubContext, setWajubContext, useWajubContext | The raw context, for your own helper |
The package declares two types: WajubContextValue and, from 1.6.0, WajubRuntimeStore, which is
what useWajubOptional() returns.