Vue 3 Composition API integration with a reusable useSigil composable and ready-to-use single-file components. Works with Vue Router and Nuxt.
npm install @sigil-oss/connectDrop this in composables/useSigil.ts. Returns reactive status, result, and error refs alongside the request() function.
// composables/useSigil.ts
import { ref } from 'vue';
import {
sigilRequest,
type SigilRequest,
type SigilCallbackResponse,
} from '@sigil-oss/connect';
type Status = 'idle' | 'pending' | 'success' | 'error';
export function useSigil() {
const status = ref<Status>('idle');
const result = ref<SigilCallbackResponse | null>(null);
const error = ref<Error | null>(null);
let pending = false;
async function request(req: SigilRequest) {
if (pending) throw new Error('A Sigil request is already in progress');
pending = true;
status.value = 'pending';
result.value = null;
error.value = null;
try {
const res = await sigilRequest(req);
result.value = res;
status.value = 'success';
return res;
} catch (err) {
error.value = err instanceof Error ? err : new Error(String(err));
status.value = 'error';
throw error.value;
} finally {
pending = false;
}
}
function reset() {
status.value = 'idle';
result.value = null;
error.value = null;
}
return { request, status, result, error, reset };
}Register a route at /__sigil__ that renders the callback component. It calls handleRedirect() on mount and closes the tab after broadcasting the result.
<!-- components/SigilCallback.vue -->
<script setup lang="ts">
import { onMounted } from 'vue';
import { handleRedirect } from '@sigil-oss/connect';
onMounted(() => {
handleRedirect(); // reads ?result=, broadcasts, closes tab
});
</script>
<template>
<!-- tab closes itself — nothing to render -->
</template>// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import SigilCallback from '@/components/SigilCallback.vue';
import Home from '@/views/Home.vue';
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/__sigil__', component: SigilCallback },
// … rest of your routes
],
});Create a page file at pages/__sigil__.vue — Nuxt's file-based routing picks it up automatically.
<!-- pages/__sigil__.vue — Nuxt auto-routing picks this up -->
<script setup lang="ts">
import { onMounted } from 'vue';
import { handleRedirect } from '@sigil-oss/connect';
onMounted(() => {
handleRedirect();
});
</script>
<template><!-- closes itself --></template><!-- components/ConnectButton.vue -->
<script setup lang="ts">
import { createConnectRequest } from '@sigil-oss/connect';
import { useSigil } from '@/composables/useSigil';
const emit = defineEmits<{ connected: [identity: string] }>();
const { request, status, result, reset } = useSigil();
async function connect() {
const res = await request(
createConnectRequest({
type: 'connect',
dapp: { name: 'My App', origin: 'https://myapp.example' },
permissions: ['transfer', 'sign_message'],
})
);
if (res.status === 'connected') emit('connected', res.identity);
}
</script>
<template>
<div v-if="status === 'success' && result?.status === 'connected'">
<p>Connected: {{ result.identity.slice(0, 8) }}…</p>
<button @click="reset">Disconnect</button>
</div>
<button v-else @click="connect" :disabled="status === 'pending'">
{{ status === 'pending' ? 'Opening Sigil…' : 'Connect Wallet' }}
</button>
</template><!-- components/SignInButton.vue -->
<script setup lang="ts">
import { createSignMessageRequest } from '@sigil-oss/connect';
import { useSigil } from '@/composables/useSigil';
const emit = defineEmits<{ signedIn: [identity: string] }>();
const { request, status, error } = useSigil();
async function signIn() {
const nonce = crypto.randomUUID();
const res = await request(
createSignMessageRequest({
type: 'sign_message',
dapp: { name: 'My App', origin: 'https://myapp.example' },
message: [`Sign in to My App`, `nonce: ${nonce}`, `issuedAt: ${new Date().toISOString()}`].join('\n'),
})
);
if (res.status !== 'signed' || res.type !== 'sign_message') return;
const response = await fetch('/api/auth/qubic', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identity: res.identity,
signature: res.signature,
public_key: res.public_key,
nonce,
}),
});
if (response.ok) emit('signedIn', res.identity);
}
</script>
<template>
<button @click="signIn" :disabled="status === 'pending'">
{{ status === 'pending' ? 'Waiting for Sigil…' : 'Sign in with Qubic' }}
</button>
<p v-if="error" style="color: red">{{ error.message }}</p>
</template><!-- components/TransferButton.vue -->
<script setup lang="ts">
import { createTransferRequest } from '@sigil-oss/connect';
import { useSigil } from '@/composables/useSigil';
const props = defineProps<{ to: string; amount: number }>();
const emit = defineEmits<{ sent: [txHash: string] }>();
const { request, status, result } = useSigil();
async function send() {
const res = await request(
createTransferRequest({
type: 'transfer',
dapp: { name: 'My App', origin: 'https://myapp.example' },
to: props.to,
amount: props.amount,
})
);
if (res.status === 'signed' && (res.type === 'transfer' || res.type === 'sc_call')) emit('sent', res.tx_hash);
}
</script>
<template>
<p v-if="status === 'success' && result?.status === 'signed' && (result.type === 'transfer' || result.type === 'sc_call')">
Sent — tx: {{ result.tx_hash.slice(0, 12) }}…
</p>
<button v-else @click="send" :disabled="status === 'pending'">
{{ status === 'pending' ? 'Waiting for Sigil…' : `Send ${amount.toLocaleString()} QU` }}
</button>
</template>