Improve Amber login flow - zero friction, auto-complete on return
- Add dedicated /amber-callback route so Amber always has a stable return URL - Handles get_public_key (login), sign_event, nip44/nip04 encrypt/decrypt - Shows animated processing screen while completing, then redirects back - Add subscribeToAmberReturn() helper in amber.ts using visibilitychange - Auto-detects when user returns to the tab after cancelling in Amber - Resets login UI to initial state gracefully - Update buildCallbackUrl() to use /amber-callback instead of current page URL - Rewrite Amber tab in LoginDialog: - Remove 2-step clipboard paste flow entirely - Single tap opens Amber, login completes automatically on return - Waiting state shows animated dots + cancel button - No manual pubkey entry needed - Save return path in sessionStorage so AmberCallbackPage redirects user back Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
aac2b7b9a2
commit
03d0fe93b0
4 changed files with 316 additions and 173 deletions
|
|
@ -41,6 +41,7 @@ import { NIP19Page } from "./pages/NIP19Page";
|
|||
import LicensePage from "./pages/LicensePage";
|
||||
import SupportersPage from "./pages/SupportersPage";
|
||||
import RiderCardPage from "./pages/RiderCardPage";
|
||||
import AmberCallbackPage from "./pages/AmberCallbackPage";
|
||||
import NotFound from "./pages/NotFound";
|
||||
|
||||
export function AppRouter() {
|
||||
|
|
@ -93,6 +94,8 @@ export function AppRouter() {
|
|||
<Route path="/card" element={<RiderCardPage />} />
|
||||
<Route path="/license" element={<LicensePage />} />
|
||||
<Route path="/supporters" element={<SupportersPage />} />
|
||||
{/* Amber NIP-55 callback — Amber redirects here after signing/login */}
|
||||
<Route path="/amber-callback" element={<AmberCallbackPage />} />
|
||||
{/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */}
|
||||
<Route path="/:nip19" element={<NIP19Page />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
// NOTE: This file is stable and usually should not be modified.
|
||||
// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested.
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Shield, AlertTriangle, UserPlus, Sparkles, Cloud, Smartphone, ClipboardPaste, Check, Loader2 } from 'lucide-react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Shield, AlertTriangle, UserPlus, Sparkles, Cloud, Smartphone, Check, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Input } from '@/components/ui/input'; // used for bunker URI input
|
||||
import { Dialog, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { useLoginActions } from '@/hooks/useLoginActions';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { subscribeToAmberReturn } from '@/lib/amber';
|
||||
|
||||
interface LoginDialogProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -32,22 +33,35 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
|||
amber?: string;
|
||||
}>({});
|
||||
const [amberStep, setAmberStep] = useState<'initial' | 'waiting' | 'success'>('initial');
|
||||
const [amberPubkeyInput, setAmberPubkeyInput] = useState('');
|
||||
const unsubAmberRef = useRef<(() => void) | null>(null);
|
||||
// Note: amberPubkeyInput removed — login now completes automatically via /amber-callback
|
||||
const login = useLoginActions();
|
||||
const { toast } = useToast();
|
||||
|
||||
// Reset all state when dialog opens/closes
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Reset state when dialog opens
|
||||
setIsLoading(false);
|
||||
setBunkerUri('');
|
||||
setErrors({});
|
||||
setAmberStep('initial');
|
||||
setAmberPubkeyInput('');
|
||||
}
|
||||
// Clean up visibility listener when dialog closes
|
||||
if (!isOpen && unsubAmberRef.current) {
|
||||
unsubAmberRef.current();
|
||||
unsubAmberRef.current = null;
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Clean up on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (unsubAmberRef.current) {
|
||||
unsubAmberRef.current();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleExtensionLogin = async () => {
|
||||
setIsLoading(true);
|
||||
setErrors(prev => ({ ...prev, extension: undefined }));
|
||||
|
|
@ -74,81 +88,25 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
|||
};
|
||||
|
||||
const handleAmberLogin = () => {
|
||||
// For PWA: Open Amber WITHOUT callback URL so it copies to clipboard
|
||||
// Then user can paste the pubkey
|
||||
setAmberStep('waiting');
|
||||
setErrors(prev => ({ ...prev, amber: undefined }));
|
||||
|
||||
// Open Amber without callback - it will copy pubkey to clipboard
|
||||
const amberUrl = 'nostrsigner:?compressionType=none&returnType=signature&type=get_public_key';
|
||||
window.location.href = amberUrl;
|
||||
};
|
||||
|
||||
const handleAmberPaste = async () => {
|
||||
setErrors(prev => ({ ...prev, amber: undefined }));
|
||||
|
||||
try {
|
||||
// Try to read from clipboard
|
||||
const text = await navigator.clipboard.readText();
|
||||
const pubkey = text.trim().toLowerCase();
|
||||
|
||||
// Validate pubkey format (64 hex characters)
|
||||
const pubkeyRegex = /^[0-9a-f]{64}$/;
|
||||
if (!pubkeyRegex.test(pubkey)) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
amber: 'Invalid pubkey format. Make sure Amber copied the pubkey to clipboard.'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete login
|
||||
login.amber(pubkey);
|
||||
setAmberStep('success');
|
||||
toast({
|
||||
title: 'Logged in',
|
||||
description: 'Successfully logged in with Amber!',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
onLogin();
|
||||
onClose();
|
||||
}, 500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Clipboard read failed:', error);
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
amber: 'Could not read clipboard. Please paste your pubkey manually below.'
|
||||
}));
|
||||
}
|
||||
};
|
||||
// Save current path so AmberCallbackPage can send user back here
|
||||
sessionStorage.setItem('amber_return_path', window.location.pathname);
|
||||
|
||||
const handleAmberManualInput = () => {
|
||||
const pubkey = amberPubkeyInput.trim().toLowerCase();
|
||||
|
||||
// Validate pubkey format (64 hex characters)
|
||||
const pubkeyRegex = /^[0-9a-f]{64}$/;
|
||||
if (!pubkeyRegex.test(pubkey)) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
amber: 'Invalid pubkey. Must be 64 hexadecimal characters.'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete login
|
||||
login.amber(pubkey);
|
||||
setAmberStep('success');
|
||||
toast({
|
||||
title: 'Logged in',
|
||||
description: 'Successfully logged in with Amber!',
|
||||
// Subscribe: when user comes back to this tab (e.g. pressed Back in Amber
|
||||
// without completing), reset to initial so they can try again
|
||||
unsubAmberRef.current = subscribeToAmberReturn(() => {
|
||||
// If we're still in 'waiting' state the callback page didn't fire —
|
||||
// user likely cancelled. Reset gracefully.
|
||||
setAmberStep(prev => prev === 'waiting' ? 'initial' : prev);
|
||||
unsubAmberRef.current = null;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
onLogin();
|
||||
onClose();
|
||||
}, 500);
|
||||
|
||||
// Open Amber — it will call /amber-callback?event=<pubkey> on approve
|
||||
const callbackUrl = `${window.location.origin}/amber-callback?event=`;
|
||||
const amberUrl = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
window.location.href = amberUrl;
|
||||
};
|
||||
|
||||
const handleBunkerLogin = async () => {
|
||||
|
|
@ -321,96 +279,96 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
|||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Step 1: Open Amber */}
|
||||
|
||||
{/* Initial state — single tap */}
|
||||
{amberStep === 'initial' && (
|
||||
<div className='text-center p-6 rounded-lg bg-black/50 border-2 border-[#ccff00]/30'>
|
||||
<Smartphone className='w-12 h-12 mx-auto mb-3 text-[#ccff00]' />
|
||||
<p className='text-sm text-gray-300 mb-2 font-bold'>
|
||||
Login with Amber (Android)
|
||||
</p>
|
||||
<p className='text-xs text-gray-400 mb-4'>
|
||||
Tap the button below to open Amber. Select your profile and approve the request. Then return here to complete login.
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
className='w-full rounded-full py-4 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 border-2 border-[#ccff00] shadow-[0_0_20px_rgba(204,255,0,0.5)] font-bold uppercase'
|
||||
onClick={handleAmberLogin}
|
||||
disabled={isLoading || isIOS}
|
||||
>
|
||||
{isIOS ? 'Not available on iOS' : '1. Open Amber'}
|
||||
</Button>
|
||||
<div className='text-center p-6 rounded-lg bg-black/50 border-2 border-[#ccff00]/30 space-y-4'>
|
||||
<div className="w-16 h-16 rounded-full bg-[#ccff00]/10 border-2 border-[#ccff00]/40 flex items-center justify-center mx-auto">
|
||||
<Smartphone className='w-8 h-8 text-[#ccff00]' />
|
||||
</div>
|
||||
<p className='text-xs text-gray-400 mt-3'>
|
||||
Don't have Amber? <a href="https://github.com/greenart7c3/Amber" target="_blank" rel="noopener noreferrer" className="text-[#ccff00] hover:underline">Download from GitHub</a> or F-Droid
|
||||
<div>
|
||||
<p className='text-sm text-white font-bold mb-1'>
|
||||
Login with Amber
|
||||
</p>
|
||||
<p className='text-xs text-gray-400'>
|
||||
Tap below — Amber opens, you approve once, and you're automatically logged in. No copy-paste needed.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className='w-full rounded-full py-4 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 border-2 border-[#ccff00] shadow-[0_0_20px_rgba(204,255,0,0.5)] font-bold uppercase text-base h-auto'
|
||||
onClick={handleAmberLogin}
|
||||
disabled={isLoading || isIOS}
|
||||
>
|
||||
<Smartphone className="w-4 h-4 mr-2" />
|
||||
{isIOS ? 'Android only' : 'Open Amber'}
|
||||
</Button>
|
||||
<p className='text-xs text-gray-500'>
|
||||
Don't have Amber?{' '}
|
||||
<a
|
||||
href="https://github.com/greenart7c3/Amber/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#ccff00] hover:underline"
|
||||
>
|
||||
Download on GitHub
|
||||
</a>
|
||||
{' '}or{' '}
|
||||
<a
|
||||
href="https://f-droid.org/packages/com.greenart7c3.nostrsigner/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#ccff00] hover:underline"
|
||||
>
|
||||
F-Droid
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Paste from clipboard */}
|
||||
|
||||
{/* Waiting — user is in Amber */}
|
||||
{amberStep === 'waiting' && (
|
||||
<div className='text-center p-6 rounded-lg bg-black/50 border-2 border-[#ccff00]/30'>
|
||||
<ClipboardPaste className='w-12 h-12 mx-auto mb-3 text-[#ccff00]' />
|
||||
<p className='text-sm text-gray-300 mb-2 font-bold'>
|
||||
Step 2: Complete Login
|
||||
</p>
|
||||
<p className='text-xs text-gray-400 mb-4'>
|
||||
After approving in Amber, your pubkey was copied to clipboard. Tap the button below to paste it and complete login.
|
||||
</p>
|
||||
|
||||
{errors.amber && (
|
||||
<Alert variant="destructive" className="mb-4 bg-red-950/50 border-red-500/50 text-left">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">{errors.amber}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
className='w-full rounded-full py-4 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 border-2 border-[#ccff00] shadow-[0_0_20px_rgba(204,255,0,0.5)] font-bold uppercase'
|
||||
onClick={handleAmberPaste}
|
||||
>
|
||||
<ClipboardPaste className="w-4 h-4 mr-2" />
|
||||
2. Paste from Clipboard
|
||||
</Button>
|
||||
|
||||
<div className="text-xs text-gray-500">or paste manually:</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={amberPubkeyInput}
|
||||
onChange={(e) => setAmberPubkeyInput(e.target.value)}
|
||||
placeholder="Paste your pubkey (64 hex chars)"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleAmberManualInput}
|
||||
disabled={!amberPubkeyInput.trim()}
|
||||
className="bg-[#ccff00]/20 text-[#ccff00] hover:bg-[#ccff00]/30"
|
||||
>
|
||||
Go
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full text-xs text-gray-500"
|
||||
onClick={() => setAmberStep('initial')}
|
||||
>
|
||||
← Back to Step 1
|
||||
</Button>
|
||||
<div className='text-center p-6 rounded-lg bg-black/50 border-2 border-[#ccff00]/40 space-y-4'>
|
||||
<div className="w-16 h-16 rounded-full bg-[#ccff00]/10 border-2 border-[#ccff00]/40 flex items-center justify-center mx-auto">
|
||||
<Loader2 className='w-8 h-8 text-[#ccff00] animate-spin' />
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-sm text-white font-bold mb-1'>Waiting for Amber...</p>
|
||||
<p className='text-xs text-gray-400'>
|
||||
Approve the request in Amber. You'll be logged in automatically when you return.
|
||||
</p>
|
||||
</div>
|
||||
{/* Animated dots */}
|
||||
<div className="flex justify-center gap-1.5">
|
||||
{[0, 1, 2].map(i => (
|
||||
<span
|
||||
key={i}
|
||||
className="w-2 h-2 rounded-full bg-[#ccff00] animate-bounce"
|
||||
style={{ animationDelay: `${i * 0.15}s` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-xs text-gray-500 hover:text-gray-300"
|
||||
onClick={() => {
|
||||
setAmberStep('initial');
|
||||
if (unsubAmberRef.current) {
|
||||
unsubAmberRef.current();
|
||||
unsubAmberRef.current = null;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success state */}
|
||||
|
||||
{/* Success */}
|
||||
{amberStep === 'success' && (
|
||||
<div className='text-center p-6 rounded-lg bg-black/50 border-2 border-green-500/30'>
|
||||
<Check className='w-12 h-12 mx-auto mb-3 text-green-500' />
|
||||
<p className='text-sm text-green-400 mb-2 font-bold'>
|
||||
Successfully logged in!
|
||||
</p>
|
||||
<Loader2 className="w-6 h-6 mx-auto animate-spin text-green-500" />
|
||||
<div className='text-center p-6 rounded-lg bg-black/50 border-2 border-green-500/30 space-y-3'>
|
||||
<Check className='w-12 h-12 mx-auto text-green-500' />
|
||||
<p className='text-sm text-green-400 font-bold'>Logged in!</p>
|
||||
<Loader2 className="w-5 h-5 mx-auto animate-spin text-green-500" />
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -239,43 +239,56 @@ function generateOperationId(): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Build callback URL for Amber operations.
|
||||
* Build the dedicated Amber callback URL.
|
||||
* Using a fixed /amber-callback path so Amber always returns to the same place.
|
||||
*/
|
||||
function buildCallbackUrl(): string {
|
||||
const baseUrl = window.location.origin + window.location.pathname;
|
||||
return `${baseUrl}?event=`;
|
||||
return `${window.location.origin}/amber-callback?event=`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Amber for get_public_key operation.
|
||||
* Uses a dedicated /amber-callback route so the app state is preserved
|
||||
* and the callback is processed reliably on return.
|
||||
*/
|
||||
export function openAmberForLogin(): void {
|
||||
const operationId = generateOperationId();
|
||||
const callbackUrl = buildCallbackUrl();
|
||||
|
||||
|
||||
const pendingOp = {
|
||||
type: 'get_public_key' as const,
|
||||
id: operationId,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
console.log('[Amber] Opening Amber for login...');
|
||||
console.log('[Amber] Callback URL:', callbackUrl);
|
||||
console.log('[Amber] Pending operation:', pendingOp);
|
||||
|
||||
|
||||
setAmberPendingOperation(pendingOp);
|
||||
|
||||
// Verify it was stored
|
||||
const stored = localStorage.getItem('amber_pending_operation');
|
||||
console.log('[Amber] Stored in localStorage:', stored);
|
||||
|
||||
// NIP-55 format for web applications
|
||||
|
||||
// NIP-55 nostrsigner: URI — Amber opens, user approves, Amber calls callbackUrl
|
||||
const amberUrl = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
console.log('[Amber] Opening URL:', amberUrl);
|
||||
|
||||
console.log('[Amber] Opening Amber for login, callback:', callbackUrl);
|
||||
window.location.href = amberUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to app becoming visible again after user switches back from Amber.
|
||||
* Calls `onReturn` once when the page regains visibility.
|
||||
* Returns an unsubscribe function.
|
||||
*
|
||||
* Usage:
|
||||
* const unsub = subscribeToAmberReturn(() => checkClipboardForPubkey());
|
||||
*/
|
||||
export function subscribeToAmberReturn(onReturn: () => void): () => void {
|
||||
const handler = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
document.removeEventListener('visibilitychange', handler);
|
||||
onReturn();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handler);
|
||||
return () => document.removeEventListener('visibilitychange', handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Amber for sign_event operation.
|
||||
* Returns a promise that resolves when user returns to the page.
|
||||
|
|
|
|||
169
src/pages/AmberCallbackPage.tsx
Normal file
169
src/pages/AmberCallbackPage.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/**
|
||||
* AmberCallbackPage
|
||||
*
|
||||
* Amber (NIP-55) redirects back to /amber-callback?event=<pubkey|signedEvent>
|
||||
* after the user approves a request. This page processes the result immediately
|
||||
* and redirects the user back to where they came from (or home).
|
||||
*
|
||||
* Having a dedicated route means:
|
||||
* - The main app state is never destroyed on outgoing redirect
|
||||
* - Amber always knows where to return regardless of which page opened it
|
||||
* - Processing is fast and automatic — no manual pasting needed
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLoginActions } from '@/hooks/useLoginActions';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import {
|
||||
getAmberCallbackResult,
|
||||
getAmberPendingOperationAsync,
|
||||
clearAmberPendingOperation,
|
||||
cleanAmberCallbackUrl,
|
||||
} from '@/lib/amber';
|
||||
import { CheckCircle2, XCircle, Loader2 } from 'lucide-react';
|
||||
|
||||
type Status = 'processing' | 'success' | 'error';
|
||||
|
||||
export default function AmberCallbackPage() {
|
||||
const navigate = useNavigate();
|
||||
const login = useLoginActions();
|
||||
const { toast } = useToast();
|
||||
const hasProcessed = useRef(false);
|
||||
const [status, setStatus] = useState<Status>('processing');
|
||||
const [message, setMessage] = useState('Completing login with Amber...');
|
||||
|
||||
useEffect(() => {
|
||||
if (hasProcessed.current) return;
|
||||
hasProcessed.current = true;
|
||||
|
||||
processCallback();
|
||||
|
||||
async function processCallback() {
|
||||
const result = getAmberCallbackResult();
|
||||
cleanAmberCallbackUrl();
|
||||
|
||||
if (!result) {
|
||||
setStatus('error');
|
||||
setMessage('No result received from Amber. The request may have been cancelled.');
|
||||
clearAmberPendingOperation();
|
||||
redirectBack(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingOp = await getAmberPendingOperationAsync();
|
||||
const pubkeyRegex = /^[0-9a-fA-F]{64}$/;
|
||||
|
||||
// ── get_public_key (login) ──────────────────────────────────────────────
|
||||
const isLoginOp = !pendingOp || pendingOp.type === 'get_public_key';
|
||||
if (isLoginOp) {
|
||||
if (pubkeyRegex.test(result)) {
|
||||
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();
|
||||
redirectBack(false);
|
||||
}
|
||||
} else {
|
||||
setStatus('error');
|
||||
setMessage('Invalid pubkey received from Amber.');
|
||||
clearAmberPendingOperation();
|
||||
redirectBack(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── sign_event ──────────────────────────────────────────────────────────
|
||||
if (pendingOp.type === 'sign_event') {
|
||||
try {
|
||||
const signedEvent = JSON.parse(result);
|
||||
sessionStorage.setItem('amber_signed_event', JSON.stringify(signedEvent));
|
||||
clearAmberPendingOperation();
|
||||
setStatus('success');
|
||||
setMessage('Event signed! Returning...');
|
||||
toast({ title: 'Event signed', description: 'Your event was signed by Amber.' });
|
||||
redirectBack(true);
|
||||
} catch {
|
||||
setStatus('error');
|
||||
setMessage('Failed to process signed event from Amber.');
|
||||
clearAmberPendingOperation();
|
||||
redirectBack(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── encrypt / decrypt ───────────────────────────────────────────────────
|
||||
if (pendingOp.type.includes('encrypt') || pendingOp.type.includes('decrypt')) {
|
||||
sessionStorage.setItem('amber_crypto_result', result);
|
||||
clearAmberPendingOperation();
|
||||
setStatus('success');
|
||||
setMessage('Operation complete! Returning...');
|
||||
redirectBack(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown
|
||||
clearAmberPendingOperation();
|
||||
redirectBack(false);
|
||||
}
|
||||
|
||||
function redirectBack(success: boolean) {
|
||||
// Try to go back to where we came from, fall back to home
|
||||
const returnTo = sessionStorage.getItem('amber_return_path') || '/';
|
||||
sessionStorage.removeItem('amber_return_path');
|
||||
setTimeout(() => navigate(returnTo, { replace: true }), success ? 800 : 1500);
|
||||
}
|
||||
}, [login, toast, navigate]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center px-4">
|
||||
<div className="max-w-sm w-full text-center space-y-6">
|
||||
{/* Icon */}
|
||||
<div className="flex justify-center">
|
||||
{status === 'processing' && (
|
||||
<div className="w-20 h-20 rounded-full bg-[#ccff00]/10 border-2 border-[#ccff00]/40 flex items-center justify-center">
|
||||
<Loader2 className="w-10 h-10 text-[#ccff00] animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{status === 'success' && (
|
||||
<div className="w-20 h-20 rounded-full bg-green-500/10 border-2 border-green-500/40 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-10 h-10 text-green-500" />
|
||||
</div>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<div className="w-20 h-20 rounded-full bg-red-500/10 border-2 border-red-500/40 flex items-center justify-center">
|
||||
<XCircle className="w-10 h-10 text-red-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Amber logo text */}
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-widest mb-1">Amber · NIP-55</p>
|
||||
<p className="text-white font-bold text-lg">{message}</p>
|
||||
</div>
|
||||
|
||||
{/* Dots loader */}
|
||||
{status === 'processing' && (
|
||||
<div className="flex justify-center gap-1.5">
|
||||
{[0, 1, 2].map(i => (
|
||||
<span
|
||||
key={i}
|
||||
className="w-2 h-2 rounded-full bg-[#ccff00] animate-bounce"
|
||||
style={{ animationDelay: `${i * 0.15}s` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue