Aller au contenu

Vue 3

Provider, composables, props d'intégration et configuration Nuxt associée.

@wajub/vue vous donne un provider, un CheckoutEmbed, sept composants de champs et quatre composables. Le rôle de chaque option est présenté dans Afficher le checkout et Champs de paiement. Cette page traite des éléments propres à Vue.

@wajub/vue

Stable · GA

npm

Version
1.8.1
Runtime
Vue 3.3+

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.

npm install @wajub/vue @wajub/js

Provider

Un seul WajubProvider suffit autour du checkout : il couvre toute une sous-arborescence. Placez-le donc au niveau de la mise en page ou de la page, pas autour de chaque composant.

layouts/checkout.vue
<script setup lang="ts">
import { WajubProvider } from '@wajub/vue';
</script>

<template>
  <WajubProvider>
    <slot />
  </WajubProvider>
</template>

Composables

La forme sûre
<script setup lang="ts">
import { useWajubOptional } from '@wajub/vue';

const props = defineProps<{ sessionId: string }>();
const runtime = useWajubOptional();

function upgrade() {
  runtime.value?.wajub.open({ sessionId: props.sessionId, onSuccess: onUpgraded });
}
</script>

<template>
  <button :disabled="!runtime" @click="upgrade">Upgrade plan</button>
</template>

La ref vaut null lorsque setup s'exécute et se remplit une fois Wajub.js chargé. Lisez runtime.value dans le script. Le template lit directement sa valeur : !runtime y suffit.

ComposableValeur renvoyéeRemarques
useWajub(){ wajub, Wajub, WajubError }Génère une erreur pendant le chargement, en cas d'échec ou sans environnement
useWajubOptional()Une ref 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 refs. loading et error permettent d'afficher un indicateur de chargement ou une nouvelle tentative
useConfirmPayment(sessionId){ confirm, isConfirming }isConfirming est une ref. Envoie un composant de champ monté. sessionId est une chaîne, une ref ou un getter

CheckoutEmbed

Le checkout hébergé dans votre propre mise en page. Ses props sont session-id, class, style, min-height, locale, appearance, layout, loading-text, show-loading et les onze callbacks.

pages/checkout.vue
<script setup lang="ts">
import { CheckoutEmbed } from '@wajub/vue';

defineProps<{ sessionId: string }>();
</script>

<template>
  <CheckoutEmbed
    :session-id="sessionId"
    layout="tabs"
    :min-height="480"
    @success="navigateTo('/order/complete')"
    @error="(error) => console.error(error.code, error.message)"
  />
</template>

Chaque callback est déclaré comme prop. @success et :on-success sont donc équivalents.

PropTypeValeur par défautFonction
session-idstringObligatoireLa valeur authorization_token de POST /payments
classstringaucuneClasse de l'élément conteneur
styleRecord<string, string>aucunStyles en ligne du conteneur
min-heightnumber480Hauteur réservée en pixels pendant le chargement du checkout

Les props peuvent changer après le montage, et un nouveau session-id charge un nouveau checkout, sans :key.

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. ComponentEmbed est le même composant avec un type explicite lorsque le choix intervient pendant l'exécution.

Votre mise en page et votre bouton
<script setup lang="ts">
import { shallowRef } from 'vue';
import type { ComponentInstance } from '@wajub/js';
import { AddressComponent, PaymentComponent, useConfirmPayment } from '@wajub/vue';

const props = defineProps<{ sessionId: string }>();
const { confirm, isConfirming } = useConfirmPayment(() => props.sessionId);
// shallowRef : l'instance est l'objet du SDK, ne la rendez jamais réactive.
const field = shallowRef<ComponentInstance | null>(null);

function pay() {
  if (field.value) confirm(field.value);
}
</script>

<template>
  <AddressComponent :session-id="sessionId" address-mode="shipping" />
  <PaymentComponent
    :session-id="sessionId"
    collect-address="shipping"
    collect-name
    @instance="(instance) => (field = instance)"
  />
  <button :disabled="isConfirming || !field" @click="pay">
    {{ isConfirming ? 'Processing…' : 'Pay now' }}
  </button>
</template>
PropTypeValeur par défautFonction
typeComponentTypeDéfini par l'aliasUniquement sur ComponentEmbed
loader'skeleton' ou 'none''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
min-heightnumberNon définieHauteur minimale du conteneur en pixels. Un min-height dans style compte de la même façon
ready-timeoutnumberNon définieMillisecondes d'attente du formulaire. Au-delà, @load-error reçoit une WajubError de code ready_timeout. Non définie, aucune limite
@instance(instance | null) => voidaucuneSe déclenche au montage, puis avec null au démontage
@ready(instance) => voidaucuneSe déclenche lorsque le champ devient interactif
@state-change({ state, method }) => voidaucuneLe paiement passe à une autre étape, par exemple APPROVAL_REQUIRED pendant que le payeur valide sur son téléphone
@resize({ height }) => voidaucuneLe champ prend une nouvelle hauteur, en pixels
@redirect({ url, method }) => voidaucuneLe paiement continue sur la page du prestataire, et votre page va s'y rendre
:on-before-submit() => boolean | Promise<boolean>aucuneS'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

Chaque handler est déclaré comme prop, les deux écritures sont donc équivalentes : @state-change="fn" ou :on-state-change="fn", et de même @ready, @instance, @change, @success, @error, @load-error, @resize, @redirect et @method-change.

useConfirmPayment prend la session comme une chaîne, une ref ou un getter tel que () => props.sessionId. Gardez une instance de composant dans un shallowRef, jamais dans un proxy réactif.

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.vue, rendu sous <WajubProvider>
<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import {
  CardComponent,
  MobileMoneyComponent,
  useConfirmPayment,
  type ComponentInstance,
  type ComponentStateChangeEvent,
} from '@wajub/vue';

const props = defineProps<{
  sessionId: string; // l'authorization_token du paiement préparé, transmis par votre serveur
  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',
];

const method = ref<'mobileMoney' | 'card'>('mobileMoney');
const component = shallowRef<ComponentInstance | null>(null);
const complete = ref(false);
const step = ref('INITIATED');
const fallback = ref(false);
const { confirm, isConfirming } = useConfirmPayment(() => props.sessionId);
const locked = computed(() => isConfirming.value || RUNNING.includes(step.value));

// 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.value = instance;
  complete.value = false;
}

function onStateChange(event: ComponentStateChangeEvent) {
  step.value = event.state;
}

function onLoadError(error: { code?: unknown }) {
  if (error.code === 'ready_timeout') fallback.value = true;
}

async function pay() {
  if (fallback.value) {
    // Un paiement préparé n'est débité qu'une fois activé : passez d'abord la commande.
    if (await placeOrder()) window.location.assign(props.hostedUrl);
    return;
  }
  if (!component.value) return;
  try {
    await confirm(component.value);
    window.location.assign('/thanks');
  } catch {
    // Le formulaire affiche la raison, et le payeur peut y réessayer.
  }
}
</script>

<template>
  <fieldset :disabled="locked">
    <label><input v-model="method" type="radio" value="mobileMoney" /> Mobile Money</label>
    <label><input v-model="method" type="radio" value="card" /> Carte</label>
  </fieldset>

  <!-- loader="skeleton" par défaut : le bloc garde sa hauteur pendant le chargement. -->
  <p v-if="fallback">Vous finirez sur la page de paiement sécurisée de Wajub.</p>
  <component
    :is="method === 'card' ? CardComponent : MobileMoneyComponent"
    v-else
    :session-id="sessionId"
    variant="fields"
    :appearance="{ inherit: true }"
    :ready-timeout="10000"
    :fields="method === 'card'
      ? { customer: 'external' }
      : { customer: 'external', country: 'CM', operator: 'detect' }"
    :on-before-submit="placeOrder"
    @instance="onInstance"
    @change="(event) => (complete = Boolean(event.complete))"
    @state-change="onStateChange"
    @load-error="onLoadError"
  />

  <p v-if="step === 'APPROVAL_REQUIRED'" role="status">Validez le paiement sur votre téléphone.</p>
  <button type="button" :disabled="locked || (!fallback && !complete)" @click="pay">Payer</button>
</template>

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 ready-timeout, 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.

Nuxt

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.

server/api/checkout/session.post.ts
import { Wajub } from '@wajub/node';

const wajub = new Wajub({ secretKey: process.env.WAJUB_API_KEY! });

export default defineEventHandler(async (event) => {
  const { cartId } = await readBody(event);

  const payment = await wajub.payments.create(
    {
      amount: 25000,
      currency: 'XAF',
      description: `Cart ${cartId}`,
      customer: { email: 'buyer@example.com' },
      callback: `${process.env.NUXT_PUBLIC_APP_URL}/order/return`,
      metadata: { cart_id: cartId },
    },
    { idempotencyKey: `cart-${cartId}` },
  );

  return { sessionId: payment.authorization_token };
});

Nuxt impose trois autres règles :

  • <ClientOnly> n'est pas nécessaire : sur le serveur, les composables renvoient null, et les champs se montent dans le navigateur.
  • Placez WajubProvider dans une mise en page, pour qu'il reste monté pendant que le payeur avance dans le flux de checkout.
  • Récupérez la session depuis server/api/ et transmettez le jeton comme prop.

É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 ?