@wajub/vue
@wajub/vue
Wajub Components for Vue 3.
Install
npm install @wajub/vue @wajub/js
Quick start — hosted checkout
<script setup lang="ts">
import { WajubProvider, CheckoutEmbed } from "@wajub/vue";
defineProps<{ sessionId: string }>();
// In <script setup>: a template cannot reach `window`.
function goToThanks() {
window.location.href = "/thanks";
}
</script>
<template>
<WajubProvider>
<CheckoutEmbed :session-id="sessionId" @success="goToThanks" />
</WajubProvider>
</template>
Create sessionId on your server, then pass it as a prop. Never expose secret keys (sk_...) in the browser.
CheckoutEmbed takes every option of wajub.mount() (EmbeddedConfig) as a
prop, appearance.inherit included. A new appearance, theme, locale or
layout updates the checkout in place (layout lays the methods out again,
since Wajub.js 1.8.0); a new sessionId, embedOrigin or runtime loads it
again. loadingText and showLoading are read when the checkout loads.
Handlers are read when the event comes, so a new one takes over without a
reload. appearance is sent whole, and a top-level key you removed since is
sent as undefined, which clears it; an appearance that becomes undefined
(or {}) clears the whole of it.
Components
useConfirmPayment() reads the provider, so call it in a component rendered
inside <WajubProvider>:
<!-- CheckoutPage.vue -->
<template>
<WajubProvider>
<PayWithCard :session-id="sessionId" />
</WajubProvider>
</template>
<!-- PayWithCard.vue -->
<script setup lang="ts">
import { shallowRef } from "vue";
import { CardComponent, useConfirmPayment, type ComponentInstance } from "@wajub/vue";
const props = defineProps<{ sessionId: string }>();
// A getter (or a ref): the session is read when you confirm.
const { confirm, isConfirming } = useConfirmPayment(() => props.sessionId);
// shallowRef: the instance is the SDK's own object, never make it reactive.
const card = shallowRef<ComponentInstance | null>(null);
</script>
<template>
<CardComponent :session-id="sessionId" @instance="(c) => (card = c)" />
<button :disabled="!card || isConfirming" @click="card && confirm(card)">Pay</button>
</template>
CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent,
AddressComponent, BankTransferComponent and CryptoComponent are
ComponentEmbed with its type set.
Props
Every option of wajub.components().create() (ComponentConfig in
@wajub/js) is a prop, plus:
| Prop | Default | |
|---|---|---|
loader |
"skeleton" |
"skeleton": the SDK holds the form's height from the first render with a placeholder in your inputs' style, and fades the fields in once ready. "none": the frame as it comes (the default of @wajub/js itself). |
minHeight |
unset | The container's minimum height in px, kept for the component's whole life. Unset, with loader="skeleton": the form's estimate (componentEstimatedHeight(type, variant, fields) of @wajub/js: card 264, Mobile Money 584 / 280 with variant="fields", less with fields.country and fields.operator: "detect"…) from the first render, server rendering and hydration included, until the form mounts; the SDK then holds the room and the container ends at the form's own height. Unset, with loader="none": card 200, mobileMoney 240, wallet 120, payment 360, address 280, bankTransfer 200, crypto 240. |
readyTimeout |
none | Milliseconds to wait for ready; past it, onLoadError gets { code: "ready_timeout" }. |
appearance |
{ inherit: true } takes your page's own look (font, text, input border and background, primary button colour, radius); inherit: { input, label, button, background } names the elements to read, as selectors or elements. Keys you set yourself win. |
|
class, style |
On the container. | |
onInstance |
The instance once mounted, null once unmounted. |
What a change does, without you doing anything:
appearance,locale,layout,frameTitleupdate the mounted component in place (instance.update()), and only when the value really changed: a template that writes:appearance="{ inherit: true }"builds a new but equal object on each render, which does nothing.appearanceis sent whole, and a top-level key you removed since is sent asundefined, which clears it: the SDK merges whatupdate()gives it. Anappearancethat becomesundefined(or{}) clears the whole of it, and the frame goes back to the merchant's look.sessionId,type,variant,fields,loader,readyTimeout,collectAddress,addressModeandcollectNamemount a new component (the previous one is destroyed first).- Handlers are read when the event comes: a new
onSuccess(or any other) takes over without remounting.onBeforeSubmitmust be there when the component mounts: the frame learns then that it has to ask before charging.
Handlers are props, so both spellings work: :on-state-change="fn" or
@state-change="fn"; likewise @ready, @instance, @change, @success,
@error, @load-error, @resize, @redirect, @method-change.
| Handler | Receives |
|---|---|
onReady |
the instance |
onStateChange |
{ state, method? }: PROCESSING, APPROVAL_REQUIRED, OTP_REQUIRED… |
onResize |
{ height }, the frame's new height in px |
onRedirect |
{ url, method } (method: "GET" or "POST"): the payment continues on the provider's page, where Wajub.js takes the payer next |
onLoadError |
the load error (code: "ready_timeout" with readyTimeout) |
onChange, onFocus, onBlur, onSuccess, onError, onMethodChange, onBeforeSubmit |
as in @wajub/js |
Recipe: Mobile Money and Card in your own checkout
Your checkout keeps its own form, its method choice and its Pay button; each method shows only its fields, in your page's style, and the order is placed when the payer pays.
- Your server prepares the session when the checkout opens:
POST /paymentswithdeferred: true, the amount, the currency and an expiry, with your secret key. It gives the page the payment'sauthorization_token(thesessionId) andauthorization_url(Wajub's hosted page, the fallback), keepstransaction.idwith the cart, andPATCHes that same payment when the total changes. - Your checkout mounts the fields of the method the payer picked.
onBeforeSubmitplaces the order: your server checks the cart, creates the order and activates the payment (POST /payments/{id}/activate) with its final total, the customer, yourreferenceandcallback. Returnfalseto stop: nothing is charged. Until it is activated, a prepared payment refuses every charge (409).- Your Pay button submits, with
useConfirmPayment()orinstance.submit(). The defaulttimeoutis enough: since Wajub.js 1.8.0 it does not run while the payer approves. onStateChangekeeps the checkout locked while the payment runs, and asks the payer to approve on their phone duringAPPROVAL_REQUIRED.onLoadErrorwithready_timeoutfalls back to the hosted page: the fields go, and the Pay button places the order first, asonBeforeSubmitdoes, then sends the payer toauthorization_url. A prepared payment cannot be charged before it is activated.
<!-- PaymentStep.vue, rendered inside <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; // the prepared payment's authorization_token, from your server
hostedUrl: string; // its authorization_url, from your server
}>();
// 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");
// shallowRef: the instance is the SDK's own object, never make it reactive.
const fields = shallowRef<ComponentInstance | null>(null);
const complete = ref(false);
const step = ref("INITIATED");
const fallback = ref(false);
// A getter (or a ref): the session is read when you confirm.
const { confirm, isConfirming } = useConfirmPayment(() => props.sessionId);
const locked = computed(() => isConfirming.value || RUNNING.includes(step.value));
// Your server checks the cart, places the order (the same one when asked again) 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) {
fields.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 (!fields.value) return;
try {
await confirm(fields.value); // or fields.value.submit(), then follow @success / @error
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>
<p v-if="fallback">You will finish on Wajub's secure payment page.</p>
<MobileMoneyComponent
v-else-if="method === 'mobileMoney'"
:session-id="sessionId"
variant="fields"
:appearance="{ inherit: true }"
:fields="{ customer: 'external', country: 'CM', operator: 'detect' }"
:ready-timeout="10000"
:on-before-submit="placeOrder"
@instance="onInstance"
@change="(event) => (complete = Boolean(event.complete))"
@state-change="onStateChange"
@load-error="onLoadError"
/>
<CardComponent
v-else
:session-id="sessionId"
variant="fields"
:appearance="{ inherit: true }"
:fields="{ customer: 'external' }"
:ready-timeout="10000"
: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>
onBeforeSubmit runs again when the payer tries again after a decline: make your order endpoint
idempotent, the same cart giving the same order (activating again with the same body answers
200). The form shows a declined payment itself, and the payer tries again in it. Mark the order
paid on your server, from the payment.succeeded webhook, never from the browser.
appearance.inherit reads a real text input of your page, its label, your main button and
the background behind the component: render your own form's fields before the payment step (or
name them with :appearance="{ inherit: { input: '#email', button: '#pay' } }"). Switching
method destroys one component and mounts the other; @instance hands you null in between.
API
| Export | Description |
|---|---|
WajubProvider |
Loads the checkout SDK once for the subtree |
CheckoutEmbed |
Full hosted checkout inline |
ComponentEmbed |
Any component, by type |
CardComponent |
Card fields only |
PaymentComponent |
Payment method selector + form |
AddressComponent |
Shipping / billing address |
MobileMoneyComponent |
Mobile money form |
WalletComponent |
Apple Pay / Google Pay |
BankTransferComponent |
Bank transfer form |
CryptoComponent |
Crypto payment |
useWajub() |
Access loaded SDK (throws while loading) |
useWajubOptional() |
A ref of the SDK, null until it has loaded |
useLoadWajub() |
Trigger SDK load manually |
useConfirmPayment(sessionId) |
Submit a mounted component: { confirm(instance), isConfirming }; sessionId as a string, a ref or a getter, read at each confirm() |
Nuxt 3
- The package renders on the server: nothing reads
windowordocumentbefore the component is mounted in the browser, where the SDK loads and the fields mount.<ClientOnly>around<WajubProvider>is not needed. - Create the session on your server — never expose
sk_...to the client.
Documentation
Integration guide: docs.wajub.com/tools/components/vue
License
MIT
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.8.1 | >=3.3 | MIT | |
| 1.8.0 | >=3.3 | MIT | |
| 1.5.1 | >=3.3 | MIT | |
| 1.5.0 | >=3.3 | MIT | |
| 1.4.0 | >=3.3 | MIT | |
| 1.3.1 | >=3.3 | MIT | |
| 1.3.0 | >=3.3 | MIT |