diff --git a/src/components/auth/LoginArea.tsx b/src/components/auth/LoginArea.tsx index 6743e39..f7b7dc3 100644 --- a/src/components/auth/LoginArea.tsx +++ b/src/components/auth/LoginArea.tsx @@ -110,6 +110,10 @@ export function LoginArea({ className, compact }: LoginAreaProps) { setNsecSignupDialogOpen(false)} + onUseAmber={() => { + setNsecSignupDialogOpen(false); + setLoginDialogOpen(true); + }} /> ); diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx index 6efccad..33dee42 100644 --- a/src/components/auth/LoginDialog.tsx +++ b/src/components/auth/LoginDialog.tsx @@ -36,6 +36,9 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog // Amber NIP-55 state const [amberStep, setAmberStep] = useState<'idle' | 'loading' | 'paste' | 'done' | 'error'>('idle'); const [amberError, setAmberError] = useState(''); + // Whether the user explicitly opened the manual-paste escape hatch. The proper + // NIP-55 redirect flow never needs this; it's only a "having trouble?" fallback. + const [showManualPaste, setShowManualPaste] = useState(false); const login = useLoginActions(); const { toast } = useToast(); @@ -77,45 +80,34 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog } // ── Web/PWA path ───────────────────────────────────────────────────────── - // Store return path before navigating away + // This is the proper NIP-55 web flow — NO copy/paste required: + // 1. We open Amber via an intent:// URL carrying a callbackUrl. + // 2. Amber signs the get_public_key request and redirects the browser to + // /amber-callback?event=. + // 3. AmberCallbackPage stores the pubkey; the poll below (or the + // visibilitychange handler) picks it up and completes login.amber(). + // The manual-paste step exists ONLY as a last-resort "having trouble?" + // escape hatch the user must explicitly open — it is never forced. const currentPath = window.location.hash.replace(/^#/, '') || '/'; localStorage.setItem('amber_return_path', currentPath); - // Listen for app regaining focus (after Amber redirect). If Amber's - // redirect succeeded, checkAmberPendingPubkey completes the login; if not, - // it drops the user on the paste step. const unsub = subscribeToAmberReturn(() => { - checkAmberPendingPubkey(); + checkAmberReturnSilently(); }); window.__amberUnsubRef = unsub; openAmberForLogin(); - - // CRITICAL web-flow fallback: on mobile browsers Amber frequently opens the - // callback in a NEW tab (so the original tab never fires visibilitychange) - // and/or the clipboard read is blocked by permissions. In those cases the - // spinner would hang until the 3-minute timeout — exactly the "stuck - // loading" bug. So after a short grace period we surface the manual paste - // step, which ALWAYS works (user taps "Copy public key" in Amber, pastes - // here). The background poll still auto-completes if the callback lands. - window.setTimeout(() => { - setAmberStep((step) => (step === 'loading' ? 'paste' : step)); - }, 2500); }; - // Check if AmberCallbackPage already wrote a pubkey to localStorage - // (happens when Amber opens the callback in a new browser tab on Android) - const checkAmberPendingPubkey = () => { + // Silent variant used when the app regains focus: complete login if Amber's + // callback delivered the pubkey, otherwise stay on the loading/waiting screen + // (do NOT force the paste step — the redirect may still be in flight). + const checkAmberReturnSilently = async () => { const storedPubkey = localStorage.getItem('amber_pending_pubkey'); if (storedPubkey && /^[0-9a-f]{64}$/i.test(storedPubkey)) { localStorage.removeItem('amber_pending_pubkey'); completeAmberLogin(storedPubkey.toLowerCase()); return; } - // Also try clipboard as fallback - tryClipboardForPubkey(); - }; - - // Try reading the pubkey from clipboard (Amber may have copied it there) - const tryClipboardForPubkey = async () => { + // Best-effort clipboard read (some Amber builds copy the key). Never blocks. try { const text = (await navigator.clipboard.readText()).trim(); if (/^[0-9a-f]{64}$/i.test(text)) { @@ -125,20 +117,15 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog if (text.startsWith('npub1')) { const { nip19 } = await import('nostr-tools'); const decoded = nip19.decode(text); - if (decoded.type === 'npub') { - completeAmberLogin(decoded.data as string); - return; - } + if (decoded.type === 'npub') completeAmberLogin(decoded.data as string); } - } catch { /* clipboard denied — fall through to manual paste */ } - // Couldn't auto-detect — show the manual paste UI - setAmberStep('paste'); + } catch { /* clipboard denied — remain on waiting screen */ } }; - // Poll for amber_pending_pubkey (written by AmberCallbackPage in a separate - // tab). We keep polling on BOTH 'loading' and 'paste' so that even after we - // drop the user on the manual paste step, a late-arriving callback still - // auto-completes the login for them. + // Poll for amber_pending_pubkey (written by AmberCallbackPage). This is what + // makes the proper NIP-55 redirect flow complete automatically — no paste. + // We keep polling on BOTH 'loading' and 'paste' so a late-arriving callback + // still auto-completes even if the user opened the manual escape hatch. useEffect(() => { if (amberStep !== 'loading' && amberStep !== 'paste') return; const interval = setInterval(() => { @@ -202,6 +189,7 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog window.__amberUnsubRef = undefined; setAmberStep('idle'); setAmberError(''); + setShowManualPaste(false); setPubkeyInput(''); setPubkeyError(''); setBunkerUri(''); @@ -305,34 +293,76 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog )} - {/* Idle / loading — open Amber button */} - {(amberStep === 'idle' || amberStep === 'loading') && ( + {/* Idle — the primary, one-tap NIP-55 flow */} + {amberStep === 'idle' && (
- {amberStep === 'loading' ? : } +

Login with Amber

-

Opens Amber → approve → come back here.

+

+ Your key stays inside Amber. One tap — approve in Amber and you're + back here, signed in. Nothing to copy. +

+ + Don't have Amber? Get it → +
)} - {/* Paste pubkey — after returning from Amber */} - {(amberStep === 'paste' || amberStep === 'error') && ( + {/* Waiting — the redirect flow is in progress. Auto-completes via + the poll. Only reveals the manual escape hatch on explicit tap. */} + {amberStep === 'loading' && !showManualPaste && ( +
+
+ +
+
+

Waiting for Amber…

+

+ Approve the request in Amber. You'll be signed in automatically + the moment you come back. +

+
+ + +
+ )} + + {/* Manual paste — LAST-RESORT escape hatch only. Reached when the + user taps "Having trouble?" or an explicit error occurs. */} + {(amberStep === 'paste' || amberStep === 'error' || (amberStep === 'loading' && showManualPaste)) && (
-

Paste your pubkey

+

Enter your public key

- In Amber: tap your profile → Copy public key. Then paste below. + Only if the automatic flow didn't work. In Amber: tap your profile → + {' '}Copy public key, then paste it here.

{amberStep === 'error' && amberError && ( @@ -356,7 +386,7 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog Connect
diff --git a/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx index faed1af..e224d85 100644 --- a/src/components/auth/SignupDialog.tsx +++ b/src/components/auth/SignupDialog.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; -import { Download, Key, Shield, User, Sparkles, LogIn, CheckCircle, Upload } from 'lucide-react'; +import { Download, Key, Shield, User, Sparkles, LogIn, CheckCircle, Upload, Smartphone } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; @@ -20,13 +20,17 @@ interface SignupDialogProps { isOpen: boolean; onClose: () => void; onComplete?: () => void; + /** Switch the user over to the Amber (NIP-55) login flow — the recommended, + * more-secure path where the private key lives inside the Amber app. */ + onUseAmber?: () => void; } const sanitizeFilename = (filename: string) => { return filename.replace(/[^a-z0-9_.-]/gi, '_'); } -const SignupDialog: React.FC = ({ isOpen, onClose, onComplete }) => { +const SignupDialog: React.FC = ({ isOpen, onClose, onComplete, onUseAmber }) => { + const isAndroid = typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent); const [step, setStep] = useState<'welcome' | 'profile' | 'done'>('welcome'); const [nsec, setNsec] = useState(''); const [keySecured, setKeySecured] = useState<'none' | 'downloaded'>('none'); @@ -249,7 +253,7 @@ const SignupDialog: React.FC = ({ isOpen, onClose, onComplete
- {/* Welcome Step — explain what's about to happen, then create on tap */} + {/* Welcome Step — recommend Amber first, then offer the quick local key */} {step === 'welcome' && (
@@ -262,37 +266,76 @@ const SignupDialog: React.FC = ({ isOpen, onClose, onComplete Create your free account

- No email, no phone, no password. We'll generate a private key that lives - only on your device — that key is your account. + No email, no phone, no password. Your account is a Nostr key — choose + where it lives.

-
- {[ - ['🔑', 'A secure key is created just for you'], - ['📱', 'It stays on this device — nobody else can see it'], - ['💾', "You'll get a chance to back it up right after"], - ].map(([icon, label]) => ( -
- {icon} - {label} + {/* RECOMMENDED: Amber keeps the key in a dedicated signer app. + Amber is Android-only, so only surface it there. */} + {onUseAmber && isAndroid && ( +
+
+ Recommended
- ))} +
+
+ +
+
+

Sign up with Amber

+

+ Your key is created and stored inside the Amber app — SkateSpots + never touches it. Most secure. One tap to approve. +

+
+
+ +
+ )} + + {onUseAmber && isAndroid && ( +
+
+ or the quick way +
+
+ )} + + {/* QUICK: local key stored on this device */} +
+
+
+ +
+
+

Quick account on this device

+

+ We generate a key that lives on this device. Fastest start — we'll + help you save a backup right after. +

+
+
+
-

- By continuing you're generating a Nostr keypair. Advanced users can - {' '} - use a signer app - {' '}instead. + Prefer another signer?{' '} + See all options

)}