Files
hanzo.ai/lib/auth/sale.ts
zooqueenandHanzo Dev 255054c324 Sign-in is one design that checks an address before IAM, /pricing prices a coupon and carries it, and a stranger's page logs no 400
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]>
2026-10-16 07:18:26 -07:00

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}}`