mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
From the live funnel review: - A sign-up address is checked whole before IAM is asked: "ada@" says "Enter a full email address, like [email protected]." under the field. Every sign-in error is in the error ink, and the address field names its error (aria-invalid, aria-describedby). - /login with a checkout's offer stays the sign-in, with the offer strip, and "Create an account" is one tap in place. The early script no longer moves it to /signup. - One sign-in design on /, /login, /signup and /join: pay's 56px pills and fields at 16px, a 32/26px title, and the terms line in both modes. Fields read @hanzo/ui 8.27.90's tokens: the value at full ink, the sample at the placeholder ink (app/gui.css regenerated). - /pricing reads ?coupon= (kept for the tab), states each covered plan's first month with the list price struck, labels it "Claim 50% off", and every way to buy carries the code and the visit's campaign to pay. - The tabs say Personal and Business, as pay's do. The plan cards and the funnel's other primary buttons are pay's 56px pills. A card states its price once. - The silent sign-in ask is made only by a browser that has held a session here, so a stranger's /, /login and /signup log no 400. - A new account with an offer link is asked its question under the offer, with Continue with Free; a returning one opens the plans. - Pricing copy: the price on your bill, not "the number the ledger books"; renders "run only while the cap has room"; Jev is named as TypeSafe's decision model. Co-authored-by: Hanzo Dev <[email protected]>
55 lines
2.5 KiB
TypeScript
55 lines
2.5 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 { 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'
|
|
|
|
/**
|
|
* 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)
|
|
if (url.origin !== HERE || !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'
|
|
|
|
/**
|
|
* 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. The page clears the 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(_){}`
|
|
|
|
/**
|
|
* The hero is held off the screen while `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[${MARK}] [data-slot="hero"]{visibility:hidden;animation:sale-hold 0s 4s forwards}@keyframes sale-hold{to{visibility:visible}}`
|