Adds a second way to sign in besides a NIP-07 browser extension: generate
a nostrconnect:// URI, show it as a QR code, and resolve once a remote
signer app (Primal mobile, etc.) scans and approves it. Session persists
in localStorage so a page reload doesn't require re-scanning.
lib/nip07.ts renamed to lib/signer.ts and generalized — it now abstracts
over both signer kinds behind the same {getPublicKey, signEvent} shape,
so buildNip98Header and the rest of the app don't care which is active.
On a branch (feature/nip46-qr-signer) rather than main, since this is
explicitly experimental and easy rollback matters here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
52 lines
1.7 KiB
Vue
52 lines
1.7 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue';
|
|
import QRCode from 'qrcode';
|
|
import { connectNip46ViaQr } from '../lib/signer';
|
|
|
|
const emit = defineEmits<{
|
|
connected: [pubkey: string];
|
|
close: [];
|
|
}>();
|
|
|
|
const qrDataUrl = ref<string | null>(null);
|
|
const connecting = ref(true);
|
|
const error = ref<string | null>(null);
|
|
|
|
async function start(): Promise<void> {
|
|
connecting.value = true;
|
|
error.value = null;
|
|
qrDataUrl.value = null;
|
|
try {
|
|
const pubkey = await connectNip46ViaQr(async (uri) => {
|
|
qrDataUrl.value = await QRCode.toDataURL(uri, { margin: 1, width: 320 });
|
|
});
|
|
emit('connected', pubkey);
|
|
} catch (err) {
|
|
error.value = (err as Error).message;
|
|
} finally {
|
|
connecting.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(start);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
|
|
<div class="card w-full max-w-sm space-y-4 text-center">
|
|
<h2 class="text-lg font-semibold text-white">Connect a remote signer</h2>
|
|
<p class="text-sm text-white/60">Scan this with Primal (or any NIP-46-compatible signer app) to sign in without a browser extension.</p>
|
|
|
|
<img v-if="qrDataUrl" :src="qrDataUrl" alt="Nostr Connect QR code" class="mx-auto rounded-lg" />
|
|
|
|
<p v-if="connecting && !error" class="text-xs text-white/50">Waiting for approval…</p>
|
|
<p v-if="error" class="text-sm text-red-400">{{ error }}</p>
|
|
|
|
<div class="flex gap-2">
|
|
<button type="button" class="btn-secondary flex-1" @click="emit('close')">Cancel</button>
|
|
<button v-if="error" type="button" class="btn-primary flex-1" @click="start">Retry</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|