The entire skate-spots codebase was accidentally copied into src/src/ and public/public/ instead of src/ and public/ directly. This left the real src/ directory as blank MKStack template stubs, explaining why the landing page showed the default template instead of SkateSpots. Changes made: - Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx, all pages, components, hooks, contexts, lib, i18n, test) - Copied all files from public/public/ up to public/ (images, manifest, etc.) - Deleted src/src/ and public/public/ nested directories - Removed HelloWorld.tsx template stub from pages/ - Retained the previously fixed index.css (Tailwind v3 directives) Full skate-spots feature set is now active: - All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.) - NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler - Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.) - Outfit Variable font, Amber pre-check in main.tsx - All hooks (useSkateSpots, useCoaches, useZaps, etc.) - i18n support (en/es) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
229 lines
8 KiB
TypeScript
229 lines
8 KiB
TypeScript
/**
|
|
* 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<string | null>(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 (
|
|
<div className="fixed top-0 left-0 right-0 z-[9999] bg-yellow-500 text-black px-4 py-2 text-center text-sm font-mono">
|
|
🔐 Amber: {debugInfo}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|