Skip to main content

Vue.js Integration (Web SDK)

This guide covers integrating Weavr Web SDK UI components with Vue.js 3 using the Composition API. You'll learn the correct patterns for script loading, lifecycle management, and the four distinct component integration patterns.

Overview and quick start

Prerequisites

  • Vue.js 3.x with Composition API
  • A Weavr account with API credentials (UI key)
  • Basic understanding of Vue.js reactivity and lifecycle hooks

5-minute minimal example

This example demonstrates a working password input component:

1. Add the SDK to your index.html:

<!doctype html>
<html>
<head>
<script src="https://sandbox.weavr.io/app/secure/static/client.1.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

2. Create a Vue component:

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

const containerRef = ref<HTMLElement | null>(null);
let form: any = null;
let input: any = null;

onMounted(() => {
if (!window.OpcUxSecureClient) {
console.error("Weavr SDK not loaded - check index.html script tag");
return;
}

window.OpcUxSecureClient.init("your_ui_key_here");
form = window.OpcUxSecureClient.form();
input = form.input("p", "password", { placeholder: "Enter password" });
input.mount(containerRef.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);
}
});

const handleSubmit = () => {
form.tokenize((tokens: Record<string, string>) => {
console.log("Password token:", tokens.password);
// Send tokens.password to your server
});
};
</script>

<template>
<form @submit.prevent="handleSubmit">
<div ref="containerRef"></div>
<button type="submit">Submit</button>
</form>
</template>

Script loading

Critical requirement

The Weavr SDK must be loaded via a <script> tag in index.html. Do not import it through Vite or any bundler.

See Framework Integration Overview for details on why this is required.

Vite configuration

Add the SDK to index.html in your project root:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Your App</title>

<!-- Weavr SDK - MUST be loaded here, not via import -->
<script src="https://sandbox.weavr.io/app/secure/static/client.1.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

Bundler configuration

For Vue command-line tool projects, add the SDK to public/index.html:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title><%= htmlWebpackPlugin.options.title %></title>

<!-- Weavr SDK -->
<script src="https://sandbox.weavr.io/app/secure/static/client.1.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

Environment-based SDK URLs

For production deployments, use different SDK URLs per environment:

<script>
// Determine SDK URL based on environment
const sdkUrl = import.meta.env.PROD
? 'https://secure.weavr.io/app/secure/static/client.1.js'
: 'https://sandbox.weavr.io/app/secure/static/client.1.js';

const script = document.createElement('script');
script.src = sdkUrl;
document.head.appendChild(script);
</script>

Or use separate HTML files for each environment.

SDK initialization

Create a Vue plugin to initialize the SDK once when your app starts:

// src/plugins/weavr.ts
import type { App } from "vue";

declare global {
interface Window {
OpcUxSecureClient: {
init: (
uiKey: string,
options?: { fonts?: Array<{ cssSrc: string }> }
) => void;
form: () => WeavrForm;
span: (type: string, token: string, options?: object) => WeavrSpan;
associate: (
token: string,
onSuccess: () => void,
onError?: (e: Error) => void
) => void;
kyb: () => { init: (...args: any[]) => void };
kyc: () => { init: (...args: any[]) => void };
consumer_kyc: () => { init: (options: object) => void };
};
}
}

interface WeavrForm {
input: (name: string, type: string, options?: object) => WeavrInput;
tokenize: (callback: (tokens: Record<string, string>) => void) => void;
}

interface WeavrInput {
mount: (element: HTMLElement | null) => void;
unmount: () => void;
on: (event: string, callback: () => void) => void;
}

interface WeavrSpan {
mount: (element: HTMLElement | null) => void;
unmount: () => void;
}

export const weavrPlugin = {
install(
app: App,
options: { uiKey: string; fonts?: Array<{ cssSrc: string }> }
) {
// Check SDK is loaded
if (!window.OpcUxSecureClient) {
console.error("Weavr SDK not loaded. Add script tag to index.html.");
return;
}

// Initialize SDK
window.OpcUxSecureClient.init(options.uiKey, {
fonts: options.fonts,
});

// Make available globally
app.config.globalProperties.$weavr = window.OpcUxSecureClient;
},
};

Register the plugin in your main entry:

// src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { weavrPlugin } from "./plugins/weavr";

const app = createApp(App);

app.use(weavrPlugin, {
uiKey: import.meta.env.VITE_WEAVR_UI_KEY,
fonts: [
{ cssSrc: "https://fonts.googleapis.com/css?family=Inter:400,500,600" },
],
});

app.mount("#app");

Per-component initialization

For simpler setups, initialize in each component:

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

onMounted(() => {
if (window.OpcUxSecureClient) {
window.OpcUxSecureClient.init("your_ui_key");
}
});
</script>
caution

Calling init() multiple times is safe, but wasteful. Prefer the plugin pattern for larger applications.

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("p", "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("p", "password", options)
// For Passcode use: form.input("passcode", "password", options) - SDK only supports "password" type
// For Confirm Password use: form.input("cp", "confirmPassword", options)
input = form.input("p", "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("p", "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("p", "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>

Reusable wrapper components

For production applications, create reusable wrapper components that encapsulate SDK logic. This approach provides a cleaner API, reduces boilerplate, and makes components easier to test and maintain.

Available wrapper components

The following wrapper components cover all Weavr SDK integration patterns:

Input + Tokenize Pattern:

ComponentPurpose
WeavrPasswordInputPassword input with tokenize
WeavrPasscodeInputPasscode input (6-digit) with tokenize
WeavrConfirmPasswordPassword + confirm password pair

Authenticated Display Pattern:

ComponentPurpose
WeavrCardNumberDisplay card number (requires auth)
WeavrCvvDisplay CVV (requires auth)
WeavrCardPinDisplay card PIN (requires auth)

Authenticated Input Pattern:

ComponentPurpose
WeavrCardPinCaptureCapture/set card PIN (requires auth)

Full-Screen Verification Pattern:

ComponentPurpose
WeavrKycConsumer KYC verification (requires auth)
WeavrKybCorporate KYB verification (requires auth)
WeavrDirectorKycDirector KYC verification (NO auth)

Provider:

ComponentPurpose
WeavrAuthProviderAuthentication context provider
tip

All authenticated components can be used standalone (pass authToken prop) or within a WeavrAuthProvider which handles authentication once for all children.

Organize wrapper components by their integration pattern:

src/components/weavr/
├── input-tokenize/
│ ├── WeavrPasswordInput.vue
│ ├── WeavrPasscodeInput.vue
│ ├── WeavrConfirmPassword.vue
│ └── index.ts
├── authenticated-display/
│ ├── WeavrCardNumber.vue
│ ├── WeavrCvv.vue
│ ├── WeavrCardPin.vue
│ └── index.ts
├── authenticated-input/
│ ├── WeavrCardPinCapture.vue
│ └── index.ts
├── verification/
│ ├── WeavrKyc.vue
│ ├── WeavrKyb.vue
│ ├── WeavrDirectorKyc.vue
│ └── index.ts
├── provider/
│ ├── WeavrAuthProvider.vue
│ └── index.ts
├── composables/
│ ├── useWeavrAuth.ts
│ └── index.ts
└── index.ts

The root index.ts re-exports all components for convenient imports:

// src/components/weavr/index.ts
export * from "./input-tokenize";
export * from "./authenticated-display";
export * from "./authenticated-input";
export * from "./verification";
export * from "./provider";
export * from "./composables";

Password input wrapper

<!-- src/components/weavr/WeavrPasswordInput.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const props = withDefaults(
defineProps<{
name?: string;
placeholder?: string;
maxlength?: number;
disabled?: boolean;
style?: Record<string, any>;
}>(),
{
name: "password",
placeholder: "Enter password",
maxlength: 50,
disabled: false,
}
);

const emit = defineEmits<{
(e: "ready"): void;
(e: "change", isValid: boolean): void;
(e: "submit"): void;
(e: "error", error: Error): void;
}>();

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

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

// Expose tokenize method to parent
const tokenize = (): Promise<string> => {
return new Promise((resolve, reject) => {
if (!form) {
reject(new Error("Form not initialized"));
return;
}
form.tokenize((tokens: Record<string, string>) => {
const token = tokens[props.name];
if (token) {
resolve(token);
} else {
reject(
new Error("No token received - input may be empty or invalid")
);
}
});
});
};

defineExpose({ tokenize });

onMounted(() => {
if (!window.OpcUxSecureClient) {
hasError.value = true;
emit("error", new Error("Weavr SDK not loaded"));
return;
}

try {
form = window.OpcUxSecureClient.form();
input = form.input(props.name, "password", {
placeholder: props.placeholder,
maxlength: props.maxlength,
disabled: props.disabled,
style: props.style,
});

input.on("ready", () => {
isReady.value = true;
emit("ready");
});

input.on("change", (event: any) => {
emit("change", event?.valid ?? false);
});

input.on("submit", () => {
emit("submit");
});

input.mount(containerRef.value);
} catch (e) {
hasError.value = true;
emit("error", e instanceof Error ? e : new Error("Unknown error"));
}
});

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

<template>
<div class="weavr-password-input">
<div v-if="hasError" class="weavr-error">Failed to load secure input</div>
<div v-else ref="containerRef" class="weavr-input-container"></div>
</div>
</template>

<style scoped>
.weavr-input-container {
min-height: 40px;
}
</style>

Usage:

<script setup lang="ts">
import { ref } from "vue";
import WeavrPasswordInput from "@/components/weavr/WeavrPasswordInput.vue";

const passwordInput = ref<InstanceType<typeof WeavrPasswordInput> | null>(
null
);
const isValid = ref(false);

const handleSubmit = async () => {
try {
const token = await passwordInput.value?.tokenize();
console.log("Password token:", token);
// Send token to your server
} catch (e) {
console.error("Tokenization failed:", e);
}
};
</script>

<template>
<form @submit.prevent="handleSubmit">
<WeavrPasswordInput
ref="passwordInput"
placeholder="Enter your password"
@change="isValid = $event"
@submit="handleSubmit"
/>
<button type="submit" :disabled="!isValid">Login</button>
</form>
</template>

Card number display wrapper

<!-- src/components/weavr/WeavrCardNumber.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted, inject } from "vue";

const props = defineProps<{
token: string;
style?: Record<string, any>;
}>();

const emit = defineEmits<{
(e: "ready"): void;
(e: "error", error: Error): void;
}>();

// Inject auth state from provider (if available)
const weavrAuth = inject<{ isAuthenticated: boolean } | null>(
"weavrAuth",
null
);

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

let span: any = null;

const mountSpan = () => {
try {
span = window.OpcUxSecureClient.span("cardNumber", props.token, {
style: props.style || {
fontSize: "16px",
fontFamily: "monospace",
letterSpacing: "2px",
},
});
span.mount(containerRef.value);
isLoading.value = false;
emit("ready");
} catch (e) {
error.value = "Failed to display card number";
emit("error", e instanceof Error ? e : new Error("Unknown error"));
}
};

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

// If using provider, auth is already handled
if (weavrAuth?.isAuthenticated) {
mountSpan();
} else {
// Component used standalone - caller must have called associate() already
mountSpan();
}
});

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

<template>
<div class="weavr-card-number">
<span v-if="isLoading" class="loading">Loading...</span>
<span v-else-if="error" class="error">{{ error }}</span>
<span v-else ref="containerRef"></span>
</div>
</template>

CVV display wrapper

<!-- src/components/weavr/WeavrCvv.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted, inject } from "vue";

const props = defineProps<{
token: string;
style?: Record<string, any>;
}>();

const emit = defineEmits<{
(e: "ready"): void;
(e: "error", error: Error): void;
}>();

const weavrAuth = inject<{ isAuthenticated: boolean } | null>(
"weavrAuth",
null
);

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

let span: any = null;

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

try {
span = window.OpcUxSecureClient.span("cvv", props.token, {
style: props.style || { fontSize: "16px", fontFamily: "monospace" },
});
span.mount(containerRef.value);
isLoading.value = false;
emit("ready");
} catch (e) {
error.value = "Failed to display CVV";
emit("error", e instanceof Error ? e : new Error("Unknown error"));
}
});

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

<template>
<div class="weavr-cvv">
<span v-if="isLoading" class="loading">...</span>
<span v-else-if="error" class="error">{{ error }}</span>
<span v-else ref="containerRef"></span>
</div>
</template>

Passcode input wrapper

For 6-digit passcode inputs (used in registration/login flows):

<!-- src/components/weavr/WeavrPasscodeInput.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const props = withDefaults(
defineProps<{
name?: string;
placeholder?: string;
maxlength?: number;
disabled?: boolean;
style?: Record<string, any>;
}>(),
{
name: "passcode",
placeholder: "Enter 6-digit passcode",
maxlength: 6,
disabled: false,
}
);

const emit = defineEmits<{
(e: "ready"): void;
(e: "change", isValid: boolean): void;
(e: "submit"): void;
(e: "error", error: Error): void;
}>();

const containerRef = ref<HTMLElement | null>(null);
const hasError = ref(false);

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

const tokenize = (): Promise<string> => {
return new Promise((resolve, reject) => {
if (!form) {
reject(new Error("Form not initialized"));
return;
}
form.tokenize((tokens: Record<string, string>) => {
const token = tokens[props.name];
if (token) {
resolve(token);
} else {
reject(new Error("No token received"));
}
});
});
};

defineExpose({ tokenize });

onMounted(() => {
if (!window.OpcUxSecureClient) {
hasError.value = true;
emit("error", new Error("Weavr SDK not loaded"));
return;
}

try {
form = window.OpcUxSecureClient.form();
input = form.input(props.name, "password", {
placeholder: props.placeholder,
maxlength: props.maxlength,
disabled: props.disabled,
style: props.style || {
base: {
fontSize: "20px",
letterSpacing: "8px",
textAlign: "center",
fontFamily: "monospace",
},
},
});

input.on("ready", () => emit("ready"));
input.on("change", (event: any) => emit("change", event?.valid ?? false));
input.on("submit", () => emit("submit"));
input.mount(containerRef.value);
} catch (e) {
hasError.value = true;
emit("error", e instanceof Error ? e : new Error("Unknown error"));
}
});

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

<template>
<div class="weavr-passcode-input">
<div v-if="hasError" class="weavr-error">Failed to load secure input</div>
<div v-else ref="containerRef" class="weavr-input-container"></div>
</div>
</template>

Card PIN display wrapper

For displaying card PIN (requires authentication):

<!-- src/components/weavr/WeavrCardPin.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted, inject } from "vue";

const props = defineProps<{
token: string;
style?: Record<string, any>;
}>();

const emit = defineEmits<{
(e: "ready"): void;
(e: "error", error: Error): void;
}>();

const weavrAuth = inject<{ isAuthenticated: boolean } | null>(
"weavrAuth",
null
);

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

let span: any = null;

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

try {
span = window.OpcUxSecureClient.span("cardPin", props.token, {
style: props.style || {
fontSize: "16px",
fontFamily: "monospace",
letterSpacing: "4px",
},
});
span.mount(containerRef.value);
isLoading.value = false;
emit("ready");
} catch (e) {
error.value = "Failed to display PIN";
emit("error", e instanceof Error ? e : new Error("Unknown error"));
}
});

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

<template>
<div class="weavr-card-pin">
<span v-if="isLoading" class="loading">****</span>
<span v-else-if="error" class="error">{{ error }}</span>
<span v-else ref="containerRef"></span>
</div>
</template>

Consumer KYC wrapper

Full-screen identity verification for consumers (requires authentication):

<!-- src/components/weavr/WeavrKyc.vue -->
<script setup lang="ts">
import { ref, onMounted, inject } from "vue";

const props = withDefaults(
defineProps<{
reference: string; // From API: POST /consumers/kyc
authToken?: string; // Optional if using WeavrAuthProvider
lang?: string;
containerId?: string;
}>(),
{
lang: "en",
containerId: "weavr-kyc-container",
}
);

const emit = defineEmits<{
(e: "ready"): void;
(e: "submitted"): void;
(e: "approved"): void;
(e: "rejected"): void;
(e: "message", message: string, additionalInfo: any): void;
(e: "error", error: Error): void;
}>();

const weavrAuth = inject<{
isAuthenticated: boolean;
authToken: string;
} | null>("weavrAuth", null);

const isLoading = ref(true);
const hasError = ref(false);
const errorMessage = ref<string | null>(null);

const initializeKyc = () => {
try {
window.OpcUxSecureClient.consumer_kyc().init({
selector: `#${props.containerId}`,
reference: props.reference,
lang: props.lang,
onMessage: (message: string, additionalInfo: any) => {
emit("message", message, additionalInfo);
if (message === "kycSubmitted") emit("submitted");
else if (message === "kycApproved") emit("approved");
else if (message === "kycRejected") emit("rejected");
},
onError: (error: string) => {
hasError.value = true;
errorMessage.value = error;
emit("error", new Error(error));
},
});
isLoading.value = false;
emit("ready");
} catch (e) {
hasError.value = true;
errorMessage.value = e instanceof Error ? e.message : "Unknown error";
emit("error", e instanceof Error ? e : new Error("Unknown error"));
}
};

onMounted(() => {
if (!window.OpcUxSecureClient) {
hasError.value = true;
errorMessage.value = "Weavr SDK not loaded";
emit("error", new Error("SDK not loaded"));
return;
}

if (weavrAuth?.isAuthenticated) {
initializeKyc();
} else if (props.authToken) {
window.OpcUxSecureClient.associate(
`Bearer ${props.authToken}`,
() => initializeKyc(),
(e: Error) => {
hasError.value = true;
errorMessage.value = "Authentication failed";
emit("error", e);
}
);
} else {
hasError.value = true;
errorMessage.value = "Auth token required";
emit(
"error",
new Error(
"Auth token required - use WeavrAuthProvider or pass authToken prop"
)
);
}
});
</script>

<template>
<div class="weavr-kyc">
<div v-if="isLoading" class="kyc-loading">
<slot name="loading">Loading verification...</slot>
</div>
<div v-if="hasError" class="kyc-error">
<slot name="error" :error="errorMessage">{{ errorMessage }}</slot>
</div>
<div :id="containerId" :class="{ hidden: isLoading || hasError }"></div>
</div>
</template>

<style scoped>
.weavr-kyc {
min-height: 500px;
}
.kyc-loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
}
.kyc-error {
padding: 1rem;
color: #dc3545;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
.hidden {
display: none;
}
</style>

Director KYC wrapper

Director identity verification accessed via email link. Does not require authentication:

<!-- src/components/weavr/WeavrDirectorKyc.vue -->
<script setup lang="ts">
import { ref, onMounted } from "vue";

const props = withDefaults(
defineProps<{
reference: string; // From email link
lang?: string;
containerId?: string;
}>(),
{
lang: "en",
containerId: "weavr-director-kyc-container",
}
);

const emit = defineEmits<{
(e: "ready"): void;
(e: "submitted"): void;
(e: "approved"): void;
(e: "rejected"): void;
(e: "message", message: string, additionalInfo: any): void;
(e: "error", error: Error): void;
}>();

const isLoading = ref(true);
const hasError = ref(false);
const errorMessage = ref<string | null>(null);

onMounted(() => {
if (!window.OpcUxSecureClient) {
hasError.value = true;
errorMessage.value = "Weavr SDK not loaded";
emit("error", new Error("SDK not loaded"));
return;
}

try {
// Director KYC does NOT require associate()
window.OpcUxSecureClient.kyc().init(
`#${props.containerId}`,
{ reference: props.reference, lang: props.lang },
(message: string, additionalInfo: any) => {
emit("message", message, additionalInfo);
if (message === "kycSubmitted") emit("submitted");
else if (message === "kycApproved") emit("approved");
else if (message === "kycRejected") emit("rejected");
},
{
onError: (error: string) => {
hasError.value = true;
errorMessage.value = error;
emit("error", new Error(error));
},
}
);
isLoading.value = false;
emit("ready");
} catch (e) {
hasError.value = true;
errorMessage.value = e instanceof Error ? e.message : "Unknown error";
emit("error", e instanceof Error ? e : new Error("Unknown error"));
}
});
</script>

<template>
<div class="weavr-director-kyc">
<div v-if="isLoading" class="kyc-loading">
<slot name="loading">Loading verification...</slot>
</div>
<div v-if="hasError" class="kyc-error">
<slot name="error" :error="errorMessage">{{ errorMessage }}</slot>
</div>
<div :id="containerId" :class="{ hidden: isLoading || hasError }"></div>
</div>
</template>
Director KYC exception

Unlike other authenticated components, WeavrDirectorKyc does not require authentication via associate(). Directors receive a verification link via email containing the reference token.

Authentication provider pattern

For pages displaying multiple authenticated components (card details, PIN, etc.), use a provider component to handle authentication once and share the auth state with all children.

Authentication provider

<!-- src/components/weavr/WeavrAuthProvider.vue -->
<script setup lang="ts">
import { ref, provide, onMounted, watch } from "vue";

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

const emit = defineEmits<{
(e: "authenticated"): void;
(e: "error", error: Error): void;
}>();

const isAuthenticated = ref(false);
const isLoading = ref(true);
const error = ref<string | null>(null);

// Provide auth state to all descendant components
provide("weavrAuth", {
isAuthenticated,
authToken: props.authToken,
});

const authenticate = () => {
if (!window.OpcUxSecureClient) {
error.value = "Weavr SDK not loaded";
isLoading.value = false;
emit("error", new Error("SDK not loaded"));
return;
}

if (!props.authToken) {
error.value = "Auth token is required";
isLoading.value = false;
emit("error", new Error("Auth token is required"));
return;
}

window.OpcUxSecureClient.associate(
`Bearer ${props.authToken}`,
() => {
isAuthenticated.value = true;
isLoading.value = false;
emit("authenticated");
},
(e: Error) => {
error.value = "Authentication failed";
isLoading.value = false;
emit("error", e);
}
);
};

onMounted(() => {
authenticate();
});

// Re-authenticate if token changes
watch(
() => props.authToken,
(newToken, oldToken) => {
if (newToken && newToken !== oldToken) {
isAuthenticated.value = false;
isLoading.value = true;
error.value = null;
authenticate();
}
}
);
</script>

<template>
<div class="weavr-auth-provider">
<div v-if="isLoading" class="auth-loading">
<slot name="loading">Authenticating...</slot>
</div>
<div v-else-if="error" class="auth-error">
<slot name="error" :error="error">{{ error }}</slot>
</div>
<slot v-else></slot>
</div>
</template>

Using the provider with card components

<script setup lang="ts">
import { ref, onMounted } from "vue";
import WeavrAuthProvider from "@/components/weavr/WeavrAuthProvider.vue";
import WeavrCardNumber from "@/components/weavr/WeavrCardNumber.vue";
import WeavrCvv from "@/components/weavr/WeavrCvv.vue";

const authToken = ref("");
const cardNumberToken = ref("");
const cvvToken = ref("");

onMounted(async () => {
// Fetch tokens from your API
const response = await fetch("/api/card/tokens");
const data = await response.json();

authToken.value = data.authToken;
cardNumberToken.value = data.cardNumberToken;
cvvToken.value = data.cvvToken;
});
</script>

<template>
<WeavrAuthProvider
v-if="authToken"
:auth-token="authToken"
@error="console.error('Auth failed:', $event)"
>
<template #loading>
<div class="skeleton">Loading card details...</div>
</template>

<template #error="{ error }">
<div class="error-message">
Failed to authenticate: {{ error }}
<button @click="$router.push('/login')">Re-login</button>
</div>
</template>

<div class="card-details">
<div class="field">
<label>Card Number</label>
<WeavrCardNumber :token="cardNumberToken" />
</div>

<div class="field">
<label>CVV</label>
<WeavrCvv :token="cvvToken" />
</div>
</div>
</WeavrAuthProvider>
</template>

Composable for consuming auth context

Create a composable to access auth state in deeply nested components:

// src/composables/useWeavrAuth.ts
import { inject, computed } from "vue";

interface WeavrAuthContext {
isAuthenticated: boolean;
authToken: string;
}

export function useWeavrAuth() {
const context = inject<WeavrAuthContext | null>("weavrAuth", null);

if (!context) {
console.warn(
"useWeavrAuth must be used within a WeavrAuthProvider component"
);
}

return {
isAuthenticated: computed(() => context?.isAuthenticated ?? false),
authToken: computed(() => context?.authToken ?? ""),
hasProvider: computed(() => context !== null),
};
}

Usage in a nested component:

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

const { isAuthenticated, hasProvider } = useWeavrAuth();

// Component can check if it's within a provider
if (!hasProvider.value) {
console.warn("This component should be used within WeavrAuthProvider");
}
</script>

Full example with wrapper components

Here's a complete card details page using wrapper components and the provider:

<!-- src/views/CardDetailsView.vue -->
<script setup lang="ts">
import { ref, onMounted } from "vue";
import WeavrAuthProvider from "@/components/weavr/WeavrAuthProvider.vue";
import WeavrCardNumber from "@/components/weavr/WeavrCardNumber.vue";
import WeavrCvv from "@/components/weavr/WeavrCvv.vue";

interface CardData {
authToken: string;
cardNumberToken: string;
cvvToken: string;
cardholderName: string;
expiryDate: string;
}

const cardData = ref<CardData | null>(null);
const isLoading = ref(true);
const fetchError = ref<string | null>(null);

onMounted(async () => {
try {
const response = await fetch("/api/cards/current");
if (!response.ok) throw new Error("Failed to fetch card data");
cardData.value = await response.json();
} catch (e) {
fetchError.value = e instanceof Error ? e.message : "Unknown error";
} finally {
isLoading.value = false;
}
});
</script>

<template>
<div class="card-details-view">
<h1>Card Details</h1>

<div v-if="isLoading" class="loading">Loading card information...</div>

<div v-else-if="fetchError" class="error">{{ fetchError }}</div>

<WeavrAuthProvider
v-else-if="cardData"
:auth-token="cardData.authToken"
@authenticated="console.log('Ready to display card')"
@error="console.error('Auth error:', $event)"
>
<template #loading>
<div class="card-skeleton">
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
</template>

<div class="card-container">
<div class="card-front">
<div class="card-number-row">
<WeavrCardNumber :token="cardData.cardNumberToken" />
</div>

<div class="card-info-row">
<div class="cardholder">
<label>Cardholder</label>
<span>{{ cardData.cardholderName }}</span>
</div>
<div class="expiry">
<label>Expires</label>
<span>{{ cardData.expiryDate }}</span>
</div>
<div class="cvv">
<label>CVV</label>
<WeavrCvv :token="cardData.cvvToken" />
</div>
</div>
</div>
</div>
</WeavrAuthProvider>
</div>
</template>

<style scoped>
.card-container {
max-width: 400px;
margin: 2rem auto;
}

.card-front {
background: linear-gradient(135deg, #1a1a2e, #16213e);
color: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

.card-number-row {
font-size: 1.5rem;
letter-spacing: 2px;
margin-bottom: 2rem;
}

.card-info-row {
display: flex;
gap: 2rem;
}

.card-info-row label {
display: block;
font-size: 0.75rem;
text-transform: uppercase;
opacity: 0.7;
margin-bottom: 0.25rem;
}
</style>

Error handling

SDK initialization errors

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

const sdkError = ref<string | null>(null);
const isReady = ref(false);

onMounted(() => {
// Check if SDK loaded
if (!window.OpcUxSecureClient) {
sdkError.value =
"Weavr SDK failed to load. Check your internet connection and ensure the script tag is in index.html.";
return;
}

try {
window.OpcUxSecureClient.init("your_ui_key");
isReady.value = true;
} catch (e) {
sdkError.value = `SDK initialization failed: ${e instanceof Error ? e.message : "Unknown error"}`;
}
});
</script>

<template>
<div v-if="sdkError" class="error-banner">{{ sdkError }}</div>
<slot v-else-if="isReady"></slot>
<div v-else>Initializing...</div>
</template>

Component mount errors

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

const containerRef = ref<HTMLElement | null>(null);
const mountError = ref<string | null>(null);

let input: any = null;

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

if (!containerRef.value) {
throw new Error("Container element not found");
}

input.mount(containerRef.value);
} catch (e) {
mountError.value =
e instanceof Error ? e.message : "Failed to mount component";
console.error("Mount error:", e);
}
});

onUnmounted(() => {
try {
input?.unmount();
} catch (e) {
// Unmount errors are typically safe to ignore
console.warn("Unmount warning:", e);
}
});
</script>

<template>
<div v-if="mountError" class="error">{{ mountError }}</div>
<div v-else ref="containerRef"></div>
</template>

Tokenization errors

<script setup lang="ts">
const handleSubmit = () => {
form.tokenize(
(tokens: Record<string, string>) => {
if (!tokens.password) {
// Empty token usually means validation failed
error.value = "Please enter a valid password";
return;
}
// Process tokens...
},
(e: Error) => {
// Tokenization error callback (if supported)
error.value = `Tokenization failed: ${e.message}`;
}
);
};
</script>

Vue error boundary

Create an error boundary component for Weavr integrations:

<!-- WeavrErrorBoundary.vue -->
<script setup lang="ts">
import { ref, onErrorCaptured } from "vue";

const error = ref<Error | null>(null);

onErrorCaptured((err) => {
error.value = err;
console.error("Weavr component error:", err);
return false; // Prevent error from propagating
});

const retry = () => {
error.value = null;
};
</script>

<template>
<div v-if="error" class="error-boundary">
<p>Something went wrong with the secure component.</p>
<button @click="retry">Try Again</button>
</div>
<slot v-else></slot>
</template>

Usage:

<template>
<WeavrErrorBoundary>
<PasswordInput />
</WeavrErrorBoundary>
</template>

Common pitfalls and troubleshooting

Error: "TypeError: P.has is not a function"

Symptoms: error appears in console immediately when component mounts.

Cause: the Weavr SDK was imported through a JavaScript bundler (Vite, Webpack) instead of being loaded via a script tag.

Solution:

  1. Remove any import statements for the Weavr SDK
  2. Add the SDK script tag to your index.html:
<script src="https://sandbox.weavr.io/app/secure/static/client.1.js"></script>
  1. Access the SDK via window.OpcUxSecureClient

Component not rendering

Symptoms: the container div exists but remains empty.

Possible causes and solutions:

  1. SDK not loaded: check window.OpcUxSecureClient exists before using it
  2. Mounting before DOM ready: ensure mount() is called in onMounted()
  3. Wrong container reference: verify ref is correctly assigned
<script setup>
onMounted(() => {
console.log("Container:", containerRef.value); // Debug: should not be null
console.log("SDK:", window.OpcUxSecureClient); // Debug: should exist
});
</script>

Component disappears after route change

Symptoms: component works initially but disappears when navigating away and back.

Cause: the SDK component wasn't properly unmounted, causing state issues.

Solution: always call unmount() in onUnmounted():

<script setup>
let input: any = null

onMounted(() => {
input = form.input('p', 'password', {})
input.mount(containerRef.value)
})

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

For route-based components, consider using a :key to force remounting:

<template>
<router-view :key="$route.fullPath" />
</template>

Token is undefined after submit

Symptoms: tokens.password is undefined in the tokenize callback.

Possible causes:

  1. User left input empty: add validation before tokenizing
  2. Input validation failed: the SDK validates input format internally
  3. Async timing issue: token is only available inside the callback

Solution:

<script setup>
const handleSubmit = () => {
form.tokenize((tokens) => {
// Check inside callback
if (!tokens.password) {
error.value = "Please enter a valid password";
return;
}

// Safe to use token here
submitToServer(tokens.password);
});

// WRONG: tokens not available here
// submitToServer(tokens.password)
};
</script>

Memory leaks

Symptoms: memory usage increases over time, especially with repeated navigation.

Cause: the SDK components are not being unmounted when Vue components are destroyed.

Solution: always clean up in onUnmounted():

<script setup>
// Track all SDK instances
let form: any = null
let inputs: any[] = []

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

const password = form.input('p', 'password', {})
password.mount(passwordRef.value)
inputs.push(password)

const confirmPassword = form.input('cp', 'confirmPassword', {})
confirmPassword.mount(confirmPasswordRef.value)
inputs.push(confirmPassword)
})

onUnmounted(() => {
// Clean up all inputs
try {
inputs.forEach(input => {
if (input && typeof input.unmount === 'function') {
input.unmount();
}
});
inputs = [];
} catch (e) {
console.error("Error unmounting components:", e);
}
})
</script>

SDK iframe initialization warning

Symptoms: console warning: "Attempting to use 'onMessage' for <inline-menu-ready> without registering a handler"

Cause: the SDK's iframe communication handlers are not fully registered before the component mounts.

Solution: add a small delay before mounting to allow SDK initialization:

<script setup>
let input: any = null

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

try {
const form = window.OpcUxSecureClient.form();
input = form.input("p", "password", {
placeholder: "Enter password"
});

// Add delay to let SDK fully initialize iframe handlers
setTimeout(() => {
if (containerRef.value) {
input.mount(containerRef.value);
}
}, 100);
} catch (e) {
console.error("Error initializing component:", e);
}
});
</script>

Note: this warning is cosmetic and doesn't prevent the component from functioning correctly, but adding the delay eliminates it.

Associate callback not firing

Symptoms: code after associate() never executes.

Possible causes:

  1. Invalid auth token: verify the token is valid and not expired
  2. Network issues: check browser network tab for failed requests
  3. Wrong environment: sandbox token used with Live SDK URL (or vice versa)

Solution: always provide an error callback:

<script setup>
onMounted(() => {
window.OpcUxSecureClient.associate(
`Bearer ${authToken}`,
() => {
console.log("Associate succeeded");
// Continue setup...
},
(error) => {
console.error("Associate failed:", error);
// Handle error - show message to user
showError.value = "Authentication failed. Please try again.";
}
);
});
</script>