Aller au contenu

Svelte

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

@wajub/svelte vous donne un provider, un CheckoutEmbed, sept composants de champs et quatre helpers basés sur des stores. Le rôle 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. Installez-la donc sur la même ligne. Le package est uniquement ESM et fonctionne avec Svelte 4 et Svelte 5.

npm install @wajub/svelte @wajub/js

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

Provider

Un seul WajubProvider suffit autour du checkout : il 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>

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>

Le store vaut null à la création du composant et se remplit une fois Wajub.js chargé. 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 pendant le chargement
useLoadWajub(){ runtime, loading, error, loadWajub }Les trois premiers sont des stores. loading et error permettent d'afficher un indicateur de chargement ou une nouvelle tentative
useConfirmPayment(sessionId){ confirm, isConfirming }isConfirming est un store. Envoie un composant de champ monté. sessionId est une chaîne, un store ou un getter

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, donc on:success n'y a aucun effet. 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.

Les props peuvent changer après le montage, et un nouveau sessionId charge un nouveau checkout, sans {#key sessionId}.

Champs de paiement

Les sept composants affichent chacun un groupe de champs : CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent, AddressComponent, BankTransferComponent et CryptoComponent. Chaque option et chaque callback de ComponentConfig est une prop, plus les props ci-dessous, 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. 'none' : aucun squelette
minHeightNon définieHauteur minimale du conteneur en pixels
readyTimeoutNon définieMillisecondes d'attente du formulaire. Au-delà, onLoadError reçoit une WajubError de code ready_timeout. Non définie, aucune limite
onInstanceon:instanceaucuneL'instance une fois montée, null une fois détruite
onReadyon:readyaucuneLe champ est monté et interactif
onStateChangeon:statechangeaucune{ state, method } : le paiement passe à une autre étape, par exemple APPROVAL_REQUIRED pendant que le payeur valide sur son téléphone
onResizeon:resizeaucune{ height } : le champ prend une nouvelle hauteur, en pixels
onRedirecton:redirectaucune{ url, method } : le paiement continue sur la page du prestataire, et votre page va s'y rendre
onBeforeSubmitaucuneS'exécute avant le débit, pour passer votre commande : false, ou une erreur, ne débite rien. Définissez-le dès le premier rendu 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.

isConfirming est un store : lisez-le sous la forme $isConfirming. useConfirmPayment prend la session comme une chaîne, un store ou un getter tel que () => sessionId (le type SessionIdSource).

Types

Chaque helper et chaque composant est typé, comme tous les exports de @wajub/js, pour votre éditeur et svelte-check en Svelte 4 comme en Svelte 5 : un nom de prop mal orthographié, un sessionId manquant ou une valeur erronée est une erreur. Les composants de champs acceptent chaque prop de ComponentEmbed sauf type, le type TypedComponentProps.

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

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 : les composants s'affichent sur le serveur, et les champs se montent dans le navigateur.
  • Placez WajubProvider dans +layout.svelte, pour qu'il reste monté pendant que le payeur avance dans le flux de checkout.

Éléments exportés par le package

Les exports sont listés dans la Référence du SDK, les types que le package ajoute dans TypeScript.

Que pensez-vous de ce contenu ?