File size: 9,052 Bytes
45a105b de6cac5 adb91eb 0f4d9ea adb91eb 004f460 70332a1 dc47d57 c7af659 de6cac5 9f48c3c 45a105b adb91eb 45a105b 004f460 0f4d9ea adb91eb dc47d57 c7af659 004f460 45a105b de6cac5 adb91eb 004f460 de6cac5 004f460 45a105b 004f460 45a105b adb91eb c7af659 adb91eb dc47d57 adb91eb 45a105b de6cac5 45a105b adb91eb 9f48c3c adb91eb 004f460 adb91eb 004f460 45a105b 36db998 adb91eb 9f48c3c adb91eb 0f4d9ea adb91eb 45a105b de6cac5 004f460 de6cac5 45a105b de6cac5 45a105b adb91eb 0f4d9ea adb91eb de6cac5 004f460 70332a1 004f460 dc47d57 9f48c3c 004f460 45a105b adb91eb 0f4d9ea adb91eb 9f48c3c adb91eb de6cac5 45a105b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | 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]
}
// Primary customer destinations (desktop nav + mobile bottom bar).
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)
// D3 (simulated wallet) is dormant on deployments that don't set its env flags (e.g. the public
// Space). Hide the Wallet/ATM entry points there so they aren't dead ends; show them once the
// runtime reports enabled (a local `make start`, or an activated deployment).
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>
)
}
|