Skip to content

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 · GA

npm

Version
1.4.0
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/js

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

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

<template>
  <ClientOnly>
    <WajubProvider>
      <slot />
    </WajubProvider>
  </ClientOnly>
</template>
PropTypeDefaultWhat it does
deferbooleanfalseSkips the automatic load, leaving it to useLoadWajub()
load-options{ jsOrigin?, jsUrl? }CDN defaultsOverrides where the runtime is fetched from

defer is for the case where a parent already loaded the runtime, or where you want the fetch to wait for a user action. Without it, the provider starts loading as soon as it mounts.

Composables

The safe form
<script setup lang="ts">
import { useWajubOptional } from '@wajub/vue';

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

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

<template>
  <button :disabled="!runtime" @click="upgrade">Upgrade plan</button>
</template>
ComposableReturnsNotes
useWajub(){ wajub, Wajub, WajubError }Throws while loading, on failure, and with no runtime
useWajubOptional()The same, or nullSafe during SSR and before the script lands
useLoadWajub(){ runtime, loading, error, loadWajub }The first three are refs. Drives the load yourself, with defer
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.

pages/checkout.vue
<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.

PropTypeDefaultWhat it does
session-idstringRequiredThe authorization_token from POST /payments
classstringnoneClass on the container element
styleRecord<string, string>noneInline styles on the container
min-heightnumber480Reserved height in pixels while the iframe loads
Switching the theme after mount
<script setup lang="ts">
import { ref, watch } from 'vue';
import type { CheckoutInstance } from '@wajub/js';

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.

Your own layout, your own button
<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>
PropTypeDefaultWhat it does
typeComponentTypeSet by the aliasOnly on ComponentEmbed
min-heightnumber200Reserved height while the field loads
@instance(instance | null) => voidnoneFires on mount and again with null on unmount
@ready(instance) => voidnoneFires 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.

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

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

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

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

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

Three more things Nuxt asks for:

  • Wrap the checkout in <ClientOnly>. The runtime needs a window, and the composables return null rather than throwing during the server render.
  • Put WajubProvider in 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

ExportWhat it is
WajubProviderLoads the runtime once for the subtree
CheckoutEmbedThe hosted checkout, inline
CardComponent, MobileMoneyComponent, WalletComponent, PaymentComponent, AddressComponentOne field group each
ComponentEmbedThe same, with the type given as a prop
useWajub, useWajubOptional, useLoadWajubReach the runtime
useConfirmPaymentSubmit a field component
WAJUB_INJECTION_KEY, useWajubContextThe raw injection, for your own composable

Types come with the package: WajubProviderProps, CheckoutEmbedProps, ComponentEmbedProps and WajubContextValue, alongside everything @wajub/js exports.

What did you think of this content?