diff --git a/app/layout.tsx b/app/layout.tsx index d413d84..c7a4169 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,8 @@ import { Analytics } from '@vercel/analytics/next' import './globals.css' import { ThemeProvider } from "@/components/theme-provider" import { Toaster } from "@/components/ui/sonner" +import { StellarWalletProvider } from "@/components/wallet-provider" +import { NetworkBanner } from "@/components/network-banner" export const metadata: Metadata = { title: 'TaskChain', @@ -43,9 +45,12 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - {children} - - + + + {children} + + + diff --git a/app/login/page.tsx b/app/login/page.tsx index c88321e..035f300 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -2,42 +2,56 @@ import React, { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' -import { isConnected, requestAccess, getAddress, signMessage } from '@stellar/freighter-api' +import { signMessage } from '@stellar/freighter-api' import { Button } from '@/components/ui/button' -import { Wallet, LogOut, AlertCircle, Loader2, ArrowLeft, FlaskConical } from 'lucide-react' +import { + Wallet, + LogOut, + AlertCircle, + Loader2, + ArrowLeft, + FlaskConical, + AlertTriangle, +} from 'lucide-react' import Link from 'next/link' +import { + useStellarWallet, + truncateStellarAddress, +} from '@/components/wallet-provider' const IS_DEV = process.env.NODE_ENV !== 'production' export default function LoginPage() { const router = useRouter() - const [address, setAddress] = useState(null) + + // ── Wallet context ──────────────────────────────────────────────────────── + const { + address, + isConnected: walletIsConnected, + isConnecting, + isWrongNetwork, + network, + connect, + disconnect, + } = useStellarWallet() + + // ── Local state ────────────────────────────────────────────────────────── const [isInitializing, setIsInitializing] = useState(true) - const [isConnecting, setIsConnecting] = useState(false) const [statusMessage, setStatusMessage] = useState(null) const [isMocking, setIsMocking] = useState(false) const [error, setError] = useState(null) - // Initialize session from local storage on mount + // ── Session check on mount ──────────────────────────────────────────────── useEffect(() => { - const initSession = async () => { + const checkSession = async () => { try { const savedAddress = localStorage.getItem('stellar_wallet_address') - if (savedAddress) { - // Verify if still connected - const connectedResponse = await isConnected() - if (connectedResponse.isConnected) { - // Also check if backend session is still active - const meRes = await fetch('/api/auth/me') - if (meRes.ok) { - setAddress(savedAddress) - router.push('/dashboard') - return - } + if (savedAddress && walletIsConnected) { + const meRes = await fetch('/api/auth/me') + if (meRes.ok) { + router.push('/dashboard') + return } - // Clear if not fully verified - localStorage.removeItem('stellar_wallet_address') - localStorage.removeItem('tc_dev_access_token') } } catch (err) { console.error('Failed to initialize wallet session:', err) @@ -46,48 +60,42 @@ export default function LoginPage() { } } - // Slight delay to ensure Freighter extension is loaded - setTimeout(initSession, 500) - }, [router]) + // Small delay to let provider's first sync complete + const t = setTimeout(checkSession, 700) + return () => clearTimeout(t) + }, [router, walletIsConnected]) + // ── Step 1: Connect wallet ──────────────────────────────────────────────── const handleConnect = async () => { setError(null) - setIsConnecting(true) - setStatusMessage('Connecting wallet...') - try { - // Check if Freighter is installed - const connectedResponse = await isConnected() - if (connectedResponse.error && !connectedResponse.isConnected) { - throw new Error(connectedResponse.error.message || 'Freighter wallet not detected. Please install the extension.') - } - - // Request connection - const accessResponse = await requestAccess() - if (accessResponse.error) { - throw new Error(accessResponse.error.message || 'Connection request was rejected.') - } + // Delegates to the global provider (handles requestAccess + address/network sync) + await connect() + } - let walletAddress = accessResponse.address; + // ── Step 2: Sign message & authenticate ───────────────────────────────── + const handleSign = async () => { + setError(null) - // Fallback if address is missing but no error was caught - if (!walletAddress) { - const addressResponse = await getAddress() - if (addressResponse.error) { - throw new Error(addressResponse.error.message || 'Failed to retrieve wallet address.') - } - walletAddress = addressResponse.address - } + if (!address) { + setError('No wallet connected. Please connect Freighter first.') + return + } - if (!walletAddress) { - throw new Error('Failed to retrieve wallet address from Freighter.') - } + // Block if on the wrong network + if (isWrongNetwork) { + setError( + `Wrong network detected (${network}). Please switch your Freighter wallet to Stellar Testnet and try again.` + ) + return + } + try { // 1. Fetch Nonce & Message setStatusMessage('Requesting authentication message...') const nonceRes = await fetch('/api/auth/nonce', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ walletAddress }), + body: JSON.stringify({ walletAddress: address }), }) const nonceData = await nonceRes.json() if (!nonceRes.ok) { @@ -99,13 +107,15 @@ export default function LoginPage() { // 2. Request signature via Freighter signMessage setStatusMessage('Approve signature request...') const signResponse = await signMessage(message) - + let signature: string | undefined if (typeof signResponse === 'string') { signature = signResponse } else if (signResponse && typeof signResponse === 'object') { if (signResponse.error) { - throw new Error(signResponse.error.message || signResponse.error || 'Message signing was rejected.') + throw new Error( + signResponse.error.message || signResponse.error || 'Message signing was rejected.' + ) } signature = signResponse.signedMessage } @@ -119,12 +129,7 @@ export default function LoginPage() { const verifyRes = await fetch('/api/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - walletAddress, - nonce, - signature, - message, - }), + body: JSON.stringify({ walletAddress: address, nonce, signature, message }), }) const verifyData = await verifyRes.json() @@ -132,22 +137,21 @@ export default function LoginPage() { throw new Error(verifyData?.error || 'Signature verification failed.') } - // 4. Set local storage & redirect - setAddress(walletAddress) - localStorage.setItem('stellar_wallet_address', walletAddress) + // 4. Persist & redirect + localStorage.setItem('stellar_wallet_address', address) if (verifyData.accessToken) { localStorage.setItem('tc_dev_access_token', verifyData.accessToken) } router.push('/dashboard') - } catch (err: any) { - console.error('Connection error:', err) - setError(err?.message || 'Unable to connect wallet. Please try again.') + } catch (err: unknown) { + console.error('Sign-in error:', err) + setError(err instanceof Error ? err.message : 'Unable to authenticate. Please try again.') } finally { - setIsConnecting(false) setStatusMessage(null) } } + // ── Dev-only mock login bypass ───────────────────────────────────────── const handleMockLogin = async () => { setError(null) setIsMocking(true) @@ -155,10 +159,7 @@ export default function LoginPage() { const res = await fetch('/api/auth/mock', { method: 'POST' }) const data = await res.json() if (!res.ok) throw new Error(data?.error ?? 'Mock auth failed') - setAddress(data.walletAddress) localStorage.setItem('stellar_wallet_address', data.walletAddress) - // Store the access token so the wizard can send it as a Bearer header - // in case the httpOnly cookie isn't forwarded by the browser in dev. if (data.accessToken) { localStorage.setItem('tc_dev_access_token', data.accessToken) } @@ -171,17 +172,11 @@ export default function LoginPage() { } const handleDisconnect = () => { - setAddress(null) - localStorage.removeItem('stellar_wallet_address') - localStorage.removeItem('tc_dev_access_token') + disconnect() setError(null) } - const formatAddress = (addr: string) => { - if (!addr || addr.length <= 10) return addr; - return `${addr.substring(0, 5)}...${addr.substring(addr.length - 4)}` - } - + // ── Render ───────────────────────────────────────────────────────────────── return (
{/* Back button */} @@ -194,16 +189,18 @@ export default function LoginPage() { - {/* Background decorations matching the app's aesthetic */} + {/* Background decorations */}
+ {/* Icon */}
+ {/* Header */}

Connect Wallet

@@ -211,6 +208,7 @@ export default function LoginPage() {

+ {/* Error banner */} {error && (
@@ -218,31 +216,80 @@ export default function LoginPage() {
)} + {/* ── State: Initializing ─────────────────────────────────────── */} {isInitializing ? (
+ ) : address ? ( + /* ── State: Wallet connected ─────────────────────────────── */
+ + {/* Connected wallet info */}
- Connected +
+ + Connected +
- {formatAddress(address)} + {truncateStellarAddress(address)}
+ {/* Wrong-network warning */} + {isWrongNetwork && ( +
+ +
+

Wrong Network

+

+ Your Freighter wallet is set to {network}. TaskChain requires{' '} + Stellar Testnet. Open Freighter → Settings → Network and switch, then try again. +

+
+
+ )} + + {/* Sign-in button */} + +
+ ) : ( + /* ── State: Not connected ─────────────────────────────────── */
- + + {/* Wallet info section */} + {isConnected && address && ( + <> +
+

Connected wallet

+

{address}

+
+ + {networkLabel(network)} + + {isWrongNetwork && ( + + Wrong network + + )} +
+
+ + + )} + - + Profile - Settings - Logout + + + + + + Logout +
diff --git a/components/navbar.tsx b/components/navbar.tsx index 2041220..1767b39 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -2,23 +2,72 @@ import Link from 'next/link' import { Button } from '@/components/ui/button' -import { Menu, X, Wallet, User, Settings, LogOut, ChevronDown } from 'lucide-react' +import { Menu, X, Wallet, User, Settings, LogOut, ChevronDown, AlertTriangle } from 'lucide-react' import { useState } from 'react' import { ThemeToggle } from './ui/ThemeToggle' +import { + useStellarWallet, + truncateStellarAddress, + networkLabel, + REQUIRED_NETWORK, + type StellarNetwork, +} from '@/components/wallet-provider' +import { useRouter } from 'next/navigation' + +// ── Standalone sub-component ────────────────────────────────────────────────── + +function NetworkBadge({ + network, + isConnected, +}: { + network: StellarNetwork + isConnected: boolean +}) { + if (!isConnected) return null + const isTestnet = network === REQUIRED_NETWORK + return ( + + {networkLabel(network)} + + ) +} export function Navbar() { + const router = useRouter() const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [profileDropdownOpen, setProfileDropdownOpen] = useState(false) - - // ⚡ Wallet Connection states (Wire these up to your Stellar/Wallet adapter hooks later) - const [isConnected, setIsConnected] = useState(true) - const walletAddress = "GBXW...4Y2T" + + const { + address, + isConnected, + isConnecting, + isWrongNetwork, + network, + connect, + disconnect, + } = useStellarWallet() const closeMenus = () => { setMobileMenuOpen(false) setProfileDropdownOpen(false) } + const handleConnect = async () => { + await connect() + } + + const handleDisconnect = () => { + disconnect() + closeMenus() + router.push('/login') + } + return (