mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-28 07:32:52 +00:00
Every word the landing, the header menus, the phone sheet, the footer,
the help corner and the way in (sign-in, sign-up, the reset, the
invitation, the callback, onboarding, the 404) write goes through
@hanzo/i18n's t(): one sentence is one message, its figures, names and
links arguments; copy written as data is phrase('…') where written and
t(label) where drawn. Plans, the chart, the API states and the lines
lib/kai and lib/enso build for the landing do the same.
Right to left: <html lang dir> is set before the first paint (the
package's bootScript first in the head); a figure, clock, range or
price outside a sentence is isolated; sides are logical gui props, the
layout's [data-join] corners logical, a bar grows from the start;
arrows along the line mirror (Onward for "words ->" links). The repo
dates read in the reader's language.
Gates select by slot, never by an English label (rtl, help). New gate
translated.spec.ts: in Arabic the landing at 1440 and 390, every menu
open, the phone sheet, sign-in, sign-up and the app signed in hold no
authored English, nothing scrolls sideways, every range is isolated;
French the same for English's commonest words. What another package
draws with no words prop is listed by name (@hanzo/ui's Footer label,
@hanzo/build's composer row and suggestions).
@hanzo/i18n 0.5.3 (the catalogs: 1,443 keys, every offered locale
complete), @hanzo/app 0.2.1 (the app's words). The link gate imports
the terms versions from @hanzo/app, the one SDK main now resolves.
121 lines
4.6 KiB
TypeScript
121 lines
4.6 KiB
TypeScript
'use client'
|
|
// The sign-in link from the email, opened (lib/auth/link). It lands on the
|
|
// callback with its address and code in the fragment, and:
|
|
//
|
|
// a tab here waits on it the tab takes the code and finishes the sign-in it
|
|
// began; this one follows it in once it has
|
|
// this browser asked no tab waits, so the sign-in finishes here
|
|
// anywhere else the code, to type where the sign-in began, and a
|
|
// press to sign in here instead
|
|
//
|
|
// Nothing is spent on arrival anywhere but the browser that asked, so a mail
|
|
// scanner that opens the link signs nobody in.
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { SignupRequired } from '@hanzo/iam'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
import { Text } from '@hanzo/ui'
|
|
import { Action } from '@hanzo/ui/marketing'
|
|
import { Focus, LARGE, Wait } from '@/components/shell/focus'
|
|
import { markMethod } from '@hanzo/app/lib/intent'
|
|
import { authorize } from '@hanzo/app/lib/tags'
|
|
import { useClient } from '@hanzo/app/lib/signout'
|
|
import { Sheet, said, Wrong } from '@/components/auth/steps'
|
|
import { CHANNEL, type Handed, marked, unmark, WAIT } from '@/lib/auth/link'
|
|
import { AUP_VERSION, TERMS_VERSION } from '@hanzo/app/lib/terms'
|
|
|
|
const POLICY = { terms: TERMS_VERSION, aup: AUP_VERSION }
|
|
|
|
/** Where the access token is kept: written by whichever tab signs in. */
|
|
const TOKEN = 'hanzo_iam_access_token'
|
|
|
|
type State = 'asking' | 'taken' | 'shown'
|
|
|
|
export function Opened({ handed }: { handed: Handed }) {
|
|
const { t } = useI18n()
|
|
const iam = useClient()
|
|
const [state, setState] = useState<State>('asking')
|
|
const [busy, setBusy] = useState(false)
|
|
const [wrong, setWrong] = useState<string | null>(null)
|
|
const asked = useRef(false)
|
|
|
|
/** The sign-in, finished in this tab. */
|
|
async function here() {
|
|
setBusy(true)
|
|
setWrong(null)
|
|
try {
|
|
const to = await iam.loginWithCode(handed.email, handed.code)
|
|
unmark()
|
|
markMethod('code')
|
|
window.location.assign(authorize(to))
|
|
} catch (err) {
|
|
if (err instanceof SignupRequired) {
|
|
try {
|
|
const to = await iam.createAccountWithCode(handed.email, handed.code, POLICY)
|
|
unmark()
|
|
markMethod('code')
|
|
window.location.assign(authorize(to))
|
|
return
|
|
} catch (createErr) {
|
|
setWrong(said(createErr, t))
|
|
}
|
|
} else {
|
|
setWrong(said(err, t))
|
|
}
|
|
setBusy(false)
|
|
setState('shown')
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (asked.current) return
|
|
asked.current = true
|
|
// The code leaves the address bar and the history.
|
|
window.history.replaceState(null, '', window.location.pathname + window.location.search)
|
|
const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(CHANNEL)
|
|
const timer = setTimeout(() => {
|
|
channel?.close()
|
|
if (marked(handed.email)) void here()
|
|
else setState('shown')
|
|
}, WAIT)
|
|
if (channel) {
|
|
channel.onmessage = (e: MessageEvent) => {
|
|
if ((e.data as { taken?: string } | null)?.taken !== handed.code) return
|
|
clearTimeout(timer)
|
|
channel.close()
|
|
setState('taken')
|
|
}
|
|
channel.postMessage(handed)
|
|
}
|
|
// Once, on arrival.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
// The tab that took the code signs in; this one follows it into the app.
|
|
useEffect(() => {
|
|
if (state !== 'taken') return
|
|
const on = (e: StorageEvent) => {
|
|
if (e.key === TOKEN && e.newValue) window.location.replace('/')
|
|
}
|
|
window.addEventListener('storage', on)
|
|
return () => window.removeEventListener('storage', on)
|
|
}, [state])
|
|
|
|
// The focused page (components/shell/focus), as every step of the way in.
|
|
if (state !== 'shown') return <Wait title={t('Signing you in')} line={state === 'taken' ? t('On the tab where you started.') : undefined} />
|
|
// The code reads left to right in any language (`data-i18n-ltr`).
|
|
return (
|
|
<Focus id="code-title" title={t('Your sign-in code')} line={t('Enter this code in the tab where you started signing in.')}>
|
|
<Sheet label={t('Your code')}>
|
|
<Text render="p" m={0} fontFamily="$mono" fontSize="$9" fontWeight="600" letterSpacing={6} color="$ink" text="center" aria-label={t('Verification code')} data-i18n-ltr>
|
|
{handed.code}
|
|
</Text>
|
|
<Wrong text={wrong} />
|
|
<Action render="button" type="button" fill disabled={busy} onClick={() => void here()} {...LARGE}>
|
|
{t(busy ? 'One moment…' : 'Sign in on this device instead')}
|
|
</Action>
|
|
</Sheet>
|
|
</Focus>
|
|
)
|
|
}
|