mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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
132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
'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>
|
||
)
|
||
}
|