SPB Git

Repository Public

Gate.tsx 2.8 KB 48 lines tsx

1import { startAuthentication, startRegistration, browserSupportsWebAuthn } from '@simplewebauthn/browser';2import { useState } from 'react';3import { post } from '../api';4import { errText, useT } from '../i18n';5import { Btn, Field, Icon, inputCls, Notice } from '../ui';67export function Gate({ setup, onDone }: { setup: boolean; onDone: () => void }) {8  const t = useT();9  const [busy, setBusy] = useState(false), [err, setErr] = useState(''), [token, setToken] = useState('');10  const supported = browserSupportsWebAuthn();11  const run = async (fn: () => Promise<void>) => {12    setBusy(true); setErr('');13    try { await fn(); onDone(); } catch (e) { setErr(errText(t, e)); } finally { setBusy(false); }14  };15  const login = () => run(async () => {16    const o = await post('/auth/login/options');17    const response = await startAuthentication({ optionsJSON: o.options });18    await post('/auth/login/verify', { challengeId: o.challengeId, response });19  });20  const doSetup = () => run(async () => {21    const o = await post('/auth/setup/options');22    const response = await startRegistration({ optionsJSON: o.options });23    await post('/auth/setup/verify', { challengeId: o.challengeId, response, setupToken: token.trim(), label: navigator.platform || 'Passkey' });24  });25  return (26    <main className="safe-t safe-b mx-auto flex min-h-full max-w-sm flex-col justify-center px-6">27      <div className="rise">28        <img src="/icon.svg" alt="" className="mb-7 h-[72px] w-[72px] rounded-[22px] shadow-[0_18px_36px_-16px_rgba(11,16,32,0.6)]" />29        <h1 className="text-[38px] font-semibold leading-none tracking-[-0.03em]">SP Wallet</h1>30        <p className="mt-2.5 text-[16px] text-mute">{t('tagline')}</p>31        <div className="mt-8 space-y-4">32          {!supported && <Notice tone="danger">{t('noWebauthn')}</Notice>}33          {setup ? (<>34            <Notice tone="info"><b>{t('setupTitle')}</b><br />{t('setupBody')}</Notice>35            <Field label={t('setupToken')}><input className={`${inputCls} font-mono`} value={token} onChange={(e) => setToken(e.target.value)} autoCapitalize="off" autoCorrect="off" spellCheck={false} autoComplete="off" /></Field>36            <Btn full busy={busy} disabled={!supported || token.trim().length < 8} onClick={doSetup}><Icon n="key" />{t('createPasskey')}</Btn>37          </>) : (<>38            <Btn full busy={busy} disabled={!supported} onClick={login}><Icon n="lock" />{t('unlock')}</Btn>39            <p className="text-center text-[13px] text-mute">{t('unlockHint')}</p>40          </>)}41          {err && <Notice tone="danger">{err}</Notice>}42        </div>43        <p className="mt-10 flex items-center justify-center gap-1.5 text-xs text-mute"><Icon n="shield" className="h-3.5 w-3.5" />{t('privateAccess')}</p>44      </div>45    </main>46  );47}48
Files main
Menu

Theme