diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx index 9c0c22d..4ea98da 100644 --- a/src/components/auth/LoginDialog.tsx +++ b/src/components/auth/LoginDialog.tsx @@ -90,20 +90,56 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onS setAmberStep('waiting'); setErrors(prev => ({ ...prev, amber: undefined })); - // Use localStorage (not sessionStorage) — survives navigation on Android browsers - localStorage.setItem('amber_return_path', window.location.pathname); - // Stamp the time so we know this is a fresh attempt - localStorage.setItem('amber_login_attempt_ts', String(Date.now())); + // Detect if running as installed PWA (standalone) — window.matchMedia + // returns true when launched from home screen on Android/iOS + const isPWA = window.matchMedia('(display-mode: standalone)').matches + || ('standalone' in window.navigator && (window.navigator as { standalone?: boolean }).standalone === true); + + if (isPWA) { + // ── PWA mode ────────────────────────────────────────────────────────── + // When installed to home screen, the app is single-tab. Opening + // nostrsigner: with a callbackUrl sends the callback to the browser, + // not back to the PWA — so the PWA never sees the result. + // + // Instead: open Amber WITHOUT a callbackUrl. Amber copies the pubkey + // to clipboard when there's no callback. When the user comes back, + // the visibilitychange fires and we auto-read the clipboard. + const amberUrl = 'nostrsigner:?compressionType=none&returnType=signature&type=get_public_key'; + window.location.href = amberUrl; + + // When user returns to the PWA after approving in Amber, auto-read clipboard + unsubAmberRef.current = subscribeToAmberReturn(async () => { + unsubAmberRef.current = null; + try { + const text = await navigator.clipboard.readText(); + const pubkey = text.trim().toLowerCase(); + const pubkeyRegex = /^[0-9a-f]{64}$/; + if (pubkeyRegex.test(pubkey)) { + login.amber(pubkey); + setAmberStep('success'); + toast({ title: '🛹 Logged in', description: 'Welcome back via Amber!' }); + setTimeout(() => { onLogin(); onClose(); }, 600); + } else { + // Clipboard had something else — show manual button + setErrors(prev => ({ ...prev, amber: 'Could not auto-read pubkey. Tap "I approved" to complete login.' })); + } + } catch { + // Clipboard permission denied — show manual button + setErrors(prev => ({ ...prev, amber: 'Tap "I approved — complete login" to finish.' })); + } + }); + return; + } + + // ── Browser mode (tab-based) ───────────────────────────────────────────── + // Use localStorage cross-tab signal: AmberCallbackPage writes + // 'amber_pending_pubkey', this tab's poll picks it up and calls login.amber() + localStorage.setItem('amber_return_path', window.location.pathname); - // Poll localStorage every 400ms for 'amber_pending_pubkey'. - // AmberCallbackPage writes this key when Amber approves — even if it runs - // in a separate tab (GrapheneOS opens callback in new tab). This tab picks - // it up, calls login.amber() here where React state is alive, and completes. if (pollRef.current) clearInterval(pollRef.current); pollRef.current = setInterval(() => { const pendingPubkey = localStorage.getItem('amber_pending_pubkey'); if (pendingPubkey) { - // Consume it immediately so we don't process twice localStorage.removeItem('amber_pending_pubkey'); clearInterval(pollRef.current!); pollRef.current = null; @@ -120,13 +156,16 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onS } }, 400); - // Also try visibilitychange for browsers that keep the tab alive + // visibilitychange fallback — if browser keeps tab alive and user cancels unsubAmberRef.current = subscribeToAmberReturn(() => { - setAmberStep(prev => prev === 'waiting' ? 'initial' : prev); + // Only reset if callback page didn't already complete login + const stillPending = !localStorage.getItem('amber_pending_pubkey') === false; + if (stillPending) { + setAmberStep(prev => prev === 'waiting' ? 'initial' : prev); + } unsubAmberRef.current = null; }); - // Open Amber — it will call /amber-callback?event= on approve const callbackUrl = `${window.location.origin}/amber-callback?event=`; const amberUrl = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=${encodeURIComponent(callbackUrl)}`; window.location.href = amberUrl; @@ -357,29 +396,24 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onS

Waiting for Amber...

- Approve the request in Amber, then come back here. + Approve in Amber, then come back here. Login completes automatically.

- {/* Animated dots */}
{[0, 1, 2].map(i => ( - + ))}
- {/* Fallback for Android browsers that navigate away — - polling handles auto-complete, but this button is a - manual escape hatch if the user lands back here stuck */} -

- Already approved in Amber? -

+ + {/* Manual fallback — handles both PWA (clipboard) and browser (localStorage) */} +

Stuck? Tap below after approving in Amber:

+ {errors.amber && (

{errors.amber}

)}