Skip to main content

Vue.js integration: component patterns

This page covers the four component integration patterns for Vue.js. If you haven't set up the SDK yet, start with Getting started.

Component mounting patterns

Basic mounting with lifecycle hooks

All Weavr SDK components follow this pattern in Vue:

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

// Template ref for the container element
const containerRef = ref<HTMLElement | null>(null);

// SDK instance references (not reactive - they're external objects)
let sdkComponent: any = null;

onMounted(() => {
// DOM is ready, safe to mount
sdkComponent =
/* create SDK component */
sdkComponent.mount(containerRef.value);
});

onUnmounted(() => {
// Clean up to prevent memory leaks
try {
if (sdkComponent && typeof sdkComponent.unmount === "function") {
sdkComponent.unmount();
}
} catch (e) {
console.error("Error unmounting component:", e);
}
});
</script>

<template>
<div ref="containerRef"></div>
</template>

Reusable composable pattern

Create a composable for cleaner component code:

// src/composables/useWeavrComponent.ts
import { ref, onMounted, onUnmounted, type Ref } from "vue";

export function useWeavrComponent<
T extends { mount: (el: HTMLElement | null) => void; unmount: () => void },
>(
factory: () => T | null
): {
containerRef: Ref<HTMLElement | null>;
component: Ref<T | null>;
} {
const containerRef = ref<HTMLElement | null>(null);
const component = ref<T | null>(null) as Ref<T | null>;

onMounted(() => {
const instance = factory();
if (instance && containerRef.value) {
instance.mount(containerRef.value);
component.value = instance;
}
});

onUnmounted(() => {
try {
if (component.value && typeof component.value.unmount === "function") {
component.value.unmount();
}
} catch (e) {
console.error("Error unmounting component:", e);
}
});

return { containerRef, component };
}

Usage:

<script setup lang="ts">
import { useWeavrComponent } from "@/composables/useWeavrComponent";

const form = window.OpcUxSecureClient.form();

const { containerRef } = useWeavrComponent(() =>
form.input("password", "password", { placeholder: "Password" })
);
</script>

<template>
<div ref="containerRef"></div>
</template>

Component patterns

The Weavr SDK components follow a few distinct patterns. This section demonstrates each pattern with a representative example. For component-specific parameters and options, refer to the individual component documentation.

PatternComponentsKey Characteristics
Input + TokenizePassword, Confirm Password, Passcode, Confirm Passcodeform.input()form.tokenize()
Authenticated DisplayCard Number, CVV, Show Card PINassociate()span()mount()
Authenticated InputCapture Card PINassociate()form.input()tokenize()
Full-screen VerificationKYC, KYB, Director KYCassociate()init() with callbacks

Input + tokenize pattern

Used for collecting sensitive credentials (passwords, passcodes). The SDK captures input securely and returns a token for server-side use.

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const inputRef = ref<HTMLElement | null>(null);
const isLoading = ref(false);
const error = ref<string | null>(null);

let form: any = null;
let input: any = null;

onMounted(() => {
if (!window.OpcUxSecureClient) {
error.value = "SDK not loaded";
return;
}

form = window.OpcUxSecureClient.form();

// For Password use: form.input("password", "password", options)
// For Passcode use: form.input("passcode", "passCode", options)
// For Confirm Password use: form.input("confirmPassword", "confirmPassword", options)
input = form.input("password", "password", {
placeholder: "Enter your password",
maxlength: 50,
style: {
base: { fontSize: "16px", color: "#333" },
invalid: { color: "#dc3545" },
},
});

input.mount(inputRef.value);

// Optional: Handle enter key
input.on("submit", () => handleSubmit());
});

onUnmounted(() => {
// Defensive cleanup to prevent errors if SDK component wasn't initialized
try {
if (input && typeof input.unmount === "function") {
input.unmount();
}
} catch (e) {
console.error("Error unmounting component:", e);
}
});

const handleSubmit = async () => {
isLoading.value = true;
error.value = null;

form.tokenize(async (tokens: Record<string, string>) => {
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: tokens.password }),
});

if (!response.ok) throw new Error("Authentication failed");
console.log("Success");
} catch (e) {
error.value = e instanceof Error ? e.message : "Failed";
} finally {
isLoading.value = false;
}
});
};
</script>

<template>
<form @submit.prevent="handleSubmit">
<div ref="inputRef" class="weavr-input"></div>
<p v-if="error" class="error">{{ error }}</p>
<button type="submit" :disabled="isLoading">Submit</button>
</form>
</template>

<style scoped>
.weavr-input {
border: 1px solid #ccc;
border-radius: 4px;
padding: 8px;
min-height: 40px;
}
</style>

Authenticated display pattern

Used for displaying sensitive card data (card number, CVV, PIN). Requires user authentication via associate() before the data can be shown.

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const displayRef = ref<HTMLElement | null>(null);
const isLoading = ref(true);
const error = ref<string | null>(null);

let span: any = null;

const props = defineProps<{
authToken: string;
token: string; // Tokenized value from API
}>();

onMounted(() => {
if (!window.OpcUxSecureClient) {
error.value = "SDK not loaded";
return;
}

// Must authenticate before displaying sensitive data
window.OpcUxSecureClient.associate(
`Bearer ${props.authToken}`,
() => {
// For Card Number use: span("cardNumber", token)
// For CVV use: span("cvv", token)
// For Card PIN use: span("cardPin", token)
span = window.OpcUxSecureClient.span("cardNumber", props.token, {
style: {
fontSize: "18px",
fontFamily: "monospace",
letterSpacing: "2px",
},
});

span.mount(displayRef.value);
isLoading.value = false;
},
(e) => {
console.error("Association failed:", e);
error.value = "Failed to authenticate";
isLoading.value = false;
}
);
});

onUnmounted(() => {
try {
if (span && typeof span.unmount === "function") {
span.unmount();
}
} catch (e) {
console.error("Error unmounting component:", e);
}
});
</script>

<template>
<div class="secure-display">
<span v-if="isLoading">Loading...</span>
<span v-else-if="error" class="error">{{ error }}</span>
<span ref="displayRef"></span>
</div>
</template>

Authenticated input pattern

Used for capturing card PINs. Combines authentication (associate()) with the input/tokenize pattern.

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const inputRef = ref<HTMLElement | null>(null);
const isReady = ref(false);

let form: any = null;
let input: any = null;

const props = defineProps<{
authToken: string;
}>();

const emit = defineEmits<{
(e: "tokenized", token: string): void;
}>();

onMounted(() => {
form = window.OpcUxSecureClient.form();

// Must authenticate before capturing card PIN
window.OpcUxSecureClient.associate(
`Bearer ${props.authToken}`,
() => {
input = form.input("cardPin", "cardPin", {
placeholder: "Enter 4-digit PIN",
});
input.mount(inputRef.value);
input.on("submit", () => handleSubmit());
isReady.value = true;
},
(e) => console.error("Associate failed:", e)
);
});

onUnmounted(() => {
// Defensive cleanup to prevent errors if SDK component wasn't initialized
try {
if (input && typeof input.unmount === "function") {
input.unmount();
}
} catch (e) {
console.error("Error unmounting component:", e);
}
});

const handleSubmit = () => {
form.tokenize((tokens: Record<string, string>) => {
emit("tokenized", tokens.cardPin);
});
};
</script>

<template>
<form @submit.prevent="handleSubmit">
<div ref="inputRef"></div>
<button type="submit" :disabled="!isReady">Set PIN</button>
</form>
</template>

Full-screen verification pattern

Used for KYC/KYB identity verification flows. These components take over a container element and manage their own UI.

tip

KYB and Director KYC follow the same pattern with minor differences:

  • KYB: uses kyb().init() instead of consumer_kyc().init()
  • Director KYC: uses kyc().init() and does not require associate() (accessed via email link)
<script setup lang="ts">
import { ref, onMounted } from "vue";

const isLoading = ref(true);

const props = defineProps<{
reference: string; // From API: POST /consumers/kyc or POST /corporates/kyb
authToken: string;
}>();

const emit = defineEmits<{
(e: "complete"): void;
(e: "error", message: string): void;
}>();

onMounted(() => {
if (!window.OpcUxSecureClient) {
emit("error", "SDK not loaded");
return;
}

window.OpcUxSecureClient.associate(
`Bearer ${props.authToken}`,
() => {
// For KYB use: window.OpcUxSecureClient.kyb().init(selector, options, callback, config)
window.OpcUxSecureClient.consumer_kyc().init({
selector: "#kyc-container",
reference: props.reference,
lang: "en",

onMessage: (message: string, additionalInfo: any) => {
console.log("KYC message:", message, additionalInfo);
if (message === "kycSubmitted") {
emit("complete");
}
},

onError: (error: string) => {
console.error("KYC error:", error);
emit("error", error);
},
});

isLoading.value = false;
},
(error) => {
console.error("Association failed:", error);
emit("error", "Failed to authenticate");
isLoading.value = false;
}
);
});

// Note: Full-screen components handle their own cleanup
</script>

<template>
<div>
<div v-if="isLoading" class="loading">Loading verification...</div>
<div id="kyc-container" :class="{ hidden: isLoading }"></div>
</div>
</template>

<style scoped>
#kyc-container {
min-height: 500px;
}
.hidden {
display: none;
}
</style>

Advanced patterns

When using Weavr components in modals or dialogs, handle visibility carefully:

<script setup lang="ts">
import { ref, watch, nextTick } from "vue";

const showModal = ref(false);
const kycRef = ref<HTMLElement | null>(null);

// Initialize when modal opens
watch(showModal, async (isOpen) => {
if (isOpen) {
await nextTick(); // Wait for DOM update
initializeKYC();
}
});

const initializeKYC = () => {
window.OpcUxSecureClient.consumer_kyc().init({
selector: "#modal-kyc-container",
reference: "your-reference",
// ... other options
});
};
</script>

<template>
<button @click="showModal = true">Start Verification</button>

<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal-content">
<button @click="showModal = false">Close</button>
<div id="modal-kyc-container" ref="kycRef"></div>
</div>
</div>
</Teleport>
</template>

Using v-show (for simple input components)

For simple inputs that need quick show/hide, v-show preserves the mounted state:

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const showPassword = ref(false);
const passwordRef = ref<HTMLElement | null>(null);
let input: any = null;

onMounted(() => {
const form = window.OpcUxSecureClient.form();
input = form.input("password", "password", { placeholder: "Password" });
input.mount(passwordRef.value);
});

onUnmounted(() => {
// Defensive cleanup to prevent errors if SDK component wasn't initialized
try {
if (input && typeof input.unmount === "function") {
input.unmount();
}
} catch (e) {
console.error("Error unmounting component:", e);
}
});
</script>

<template>
<button @click="showPassword = !showPassword">Toggle Password</button>
<div v-show="showPassword" ref="passwordRef"></div>
</template>

Dynamic component loading

Use defineAsyncComponent for code-splitting Weavr-integrated components:

// src/components/LazyKYC.ts
import { defineAsyncComponent } from "vue";

export const LazyKYC = defineAsyncComponent({
loader: () => import("./KYCComponent.vue"),
loadingComponent: {
template: "<div>Loading verification module...</div>",
},
delay: 200,
});
<script setup lang="ts">
import { ref } from "vue";
import { LazyKYC } from "./LazyKYC";

const showKYC = ref(false);
const kycReference = ref("");

const startKYC = async () => {
// Fetch KYC reference from your API
const response = await fetch("/api/kyc/start");
const data = await response.json();
kycReference.value = data.reference;
showKYC.value = true;
};
</script>

<template>
<button @click="startKYC">Start Verification</button>
<LazyKYC v-if="showKYC" :reference="kycReference" />
</template>

Form integration with validation libraries

Integrating Weavr inputs with VeeValidate or other validation libraries:

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { useForm, useField } from "vee-validate";
import * as yup from "yup";

const schema = yup.object({
email: yup.string().required().email(),
// Password is handled by Weavr, so we track it separately
});

const { handleSubmit, errors } = useForm({ validationSchema: schema });
const { value: email } = useField("email");

const passwordRef = ref<HTMLElement | null>(null);
const passwordToken = ref<string | null>(null);
const passwordError = ref<string | null>(null);

let form: any = null;
let passwordInput: any = null;

onMounted(() => {
form = window.OpcUxSecureClient.form();
passwordInput = form.input("password", "password", {
placeholder: "Password (min 8 characters)",
});
passwordInput.mount(passwordRef.value);
});

onUnmounted(() => {
try {
if (passwordInput && typeof passwordInput.unmount === "function") {
passwordInput.unmount();
}
} catch (e) {
console.error("Error unmounting component:", e);
}
});

const onSubmit = handleSubmit(async (values) => {
// First tokenize the password
form.tokenize(async (tokens: Record<string, string>) => {
if (!tokens.password) {
passwordError.value = "Password is required";
return;
}

try {
const response = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: values.email,
password: tokens.password,
}),
});

if (!response.ok) throw new Error("Registration failed");

console.log("Registered successfully");
} catch (e) {
console.error(e);
}
});
});
</script>

<template>
<form @submit="onSubmit">
<div>
<label>Email</label>
<input v-model="email" type="email" />
<span v-if="errors.email">{{ errors.email }}</span>
</div>

<div>
<label>Password</label>
<div ref="passwordRef"></div>
<span v-if="passwordError">{{ passwordError }}</span>
</div>

<button type="submit">Register</button>
</form>
</template>

Multi-component card display

Display multiple card details together:

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const cardNumberRef = ref<HTMLElement | null>(null);
const cvvRef = ref<HTMLElement | null>(null);
const pinRef = ref<HTMLElement | null>(null);

let cardNumberSpan: any = null;
let cvvSpan: any = null;
let pinSpan: any = null;

const props = defineProps<{
authToken: string;
cardNumberToken: string;
cvvToken: string;
pinToken?: string; // Optional for physical cards
}>();

onMounted(() => {
window.OpcUxSecureClient.associate(
`Bearer ${props.authToken}`,
() => {
// Mount all card components
cardNumberSpan = window.OpcUxSecureClient.span(
"cardNumber",
props.cardNumberToken
);
cardNumberSpan.mount(cardNumberRef.value);

cvvSpan = window.OpcUxSecureClient.span("cvv", props.cvvToken);
cvvSpan.mount(cvvRef.value);

if (props.pinToken) {
pinSpan = window.OpcUxSecureClient.span("cardPin", props.pinToken);
pinSpan.mount(pinRef.value);
}
},
(e) => console.error("Associate failed:", e)
);
});

onUnmounted(() => {
try {
if (cardNumberSpan && typeof cardNumberSpan.unmount === "function") {
cardNumberSpan.unmount();
}
if (cvvSpan && typeof cvvSpan.unmount === "function") {
cvvSpan.unmount();
}
if (pinSpan && typeof pinSpan.unmount === "function") {
pinSpan.unmount();
}
} catch (e) {
console.error("Error unmounting components:", e);
}
});
</script>

<template>
<div class="card-display">
<div class="card-field">
<label>Card Number</label>
<span ref="cardNumberRef"></span>
</div>
<div class="card-field">
<label>CVV</label>
<span ref="cvvRef"></span>
</div>
<div v-if="pinToken" class="card-field">
<label>PIN</label>
<span ref="pinRef"></span>
</div>
</div>
</template>

Next steps