mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Try Hanzo opens /signup directly instead of /chat bouncing a stranger there, which cost a whole page load on a phone (13 s throttled). /login no longer sends a new address to /signup: both pages send the link at once and make the account once the code proves the address, so the person stays on the page they chose. /login, /signup and /auth/callback preconnect to api.hanzo.ai from the head, so the methods read, the token exchange and the discovery a provider press needs start on a warm connection. A member at /login is marked before first paint (the app's BOOT under data-member), the form stays off screen while the page moves them on, and the panel asks IAM for no sign-in methods it would never draw. The callback starts the bind and the standing read together instead of one after the other.
70 lines
3.3 KiB
TypeScript
70 lines
3.3 KiB
TypeScript
/**
|
|
* The checkout a sign-in interrupts.
|
|
*
|
|
* pay (hanzo.ai/pay) sends a buyer with no session to /login or /signup with
|
|
* the checkout they were on as `?next=` (hanzo-inc/pay lib/auth `login`), and a
|
|
* plan bought from /pricing does the same. Such a sign-in page is the checkout's
|
|
* first step: it is drawn at the checkout's measure, with no demo beside it, and
|
|
* a coupon the checkout carries is said at the top (components/auth/page.tsx).
|
|
* Once signed in, the buyer goes back to that checkout exactly as it was asked
|
|
* for (components/onboarding/flow.tsx).
|
|
*/
|
|
|
|
import { APP, BOOT } from '@hanzo/app/lib/session'
|
|
import { clean } from '@/lib/coupon'
|
|
|
|
/** What the checkout names: its plan's catalog slug and the coupon it carries, each when it carries one. */
|
|
export interface Sale {
|
|
plan: string | null
|
|
coupon: string | null
|
|
}
|
|
|
|
/** pay's mount: `/pay` and every path under it, as the browser resolves the address. */
|
|
const PAY = /^\/pay(?:\/|$)/
|
|
|
|
/** An origin no address can name, to resolve a path against. */
|
|
const HERE = 'https://hanzo.invalid'
|
|
|
|
const HANZO_HOST = /(?:^|\.)(?:hanzo\.(?:ai|app|computer)|localhost)$/i
|
|
|
|
/**
|
|
* The checkout `next` names, or null when it names none: an address on this
|
|
* site whose path, as the browser resolves it, is under /pay. `next` is read
|
|
* only after lib/auth/destination `onward` has held it to this site.
|
|
*/
|
|
export function sale(next: string | undefined | null): Sale | null {
|
|
if (!next) return null
|
|
const url = new URL(next, HERE)
|
|
const allowed = url.origin === HERE || HANZO_HOST.test(url.hostname)
|
|
if (!allowed || !PAY.test(url.pathname)) return null
|
|
return { plan: url.searchParams.get('plan') || null, coupon: clean(url.searchParams.get('coupon')) }
|
|
}
|
|
|
|
/** The mark on <html> while a checkout's sign-in page waits for its script: the hero is held off the screen. */
|
|
export const MARK = 'data-sale'
|
|
|
|
/**
|
|
* The mark on <html> while a sign-in page with a live session here decides where
|
|
* the reader goes: on to the page they were headed for, or a spinner for the site
|
|
* that asked. The form is held off the screen meanwhile, so a signed-in reader
|
|
* never sees it flash on the way through (components/auth/panel `Gate` clears the
|
|
* mark once it draws anything).
|
|
*/
|
|
export const MEMBER = 'data-member'
|
|
|
|
/**
|
|
* Run as the sign-in page is parsed, before anything under it paints: a `next`
|
|
* under /pay marks <html> (`MARK`), so the plain column never flashes before
|
|
* the checkout's page, with its offer, replaces it; and a session live in this
|
|
* browser marks it `MEMBER`, by @hanzo/app's own test for one (`BOOT`, the same
|
|
* keys and expiry, marking this page rather than the app). The page clears each
|
|
* mark once it is drawn.
|
|
*/
|
|
export const EARLY = `try{var q=new URLSearchParams(location.search),n=q.get('next')||q.get('redirect');if(n&&/^\\/(?![/\\\\])/.test(n)){var u=new URL(n,location.origin);if(u.origin===location.origin&&${PAY}.test(u.pathname)){document.documentElement.setAttribute('${MARK}','')}}}catch(_){}${BOOT.replace(`'${APP}'`, `'${MEMBER}'`)}`
|
|
|
|
/**
|
|
* The hero is held off the screen while either mark is set, and shows by itself
|
|
* after four seconds, so a page whose script never runs is not left blank.
|
|
*/
|
|
export const HOLD = `html:is([${MARK}],[${MEMBER}]) [data-slot="hero"]{visibility:hidden;animation:sale-hold 0s 4s forwards}@keyframes sale-hold{to{visibility:visible}}`
|