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:
parent
24d71679af
commit
bd149fdb2c
1 changed files with 53 additions and 21 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue