| import { useEffect, useState } from 'react' |
| import { EnrollPage } from './pages/EnrollPage' |
| import { PayPage } from './pages/PayPage' |
| import { NotFoundPage } from './pages/NotFoundPage' |
| import { HomePage } from './pages/HomePage' |
| import { SecurityPage } from './pages/SecurityPage' |
| import { BiometricsLabPage } from './pages/BiometricsLabPage' |
| import { AgenticDemoPage } from './pages/AgenticDemoPage' |
| import { FederatedDemoPage } from './pages/FederatedDemoPage' |
| import { ActivityPage } from './pages/ActivityPage' |
| import { BiometricsDashboard } from './pages/BiometricsDashboard' |
| import { BiometricEnrollPage } from './pages/BiometricEnrollPage' |
| import { BiometricVerifyPage } from './pages/BiometricVerifyPage' |
| import { LivenessPage } from './pages/LivenessPage' |
| import { ResultsPage } from './pages/ResultsPage' |
| import { ReportCardPage } from './pages/ReportCardPage' |
| import { OobPage } from './pages/OobPage' |
| import { D2EnrollPage } from './pages/D2EnrollPage' |
| import { D2SignInPage } from './pages/D2SignInPage' |
| import { D2AccountPage } from './pages/D2AccountPage' |
| import { D2SecurityPage } from './pages/D2SecurityPage' |
| import { D2SessionsPage } from './pages/D2SessionsPage' |
| import { D2AccountStatusPage } from './pages/D2AccountStatusPage' |
| import { D2OperatorPage } from './pages/D2OperatorPage' |
| import { D2RecoveryPage } from './pages/D2RecoveryPage' |
| import { FinancePage } from './pages/FinancePage' |
| import { AtmPage } from './pages/AtmPage' |
| import { AppFooter } from './components/AppFooter' |
| import { useSession } from './hooks/useSession' |
| import { useD3Enabled } from './hooks/useD3Enabled' |
| import { useI18n } from './i18n' |
| import { Button } from './components/ui' |
| import { AGENTIC_DEMO_ENABLED } from './api/ai' |
|
|
| const ROUTES = [ |
| 'home', 'pay', 'security', 'activity', 'demo/agentic-security', 'demo/federated-learning', 'biometrics/lab', |
| 'enroll', 'biometrics', 'biometrics/enroll', 'biometrics/verify', |
| 'biometrics/liveness', 'biometrics/reportcard', 'biometrics/oob', 'results', |
| 'id/enroll', 'id/signin', 'id/account', 'id/security', 'id/sessions', 'id/status', |
| 'id/operator', 'id/recovery', 'finance', 'atm', 'notfound', |
| ] as const |
| type Route = (typeof ROUTES)[number] |
|
|
| function parse(): Route { |
| const raw = window.location.hash.replace(/^#\/?/, '') |
| if (raw === '' || raw === 'home') return 'home' |
| return (ROUTES as readonly string[]).includes(raw) ? (raw as Route) : 'notfound' |
| } |
|
|
| function useHashRoute(): [Route, (r: string) => void] { |
| const [route, setRoute] = useState<Route>(parse()) |
| useEffect(() => { |
| const onHash = () => setRoute(parse()) |
| window.addEventListener('hashchange', onHash) |
| return () => window.removeEventListener('hashchange', onHash) |
| }, []) |
| const nav = (r: string) => { window.location.hash = `#/${r}`; setRoute(parse()) } |
| return [route, nav] |
| } |
|
|
| |
| const PRIMARY: { route: string; key: string }[] = [ |
| { route: 'home', key: 'nav.home' }, |
| { route: 'finance', key: 'nav.finance' }, |
| { route: 'pay', key: 'nav.pay' }, |
| { route: 'security', key: 'nav.security' }, |
| { route: 'activity', key: 'nav.activity' }, |
| { route: 'id/account', key: 'nav.account' }, |
| ] |
|
|
| export function App() { |
| const { t, locale, setLocale } = useI18n() |
| const { activeUser } = useSession() |
| const [route, nav] = useHashRoute() |
| const [moreOpen, setMoreOpen] = useState(false) |
| |
| |
| |
| const d3Enabled = useD3Enabled() |
| const primary = PRIMARY.filter((p) => p.route !== 'finance' || d3Enabled === true) |
| const matches = (r: string) => route === r || (r === 'home' && route === 'home') |
| const tab = (r: string, label: string) => ( |
| <button className={`tab ${matches(r) ? 'active' : ''}`} aria-current={matches(r)} onClick={() => nav(r)}>{label}</button> |
| ) |
| return ( |
| <div className="app"> |
| <header className="topbar"> |
| <div className="brand"> |
| <svg className="brand-lock" viewBox="0 0 24 24" width="22" height="22" role="img" aria-label="secure"> |
| <path d="M7 10V7a5 5 0 0 1 10 0v3" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /> |
| <rect x="4.5" y="10" width="15" height="11" rx="3" fill="currentColor" /> |
| <circle cx="12" cy="14.5" r="1.7" className="brand-lock-hole" /> |
| <rect x="11.2" y="15" width="1.6" height="3.4" rx="0.8" className="brand-lock-hole" /> |
| </svg> |
| <span>{t('app.title')}</span> |
| </div> |
| <nav aria-label="Primary" className="desktop-nav"> |
| {primary.map((p) => <span key={p.route}>{tab(p.route, t(p.key as never))}</span>)} |
| {AGENTIC_DEMO_ENABLED && tab('demo/agentic-security', t('nav.demo'))} |
| {AGENTIC_DEMO_ENABLED && tab('demo/federated-learning', t('nav.federatedDemo'))} |
| {tab('biometrics/lab', t('nav.lab'))} |
| </nav> |
| <div className="topbar-right"> |
| <span className="active-user">{activeUser ? t('header.activeUser', { user: activeUser.userId }) : t('header.noUser')}</span> |
| <Button onClick={() => setLocale(locale === 'ar' ? 'en' : 'ar')} aria-label={t('lang.toggle')}>{t('lang.toggle')}</Button> |
| </div> |
| </header> |
| <p className="demo-banner" role="note">{t('demo.banner')}</p> |
| <main className="content"> |
| {route === 'home' && <HomePage onNav={(r) => nav(r)} />} |
| {route === 'pay' && <PayPage onNavEnroll={() => nav('enroll')} onNav={(r) => nav(r)} />} |
| {route === 'security' && <SecurityPage onNav={(r) => nav(r)} />} |
| {route === 'demo/agentic-security' && <AgenticDemoPage />} |
| {route === 'demo/federated-learning' && <FederatedDemoPage />} |
| {route === 'biometrics/lab' && <BiometricsLabPage onNav={(r) => nav(r)} />} |
| {route === 'activity' && <ActivityPage />} |
| {route === 'enroll' && <EnrollPage />} |
| {route === 'biometrics' && <BiometricsDashboard onNav={(r) => nav(r)} />} |
| {route === 'biometrics/enroll' && <BiometricEnrollPage />} |
| {route === 'biometrics/verify' && <BiometricVerifyPage />} |
| {route === 'biometrics/liveness' && <LivenessPage />} |
| {route === 'biometrics/reportcard' && <ReportCardPage />} |
| {route === 'biometrics/oob' && <OobPage />} |
| {route === 'results' && <ResultsPage />} |
| {route === 'id/enroll' && <D2EnrollPage onNav={(r) => nav(r)} />} |
| {route === 'id/signin' && <D2SignInPage onNav={(r) => nav(r)} />} |
| {route === 'id/account' && <D2AccountPage onNav={(r) => nav(r)} />} |
| {route === 'id/security' && <D2SecurityPage />} |
| {route === 'id/sessions' && <D2SessionsPage />} |
| {route === 'id/status' && <D2AccountStatusPage onNav={(r) => nav(r)} />} |
| {route === 'id/operator' && <D2OperatorPage onNav={(r) => nav(r)} />} |
| {route === 'id/recovery' && <D2RecoveryPage onNav={(r) => nav(r)} />} |
| {/* Wallet/ATM routes only resolve when D3 is enabled; otherwise fall through to 404 so a |
| dormant deployment can't reach a broken page (null = still probing → render nothing). */} |
| {route === 'finance' && d3Enabled === true && <FinancePage onNav={(r) => nav(r)} />} |
| {route === 'atm' && d3Enabled === true && <AtmPage onNav={(r) => nav(r)} />} |
| {(route === 'finance' || route === 'atm') && d3Enabled === false && <NotFoundPage onNav={(r) => nav(r)} />} |
| {route === 'notfound' && <NotFoundPage onNav={(r) => nav(r)} />} |
| </main> |
| {moreOpen && ( |
| <div className="more-sheet" role="menu" aria-label={t('nav.more')}> |
| {AGENTIC_DEMO_ENABLED && ( |
| <button role="menuitem" className="more-item" onClick={() => { setMoreOpen(false); nav('demo/agentic-security') }}>{t('nav.demo')}</button> |
| )} |
| {AGENTIC_DEMO_ENABLED && ( |
| <button role="menuitem" className="more-item" onClick={() => { setMoreOpen(false); nav('demo/federated-learning') }}>{t('nav.federatedDemo')}</button> |
| )} |
| <button role="menuitem" className="more-item" onClick={() => { setMoreOpen(false); nav('biometrics/lab') }}>{t('nav.lab')}</button> |
| </div> |
| )} |
| <nav className="bottom-nav" aria-label="Primary mobile"> |
| {primary.map((p) => ( |
| <button key={p.route} className={`bnav ${matches(p.route) ? 'active' : ''}`} |
| aria-current={matches(p.route)} onClick={() => { setMoreOpen(false); nav(p.route) }}> |
| <span className="bnav-label">{t(p.key as never)}</span> |
| </button> |
| ))} |
| <button className={`bnav ${moreOpen ? 'active' : ''}`} aria-expanded={moreOpen} |
| aria-haspopup="menu" onClick={() => setMoreOpen((o) => !o)}> |
| <span className="bnav-label">{t('nav.more')}</span> |
| </button> |
| </nav> |
| <AppFooter /> |
| </div> |
| ) |
| } |
|
|