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>
  )
}