From e8bc072787ed5b9d6ae8cae5c006b3c3bf49e0c1 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 18 Jun 2026 23:33:42 +0700 Subject: [PATCH] fix: replace NIP-55 Amber login with NIP-46 bunker flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NIP-55 (nostrsigner: intents) is fundamentally broken from Capacitor WebView — no combination of window.open, window.location.href, or anchor clicks works reliably across all Android WebView versions. Switch to NIP-46 (bunker://) which works everywhere via WebSockets: - User opens Amber → Connect apps → Add app → copies bunker:// URI - Pastes it into the login dialog - NLogin.fromBunker() completes the handshake over a relay - Login works in browser, PWA, and Capacitor APK identically This is exactly how Ditto (soapbox-pub) implements Amber login. Co-authored-by: shakespeare.diy --- src/components/auth/LoginDialog.tsx | 210 ++++++++++------------------ 1 file changed, 71 insertions(+), 139 deletions(-) diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx index 0c706fa..379c57a 100644 --- a/src/components/auth/LoginDialog.tsx +++ b/src/components/auth/LoginDialog.tsx @@ -11,7 +11,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { useLoginActions } from '@/hooks/useLoginActions'; import { useToast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; -import { subscribeToAmberReturn, openAmberForLogin, openAmberForLoginNoCallback } from '@/lib/amber'; + interface LoginDialogProps { isOpen: boolean; @@ -87,82 +87,9 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onS }; const handleAmberLogin = () => { + // Show the bunker URI input — the user copies their bunker:// URI from Amber setAmberStep('waiting'); setErrors(prev => ({ ...prev, amber: undefined })); - - // Detect if running as Capacitor native app or installed PWA (standalone) - const isCapacitor = !!(window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor?.isNativePlatform?.(); - const isPWA = window.matchMedia('(display-mode: standalone)').matches - || ('standalone' in window.navigator && (window.navigator as { standalone?: boolean }).standalone === true); - - if (isCapacitor || isPWA) { - // ── Capacitor APK / PWA mode ─────────────────────────────────────────── - // In Capacitor, callbackUrl flows don't work — Amber treats the custom - // scheme as a malformed request. Use no-callback mode: Amber copies the - // pubkey to clipboard, we read it when the app regains focus. - openAmberForLoginNoCallback(); - - // 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); - - if (pollRef.current) clearInterval(pollRef.current); - pollRef.current = setInterval(() => { - const pendingPubkey = localStorage.getItem('amber_pending_pubkey'); - if (pendingPubkey) { - localStorage.removeItem('amber_pending_pubkey'); - clearInterval(pollRef.current!); - pollRef.current = null; - try { - login.amber(pendingPubkey); - setAmberStep('success'); - toast({ title: '🛹 Logged in', description: 'Welcome back via Amber!' }); - setTimeout(() => { onLogin(); onClose(); }, 600); - } catch (err) { - console.error('[Amber] login.amber failed in poll:', err); - setErrors(prev => ({ ...prev, amber: 'Login failed. Please try again.' })); - setAmberStep('initial'); - } - } - }, 400); - - // visibilitychange fallback — if browser keeps tab alive and user cancels - unsubAmberRef.current = subscribeToAmberReturn(() => { - // 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; - }); - - // Browser (tab) mode — use the callback URL flow - openAmberForLogin(); }; const handleBunkerLogin = async () => { @@ -347,7 +274,7 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onS Login with Amber

- Tap below — Amber opens, you approve once, and you're automatically logged in. No copy-paste needed. + Uses NIP-46 — your keys stay in Amber, we connect via relay. Tap below to get started.

Don't have Amber?{' '} @@ -381,84 +308,89 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onS )} - {/* Waiting — user is in Amber */} + {/* Waiting — show bunker URI instructions */} {amberStep === 'waiting' && ( -

-
- -
-
-

Waiting for Amber...

+
+
+

Connect with Amber (NIP-46)

- Approve in Amber, then come back here. Login completes automatically. + Get your Bunker URI from Amber and paste it below.

-
- {[0, 1, 2].map(i => ( - - ))} + + {/* Step by step */} +
+
+ 1. + Open Amber app +
+
+ 2. + Tap the key icon → Connect apps → Add app +
+
+ 3. + Copy the bunker:// connection string +
+
+ 4. + Paste it below and tap Connect +
- {/* Manual fallback — handles both PWA (clipboard) and browser (localStorage) */} -

Stuck? Tap below after approving in Amber:

- + className='bg-black/60 border-[#ccff00]/30 text-white placeholder:text-gray-600 text-xs' + autoComplete='off' + autoCorrect='off' + spellCheck={false} + /> {errors.amber && (

{errors.amber}

)} + + +
)}