Files
zeekay ac2da21e5f sign-in: wallet and phone, and a person claims a wallet's account
/login and /signup draw "Continue with wallet" and "Continue with phone"
after Google and GitHub, each only where IAM's methods say it is on.

Wallet: where IAM signs a wallet in (@hanzo/iam 0.21.37 `wallet`) and the
browser injects an EIP-1193 wallet; the press is loginWithWallet (IAM's
challenge, personal_sign of its EIP-4361 text, IAM's verify) and ends as a
password's sign-in ends. A wallet IAM has not seen is made an account in an
org of its own, with no email.

Phone: where IAM says it sends codes (`code`) and has answered. The number
is read in its country with the SMS consent ticked, then IAM's code flow.
IAM's address lookup answers an email address only, so a number is texted as
any is and the account that holds it is signed in by the code.

Settings -> General -> Sign in another way adds an email or a phone to the
signed-in account through IAM's own proof (addAddress, PUT /v1/iam/address),
drawn only where IAM's methods say `address`. Same account, org, keys and
balance; nothing is created, so nothing earns twice.

Gates: wallet.spec (a throwaway secp256k1 key behind an injected provider),
phone.spec, address.spec. login.spec expects the phone after the providers.
2026-10-26 16:47:32 -07:00

69 lines
2.7 KiB
TypeScript

'use client'
/**
* The terms, agreed once, by an account that has accepted none on record: the
* one a provider made a moment ago (/onboarding), or an address a code proved
* that holds no account yet (the sign-in panel's last step). IAM records the
* acceptance; this page asks for it and says nothing else. It is the focused
* page (components/shell/focus) like every page of the way in.
*/
import { useId, useState } from 'react'
import { useI18n } from '@hanzo/i18n/react'
import { Checkbox, Text, XStack, YStack } from '@hanzo/ui'
import { TextLink } from '@hanzo/ui/marketing'
import { Wrong } from '@/components/auth/steps'
import { Focus } from '@/components/shell/focus'
import { Pill, TILE } from './look'
export function Terms({
email,
busy,
wrong,
onCreate,
onOther,
title,
say,
}: {
/** The verified address the account is for; empty for an account a wallet made. */
email: string
busy: boolean
wrong: string | null
onCreate: () => void
/** Another address: the page this one was reached from decides what that means. */
onOther: () => void
/** The page's heading, in the reader's language: /login names a new address's lack of an account here. */
title?: string
/** A line under it, in place of the address alone. */
say?: string
}) {
const { t } = useI18n()
const [agree, setAgree] = useState(false)
const id = useId()
return (
<Focus label={t('Terms')} title={title ?? t('Create your account')} line={say ?? (email || undefined)}>
<YStack gap="$4">
<XStack gap="$3" items="flex-start" p="$5" {...TILE} rounded={12}>
<Checkbox id={id} checked={agree} onCheckedChange={(v) => setAgree(v === true)} mt="$1" />
<Text render="label" htmlFor={id} flex={1} fontSize="$4" lineHeight="1.375rem" color="var(--foreground)">
{t("I agree to Hanzo's {terms} and {use} and confirm that I am at least 18 years of age.", {
terms: <TextLink href="/terms">{t('Terms of Service')}</TextLink>,
use: <TextLink href="/aup">{t('Acceptable Use Policy')}</TextLink>,
})}
</Text>
</XStack>
<Wrong text={wrong} />
<Pill fill disabled={!agree || busy} onPress={onCreate}>
{busy ? t('One moment…') : t('Create account')}
</Pill>
<XStack justify="center">
{/* An account with no address (one a wallet made) is left for another account, not another address. */}
<TextLink onPress={onOther} disabled={busy} display="inline-flex" items="center" minH={44}>
{email ? t('Use a different email') : t('Use a different account')}
</TextLink>
</XStack>
</YStack>
</Focus>
)
}