amanpay / web /src /pages /D2SignInPage.tsx
MHamdan's picture
CI deploy 7dac85a (part 2)
134ecb7 verified
Raw
History Blame Contribute Delete
4.75 kB
import { useEffect, useState } from 'react'
import { useI18n } from '../i18n'
import { localizeError } from '../utils/errors'
import { Button, Field, Callout, Spinner } from '../components/ui'
import { PasskeyEnvWarnings, OpenInNewTabBanner } from '../components/d2'
import { PasskeyCancelled, PasskeyUnsupported } from '../auth/passkey'
import { signIn, demoQuickSession, getIdentityStatus } from '../api/identity'
type Busy = 'passkey' | 'handle' | 'demo' | null
type Msg = { tone: 'ok' | 'bad' | 'info'; text: string } | null
export function D2SignInPage({ onNav }: { onNav: (r: string) => void }) {
const { t } = useI18n()
const [busy, setBusy] = useState<Busy>(null)
const [msg, setMsg] = useState<Msg>(null)
const [showHandle, setShowHandle] = useState(false)
const [handle, setHandle] = useState('')
const [demoOpen, setDemoOpen] = useState(false)
useEffect(() => {
let alive = true
// Tolerate a mocked/absent status call (undefined return) as well as a 404 (D2/demo off).
Promise.resolve(getIdentityStatus?.())
.then((s) => { if (alive && s) setDemoOpen(!!s.demo_open_enroll) })
.catch(() => {})
return () => { alive = false }
}, [])
async function runDemo(role: 'customer' | 'operator' = 'customer') {
setBusy('demo'); setMsg(null)
try {
// No-passkey demo session: works in the HF iframe and on any device (no WebAuthn).
const r = await demoQuickSession(role)
setMsg({ tone: 'ok', text: t('d2.signin.success', { name: r.account.display_alias }) })
onNav(role === 'operator' ? 'id/operator' : 'finance')
} catch (e) {
setMsg({ tone: 'bad', text: localizeError(e, t, 'd2.signin.error') })
} finally {
setBusy(null)
}
}
async function run(which: 'passkey' | 'handle') {
if (which === 'handle' && !handle.trim()) {
setMsg({ tone: 'info', text: t('d2.signin.needHandle') }); return
}
setBusy(which); setMsg(null)
try {
const r = await signIn(which === 'handle' ? handle.trim() : undefined)
setMsg({ tone: 'ok', text: t('d2.signin.success', { name: r.account.display_alias }) })
onNav('id/account')
} catch (e) {
if (e instanceof PasskeyUnsupported) setMsg({ tone: 'info', text: t('d2.unsupported') })
else if (e instanceof PasskeyCancelled) setMsg({ tone: 'info', text: t('d2.cancelled') })
else setMsg({ tone: 'bad', text: localizeError(e, t, 'd2.signin.error') })
} finally {
setBusy(null)
}
}
return (
<section className="card" aria-labelledby="d2signin-h">
<h2 id="d2signin-h">{t('d2.signin.title')}</h2>
<p className="hint">{t('d2.signin.intro')}</p>
<OpenInNewTabBanner />
<PasskeyEnvWarnings />
{demoOpen && (
<fieldset className="action-group">
<legend>{t('d2.signin.demoTitle')}</legend>
<Callout tone="info">{t('d2.signin.demoHint')}</Callout>
<Button onClick={() => runDemo('customer')} disabled={busy !== null}>
{busy === 'demo' ? <Spinner label={t('d2.working')} /> : t('d2.signin.demo')}
</Button>
<Button onClick={() => runDemo('operator')} disabled={busy !== null}>
{t('d2.signin.demoOperator')}
</Button>
</fieldset>
)}
<fieldset className="action-group">
<legend>{t('d2.signin.withPasskey')}</legend>
<Button onClick={() => run('passkey')} disabled={busy !== null}>
{busy === 'passkey' ? <Spinner label={t('d2.working')} /> : t('d2.signin.withPasskey')}
</Button>
<p className="hint">
<button type="button" className="link" onClick={() => setShowHandle((v) => !v)} aria-expanded={showHandle}>
{t('d2.signin.handleToggle')}
</button>
</p>
{showHandle && (
<>
<Field id="d2_signin_handle" label={t('d2.signin.handleLabel')} value={handle}
onChange={(e) => setHandle(e.target.value)} autoComplete="username" />
<p className="hint">{t('d2.signin.handleHint')}</p>
<Button onClick={() => run('handle')} disabled={busy !== null}>
{busy === 'handle' ? <Spinner label={t('d2.working')} /> : t('d2.signin.signInHandle')}
</Button>
</>
)}
</fieldset>
<div className="row">
<button type="button" className="link" onClick={() => onNav('id/enroll')}>{t('d2.signin.noAccount')}</button>
<button type="button" className="link" onClick={() => onNav('id/recovery')}>{t('d2.signin.recover')}</button>
</div>
<p aria-live="polite" className="sr-live">{busy ? t('d2.working') : ''}</p>
{msg && <Callout tone={msg.tone}>{msg.text}</Callout>}
</section>
)
}