mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
112 lines
5.1 KiB
TypeScript
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} />
|
|
</>
|
|
)
|
|
}
|