Files
hanzo.ai/app/(marketing)/forget/page.tsx
zeekay 0f5536b707 hanzo.ai reads in the reader's language, and Arabic right to left
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.
2026-10-22 19:54:31 -07:00

173 lines
6.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* /forget — a new password for an account that cannot sign in.
*
* The address first (`?login_hint=` fills it in), and IAM sends a code to it
* (@hanzo/iam `sendLoginCode`, POST /v1/iam/verification-codes on this origin).
* Then the code and the new password together (@hanzo/iam `resetPassword`, PUT
* /v1/iam/password): IAM spends the code against the account that address
* signs in as, checks the password against its organization's rules, and writes
* it. It signs nobody in and says nothing about which addresses have accounts;
* the person signs in afterwards like anybody else, second factor included.
* Signing in goes on to `?next=` (a client's sign-in this page was opened from)
* or to /login.
*/
import { useId, useState, type FormEvent } from 'react'
import { useI18n } from '@hanzo/i18n/react'
import { Input, Text, YStack } from '@hanzo/ui'
import { Action, TextLink } from '@hanzo/ui/marketing'
import Hero from '@/components/home/Hero'
import { Field } from '@/components/account/form'
import { Code, Held, Note, Other, said, Say, Sheet, Wrong } from '@/components/auth/steps'
import { onward, LOGIN } from '@hanzo/app/lib/destination'
import { useClient } from '@hanzo/app/lib/signout'
import { useQuery } from '@/lib/auth/query'
/** IAM's floor for a password (`passwordPolicyError`), said before anything is sent. */
const FLOOR = 8
type Step = 'email' | 'code' | 'done'
export default function Page() {
const { t } = useI18n()
const iam = useClient()
const q = useQuery()
const [step, setStep] = useState<Step>('email')
// The address as typed, or the hint the page was opened with until something is.
const [typed, setEmail] = useState<string | null>(null)
const email = typed ?? q.get('login_hint') ?? ''
const [code, setCode] = useState('')
const [password, setPassword] = useState('')
const [busy, setBusy] = useState(false)
const [wrong, setWrong] = useState<string | null>(null)
const [note, setNote] = useState<string | null>(null)
const ids = { email: useId(), code: useId(), password: useId() }
// Where signing in goes once the password is set.
const next = onward(`?${q}`) ?? LOGIN
/** Run one call to IAM, saying its refusal, and go on to `then` (saying `told`) when it answers. */
function run(call: () => Promise<void>, then: Step, told?: string) {
setBusy(true)
setWrong(null)
setNote(null)
call()
.then(() => {
setStep(then)
if (told) setNote(told)
})
.catch((err: unknown) => setWrong(said(err, t)))
.finally(() => setBusy(false))
}
function send(e: FormEvent) {
e.preventDefault()
if (!email.trim().includes('@')) {
setWrong(t('Enter your email address.'))
return
}
run(() => iam.sendLoginCode(email.trim()), 'code')
}
function reset(e: FormEvent) {
e.preventDefault()
if (!code.trim()) {
setWrong(t('Enter the code from the email.'))
return
}
if (password.length < FLOOR) {
setWrong(t('Use at least {n} characters.', { n: FLOOR }))
return
}
run(() => iam.resetPassword(email.trim(), code.trim(), password), 'done')
}
const back = () => {
setStep('email')
setCode('')
setPassword('')
setWrong(null)
setNote(null)
}
return (
<Hero
title={t('Reset your password')}
line={t('We’ll send a code to your email to set a new password.')}
switcher={
<Text render="p" m={0} fontSize="$2" color="var(--muted-foreground)" text="center">
{t('Remembered it? {signin}', { signin: <TextLink href={next}>{t('Sign in')}</TextLink> })}
</Text>
}
terms={null}
signin={
<Sheet label={t('Reset your password')}>
{step === 'email' ? (
<form onSubmit={send} noValidate>
<YStack gap="$4">
<Field id={ids.email} label={t('Email')} center>
<Input
id={ids.email}
size="lg"
fill
type="email"
name="email"
autoComplete="username"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
required
autoFocus
/>
</Field>
<Wrong text={wrong} />
<Action render="button" type="submit" fill disabled={busy}>
{busy ? t('One moment…') : t('Send code')}
</Action>
</YStack>
</form>
) : step === 'code' ? (
<form onSubmit={reset} noValidate>
<YStack gap="$4">
<Held address={email.trim()} title={t('Choose a new password')} onBack={back} />
<Say>{t('If an account uses this address, we sent it a code. Enter it with your new password.')}</Say>
<Code id={ids.code} value={code} onChange={setCode} />
<Field id={ids.password} label={t('New password')} center>
<Input
id={ids.password}
size="lg"
fill
type="password"
name="password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t('At least {n} characters', { n: FLOOR })}
required
/>
</Field>
<Wrong text={wrong} />
<Note text={note} />
<Action render="button" type="submit" fill disabled={busy}>
{busy ? t('One moment…') : t('Set password')}
</Action>
<Other onClick={() => run(() => iam.sendLoginCode(email.trim()), 'code', t('A new code is on its way.'))} disabled={busy}>
{t('Send a new code')}
</Other>
</YStack>
</form>
) : (
<YStack gap="$4">
<Say>{t('Your password is set. Sign in with it to continue.')}</Say>
<Action href={next} fill>
{t('Sign in')}
</Action>
</YStack>
)}
</Sheet>
}
/>
)
}