Fix Amber nostrconnect flow: 20s timeout, error state, stale closure fix, better relays, debug logs

- Replace wss://relay.primal.net with wss://nos.lol (more reliable for NIP-46)
- Shorten timeout from 10 minutes to 20 seconds
- Fix stale closure bug: track amberStep in amberStepRef so timeout can correctly reset UI
- Add 'error' step with red UI and 'Try Again' button instead of endless spinner
- Extend since filter from 5s to 10s in the past
- Add console logs for URI, relays, incoming events, decryption result, secret matching

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-03 11:48:03 +07:00
parent 24d71679af
commit bd149fdb2c

View file

@ -25,7 +25,7 @@ interface LoginDialogProps {
const NOSTRCONNECT_RELAYS = [ const NOSTRCONNECT_RELAYS = [
'wss://relay.nsec.app', 'wss://relay.nsec.app',
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://relay.primal.net', 'wss://nos.lol',
]; ];
// Key for storing pending nostrconnect session across app restarts // Key for storing pending nostrconnect session across app restarts
@ -91,13 +91,14 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
const [nsecInput, setNsecInput] = useState(''); const [nsecInput, setNsecInput] = useState('');
const [nsecError, setNsecError] = useState(''); const [nsecError, setNsecError] = useState('');
const [amberStep, setAmberStep] = useState<'idle' | 'connecting' | 'waiting' | 'success'>('idle'); const [amberStep, setAmberStep] = useState<'idle' | 'connecting' | 'waiting' | 'error' | 'success'>('idle');
const [amberStatus, setAmberStatus] = useState(''); const [amberStatus, setAmberStatus] = useState('');
const [nostrConnectUri, setNostrConnectUri] = useState(''); const [nostrConnectUri, setNostrConnectUri] = useState('');
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
const poolRef = useRef<SimplePool | null>(null); const poolRef = useRef<SimplePool | null>(null);
const subRef = useRef<{ close: () => void } | null>(null); const subRef = useRef<{ close: () => void } | null>(null);
const pendingRef = useRef<PendingNostrConnect | null>(null); const pendingRef = useRef<PendingNostrConnect | null>(null);
const amberStepRef = useRef<'idle' | 'connecting' | 'waiting' | 'error' | 'success'>('idle');
const login = useLoginActions(); const login = useLoginActions();
const { addLogin, setLogin } = useNostrLogin(); const { addLogin, setLogin } = useNostrLogin();
@ -144,6 +145,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
setLogin(nlogin.id); setLogin(nlogin.id);
clearPendingNC(); clearPendingNC();
cleanup(); cleanup();
amberStepRef.current = 'success';
setAmberStep('success'); setAmberStep('success');
toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' }); toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' });
setTimeout(() => { onLogin(); onClose(); }, 800); setTimeout(() => { onLogin(); onClose(); }, 800);
@ -218,6 +220,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
abortRef.current?.abort(); abortRef.current?.abort();
abortRef.current = null; abortRef.current = null;
cleanup(); cleanup();
amberStepRef.current = 'idle';
setAmberStep('idle'); setAmberStep('idle');
setAmberStatus(''); setAmberStatus('');
setNostrConnectUri(''); setNostrConnectUri('');
@ -261,18 +264,26 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
savePendingNC(pending); savePendingNC(pending);
setNostrConnectUri(uri); setNostrConnectUri(uri);
amberStepRef.current = 'connecting';
setAmberStep('connecting'); setAmberStep('connecting');
setAmberStatus('Opening Amber...'); setAmberStatus('Opening Amber...');
console.log('[Amber] Generated nostrconnect URI:', uri);
console.log('[Amber] Relays:', NOSTRCONNECT_RELAYS);
console.log('[Amber] Client pubkey:', clientPubkey);
try { window.open(uri, '_blank'); } catch { /* user copies manually */ } try { window.open(uri, '_blank'); } catch { /* user copies manually */ }
amberStepRef.current = 'waiting';
setAmberStep('waiting'); setAmberStep('waiting');
setAmberStatus('Waiting for Amber to approve...'); setAmberStatus('Waiting for Amber to approve...');
// Also keep a live relay subscription running (works on web/PWA, may sleep in Capacitor) // Live relay subscription (works on web/PWA; may be backgrounded in Capacitor)
const pool = new SimplePool(); const pool = new SimplePool();
poolRef.current = pool; poolRef.current = pool;
const since = Math.floor(Date.now() / 1000) - 5; const since = Math.floor(Date.now() / 1000) - 10;
console.log('[Amber] Subscribing to relays since:', since);
const sub = pool.subscribeMany( const sub = pool.subscribeMany(
NOSTRCONNECT_RELAYS, NOSTRCONNECT_RELAYS,
@ -280,33 +291,45 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
{ {
onevent: async (event) => { onevent: async (event) => {
if (controller.signal.aborted) return; if (controller.signal.aborted) return;
console.log('[Amber] Received event from:', event.pubkey);
try { try {
const decrypted = await nip04.decrypt(clientSk, event.pubkey, event.content); const decrypted = await nip04.decrypt(clientSk, event.pubkey, event.content);
const parsed = JSON.parse(decrypted) as { id: string; result: string; error?: string }; const parsed = JSON.parse(decrypted) as { id: string; result: string; error?: string };
if (parsed.error && parsed.error !== '') return; console.log('[Amber] Decrypted response:', parsed);
if (parsed.result !== secret && parsed.result !== 'ack') return; if (parsed.error && parsed.error !== '') {
console.warn('[Amber] Error in response:', parsed.error);
return;
}
if (parsed.result !== secret && parsed.result !== 'ack') {
console.warn('[Amber] Secret mismatch, ignoring');
return;
}
console.log('[Amber] Valid response — completing login');
controller.abort(); controller.abort();
pendingRef.current = null; pendingRef.current = null;
await completeLogin(event.pubkey, clientSk, clientNsec, NOSTRCONNECT_RELAYS); await completeLogin(event.pubkey, clientSk, clientNsec, NOSTRCONNECT_RELAYS);
} catch { /* ignore */ } } catch (e) {
console.warn('[Amber] Failed to decrypt event:', e);
}
}, },
} }
); );
subRef.current = sub; subRef.current = sub;
// Timeout after 10 minutes // Timeout after 20 seconds — show error, let user retry
setTimeout(() => { setTimeout(() => {
if (!controller.signal.aborted) { if (!controller.signal.aborted) {
console.warn('[Amber] Timed out after 20s — no response received');
controller.abort(); controller.abort();
cleanup(); cleanup();
if (amberStep === 'waiting') { if (amberStepRef.current === 'waiting') {
setAmberStep('idle'); amberStepRef.current = 'error';
toast({ title: 'Timed out', description: 'Amber did not respond. Try again.', variant: 'destructive' }); setAmberStep('error');
setAmberStatus('Amber did not respond in time. Did you approve the connection?');
} }
} }
}, 10 * 60 * 1000); }, 20 * 1000);
}, [cleanup, completeLogin, amberStep, toast]); }, [cleanup, completeLogin, toast]);
const handleBunkerLogin = async () => { const handleBunkerLogin = async () => {
const uri = bunkerUri.trim(); const uri = bunkerUri.trim();
@ -419,11 +442,16 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
</div> </div>
)} )}
{amberStep === 'waiting' && ( {(amberStep === 'waiting' || amberStep === 'error') && (
<div className='p-5 rounded-xl bg-black/50 border-2 border-[#ccff00]/40 space-y-4'> <div className={`p-5 rounded-xl bg-black/50 border-2 space-y-4 ${amberStep === 'error' ? 'border-red-500/40' : 'border-[#ccff00]/40'}`}>
<div className='text-center space-y-2'> <div className='text-center space-y-2'>
<Loader2 className='w-8 h-8 text-[#ccff00] animate-spin mx-auto' /> {amberStep === 'error'
<p className='text-sm text-white font-bold'>Waiting for Amber...</p> ? <p className='text-sm text-red-400 font-bold'>⏱ Timed out</p>
: <Loader2 className='w-8 h-8 text-[#ccff00] animate-spin mx-auto' />
}
<p className='text-sm text-white font-bold'>
{amberStep === 'error' ? 'No response from Amber' : 'Waiting for Amber...'}
</p>
<p className='text-xs text-gray-400'>{amberStatus}</p> <p className='text-xs text-gray-400'>{amberStatus}</p>
</div> </div>
@ -451,11 +479,15 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
)} )}
<div className='flex gap-2'> <div className='flex gap-2'>
<Button variant="ghost" className='flex-1 text-xs text-gray-500 h-auto py-2' onClick={() => { abortRef.current?.abort(); clearPendingNC(); cleanup(); setAmberStep('idle'); }}> <Button variant="ghost" className='flex-1 text-xs text-gray-500 h-auto py-2' onClick={() => { abortRef.current?.abort(); clearPendingNC(); cleanup(); amberStepRef.current = 'idle'; setAmberStep('idle'); }}>
Cancel Cancel
</Button> </Button>
<Button variant="ghost" className='flex-1 text-xs text-[#ccff00] h-auto py-2' onClick={startAmberConnect}> <Button
<RefreshCw className='w-3 h-3 mr-1' />Retry variant="ghost"
className={`flex-1 text-xs h-auto py-2 ${amberStep === 'error' ? 'text-red-400 font-bold' : 'text-[#ccff00]'}`}
onClick={startAmberConnect}
>
<RefreshCw className='w-3 h-3 mr-1' />{amberStep === 'error' ? 'Try Again' : 'Retry'}
</Button> </Button>
</div> </div>
</div> </div>