Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 33 additions & 21 deletions frontend/app/auth/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { useToast } from '@/components/ui/ToastProvider';
import { useStellarWalletAuth } from '@/hooks/useStellarWalletAuth';
import { useAuth } from '@/hooks/useAuth';
import { WalletType } from '@/lib/stellar/types';
import WalletModal, { WalletType as ModalWalletType } from '@/components/ui/WalletModal';
import { useWalletModal } from '@/hooks/useWalletModal';

const SignInPage = () => {
const router = useRouter();
Expand All @@ -24,6 +26,7 @@ const SignInPage = () => {
clearError,
} = useStellarWalletAuth();
const { loginSuccess, loginFailure, setLoading } = useAuth();
const { isOpen: isWalletModalOpen, openModal: openWalletModal, closeModal: closeWalletModal } = useWalletModal();
const [formData, setFormData] = useState({
username: '',
password: ''
Expand Down Expand Up @@ -197,41 +200,45 @@ const SignInPage = () => {
window.location.href = "http://localhost:3000/auth/google-authentication";
};

const handleWalletLogin = async () => {
const handleWalletSelect = async (walletType: ModalWalletType) => {
closeWalletModal();

if (walletType !== 'freighter') {
showInfo('Coming Soon', `${walletType.charAt(0).toUpperCase() + walletType.slice(1)} wallet support is coming soon!`);
return;
}

clearError();

try {
await connectAndLogin('freighter' as WalletType);

// Success - show toast and redirect
showSuccess('Login Successful', 'Welcome back!');
router.push('/dashboard');
} catch (error) {
console.error("Wallet Connection Error:", error);
// Error handling with user-friendly messages

const isErrorWithCode = (e: unknown): e is { code?: string; message?: string } => {
return typeof e === 'object' && e !== null;
};
if (isErrorWithCode(error)) {
if (error?.code === 'WALLET_NOT_INSTALLED') {
showError(
'Wallet Not Installed',
'Please install Freighter wallet from freighter.app to continue'
);
} else if (error?.code === 'USER_REJECTED') {
showWarning('Request Cancelled', 'You cancelled the wallet request');
} else if (error?.code === 'NONCE_EXPIRED') {
showError('Authentication Expired', 'Please try again');
} else if (error?.code === 'INVALID_SIGNATURE') {
showError('Authentication Failed', 'Invalid signature or expired nonce');
} else if (error?.code === 'NETWORK_ERROR') {
showError('Network Error', 'Unable to connect to server. Please try again.');
} else {
showError('Login Failed', error?.message || 'An unexpected error occurred');
if (error?.code === 'WALLET_NOT_INSTALLED') {
showError(
'Wallet Not Installed',
'Please install Freighter wallet from freighter.app to continue'
);
} else if (error?.code === 'USER_REJECTED') {
showWarning('Request Cancelled', 'You cancelled the wallet request');
} else if (error?.code === 'NONCE_EXPIRED') {
showError('Authentication Expired', 'Please try again');
} else if (error?.code === 'INVALID_SIGNATURE') {
showError('Authentication Failed', 'Invalid signature or expired nonce');
} else if (error?.code === 'NETWORK_ERROR') {
showError('Network Error', 'Unable to connect to server. Please try again.');
} else {
showError('Login Failed', error?.message || 'An unexpected error occurred');
}
}
}
}
};

return (
Expand Down Expand Up @@ -325,7 +332,7 @@ const SignInPage = () => {
</button>

<button
onClick={handleWalletLogin}
onClick={openWalletModal}
disabled={isConnecting || isSigning || isLoggingIn}
className="w-full h-12 border-2 border-blue-500 text-blue-400 rounded-lg flex items-center justify-center gap-3 hover:bg-blue-500/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Expand All @@ -351,6 +358,11 @@ const SignInPage = () => {
</div>
</div>
</div>
<WalletModal
isOpen={isWalletModalOpen}
onClose={closeWalletModal}
onSelect={handleWalletSelect}
/>
</ErrorBoundary>
);
};
Expand Down
186 changes: 186 additions & 0 deletions frontend/components/ui/WalletModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
'use client';

import React, { useEffect, useCallback, useRef } from 'react';
import { X } from 'lucide-react';
import WalletOption from './wallet/WalletOption';
import {
PhantomIcon,
MetaMaskIcon,
CoinbaseIcon,
TrustIcon,
FreighterIcon,
} from './wallet/WalletIcons';

export type WalletType = 'phantom' | 'metamask' | 'coinbase' | 'trust' | 'freighter';

export interface WalletModalProps {
isOpen: boolean;
onClose: () => void;
onSelect: (walletType: WalletType) => void;
}

interface WalletConfig {
id: WalletType;
name: string;
icon: React.ReactNode;
isDetected: boolean;
}

const WALLETS: WalletConfig[] = [
{
id: 'freighter',
name: 'Freighter',
icon: <FreighterIcon />,
isDetected: typeof window !== 'undefined' && !!(window as unknown as Record<string, unknown>).freighter,
},
{
id: 'phantom',
name: 'Phantom',
icon: <PhantomIcon />,
isDetected: typeof window !== 'undefined' && !!(window as unknown as Record<string, unknown>).phantom,
},
{
id: 'metamask',
name: 'MetaMask',
icon: <MetaMaskIcon />,
isDetected:
typeof window !== 'undefined' &&
!!(window as unknown as { ethereum?: { isMetaMask?: boolean } }).ethereum?.isMetaMask,
},
{
id: 'coinbase',
name: 'Coinbase Wallet',
icon: <CoinbaseIcon />,
isDetected:
typeof window !== 'undefined' &&
!!(window as unknown as { ethereum?: { isCoinbaseWallet?: boolean } }).ethereum?.isCoinbaseWallet,
},
{
id: 'trust',
name: 'Trust',
icon: <TrustIcon />,
isDetected:
typeof window !== 'undefined' &&
!!(window as unknown as { ethereum?: { isTrust?: boolean } }).ethereum?.isTrust,
},
];

const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose, onSelect }) => {
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);

// Close on Escape key
useEffect(() => {
if (!isOpen) return;

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};

document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);

// Focus close button on open, restore focus on close
useEffect(() => {
if (isOpen) {
// Small delay to ensure the DOM is ready
const timer = setTimeout(() => closeButtonRef.current?.focus(), 50);
return () => clearTimeout(timer);
}
}, [isOpen]);

// Prevent body scroll when modal is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);

// Focus trap within modal
const handleTabKey = useCallback(
(e: React.KeyboardEvent) => {
if (e.key !== 'Tab' || !modalRef.current) return;

const focusable = modalRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];

if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last?.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first?.focus();
}
}
},
[]
);

const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) onClose();
};

if (!isOpen) return null;

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm px-4"
onClick={handleBackdropClick}
role="dialog"
aria-modal="true"
aria-labelledby="wallet-modal-title"
>
<div
ref={modalRef}
className="relative w-full max-w-sm bg-[#0A1628] border border-white/10 rounded-2xl shadow-2xl p-6"
onKeyDown={handleTabKey}
>
{/* Close button */}
<button
ref={closeButtonRef}
onClick={onClose}
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500/50 rounded-lg p-1"
aria-label="Close wallet modal"
>
<X size={20} />
</button>

{/* Title */}
<h2
id="wallet-modal-title"
className="text-center text-[#E6E6E6] font-semibold text-base mb-6 pr-6"
>
Connect a wallet to continue
</h2>

{/* Wallet list */}
<div className="flex flex-col gap-1" role="list">
{WALLETS.map((wallet) => (
<div key={wallet.id} role="listitem">
<WalletOption
name={wallet.name}
icon={wallet.icon}
isDetected={wallet.isDetected}
onClick={() => onSelect(wallet.id)}
/>
</div>
))}
</div>
</div>
</div>
);
};

export default WalletModal;
Loading