@wajub/vue

Wajub Components for Vue 3 — Provider, composables, and embed components
895
Install
npm install @wajub/vue
Latest Version:1.8.1
Vue:3
License:MIT
Last Updated:Oct 14, 2026
Links: npm
Maintainer: wajubhq

@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, frameTitle update 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.
  • appearance is sent whole, and a top-level key you removed since is sent as undefined, which clears it: the SDK merges what update() gives it. An appearance that becomes undefined (or {}) clears the whole of it, and the frame goes back to the merchant's look.
  • sessionId, type, variant, fields, loader, readyTimeout, collectAddress, addressMode and collectName mount 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. onBeforeSubmit must 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.

  1. Your server prepares the session when the checkout opens: POST /payments with deferred: true, the amount, the currency and an expiry, with your secret key. It gives the page the payment's authorization_token (the sessionId) and authorization_url (Wajub's hosted page, the fallback), keeps transaction.id with the cart, and PATCHes that same payment when the total changes.
  2. Your checkout mounts the fields of the method the payer picked.
  3. onBeforeSubmit places the order: your server checks the cart, creates the order and activates the payment (POST /payments/{id}/activate) with its final total, the customer, your reference and callback. Return false to stop: nothing is charged. Until it is activated, a prepared payment refuses every charge (409).
  4. Your Pay button submits, with useConfirmPayment() or instance.submit(). The default timeout is enough: since Wajub.js 1.8.0 it does not run while the payer approves.
  5. onStateChange keeps the checkout locked while the payment runs, and asks the payer to approve on their phone during APPROVAL_REQUIRED.
  6. onLoadError with ready_timeout falls back to the hosted page: the fields go, and the Pay button places the order first, as onBeforeSubmit does, then sends the payer to authorization_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 window or document before 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