mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Every import, the catalog pin and the docs name @hanzo/app and github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and components/rooms.tsx is components/app.tsx.
122 lines
4.2 KiB
TypeScript
122 lines
4.2 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'
|
||
|
||
/** Where a paid buyer comes back to, the same as a pricing card's checkout. */
|
||
const BACK = 'https://hanzo.ai/'
|
||
|
||
function Invitation() {
|
||
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">
|
||
You’re invited to Hanzo
|
||
</Line>
|
||
<Line size="base" tone="muted" align="center">
|
||
{by ? `${by} invited you to Hanzo` : 'You’ve been invited to Hanzo'}
|
||
{plan ? ` ${plan.name}.` : '.'}
|
||
</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">
|
||
{' '}
|
||
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="the plan" />
|
||
) : slug && reading.state === 'unreachable' ? (
|
||
<Unreachable what="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}>
|
||
Continue
|
||
</Action>
|
||
</YStack>
|
||
<Line render="p" size="xs" tone="muted" align="center">
|
||
Sign in, or create your account, then {plan ? 'pay for the plan on the next page.' : 'choose a plan.'}
|
||
</Line>
|
||
</YStack>
|
||
</YStack>
|
||
)
|
||
}
|
||
|
||
export default function InvitePage() {
|
||
return (
|
||
<Suspense fallback={<Waiting title="One moment" />}>
|
||
<Invitation />
|
||
</Suspense>
|
||
)
|
||
}
|