Fix Amber login not completing on GrapheneOS new-tab callback

Root cause: on GrapheneOS, Amber opens /amber-callback in a BRAND NEW TAB.
login.amber() was being called in that new tab's React instance, which has
no effect on the original tab where the user is waiting.

Fix:
- AmberCallbackPage now writes pubkey to localStorage('amber_pending_pubkey')
  instead of calling login.amber() — a cross-tab signal
- LoginDialog polls for 'amber_pending_pubkey' every 400ms and calls
  login.amber() itself in the correct React context
- Manual "I approved" button also reads the same key
- Removed reliance on getAmberLogin() which checked the wrong localStorage key

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-06-17 22:46:39 +07:00
parent 67850348e6
commit 0c87fa5827
2 changed files with 44 additions and 29 deletions

View file

@ -11,7 +11,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
import { useLoginActions } from '@/hooks/useLoginActions';
import { useToast } from '@/hooks/useToast';
import { cn } from '@/lib/utils';
import { subscribeToAmberReturn, getAmberLogin } from '@/lib/amber';
import { subscribeToAmberReturn } from '@/lib/amber';
interface LoginDialogProps {
isOpen: boolean;
@ -95,20 +95,30 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
// Stamp the time so we know this is a fresh attempt
localStorage.setItem('amber_login_attempt_ts', String(Date.now()));
// Poll localStorage every 500ms — on Android, the page navigates away and
// AmberCallbackPage completes the login in a new page load. When user comes
// back here, polling will detect the stored amber_login pubkey and complete.
// Poll localStorage every 400ms for 'amber_pending_pubkey'.
// AmberCallbackPage writes this key when Amber approves — even if it runs
// in a separate tab (GrapheneOS opens callback in new tab). This tab picks
// it up, calls login.amber() here where React state is alive, and completes.
if (pollRef.current) clearInterval(pollRef.current);
pollRef.current = setInterval(() => {
const storedPubkey = getAmberLogin();
if (storedPubkey) {
const pendingPubkey = localStorage.getItem('amber_pending_pubkey');
if (pendingPubkey) {
// Consume it immediately so we don't process twice
localStorage.removeItem('amber_pending_pubkey');
clearInterval(pollRef.current!);
pollRef.current = null;
setAmberStep('success');
toast({ title: '🛹 Logged in', description: 'Welcome back via Amber!' });
setTimeout(() => { onLogin(); onClose(); }, 600);
try {
login.amber(pendingPubkey);
setAmberStep('success');
toast({ title: '🛹 Logged in', description: 'Welcome back via Amber!' });
setTimeout(() => { onLogin(); onClose(); }, 600);
} catch (err) {
console.error('[Amber] login.amber failed in poll:', err);
setErrors(prev => ({ ...prev, amber: 'Login failed. Please try again.' }));
setAmberStep('initial');
}
}
}, 500);
}, 400);
// Also try visibilitychange for browsers that keep the tab alive
unsubAmberRef.current = subscribeToAmberReturn(() => {
@ -369,16 +379,22 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
<Button
className="w-full rounded-full bg-[#ccff00]/20 border border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/30 font-bold text-sm h-auto py-3"
onClick={() => {
// Check localStorage right now — AmberCallbackPage may
// have already stored the pubkey during the redirect
const storedPubkey = getAmberLogin();
if (storedPubkey) {
// Check for pending pubkey written by AmberCallbackPage
const pendingPubkey = localStorage.getItem('amber_pending_pubkey');
if (pendingPubkey) {
localStorage.removeItem('amber_pending_pubkey');
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
setAmberStep('success');
toast({ title: '🛹 Logged in', description: 'Welcome back via Amber!' });
setTimeout(() => { onLogin(); onClose(); }, 600);
try {
login.amber(pendingPubkey);
setAmberStep('success');
toast({ title: '🛹 Logged in', description: 'Welcome back via Amber!' });
setTimeout(() => { onLogin(); onClose(); }, 600);
} catch {
setErrors(prev => ({ ...prev, amber: 'Login failed. Please try again.' }));
setAmberStep('initial');
}
} else {
setErrors(prev => ({ ...prev, amber: 'Not logged in yet — make sure you approved in Amber first.' }));
setErrors(prev => ({ ...prev, amber: 'Not approved yet — go back to Amber and approve first.' }));
}
}}
>

View file

@ -56,19 +56,18 @@ export default function AmberCallbackPage() {
const isLoginOp = !pendingOp || pendingOp.type === 'get_public_key';
if (isLoginOp) {
if (pubkeyRegex.test(result)) {
// Store pubkey under a SEPARATE key so the original tab's polling
// detects it and calls login.amber() itself — this works even when
// Amber opens the callback in a brand new tab (GrapheneOS behaviour)
localStorage.setItem('amber_pending_pubkey', result.toLowerCase());
clearAmberPendingOperation();
setStatus('success');
setMessage('Approved! You can close this tab and return to SkateSpots.');
// Also try to complete login in THIS tab in case it is the same tab
try {
login.amber(result.toLowerCase());
clearAmberPendingOperation();
setStatus('success');
setMessage('Logged in! Returning to SkateSpots...');
toast({ title: '🛹 Logged in', description: 'Welcome back via Amber!' });
redirectBack(true);
} catch (err) {
console.error('[AmberCallback] login.amber failed:', err);
setStatus('error');
setMessage('Login failed. Please try again.');
clearAmberPendingOperation();
}
} catch { /* ignore — may fail if called twice */ }
redirectBack(true);
} else {
setStatus('error');
setMessage('Invalid response from Amber. Please try again.');