Files

112 lines
5.1 KiB
TypeScript

'use client'
/**
* /login and /signup, one page in two modes, drawn as pay's checkout is drawn
* (hanzo-inc/pay): the focused page (components/home/Hero over
* components/shell/focus) with its 32px title (26px on a phone), the sign-in
* card's 56px pills and fields (components/auth/panel, steps `Sheet`) and the
* terms line under it, and no eyebrow, no line of marketing and no demo. IAM
* checks every credential and creates every account; nothing of either is kept
* here.
*
* WHERE IT GOES ON TO is `?next=` (or `?redirect=`) on this site, held there by
* lib/auth/destination `onward`; on /signup a paid `?plan=` goes on to /pay. The
* switch to the other page carries the whole query, so `next` (and the plan,
* coupon and campaign inside it) survives a change of mind.
*
* A CHECKOUT'S SIGN-IN. A `next` under /pay is the checkout this sign-in
* interrupts (lib/auth/sale): a coupon the checkout carries is said across the
* top in pay's own words (components/auth/offer). The route's mode stands —
* /login signs in, so a returning customer is one step from the checkout — and
* one tap switches the mode in place, the offer and the query untouched. Signed
* in, the buyer is back on that checkout as it was asked for
* (components/onboarding/flow.tsx).
*
* The query is read after hydration (lib/auth/query), so the export's markup is
* the plain column; `MARK`, set by the early script, holds it off the screen
* until this page has drawn the checkout's.
*/
import { useLayoutEffect, useState } from 'react'
import { useAnalytics } from '@hanzo/event/react'
import { phrase } from '@hanzo/i18n'
import { useI18n } from '@hanzo/i18n/react'
import { EVENTS } from '@hanzo/event'
import { onward } from '@hanzo/app/lib/destination'
import { Text } from '@hanzo/ui'
import { TextLink } from '@hanzo/ui/marketing'
import Hero from '@/components/home/Hero'
import { Gate, type Mode } from '@/components/auth/panel'
import { Offer } from '@/components/auth/offer'
import { useQuery } from '@/lib/auth/query'
import { MARK, sale } from '@/lib/auth/sale'
import { foreign } from '@/lib/auth/signin'
/** Each mode's words: its title, said in the reader's language where it is drawn. */
const WORDS: Record<Mode, { title: string }> = {
login: { title: phrase('Sign in to Hanzo AI') },
signup: { title: phrase('Sign in to Hanzo AI') },
}
const OTHER: Record<Mode, Mode> = { login: 'signup', signup: 'login' }
const ROUTE: Record<Mode, string> = { login: '/login', signup: '/signup' }
/** Where the sign-in goes on to, read from a query: `?next=` or `?redirect=` on this site, else on /signup a paid plan's /pay. */
function destination(mode: Mode, search: string): string | undefined {
const p = new URLSearchParams(search)
const target = p.get('next') || p.get('redirect')
if (target) return onward(`?${new URLSearchParams({ next: target })}`)
const isDesktop = typeof window !== 'undefined' && Boolean((window as unknown as { __TAURI__?: unknown; __TAURI_INTERNALS__?: unknown }).__TAURI__ || (window as unknown as { __TAURI__?: unknown; __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__)
if (isDesktop) return '/chat'
const plan = p.get('plan')
return mode === 'signup' && plan && plan !== 'free' ? `/pay?${new URLSearchParams({ plan })}` : undefined
}
export function Page({ mode: route }: { mode: Mode }) {
const { t } = useI18n()
const analytics = useAnalytics()
// The Gate acts on its destination in effects alone, the first of which runs on
// the hydration render, so it reads the window; what is drawn reads the query.
const to = typeof window === 'undefined' ? undefined : destination(route, window.location.search)
const query = useQuery()
const search = query.toString()
const sold = sale(destination(route, search))
// The route's own mode: /login is the sign-in, /signup the new account. A
// checkout's page switches in place, so the query and the offer stay put.
const [chosen, setChosen] = useState<Mode | null>(null)
const mode = chosen ?? route
const app = typeof window === 'undefined' ? null : foreign()
const isForge = app?.clientId === 'hanzo-git' || app?.redirectUri?.includes('git.hanzo.ai') || app?.redirectUri?.includes('forge.hanzo.ai')
const words = isForge ? { title: phrase('Sign in to Hanzo Forge') } : WORDS[mode]
// Drawn as the query asks: ensure hero is always revealed on mount
useLayoutEffect(() => {
document.documentElement.removeAttribute(MARK)
}, [search])
const gate = (
<Gate
mode={mode}
to={to}
onShown={
route === 'signup'
? () => {
const refCode = new URLSearchParams(window.location.search).get('ref')
if (refCode) analytics.capture(EVENTS.REFERRAL_USED, { refCode })
}
: undefined
}
/>
)
// One tree for both modes: the panel keeps its place whichever mode this
// settles into after hydration, so it is never mounted twice. Both modes carry
// the terms line (Hero's default) under the switch.
return (
<>
{sold ? <Offer coupon={sold.coupon} plan={sold.plan} /> : null}
<Hero title={t(words.title)} signin={gate} />
</>
)
}