/** * AmberCallbackHandler * * This component handles Amber (NIP-55) callbacks at the app level. * It processes results from Amber after signing operations and login. * * For web apps, Amber communication works via URL redirects: * 1. App opens nostrsigner: URL to request signing * 2. Amber opens and user approves/rejects * 3. Amber redirects back to callbackUrl with result in query params * 4. This component processes the result * * PWA NOTE: PWAs can lose localStorage when redirecting to external apps. * This handler uses multiple fallback strategies: * 1. Check localStorage, sessionStorage, and IndexedDB for pending operation * 2. If all storage is lost but result looks like a pubkey, assume it's a login */ import { useEffect, useRef, useState } from 'react'; import { useLoginActions } from '@/hooks/useLoginActions'; import { useToast } from '@/hooks/useToast'; import { isAmberCallback, getAmberCallbackResult, getAmberPendingOperationAsync, clearAmberPendingOperation, cleanAmberCallbackUrl, } from '@/lib/amber'; export function AmberCallbackHandler() { const login = useLoginActions(); const { toast } = useToast(); const hasProcessed = useRef(false); const [debugInfo, setDebugInfo] = useState(null); useEffect(() => { // Log on mount for debugging console.log('[AmberCallback] Component mounted'); console.log('[AmberCallback] URL:', window.location.href); console.log('[AmberCallback] Search:', window.location.search); console.log('[AmberCallback] Pre-captured event:', sessionStorage.getItem('amber_callback_event')); // Check if this is an Amber callback const isCallback = isAmberCallback(); console.log('[AmberCallback] isAmberCallback():', isCallback); if (!isCallback) { return; } // Only process once per page load if (hasProcessed.current) { console.log('[AmberCallback] Already processed, skipping'); return; } hasProcessed.current = true; console.log('[AmberCallback] Processing Amber callback...'); // Show debug info temporarily setDebugInfo('Processing Amber login...'); // Process async to allow IndexedDB lookup processAmberCallback(); async function processAmberCallback() { const result = getAmberCallbackResult(); console.log('[AmberCallback] Result:', result); setDebugInfo(`Got result: ${result ? result.substring(0, 16) + '...' : 'null'}`); // Clean up URL immediately to prevent re-processing on refresh cleanAmberCallbackUrl(); if (!result) { // User cancelled or Amber failed clearAmberPendingOperation(); setDebugInfo('No result from Amber'); toast({ title: 'Amber', description: 'No result received from Amber. Operation may have been cancelled.', variant: 'destructive', }); return; } // Try to get pending operation from any storage const pendingOp = await getAmberPendingOperationAsync(); console.log('[AmberCallback] Pending operation:', pendingOp); // FALLBACK: If no pending operation found, but result looks like a valid pubkey, // assume this is a login callback (PWA storage was cleared during redirect) const pubkeyRegex = /^[0-9a-fA-F]{64}$/; if (!pendingOp) { console.warn('[AmberCallback] No pending operation found - storage may have been cleared'); // Check if result looks like a pubkey (login callback) if (pubkeyRegex.test(result)) { console.log('[AmberCallback] Result looks like pubkey, treating as login callback'); setDebugInfo('Valid pubkey! Logging in...'); try { login.amber(result.toLowerCase()); setDebugInfo('Login successful!'); toast({ title: 'Logged in', description: 'Successfully logged in with Amber!', }); // Clear debug after success setTimeout(() => setDebugInfo(null), 2000); return; } catch (error) { console.error('[AmberCallback] Amber login failed:', error); setDebugInfo(`Login error: ${error}`); toast({ title: 'Login failed', description: 'Failed to complete Amber login. Please try again.', variant: 'destructive', }); return; } } // Not a pubkey - can't determine what operation this was for setDebugInfo(`Invalid result format: ${result.substring(0, 20)}...`); toast({ title: 'Amber', description: 'Session data was lost. Please try again.', variant: 'destructive', }); return; } // Process based on operation type switch (pendingOp.type) { case 'get_public_key': { console.log('[AmberCallback] Processing get_public_key...'); if (pubkeyRegex.test(result)) { try { login.amber(result.toLowerCase()); clearAmberPendingOperation(); console.log('[AmberCallback] Login successful!'); toast({ title: 'Logged in', description: 'Successfully logged in with Amber!', }); } catch (error) { console.error('[AmberCallback] Amber login failed:', error); clearAmberPendingOperation(); toast({ title: 'Login failed', description: 'Failed to complete Amber login. Please try again.', variant: 'destructive', }); } } else { console.warn('[AmberCallback] Invalid pubkey format:', result); clearAmberPendingOperation(); toast({ title: 'Login failed', description: 'Invalid pubkey received from Amber.', variant: 'destructive', }); } break; } case 'sign_event': { // Store the signed event result for the caller to retrieve // This is tricky because the original caller's promise can't be resolved // after a page redirect. For now, we'll store it and show a toast. try { const signedEvent = JSON.parse(result); // Store in sessionStorage for potential retrieval sessionStorage.setItem('amber_signed_event', JSON.stringify(signedEvent)); clearAmberPendingOperation(); toast({ title: 'Event signed', description: 'Your event has been signed by Amber.', }); // Reload the page to re-submit the event // This is a workaround since we can't resolve the original promise window.location.reload(); } catch (e) { console.error('Failed to parse signed event:', e); clearAmberPendingOperation(); toast({ title: 'Signing failed', description: 'Failed to process signed event from Amber.', variant: 'destructive', }); } break; } case 'nip04_encrypt': case 'nip44_encrypt': case 'nip04_decrypt': case 'nip44_decrypt': { // Store encryption/decryption results sessionStorage.setItem('amber_crypto_result', result); clearAmberPendingOperation(); toast({ title: 'Operation complete', description: `${pendingOp.type.replace('_', ' ')} completed.`, }); break; } default: { console.warn('Unknown Amber operation type:', pendingOp.type); clearAmberPendingOperation(); } } } }, [login, toast]); // Show debug overlay when processing Amber callback if (debugInfo) { return (
🔐 Amber: {debugInfo}
); } return null; }