Vue 3
Provider, composables, embed props, and the Nuxt wiring that goes with them.
@wajub/vue gives you a provider, a CheckoutEmbed, seven field components and four composables.
What every option does is documented on Display the checkout
and Payment fields. This page covers what is specific to Vue.
@wajub/vue
Stable · GAnpm
- Version
- 1.8.1
- Runtime
- Vue 3.3+
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.
npm install @wajub/vue @wajub/jsThe provider
Wrap the checkout in one WajubProvider. It covers a whole subtree, so it belongs at the layout or
page level rather than around each component.
<script setup lang="ts">
import { WajubProvider } from '@wajub/vue';
</script>
<template>
<WajubProvider>
<slot />
</WajubProvider>
</template>Composables
useWajub throws while Wajub.js is loading
It throws until Wajub.js has loaded, and if the load failed. A component that calls it in setup
under the provider will crash before the first paint. Use useWajubOptional(), which returns a
ref that holds null until the runtime has loaded, unless you already know the runtime is there.
<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>The ref is null when setup runs and fills once Wajub.js has loaded. Read runtime.value in the
script. The template unwraps it, so !runtime is enough there.
| Composable | Returns | Notes |
|---|---|---|
useWajub() | { wajub, Wajub, WajubError } | Throws while loading, on failure, and with no runtime |
useWajubOptional() | A read-only ref of the same, null until the runtime has loaded | Safe during SSR and while loading |
useLoadWajub() | { runtime, loading, error, loadWajub } | The first three are refs. loading and error let you render a spinner or a retry |
useConfirmPayment(sessionId) | { confirm, isConfirming } | isConfirming is a ref. Submits a mounted field component. sessionId is a string, a ref or a getter |
CheckoutEmbed
The hosted checkout inside your own layout. Its props are session-id, class, style,
min-height, locale, appearance, layout, loading-text, show-loading, and the eleven
callbacks.
<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>Every callback is declared as a prop, so @success and :on-success are the same thing.
| Prop | Type | Default | What it does |
|---|---|---|---|
session-id | string | Required | The authorization_token from POST /payments |
class | string | none | Class on the container element |
style | Record<string, string> | none | Inline styles on the container |
min-height | number | 480 | Reserved height in pixels while the checkout loads |
Props can change after mount, and a new session-id loads a new checkout, with no :key 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.
ComponentEmbed is the same component with an explicit type, when the choice is made at runtime.
<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: the instance is the SDK's own object, never make it reactive.
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>| Prop | Type | Default | What it does |
|---|---|---|---|
type | ComponentType | Set by the alias | Only on ComponentEmbed |
loader | 'skeleton' or 'none' | 'skeleton' | 'skeleton': the container holds the form's height from the first render, with a placeholder in your inputs' style. 'none': no placeholder |
min-height | number | Unset | The container's minimum height in pixels. A min-height in style counts the same |
ready-timeout | number | Unset | Milliseconds to wait for the form. Past it, @load-error receives a WajubError with the code ready_timeout. Unset, no limit |
@instance | (instance | null) => void | none | Fires on mount and again with null on unmount |
@ready | (instance) => void | none | Fires once the field is interactive |
@state-change | ({ state, method }) => void | none | The payment moved to another step, APPROVAL_REQUIRED while the payer approves on the phone for example |
@resize | ({ height }) => void | none | The field took a new height, in pixels |
@redirect | ({ url, method }) => void | none | The payment continues on the provider's own page, and your page is about to go there |
:on-before-submit | () => boolean | Promise<boolean> | none | Runs before the charge, to place your order: false, or an error, charges nothing. Set it from the field's first render |
Every handler is declared as a prop, so both spellings work: @state-change="fn" or
:on-state-change="fn", and likewise @ready, @instance, @change, @success, @error,
@load-error, @resize, @redirect and @method-change.
useConfirmPayment takes the session as a string, a ref or a getter such as
() => props.sessionId. Keep a component instance in a shallowRef, never in a reactive proxy.
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.
<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; // the prepared payment's authorization_token, from your server
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',
];
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));
// 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.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) {
// A prepared payment is charged only once activated: place the order first.
if (await placeOrder()) window.location.assign(props.hostedUrl);
return;
}
if (!component.value) return;
try {
await confirm(component.value);
window.location.assign('/thanks');
} catch {
// The form shows the reason, and the payer can try again in it.
}
}
</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" /> Card</label>
</fieldset>
<!-- loader="skeleton" is the default: the box keeps its height while the form loads. -->
<p v-if="fallback">You will finish on Wajub's secure payment page.</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">Approve the payment on your phone.</p>
<button type="button" :disabled="locked || (!fallback && !complete)" @click="pay">Pay</button>
</template>The fields show a declined payment themselves, and the payer tries again in them. When they do not
come up within ready-timeout, 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.
Nuxt
The session is created on your server. Nothing in this package needs a secret key, and nothing should ever receive one.
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 };
});runtimeConfig.public is for the publishable key only
Anything under public is serialised into the page. pk. belongs there, sk. never does. A
secret key sent from a browser is answered 403 and its owner is emailed about it.
Three more things Nuxt asks for:
<ClientOnly>is not needed: on the server the composables returnnull, and the fields mount in the browser.- Put
WajubProviderin a layout, so that it stays mounted while the payer moves through the checkout flow. - Fetch the session from
server/api/and pass the token down as a prop.
What the package exports
The exports are listed in the SDK reference, the types the package adds in TypeScript.