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 = [
'wss://relay.nsec.app',
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://nos.lol',
];
// 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 [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 [nostrConnectUri, setNostrConnectUri] = useState('');
const abortRef = useRef<AbortController | null>(null);
const poolRef = useRef<SimplePool | null>(null);
const subRef = useRef<{ close: () => void } | null>(null);
const pendingRef = useRef<PendingNostrConnect | null>(null);
const amberStepRef = useRef<'idle' | 'connecting' | 'waiting' | 'error' | 'success'>('idle');
const login = useLoginActions();
const { addLogin, setLogin } = useNostrLogin();
@ -144,6 +145,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
setLogin(nlogin.id);
clearPendingNC();
cleanup();
amberStepRef.current = 'success';
setAmberStep('success');
toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' });
setTimeout(() => { onLogin(); onClose(); }, 800);
@ -218,6 +220,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
abortRef.current?.abort();
abortRef.current = null;
cleanup();
amberStepRef.current = 'idle';
setAmberStep('idle');
setAmberStatus('');
setNostrConnectUri('');
@ -261,18 +264,26 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
savePendingNC(pending);
setNostrConnectUri(uri);
amberStepRef.current = 'connecting';
setAmberStep('connecting');
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 */ }
amberStepRef.current = 'waiting';
setAmberStep('waiting');
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();
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(
NOSTRCONNECT_RELAYS,
@ -280,33 +291,45 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
{
onevent: async (event) => {
if (controller.signal.aborted) return;
console.log('[Amber] Received event from:', event.pubkey);
try {
const decrypted = await nip04.decrypt(clientSk, event.pubkey, event.content);
const parsed = JSON.parse(decrypted) as { id: string; result: string; error?: string };
if (parsed.error && parsed.error !== '') return;
if (parsed.result !== secret && parsed.result !== 'ack') return;
console.log('[Amber] Decrypted response:', parsed);
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();
pendingRef.current = null;
await completeLogin(event.pubkey, clientSk, clientNsec, NOSTRCONNECT_RELAYS);
} catch { /* ignore */ }
} catch (e) {
console.warn('[Amber] Failed to decrypt event:', e);
}
},
}
);
subRef.current = sub;
// Timeout after 10 minutes
// Timeout after 20 seconds — show error, let user retry
setTimeout(() => {
if (!controller.signal.aborted) {
console.warn('[Amber] Timed out after 20s — no response received');
controller.abort();
cleanup();
if (amberStep === 'waiting') {
setAmberStep('idle');
toast({ title: 'Timed out', description: 'Amber did not respond. Try again.', variant: 'destructive' });
if (amberStepRef.current === 'waiting') {
amberStepRef.current = 'error';
setAmberStep('error');
setAmberStatus('Amber did not respond in time. Did you approve the connection?');
}
}
}, 10 * 60 * 1000);
}, [cleanup, completeLogin, amberStep, toast]);
}, 20 * 1000);
}, [cleanup, completeLogin, toast]);
const handleBunkerLogin = async () => {
const uri = bunkerUri.trim();
@ -419,11 +442,16 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
</div>
)}
{amberStep === 'waiting' && (
<div className='p-5 rounded-xl bg-black/50 border-2 border-[#ccff00]/40 space-y-4'>
{(amberStep === 'waiting' || amberStep === 'error') && (
<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'>
<Loader2 className='w-8 h-8 text-[#ccff00] animate-spin mx-auto' />
<p className='text-sm text-white font-bold'>Waiting for Amber...</p>
{amberStep === 'error'
? <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>
</div>
@ -451,11 +479,15 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
)}
<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
</Button>
<Button variant="ghost" className='flex-1 text-xs text-[#ccff00] h-auto py-2' onClick={startAmberConnect}>
<RefreshCw className='w-3 h-3 mr-1' />Retry
<Button
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>
</div>
</div>