Aller au contenu

Svelte

Provider, stores, props d'intégration et configuration SvelteKit associée.

@wajub/svelte enveloppe @wajub/js avec la syntaxe Svelte : un provider qui charge une fois l'environnement d'exécution, sept composants de champs et quatre helpers basés sur des stores. L'API de paiement ne change pas. Le comportement de chaque option est présenté dans Afficher le checkout et Champs de paiement. Cette page traite des éléments propres à Svelte.

@wajub/svelte

Stable · GA

npm

Version
1.8.1
Runtime
Svelte 4 et 5 · SvelteKit

Couvre

  • Paiements
  • Liens de paiement

Absent de ce paquet : Facturation, Transferts, Sync, Shield, Taxes.

Installer

@wajub/js est une dépendance homologue, non intégrée. Installez-la donc sur la même ligne. Le package est uniquement ESM et distribue ses composants en source, sous une condition d'export svelte : le Svelte de votre projet les compile. Le même package sert donc Svelte 4 et Svelte 5.

npm install @wajub/svelte @wajub/js

Les composants sont écrits sans runes, ce que les deux versions compilent : Svelte 5 les compile en mode legacy, fichier par fichier. Un projet Svelte 5 qui impose les runes à tous les fichiers voit sa compilation s'arrêter sur Cannot use export let in runes mode. Imposez-les donc à vos seuls fichiers, comme le fait npx sv create.

Les runes pour vos fichiers, pas pour node_modules
compilerOptions: {
  runes: ({ filename }) =>
    filename.split(/[/\\]/).includes('node_modules') ? undefined : true,
},

Dans un composant écrit avec les runes, passez les handlers comme props : depuis la 1.8.0, les composants de champs reçoivent onReady, onInstance, onStateChange et chaque autre handler comme prop. Leurs événements Svelte, on:ready, on:instance et les autres, restent là pour le balisage écrit sans runes. Jusqu'à la 1.7.2, on:ready et on:instance n'étaient que des événements.

Provider

WajubProvider appelle loadWajub() depuis @wajub/js/pure et place l'environnement d'exécution dans le contexte Svelte. Un provider couvre toute une sous-arborescence. Placez-le donc au niveau de la mise en page, pas autour de chaque composant.

src/routes/checkout/+layout.svelte
<script lang="ts">
  import { WajubProvider } from '@wajub/svelte';
</script>

<WajubProvider>
  <slot />
</WajubProvider>
PropTypeValeur par défautFonction
deferbooleanfalseIgnore le chargement automatique. Laissez-le désactivé : en 1.8.1, rien ne charge ensuite l'environnement d'exécution dans le provider
loadOptions{ jsOrigin?, jsUrl? }Valeurs du CDNRemplace l'origine de chargement de l'environnement d'exécution

Stores et helpers

Tous les éléments réactifs sont des stores Svelte. Lisez-les donc avec $ dans le template.

La forme sûre
<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>

C'est le store qui fait fonctionner ce bouton : il vaut null à la création du composant, car le provider ne commence le chargement qu'une fois monté, puis il se remplit à l'arrivée de l'environnement d'exécution. Lisez-le avec $, comme les autres stores de cette page.

HelperValeur renvoyéeRemarques
useWajub(){ wajub, Wajub, WajubError }Génère une erreur si l'environnement est indisponible
useWajubOptional()Un store en lecture seule de la même valeur, null tant que l'environnement n'est pas chargéSûr pendant le SSR et avant l'arrivée du script
useLoadWajub(){ runtime, loading, error, loadWajub }Les trois premiers sont des stores. Son loadWajub() ne met pas le provider à jour
useConfirmPayment(sessionId){ confirm, isConfirming }isConfirming est un store modifiable. Envoie un composant de champ monté. Depuis la 1.8.0, sessionId est une chaîne, un store ou un getter, lu à chaque confirm()

useWajubContext() renvoie les mêmes stores { runtime, loading, error } si vous préférez créer votre propre helper. Il s'agit d'un alias de getWajubContext(). Le provider appelle lui-même setWajubContext().

CheckoutEmbed

Le checkout hébergé dans votre propre mise en page. Chaque option de EmbeddedConfig est une prop, comme chaque 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)}
/>

Les handlers sont des props, sur l'intégration comme sur les champs

CheckoutEmbed reçoit ses callbacks comme props : onSuccess, onError, onReady et les autres, et n'émet aucun événement Svelte : on:success y reste sans effet. Depuis la 1.8.0, les composants de champs reçoivent eux aussi chaque handler comme prop, onInstance et onReady compris, et émettent aussi on:ready, on:instance, on:statechange, on:resize et on:redirect. Jusqu'à la 1.7.2, un champ n'avait que les deux événements on:ready et on:instance, et une prop onInstance restait sans effet.

Depuis la 1.8.0, un nouveau sessionId charge un nouveau checkout de lui-même : {#key sessionId} n'est nécessaire que jusqu'à la 1.7.2.

Ce que les quatre wrappers ont en commun, depuis la 1.8.0 chaque prop qui atteint le checkout par update() et une nouvelle session qui charge un nouveau checkout, est décrit dans Afficher le checkout, avec cet exemple dans chaque framework.

Champs de paiement

Les sept composants affichent chacun un groupe de champs : CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent, AddressComponent et, depuis la 1.8.0, BankTransferComponent et CryptoComponent. Ils partagent les props de ComponentConfig, loader, readyTimeout et les callbacks compris, et chacun est un ComponentEmbed avec son type déjà défini. Utilisez directement ComponentEmbed lorsque le choix intervient pendant l'exécution.

Votre mise en page et votre bouton
<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Événement SvelteValeur par défautFonction
typeDéfinie par l'aliasUniquement sur ComponentEmbed
loader'skeleton''skeleton' : le conteneur garde la hauteur du formulaire dès le premier rendu, avec un squelette dans le style de vos champs, et le formulaire apparaît en fondu une fois prêt. 'none' : l'iframe telle qu'elle arrive, la valeur par défaut de @wajub/js lui-même. Depuis la 1.8.0
minHeightNon définieHauteur minimale du conteneur en pixels, gardée pendant toute la vie du composant. Non définie, voir ci-dessous
readyTimeoutNon définieMillisecondes d'attente du formulaire. Au-delà, onLoadError reçoit une WajubError de code ready_timeout. Non définie, jamais. Depuis la 1.8.0
onInstanceon:instanceaucuneL'instance une fois montée, null une fois détruite. La prop depuis la 1.8.0
onReadyon:readyaucuneLe champ est monté et interactif. La prop depuis la 1.8.0
onStateChangeon:statechangeaucune{ state, method } : le paiement passe à une autre étape, par exemple APPROVAL_REQUIRED pendant que le payeur valide sur son téléphone. Depuis la 1.8.0
onResizeon:resizeaucune{ height } : l'iframe prend une nouvelle hauteur, en pixels. Depuis la 1.8.0
onRedirecton:redirectaucune{ url, method } : le paiement continue sur la page du prestataire, où Wajub.js emmène votre page juste après. Depuis la 1.8.0
onBeforeSubmitaucuneS'exécute avant le débit, pour passer votre commande. Il compte s'il est défini au montage du champ

Un événement Svelte porte sa donnée dans event.detail. Les autres handlers, onChange, onSuccess, onError, onLoadError et les autres, sont seulement des props.

Sans minHeight, la place suit loader, de la même façon dans les quatre wrappers : ce que tient le conteneur au rendu serveur, à l'hydratation et aux rendus suivants est décrit dans Champs de paiement. Il finit à la hauteur du formulaire : un bloc variant="fields" épouse ses champs sans minHeight={0}.

isConfirming est un store : lisez-le sous la forme $isConfirming. Depuis la 1.8.0, useConfirmPayment prend la session comme une chaîne, un store ou un getter tel que () => sessionId (le type SessionIdSource), lu à chaque confirm(). Une prop atteint le champ quand son propre contenu change, cette option seule, et un objet modifié sur place puis réassigné compte comme modifié. L'événement redirect a une prop et un événement Svelte depuis la 1.8.0, onRedirect et on:redirect.

Ce que les composants de champs ont en commun, depuis la 1.8.0 chaque prop qui atteint le formulaire par update() ou par un nouveau montage, l'événement redirect et ce dont useConfirmPayment vous protège, est décrit dans Champs de paiement.

Types

Chaque helper est entièrement typé, comme tous les exports de @wajub/js. Depuis la version 1.6.0, les composants le sont aussi : chacun est livré avec une déclaration générée à partir de ses props, que votre éditeur et svelte-check lisent en Svelte 4 comme en Svelte 5.

ComposantCe que ses types vérifient
CheckoutEmbed, ComponentEmbedChaque prop : un nom mal orthographié, un sessionId manquant ou une valeur erronée est une erreur
CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent, AddressComponent, BankTransferComponent, CryptoComponentDepuis la 1.8.0, chaque prop de ComponentEmbed sauf type, le type TypedComponentProps. De la 1.6.0 à la 1.7.2, seulement sessionId, minHeight et les deux événements, les autres props passant sans type

Depuis la 1.8.0, le package exporte ComponentEmbedProps, TypedComponentProps et SessionIdSource, la session que prend useConfirmPayment(). Il n'exporte aucun CheckoutEmbedProps.

Typer vous-même la configuration
import type { AppearanceConfig, CheckoutLayout } from '@wajub/js';

const appearance: AppearanceConfig = { primaryColor: '#0f172a', labels: 'floating' };
const layout: CheckoutLayout = 'tabs';

Mobile Money et carte dans votre propre checkout

Votre checkout garde son formulaire, son choix de moyen de paiement et son bouton Payer, et chaque moyen n'affiche que ses champs, dans le style de votre page. Les étapes, les appels serveur et la raison de chaque option sont dans Champs de paiement. Ici, votre serveur a préparé le paiement avec deferred: true à l'ouverture du checkout, et a transmis à la page son authorization_token et son authorization_url.

PaymentStep.svelte, rendu sous <WajubProvider>
<script lang="ts">
  import {
    CardComponent,
    MobileMoneyComponent,
    useConfirmPayment,
    type ComponentInstance,
    type ComponentStateChangeEvent,
  } from '@wajub/svelte';

  export let sessionId: string; // l'authorization_token du paiement préparé, transmis par votre serveur
  export let hostedUrl: string; // son authorization_url, pour la solution de repli

  // Le paiement est en cours, ou attend le payeur : rien ne doit changer sur la page.
  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);

  // Votre serveur vérifie le panier, passe la commande et active le paiement.
  async function placeOrder() {
    const response = await fetch('/checkout/place-order', { method: 'POST' });
    return response.ok; // false : rien n'est débité
  }

  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) {
      // Un paiement préparé n'est débité qu'une fois activé : passez d'abord la commande.
      if (await placeOrder()) window.location.assign(hostedUrl);
      return;
    }
    if (!component) return;
    try {
      await confirm(component);
      window.location.assign('/thanks');
    } catch {
      // Le formulaire affiche la raison, et le payeur peut y réessayer.
    }
  }
</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" /> Carte</label>
</fieldset>

{#if fallback}
  <p>Vous finirez sur la page de paiement sécurisée de Wajub.</p>
{:else}
  <!-- loader="skeleton" par défaut : le bloc garde sa hauteur pendant le chargement. -->
  <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">Validez le paiement sur votre téléphone.</p>{/if}
<button type="button" disabled={locked || (!fallback && !complete)} on:click={pay}>Payer</button>

Les champs affichent eux-mêmes un paiement refusé, et le payeur y réessaie. S'ils ne s'affichent pas dans le délai readyTimeout, le bouton Payer passe la commande puis envoie le payeur sur la page de Wajub : un paiement préparé n'est débité qu'une fois activé. appearance.inherit lit un champ texte de votre page, son libellé, votre bouton principal et le fond derrière les champs. Affichez donc les champs de votre propre formulaire avant cette étape, ou nommez-les avec appearance={{ inherit: { input: '#email', button: '#pay' } }}. Marquez la commande payée sur votre serveur, à partir du webhook payment.succeeded, jamais depuis le navigateur.

SvelteKit

La session est créée sur votre serveur. Aucun élément de ce package ne nécessite ni ne doit recevoir de clé secrète.

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 });
};

SvelteKit impose deux autres règles :

  • export const ssr = false n'est pas nécessaire en 1.8.1. Les composants s'affichent sur le serveur, où rien ne lit window ni document : un composant de champ affiche son conteneur, qui tient déjà la hauteur estimée du formulaire, la même à l'hydratation, et l'environnement d'exécution se charge et les champs se montent dans le navigateur.
  • Placez WajubProvider dans +layout.svelte pour éviter qu'une navigation dans le flux de checkout recharge l'environnement d'exécution.

Éléments exportés par le package

Les exports sont les mêmes dans les packages React, Vue et Svelte et sont listés dans la Référence du SDK, les types qu'ils ajoutent dans TypeScript.

Que pensez-vous de ce contenu ?