refactor: simplify Nostr login button and improve Amber login flow
- Removed conditional rendering for the Nostr login button, ensuring it is always displayed when the extension is available. - Enhanced the Amber login flow with clearer messaging during the waiting phases for pubkey and signature. - Added paste fallback options for users in case clipboard functionality fails, improving user experience during the login process. These changes streamline the authentication experience and provide better feedback to users during the login phases.
This commit is contained in:
@@ -64,31 +64,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nostr Login Button (NIP-07 Browser Extension) — only shown when extension is installed -->
|
||||
<template v-if="hasNostrExtension">
|
||||
<button
|
||||
@click="handleNostrLogin"
|
||||
:disabled="isLoading"
|
||||
class="nostr-login-button w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
Sign in with Nostr Extension
|
||||
</button>
|
||||
<!-- Nostr Login Button (NIP-07 Browser Extension) -->
|
||||
<button
|
||||
@click="handleNostrLogin"
|
||||
:disabled="isLoading"
|
||||
class="nostr-login-button w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
Sign in with Nostr Extension
|
||||
</button>
|
||||
|
||||
<!-- Divider between Nostr methods -->
|
||||
<div class="relative my-4">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-white/10"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-4 bg-transparent text-white/30 text-xs">or</span>
|
||||
</div>
|
||||
<!-- Divider between Nostr methods -->
|
||||
<div class="relative my-4">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-white/10"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-4 bg-transparent text-white/30 text-xs">or</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amber Login (NIP-55 Android Signer) — 3-phase flow -->
|
||||
<!-- Amber Login (NIP-55 Android Signer) -->
|
||||
<button
|
||||
v-if="amberPhase === 'idle'"
|
||||
@click="handleAmberOpen"
|
||||
@@ -102,54 +100,97 @@
|
||||
Sign in with Amber
|
||||
</button>
|
||||
|
||||
<!-- Phase 2: Read pubkey from clipboard -->
|
||||
<!-- Waiting for pubkey from Amber (auto-detects on return) -->
|
||||
<div v-else-if="amberPhase === 'waiting-pubkey'" class="mt-3 space-y-3">
|
||||
<p class="text-sm text-white/60 text-center">
|
||||
Approved in Amber? Tap below to continue.
|
||||
</p>
|
||||
<button
|
||||
@click="handleAmberPubkeyRead"
|
||||
:disabled="isLoading"
|
||||
class="amber-login-button amber-login-button--active w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg class="w-5 h-5 animate-pulse" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2L3 7v6c0 5.25 3.75 10.15 9 11.25C17.25 23.15 21 18.25 21 13V7l-9-5z" fill="#F7931A" opacity="0.3" stroke="#F7931A" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M9 12l2 2 4-4" stroke="#F7931A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<div class="flex items-center justify-center gap-2 text-sm text-[#F7931A]">
|
||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Continue
|
||||
</button>
|
||||
<span>Waiting for Amber...</span>
|
||||
</div>
|
||||
<p class="text-xs text-white/40 text-center">Approve the request in Amber, then come back here</p>
|
||||
|
||||
<!-- Paste fallback (shown after timeout) -->
|
||||
<div v-if="showPasteFallback" class="space-y-2 pt-2 border-t border-white/5">
|
||||
<p class="text-xs text-white/50 text-center">Clipboard didn't work? Paste your public key:</p>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="amberPasteInput"
|
||||
type="text"
|
||||
placeholder="npub1... or hex pubkey"
|
||||
class="auth-input flex-1 text-xs"
|
||||
/>
|
||||
<button
|
||||
@click="handleAmberPasteSubmit"
|
||||
:disabled="!amberPasteInput.trim()"
|
||||
class="amber-login-button px-4 text-xs"
|
||||
:class="{ 'opacity-40 cursor-not-allowed': !amberPasteInput.trim() }"
|
||||
>
|
||||
Go
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="amberPhase = 'idle'; amberPubkey = null; amberUnsignedEvent = null"
|
||||
@click="cancelAmber"
|
||||
class="w-full text-center text-sm text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Phase 3: Read signed NIP-98 event from clipboard -->
|
||||
<!-- Waiting for NIP-98 signature from Amber (auto-detects on return) -->
|
||||
<div v-else-if="amberPhase === 'waiting-signature'" class="mt-3 space-y-3">
|
||||
<p class="text-sm text-white/60 text-center">
|
||||
Approve the sign-in request in Amber, then tap below.
|
||||
</p>
|
||||
<button
|
||||
@click="handleAmberSignComplete"
|
||||
:disabled="isLoading"
|
||||
class="amber-login-button amber-login-button--active w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg class="w-5 h-5 animate-pulse" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2L3 7v6c0 5.25 3.75 10.15 9 11.25C17.25 23.15 21 18.25 21 13V7l-9-5z" fill="#F7931A" opacity="0.3" stroke="#F7931A" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M9 12l2 2 4-4" stroke="#F7931A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<div class="flex items-center justify-center gap-2 text-sm text-[#F7931A]">
|
||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Complete Sign-in
|
||||
</button>
|
||||
<span>Approve sign-in in Amber...</span>
|
||||
</div>
|
||||
<p class="text-xs text-white/40 text-center">Approve the signing request, then come back here</p>
|
||||
|
||||
<!-- Paste fallback (shown after timeout) -->
|
||||
<div v-if="showPasteFallback" class="space-y-2 pt-2 border-t border-white/5">
|
||||
<p class="text-xs text-white/50 text-center">Clipboard didn't work? Paste the signature:</p>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="amberPasteInput"
|
||||
type="text"
|
||||
placeholder="Hex signature or signed event JSON"
|
||||
class="auth-input flex-1 text-xs"
|
||||
/>
|
||||
<button
|
||||
@click="handleAmberSignPasteSubmit"
|
||||
:disabled="!amberPasteInput.trim()"
|
||||
class="amber-login-button px-4 text-xs"
|
||||
:class="{ 'opacity-40 cursor-not-allowed': !amberPasteInput.trim() }"
|
||||
>
|
||||
Go
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="amberPhase = 'idle'; amberPubkey = null; amberUnsignedEvent = null"
|
||||
@click="cancelAmber"
|
||||
class="w-full text-center text-sm text-white/40 hover:text-white/60 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Completing sign-in -->
|
||||
<div v-else-if="amberPhase === 'completing'" class="mt-3">
|
||||
<div class="flex items-center justify-center gap-2 text-sm text-[#F7931A]">
|
||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
<span>Completing sign-in...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Mode -->
|
||||
<div class="mt-6 text-center text-sm text-white/60">
|
||||
{{ mode === 'register' ? 'Already have an account?' : "Don't have an account?" }}
|
||||
@@ -256,14 +297,14 @@ const mode = ref<'login' | 'register' | 'forgot'>(props.defaultMode)
|
||||
const errorMessage = ref<string | null>(null)
|
||||
const isLoading = computed(() => authLoading.value || sovereignGenerating.value)
|
||||
|
||||
// NIP-07 extension detection — hide the button entirely when no extension is installed
|
||||
const hasNostrExtension = ref(!!(window as any).nostr)
|
||||
|
||||
// Amber three-phase login state
|
||||
const amberPhase = ref<'idle' | 'waiting-pubkey' | 'waiting-signature'>('idle')
|
||||
// Amber login state
|
||||
const amberPhase = ref<'idle' | 'waiting-pubkey' | 'waiting-signature' | 'completing'>('idle')
|
||||
const amberPubkey = ref<string | null>(null)
|
||||
/** The exact unsigned NIP-98 event sent to Amber for signing (reused in Phase 3) */
|
||||
const amberUnsignedEvent = ref<any>(null)
|
||||
const amberPasteInput = ref('')
|
||||
const showPasteFallback = ref(false)
|
||||
let amberFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let amberVisibilityHandler: (() => void) | null = null
|
||||
|
||||
// ── Sovereign Identity Flow ──────────────────────────────────────
|
||||
type SovereignPhase = 'normal' | 'nah' | 'own-privacy' | 'generated'
|
||||
@@ -281,17 +322,14 @@ watch(() => props.isOpen, (open) => {
|
||||
sovereignDismissed.value = false
|
||||
generatedKeys.value = null
|
||||
errorMessage.value = null
|
||||
amberPhase.value = 'idle'
|
||||
amberUnsignedEvent.value = null
|
||||
cancelAmber()
|
||||
}
|
||||
})
|
||||
|
||||
function closeModal() {
|
||||
emit('close')
|
||||
errorMessage.value = null
|
||||
amberPhase.value = 'idle'
|
||||
amberPubkey.value = null
|
||||
amberUnsignedEvent.value = null
|
||||
cancelAmber()
|
||||
sovereignPhase.value = 'normal'
|
||||
sovereignDismissed.value = false
|
||||
generatedKeys.value = null
|
||||
@@ -434,78 +472,126 @@ async function handleNostrLogin() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Amber Login — Phase 1: Open Amber via Android intent to get the pubkey.
|
||||
* Clean up Amber flow state, timers, and event listeners.
|
||||
*/
|
||||
function cancelAmber() {
|
||||
amberPhase.value = 'idle'
|
||||
amberPubkey.value = null
|
||||
amberUnsignedEvent.value = null
|
||||
amberPasteInput.value = ''
|
||||
showPasteFallback.value = false
|
||||
if (amberFallbackTimer) {
|
||||
clearTimeout(amberFallbackTimer)
|
||||
amberFallbackTimer = null
|
||||
}
|
||||
if (amberVisibilityHandler) {
|
||||
document.removeEventListener('visibilitychange', amberVisibilityHandler)
|
||||
amberVisibilityHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening for the user to return from Amber.
|
||||
* On visibilitychange → visible, waits 500ms then tries to read
|
||||
* clipboard. If clipboard read fails or is empty, shows paste fallback.
|
||||
*
|
||||
* Amber copies the pubkey to the clipboard. Mobile browsers block
|
||||
* automatic clipboard reads (navigator.clipboard.readText requires a
|
||||
* user gesture), so we move to phase 2 where the user taps a button
|
||||
* to trigger the read inside a real user-gesture event handler.
|
||||
* @param onSuccess Called with clipboard text when auto-read succeeds.
|
||||
*/
|
||||
function listenForAmberReturn(onSuccess: (text: string) => void) {
|
||||
// Show paste fallback after 8 seconds regardless
|
||||
amberFallbackTimer = setTimeout(() => {
|
||||
showPasteFallback.value = true
|
||||
}, 8000)
|
||||
|
||||
amberVisibilityHandler = () => {
|
||||
if (document.visibilityState !== 'visible') return
|
||||
|
||||
// User returned to browser — try clipboard after a short delay
|
||||
// (same 500ms + 200ms pattern as AmberClipboardSigner)
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
if (!navigator.clipboard?.readText) {
|
||||
showPasteFallback.value = true
|
||||
return
|
||||
}
|
||||
const text = (await navigator.clipboard.readText()).trim()
|
||||
if (!text) {
|
||||
showPasteFallback.value = true
|
||||
return
|
||||
}
|
||||
// Clipboard read succeeded — clean up listener and proceed
|
||||
if (amberVisibilityHandler) {
|
||||
document.removeEventListener('visibilitychange', amberVisibilityHandler)
|
||||
amberVisibilityHandler = null
|
||||
}
|
||||
if (amberFallbackTimer) {
|
||||
clearTimeout(amberFallbackTimer)
|
||||
amberFallbackTimer = null
|
||||
}
|
||||
onSuccess(text)
|
||||
} catch {
|
||||
// Clipboard permission denied or other error — show paste input
|
||||
showPasteFallback.value = true
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', amberVisibilityHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Amber Login — Step 1: Open Amber to get the public key.
|
||||
* Sets up auto-detection via visibilitychange for when the user returns.
|
||||
*/
|
||||
function handleAmberOpen() {
|
||||
errorMessage.value = null
|
||||
showPasteFallback.value = false
|
||||
amberPasteInput.value = ''
|
||||
|
||||
// Check platform support
|
||||
const isAndroid = navigator.userAgent.includes('Android')
|
||||
if (!isAndroid) {
|
||||
errorMessage.value = 'Amber is only available on Android devices. Please use a Nostr browser extension instead.'
|
||||
return
|
||||
}
|
||||
|
||||
// Open Amber via the nostrsigner intent to get pubkey
|
||||
// Open Amber via intent to get pubkey
|
||||
const intent = 'intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end'
|
||||
window.open(intent, '_blank')
|
||||
|
||||
// Switch to phase 2 — user will tap "Continue" after Amber approves
|
||||
amberPhase.value = 'waiting-pubkey'
|
||||
|
||||
// Auto-detect: when user returns, try to read pubkey from clipboard
|
||||
listenForAmberReturn((clipboardText) => {
|
||||
processPubkey(clipboardText)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a pubkey from clipboard text. Handles hex, npub, and nprofile formats.
|
||||
* Decode a pubkey from text. Handles hex, npub, and nprofile formats.
|
||||
*/
|
||||
async function decodePubkeyFromClipboard(clipboardText: string): Promise<string> {
|
||||
if (/^[0-9a-f]{64}$/i.test(clipboardText)) {
|
||||
return clipboardText
|
||||
}
|
||||
if (clipboardText.startsWith('npub') || clipboardText.startsWith('nprofile')) {
|
||||
async function decodePubkeyText(text: string): Promise<string> {
|
||||
if (/^[0-9a-f]{64}$/i.test(text)) return text
|
||||
if (text.startsWith('npub') || text.startsWith('nprofile')) {
|
||||
const { decodeProfilePointer } = await import('applesauce-core/helpers')
|
||||
const decoded = decodeProfilePointer(clipboardText)
|
||||
if (!decoded?.pubkey) throw new Error('Could not decode npub from clipboard')
|
||||
const decoded = decodeProfilePointer(text)
|
||||
if (!decoded?.pubkey) throw new Error('Could not decode npub')
|
||||
return decoded.pubkey
|
||||
}
|
||||
throw new Error('Clipboard does not contain a valid Nostr public key. Got: ' + clipboardText.slice(0, 20) + '...')
|
||||
throw new Error('Not a valid public key. Expected hex or npub format.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Amber Login — Phase 2: Read the pubkey from the clipboard, then
|
||||
* construct the NIP-98 event and open Amber again to sign it.
|
||||
*
|
||||
* This runs inside a click handler (user gesture) so
|
||||
* navigator.clipboard.readText() is permitted by the browser.
|
||||
* Process a pubkey (from auto-read or paste), then open Amber to sign NIP-98.
|
||||
*/
|
||||
async function handleAmberPubkeyRead() {
|
||||
async function processPubkey(text: string) {
|
||||
errorMessage.value = null
|
||||
|
||||
try {
|
||||
if (!navigator.clipboard?.readText) {
|
||||
throw new Error('Clipboard API not available')
|
||||
}
|
||||
const clipboardText = (await navigator.clipboard.readText()).trim()
|
||||
|
||||
if (!clipboardText) {
|
||||
throw new Error('Clipboard is empty. Please try again — make sure you approved the request in Amber.')
|
||||
}
|
||||
|
||||
const pubkey = await decodePubkeyFromClipboard(clipboardText)
|
||||
const pubkey = await decodePubkeyText(text)
|
||||
amberPubkey.value = pubkey
|
||||
showPasteFallback.value = false
|
||||
amberPasteInput.value = ''
|
||||
|
||||
// NOTE: We do NOT register the Amber account in accountManager here.
|
||||
// That happens in Phase 3 after backend login succeeds, to avoid a
|
||||
// split state where accountManager.active is set but authStore
|
||||
// isn't authenticated (which confuses guards and the UI).
|
||||
|
||||
// Build the unsigned NIP-98 event that the backend expects.
|
||||
// Store it so Phase 3 can reuse the exact same event data
|
||||
// (same created_at) when combining with the returned signature.
|
||||
// Build the unsigned NIP-98 event
|
||||
const sessionUrl = authService.getNostrSessionUrl()
|
||||
const unsignedEvent = {
|
||||
kind: 27235,
|
||||
@@ -519,94 +605,69 @@ async function handleAmberPubkeyRead() {
|
||||
}
|
||||
amberUnsignedEvent.value = unsignedEvent
|
||||
|
||||
// Open Amber to sign this NIP-98 event
|
||||
// Open Amber to sign this event
|
||||
const eventJson = JSON.stringify(unsignedEvent)
|
||||
const intent = `intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=event;S.type=sign_event;S.event=${encodeURIComponent(eventJson)};end`
|
||||
const intent = `intent:${encodeURIComponent(eventJson)}#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=sign_event;end`
|
||||
window.open(intent, '_blank')
|
||||
|
||||
// Move to phase 3 — user will tap "Complete Sign-in" after Amber signs
|
||||
amberPhase.value = 'waiting-signature'
|
||||
} catch (error: any) {
|
||||
console.error('Amber pubkey read failed:', error)
|
||||
handleAmberError(error)
|
||||
|
||||
// Auto-detect: when user returns, try to read signature from clipboard
|
||||
listenForAmberReturn((clipboardText) => {
|
||||
processSignature(clipboardText)
|
||||
})
|
||||
} catch (err: any) {
|
||||
errorMessage.value = err.message || 'Invalid public key. Please try again.'
|
||||
showPasteFallback.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Amber Login — Phase 3: Read the signed NIP-98 event from the
|
||||
* clipboard and complete authentication.
|
||||
*
|
||||
* This runs inside a click handler (user gesture) so
|
||||
* navigator.clipboard.readText() is permitted by the browser.
|
||||
* Manual paste submit for pubkey.
|
||||
*/
|
||||
async function handleAmberSignComplete() {
|
||||
function handleAmberPasteSubmit() {
|
||||
const text = amberPasteInput.value.trim()
|
||||
if (!text) return
|
||||
processPubkey(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a signature (from auto-read or paste) and complete login.
|
||||
*/
|
||||
async function processSignature(text: string) {
|
||||
errorMessage.value = null
|
||||
|
||||
const pubkey = amberPubkey.value
|
||||
if (!pubkey) { errorMessage.value = 'Lost pubkey. Please start over.'; cancelAmber(); return }
|
||||
const storedEvent = amberUnsignedEvent.value
|
||||
if (!storedEvent) { errorMessage.value = 'Lost signing request. Please start over.'; cancelAmber(); return }
|
||||
|
||||
amberPhase.value = 'completing'
|
||||
|
||||
try {
|
||||
if (!navigator.clipboard?.readText) {
|
||||
throw new Error('Clipboard API not available')
|
||||
}
|
||||
const clipboardText = (await navigator.clipboard.readText()).trim()
|
||||
|
||||
if (!clipboardText) {
|
||||
throw new Error('Clipboard is empty. Make sure you approved the signing request in Amber.')
|
||||
}
|
||||
|
||||
const pubkey = amberPubkey.value
|
||||
if (!pubkey) throw new Error('Lost track of the pubkey. Please start over.')
|
||||
|
||||
const storedEvent = amberUnsignedEvent.value
|
||||
if (!storedEvent) throw new Error('Lost track of the signing request. Please start over.')
|
||||
|
||||
// Amber may return different formats depending on version:
|
||||
// 1. Full signed event JSON (starts with '{')
|
||||
// 2. Just the hex signature (64-byte Schnorr = 128 hex chars)
|
||||
// 3. Hex string of varying length (some Amber versions)
|
||||
let authHeader: string
|
||||
|
||||
// Try 1: Full signed event JSON
|
||||
if (clipboardText.startsWith('{')) {
|
||||
try {
|
||||
const signedEvent = JSON.parse(clipboardText)
|
||||
if (signedEvent.sig) {
|
||||
authHeader = `Nostr ${btoa(JSON.stringify(signedEvent))}`
|
||||
} else {
|
||||
throw new Error('missing sig')
|
||||
}
|
||||
} catch {
|
||||
throw new Error('Clipboard contains JSON but it is not a valid signed event. Please try again.')
|
||||
}
|
||||
// Full signed event JSON
|
||||
if (text.startsWith('{')) {
|
||||
const signedEvent = JSON.parse(text)
|
||||
if (!signedEvent.sig) throw new Error('Signed event missing signature')
|
||||
authHeader = `Nostr ${btoa(JSON.stringify(signedEvent))}`
|
||||
}
|
||||
// Try 2: Hex string (signature) — combine with the stored unsigned event
|
||||
else if (/^[0-9a-f]+$/i.test(clipboardText)) {
|
||||
const sig = clipboardText
|
||||
|
||||
// Reconstruct the full event using the EXACT unsigned event we
|
||||
// sent to Amber (same created_at, tags, etc.) + the signature
|
||||
const event = {
|
||||
...storedEvent,
|
||||
sig,
|
||||
}
|
||||
|
||||
// Calculate the event ID
|
||||
// Hex signature — combine with stored unsigned event
|
||||
else if (/^[0-9a-f]+$/i.test(text)) {
|
||||
const event = { ...storedEvent, sig: text }
|
||||
const { getEventHash } = await import('nostr-tools/pure')
|
||||
event.id = getEventHash(event as any)
|
||||
|
||||
authHeader = `Nostr ${btoa(JSON.stringify(event))}`
|
||||
}
|
||||
// Nothing we recognise
|
||||
else {
|
||||
throw new Error(
|
||||
'Could not read the signed event from Amber. '
|
||||
+ 'Please make sure you approved the request in Amber, then try again.'
|
||||
)
|
||||
throw new Error('Unrecognised signature format from Amber.')
|
||||
}
|
||||
|
||||
// Authenticate with the pre-signed NIP-98 header (bypasses signer.signEvent)
|
||||
// Authenticate with the pre-signed NIP-98 header
|
||||
await loginWithNostr(pubkey, 'amber-nip55', {}, authHeader)
|
||||
|
||||
// Backend login succeeded — NOW register the Amber account in
|
||||
// accountManager so the rest of the UI reflects the logged-in state.
|
||||
// Register the Amber account in accountManager
|
||||
try {
|
||||
const { AmberClipboardSigner, AmberClipboardAccount } = await import('../lib/accounts')
|
||||
const signer = new AmberClipboardSigner()
|
||||
@@ -618,30 +679,24 @@ async function handleAmberSignComplete() {
|
||||
console.warn('[Amber] Account registration after login:', accountErr)
|
||||
}
|
||||
|
||||
amberPhase.value = 'idle'
|
||||
amberPubkey.value = null
|
||||
amberUnsignedEvent.value = null
|
||||
cancelAmber()
|
||||
emit('success')
|
||||
closeModal()
|
||||
} catch (error: any) {
|
||||
console.error('Amber sign-in failed:', error)
|
||||
handleAmberError(error)
|
||||
} catch (err: any) {
|
||||
console.error('Amber sign-in failed:', err)
|
||||
errorMessage.value = err.message || 'Failed to complete Amber sign-in. Please try again.'
|
||||
amberPhase.value = 'waiting-signature'
|
||||
showPasteFallback.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared error handler for Amber flow phases.
|
||||
* Manual paste submit for signature.
|
||||
*/
|
||||
function handleAmberError(error: any) {
|
||||
if (error.message?.includes('Clipboard is empty')) {
|
||||
errorMessage.value = 'Clipboard is empty. Open Amber, approve the request, then come back and tap the button.'
|
||||
} else if (error.message?.includes('not available')) {
|
||||
errorMessage.value = 'Clipboard access is not available. Try using Chrome on Android.'
|
||||
} else if (error.message?.includes('permission')) {
|
||||
errorMessage.value = 'Clipboard permission denied. Please allow clipboard access and try again.'
|
||||
} else {
|
||||
errorMessage.value = error.message || 'Failed to read from Amber. Please try again.'
|
||||
}
|
||||
function handleAmberSignPasteSubmit() {
|
||||
const text = amberPasteInput.value.trim()
|
||||
if (!text) return
|
||||
processSignature(text)
|
||||
}
|
||||
|
||||
// Declare window.nostr for TypeScript
|
||||
|
||||
Reference in New Issue
Block a user