Files
zeekayandGitHub 949608fd9d i18n: pages and components c–l draw their copy through @hanzo/i18n (#67)
Calculator, captable, careers, cloud, compare, contact and contact-sales,
customers, dashboards, dataroom, datastore, dev, download, enso,
enterprise, extension, family, integrations, investors, kai, learn,
legal and their components say their words through t() in client code,
<T> in server components and phrase() where copy is held as data; one
sentence is one key, figures and links its arguments.

Rebased on main: in every conflict main's file stands (its copy, layout
and styling) and only the wrapping is added back, including copy main
added since.

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
2026-10-30 01:15:31 -07:00

132 lines
4.6 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'
/**
* /invite — an affiliate's invitation: who invited the reader, the plan they were
* invited to, and Continue, which signs in or creates the account and then goes
* through /onboarding (lib/auth/destination `picker`, the terms a new account
* owes) to that plan's checkout on pay (lib/checkout).
*
* ?aff=<code> the affiliate's code; the app keeps it and names it to the
* reader's organization before checkout (lib/hanzo/affiliate)
* ?by=<name> the name the invitation is from, as the link states it
* ?plan=<slug> the plan offered; without one, Continue signs in, and a new
* account is asked how it will use Hanzo (components/onboarding/flow)
*/
import { Suspense } from 'react'
import { useSearchParams } from 'next/navigation'
import { XStack, YStack } from '@hanzo/ui'
import { Check } from 'lucide-react'
import { Waiting } from '@hanzo/app/waiting'
import { Line } from '@/components/type'
import { LOGIN } from '@hanzo/app/lib/destination'
import { money } from '@hanzo/app/lib/pay'
import { usePlans } from '@/lib/api/use'
import { Loading, Unreachable } from '@/components/api/state'
import { Action } from '@hanzo/ui/marketing'
import { checkout } from '@/lib/checkout'
import { useI18n } from '@hanzo/i18n/react'
/** Where a paid buyer comes back to, the same as a pricing card's checkout. */
const BACK = 'https://hanzo.ai/'
function Invitation() {
const { t } = useI18n()
const q = useSearchParams()
const by = (q.get('by') ?? '').trim().slice(0, 60)
const slug = (q.get('plan') ?? '').trim()
const reading = usePlans()
const plan = reading.state === 'ready' ? reading.data.find((p) => p.id === slug) : undefined
const go = () => {
const pay = plan ? checkout({ plan: plan.checkoutId || plan.id, returnUrl: BACK }) : ''
window.location.assign(pay ? `${LOGIN}?next=${encodeURIComponent(pay)}` : LOGIN)
}
return (
<YStack items="center" gap="$5" maxW={560} mx="auto" width="100%" px="$4" py="$8" $sm={{ gap: '$6', py: '$10' }}>
<YStack items="center" gap="$2">
<Line render="h1" size="x3" weight="600" align="center">
{t('You’re invited to Hanzo')}
</Line>
<Line size="base" tone="muted" align="center">
{by
? plan
? t('{by} invited you to Hanzo {plan}.', { by, plan: plan.name })
: t('{by} invited you to Hanzo.', { by })
: plan
? t('You’ve been invited to Hanzo {plan}.', { plan: plan.name })
: t('You’ve been invited to Hanzo.')}
</Line>
</YStack>
{plan ? (
<YStack
width="100%"
gap="$3"
p="$5"
$sm={{ p: '$6' }}
rounded="var(--radius-xl)"
borderWidth={1}
borderColor="$edge"
bg="$panel"
>
<Line size="x2" weight="600">
{plan.name}
</Line>
{plan.priceMonthly ? (
<Line size="x3" weight="600">
{money(plan.priceMonthly)}
<Line render="span" size="sm" tone="muted">
{' '}
{t('USD/month')}
</Line>
</Line>
) : null}
{plan.description ? (
<Line size="sm" tone="muted">
{plan.description}
</Line>
) : null}
<YStack gap="$2" pt="$2">
{plan.features.slice(0, 5).map((f) => (
<XStack key={f} gap="$2" items="flex-start">
<YStack mt={3} shrink={0}>
<Check size={16} aria-hidden />
</YStack>
<Line size="sm">{f}</Line>
</XStack>
))}
</YStack>
</YStack>
) : slug && reading.state === 'loading' ? (
<Loading what={t('the plan')} />
) : slug && reading.state === 'unreachable' ? (
<Unreachable what={t('the plan and its price')} />
) : null}
<YStack items="center" gap="$2" width="100%">
<YStack width="100%" maxW={360}>
<Action render="button" type="button" fill onClick={go}>
{t('Continue')}
</Action>
</YStack>
<Line render="p" size="xs" tone="muted" align="center">
{plan
? t('Sign in, or create your account, then pay for the plan on the next page.')
: t('Sign in, or create your account, then choose a plan.')}
</Line>
</YStack>
</YStack>
)
}
export default function InvitePage() {
const { t } = useI18n()
return (
<Suspense fallback={<Waiting title={t('One moment')} />}>
<Invitation />
</Suspense>
)
}