Amber login: poll relay every 3s after returning from Amber, add elapsed timer + Check Now button
- Replace one-shot visibility handler with pollRelay() called immediately on foreground + every 3s - Add elapsed time counter (M:SS) so user can see the app is still trying - Add 'Check Now' button for manual relay poll without generating a new URI - Poll interval auto-clears on success, timeout, or cancel Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
dfb60674c5
commit
03ea569a2a
1 changed files with 98 additions and 52 deletions
|
|
@ -93,12 +93,15 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
|||
|
||||
const [amberStep, setAmberStep] = useState<'idle' | 'connecting' | 'waiting' | 'error' | 'success'>('idle');
|
||||
const [amberStatus, setAmberStatus] = useState('');
|
||||
const [amberElapsed, setAmberElapsed] = useState(0);
|
||||
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 pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const elapsedIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const login = useLoginActions();
|
||||
const { addLogin, setLogin } = useNostrLogin();
|
||||
|
|
@ -112,6 +115,8 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
|||
try { poolRef.current?.close(NOSTRCONNECT_RELAYS); } catch { /* ignore */ }
|
||||
subRef.current = null;
|
||||
poolRef.current = null;
|
||||
if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; }
|
||||
if (elapsedIntervalRef.current) { clearInterval(elapsedIntervalRef.current); elapsedIntervalRef.current = null; }
|
||||
}, []);
|
||||
|
||||
// Complete login once we have signerPubkey from any source
|
||||
|
|
@ -151,68 +156,93 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
|||
setTimeout(() => { onLogin(); onClose(); }, 800);
|
||||
}, [addLogin, setLogin, cleanup, onLogin, onClose, toast]);
|
||||
|
||||
// Listen for app visibility change (user returns from Amber) — then poll relay
|
||||
// This is more reliable in Capacitor than keeping a WebSocket open in background
|
||||
// Poll relay once — returns true if Amber's response was found and login completed
|
||||
const pollRelay = useCallback(async (): Promise<boolean> => {
|
||||
const pending = pendingRef.current || loadPendingNC();
|
||||
if (!pending || amberStepRef.current !== 'waiting') return false;
|
||||
|
||||
console.log('[Amber] Polling relay for response...');
|
||||
setAmberStatus('Checking relay for Amber response...');
|
||||
|
||||
try {
|
||||
const decoded = nip19.decode(pending.clientNsec);
|
||||
if (decoded.type !== 'nsec') return false;
|
||||
const clientSk = decoded.data;
|
||||
const since = Math.floor(pending.timestamp / 1000) - 30;
|
||||
const freshPool = new SimplePool();
|
||||
let found = false;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => { freshPool.close(NOSTRCONNECT_RELAYS); resolve(); }, 6000);
|
||||
|
||||
const sub = freshPool.subscribeMany(
|
||||
NOSTRCONNECT_RELAYS,
|
||||
[{ kinds: [24133], '#p': [pending.clientPubkey], since }],
|
||||
{
|
||||
onevent: async (event) => {
|
||||
if (found) return;
|
||||
console.log('[Amber] Poll 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 !== pending.secret && parsed.result !== 'ack') return;
|
||||
found = true;
|
||||
clearTimeout(timeout);
|
||||
sub.close();
|
||||
freshPool.close(NOSTRCONNECT_RELAYS);
|
||||
pendingRef.current = null;
|
||||
abortRef.current?.abort();
|
||||
await completeLogin(event.pubkey, clientSk, pending.clientNsec, pending.relays);
|
||||
resolve();
|
||||
} catch { /* wrong encryption key or malformed event */ }
|
||||
},
|
||||
oneose: () => {
|
||||
setTimeout(() => { sub.close(); freshPool.close(NOSTRCONNECT_RELAYS); resolve(); }, 1500);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!found && amberStepRef.current === 'waiting') {
|
||||
setAmberStatus('Nothing yet — still waiting for Amber...');
|
||||
}
|
||||
return found;
|
||||
} catch { return false; }
|
||||
}, [completeLogin]);
|
||||
|
||||
// When app returns from Amber: immediately poll, then keep polling every 3s
|
||||
useEffect(() => {
|
||||
const handleVisibility = async () => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
const pending = pendingRef.current || loadPendingNC();
|
||||
if (!pending || amberStep !== 'waiting') return;
|
||||
if (amberStepRef.current !== 'waiting') return;
|
||||
|
||||
setAmberStatus('Checking for Amber response...');
|
||||
console.log('[Amber] App foregrounded — starting poll loop');
|
||||
|
||||
try {
|
||||
const decoded = nip19.decode(pending.clientNsec);
|
||||
if (decoded.type !== 'nsec') return;
|
||||
const clientSk = decoded.data;
|
||||
// Poll immediately on return
|
||||
const found = await pollRelay();
|
||||
if (found) return;
|
||||
|
||||
const since = Math.floor(pending.timestamp / 1000) - 30;
|
||||
|
||||
// Re-subscribe with a fresh pool each time app comes to foreground
|
||||
const freshPool = new SimplePool();
|
||||
let found = false;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => { resolve(); }, 6000);
|
||||
|
||||
const sub = freshPool.subscribeMany(
|
||||
NOSTRCONNECT_RELAYS,
|
||||
[{ kinds: [24133], '#p': [pending.clientPubkey], since }],
|
||||
{
|
||||
onevent: async (event) => {
|
||||
if (found) return;
|
||||
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 !== pending.secret && parsed.result !== 'ack') return;
|
||||
found = true;
|
||||
clearTimeout(timeout);
|
||||
sub.close();
|
||||
freshPool.close(NOSTRCONNECT_RELAYS);
|
||||
pendingRef.current = null;
|
||||
abortRef.current?.abort();
|
||||
await completeLogin(event.pubkey, clientSk, pending.clientNsec, pending.relays);
|
||||
resolve();
|
||||
} catch { /* wrong encryption */ }
|
||||
},
|
||||
oneose: () => {
|
||||
// All caught up — wait a bit more then give up
|
||||
setTimeout(() => { sub.close(); freshPool.close(NOSTRCONNECT_RELAYS); resolve(); }, 2000);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
setAmberStatus('Not received yet. Did you approve in Amber? Tap retry or come back after approving.');
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
// Then poll every 3 seconds until success or timeout/cancel clears the interval
|
||||
if (!pollIntervalRef.current) {
|
||||
pollIntervalRef.current = setInterval(async () => {
|
||||
if (amberStepRef.current !== 'waiting') {
|
||||
clearInterval(pollIntervalRef.current!);
|
||||
pollIntervalRef.current = null;
|
||||
return;
|
||||
}
|
||||
const done = await pollRelay();
|
||||
if (done) {
|
||||
clearInterval(pollIntervalRef.current!);
|
||||
pollIntervalRef.current = null;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
return () => document.removeEventListener('visibilitychange', handleVisibility);
|
||||
}, [amberStep, completeLogin]);
|
||||
}, [pollRelay]);
|
||||
|
||||
// Reset when dialog closes
|
||||
useEffect(() => {
|
||||
|
|
@ -223,6 +253,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
|||
amberStepRef.current = 'idle';
|
||||
setAmberStep('idle');
|
||||
setAmberStatus('');
|
||||
setAmberElapsed(0);
|
||||
setNostrConnectUri('');
|
||||
setBunkerUri('');
|
||||
setBunkerError('');
|
||||
|
|
@ -277,6 +308,11 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
|||
amberStepRef.current = 'waiting';
|
||||
setAmberStep('waiting');
|
||||
setAmberStatus('Approve in Amber, then come back here.');
|
||||
setAmberElapsed(0);
|
||||
if (elapsedIntervalRef.current) clearInterval(elapsedIntervalRef.current);
|
||||
elapsedIntervalRef.current = setInterval(() => {
|
||||
setAmberElapsed(s => s + 1);
|
||||
}, 1000);
|
||||
|
||||
// Live relay subscription (works on web/PWA; may be backgrounded in Capacitor)
|
||||
const pool = new SimplePool();
|
||||
|
|
@ -456,6 +492,11 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
|||
{amberStep === 'error' ? 'No response from Amber' : 'Waiting for Amber...'}
|
||||
</p>
|
||||
<p className='text-xs text-gray-400'>{amberStatus}</p>
|
||||
{amberStep === 'waiting' && (
|
||||
<p className='text-xs text-gray-600 tabular-nums'>
|
||||
{Math.floor(amberElapsed / 60)}:{String(amberElapsed % 60).padStart(2, '0')} elapsed
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-gray-300 bg-black/40 rounded-lg p-3 space-y-1 border border-gray-800'>
|
||||
|
|
@ -474,6 +515,11 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
|||
<Copy className='w-3 h-3 mr-1' />Copy URI
|
||||
</Button>
|
||||
</div>
|
||||
{amberStep === 'waiting' && (
|
||||
<Button variant="outline" className='w-full rounded-full border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10 text-xs h-auto py-2' onClick={pollRelay}>
|
||||
<RefreshCw className='w-3 h-3 mr-1' />Check Now
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isCapacitor && (
|
||||
<p className='text-xs text-center text-gray-500'>
|
||||
|
|
|
|||
Loading…
Reference in a new issue