amanpay / web /src /App.tsx
MHamdan's picture
CI deploy 02b619b (part 2)
9f48c3c verified
Raw
History Blame Contribute Delete
9.05 kB
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>
)
}