Vue 3
Provider, composables, embed props, and the Nuxt wiring that goes with them.
@wajub/vue wraps @wajub/js in Vue 3 syntax: a provider that loads the runtime once, five field
components, and four composables. The payment API itself does not change, so the behaviour of every
option is documented on Hosted checkout and
Payment fields. This page covers what is specific to Vue.
@wajub/vue
Stable · GAnpm
- Version
- 1.5.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, not a bundled one, so it goes on the same line. The package is ESM
only and declares sideEffects: false.
npm install @wajub/vue @wajub/jsThe provider
WajubProvider calls loadWajub() from @wajub/js/pure and provides the runtime through Vue's
injection. One provider 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>
<ClientOnly>
<WajubProvider>
<slot />
</WajubProvider>
</ClientOnly>
</template>| Prop | Type | Default | What it does |
|---|---|---|---|
defer | boolean | false | Skips the automatic load. Leave it off: in 1.5.1 nothing loads the runtime into the provider afterwards |
load-options | { jsOrigin?, jsUrl? } | CDN defaults | Overrides where the runtime is fetched from |
defer does not work in 1.5.1. It stops the provider from loading the runtime, and the loadWajub()
that useLoadWajub() returns resolves the runtime to you without storing it in the provider, so the
embeds and field components never mount. Leave defer off, and the provider starts loading as soon
as it mounts.
Composables
useWajub throws while the runtime is loading
It throws on every call until the script has arrived, and again 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 what makes this button work: it is null when setup runs, because the provider only
starts loading once it is mounted, and it fills when the runtime arrives. Read runtime.value in
the script. The template unwraps it, so !runtime is enough there.
A ref from 1.6.0, a plain value before
In 1.5.1 and earlier useWajubOptional() returned the value read once in setup, which is
null before the load, so the button above stayed disabled for good. 1.6.0 returns the ref. Code
written for the old form that reads runtime.wajub in the script still works once the runtime
has loaded, but if (!runtime) is no longer a test: move both to runtime.value.
| 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 before the script lands |
useLoadWajub() | { runtime, loading, error, loadWajub } | The first three are refs. Its loadWajub() does not update the provider |
useConfirmPayment(sessionId) | { confirm, isConfirming } | isConfirming is a ref. Submits a mounted field component |
useWajubContext() returns the same { runtime, loading, error } refs if you would rather build
your own composable on top, and WAJUB_INJECTION_KEY is the key the provider uses.
CheckoutEmbed
The hosted checkout inside your own layout. Unlike the other frameworks, the props are a closed
list: session-id, class, style, min-height, locale, embed-origin, theme, 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
:key="sessionId"
: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. Vue turns
the listener into the prop for you.
| 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 iframe loads |
The embed only follows session-id
A new session-id remounts the checkout on the new session. Every other prop is read once:
changing appearance, locale or layout afterwards does nothing. Bind :key to the session
when you want Vue to rebuild the whole component, and keep the instance from @ready for live
changes through update().
<script setup lang="ts">
import { ref, watch } from 'vue';
import type { CheckoutInstance } from '@wajub/js';
import { CheckoutEmbed } from '@wajub/vue';
const props = defineProps<{ sessionId: string; dark: boolean }>();
const checkout = ref<CheckoutInstance | null>(null);
watch(
() => props.dark,
(dark) => checkout.value?.update({ appearance: { colorScheme: dark ? 'dark' : 'light' } }),
);
</script>
<template>
<CheckoutEmbed
:key="sessionId"
:session-id="sessionId"
@ready="(instance) => (checkout = instance)"
/>
</template>Payment fields
Five components render one field group each: CardComponent, MobileMoneyComponent,
WalletComponent, PaymentComponent and AddressComponent. They share the props of
ComponentConfig plus the wrapper props below. ComponentEmbed
is the same component with an explicit type, when the choice is made at runtime.
<script setup lang="ts">
import { ref } 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);
const field = ref<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 |
min-height | number | 200 (card), 240 (mobileMoney), 120 (wallet), 360 (payment), 280 (address) | Reserved height while the field loads |
@instance | (instance | null) => void | none | Fires on mount and again with null on unmount |
@ready | (instance) => void | none | Fires once the field is interactive |
useConfirmPayment guards against a double submission, so a second click while isConfirming is
true is ignored. It resolves with { status } and rejects with a WajubError, including
confirmation_timeout after 60 seconds.
Field components do update in place
Unlike the embed, they watch appearance, locale and layout and call update() with all
three when any of them changes. Changing session-id or type remounts the field instead.
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: '[email protected]' },
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:
- Wrap the checkout in
<ClientOnly>. The runtime needs awindow, and the composables returnnullrather than throwing during the server render. - Put
WajubProviderin a layout so a navigation inside the checkout flow does not reload the runtime. - Fetch the session from
server/api/and pass the token down as a prop.
What the package exports
| Export | What it is |
|---|---|
WajubProvider | Loads the runtime once for the subtree |
CheckoutEmbed | The hosted checkout, inline |
CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent, AddressComponent | One field group each |
ComponentEmbed | The same, with the type given as a prop |
useWajub, useWajubOptional, useLoadWajub | Reach the runtime |
useConfirmPayment | Submit a field component |
WAJUB_INJECTION_KEY, useWajubContext | The raw injection, for your own composable |
Types come with the package: WajubProviderProps, CheckoutEmbedProps, ComponentEmbedProps,
WajubContextValue and, from 1.6.0, WajubRuntimeRef, which is what useWajubOptional() returns,
alongside everything @wajub/js exports.