Skip to content

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

npm

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

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.

Runes for your files, not for node_modules
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.

src/routes/checkout/+layout.svelte
<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.

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

HelperReturnsNotes
useWajub(){ wajub, Wajub, WajubError }Throws when the runtime is unavailable
useWajubOptional()A read-only store of the same, null until the runtime has loadedSafe 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.

src/routes/checkout/+page.svelte
<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.

Your own layout, your own button
<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>
PropSvelte eventDefaultWhat it does
typeSet by the aliasOnly 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
minHeightUnsetThe container's minimum height in pixels
readyTimeoutUnsetMilliseconds to wait for the form. Past it, onLoadError receives a WajubError with the code ready_timeout. Unset, no limit
onInstanceon:instancenoneThe instance once mounted, null once destroyed
onReadyon:readynoneThe field is mounted and interactive
onStateChangeon:statechangenone{ state, method }: the payment moved to another step, APPROVAL_REQUIRED while the payer approves on the phone for example
onResizeon:resizenone{ height }: the field took a new height, in pixels
onRedirecton:redirectnone{ url, method }: the payment continues on the provider's own page, and your page is about to go there
onBeforeSubmitnoneRuns 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.

PaymentStep.svelte, rendered inside <WajubProvider>
<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.

src/routes/api/checkout/session/+server.ts
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 });
};

Two more things SvelteKit asks for:

  • export const ssr = false is not needed: the components render on the server, and the fields mount in the browser.
  • Put WajubProvider in +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.

What did you think of this content?