Files
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

48 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test'
import { EARLY, MARK, sale } from '../../lib/auth/sale'
/**
* WHICH SIGN-IN IS A CHECKOUT'S, DECIDED TWICE THE SAME WAY.
*
* `sale` reads the checkout a validated `next` names (lib/auth/sale), and
* `EARLY` decides the same thing before the page paints, from the raw query: a
* `next` under /pay marks <html>, and the page stays where it is — /login signs
* in, coupon or not. Anything else — another page, another origin, a path that
* only begins with the letters — is left alone.
*/
test('a checkout is a path under /pay, with its plan and a coupon of a code’s shape', () => {
expect(sale('/pay/confirm/card?plan=dev&quantity=1&coupon=50OFFFOUNDER&utm_source=hanzo')).toEqual({ plan: 'dev', coupon: '50OFFFOUNDER' })
expect(sale('/pay?plan=pro')).toEqual({ plan: 'pro', coupon: null })
expect(sale('/pay/cart?coupon=50OFFfounder')).toEqual({ plan: null, coupon: '50OFFfounder' })
expect(sale('/pay')).toEqual({ plan: null, coupon: null })
expect(sale('/pay/confirm/card?plan=dev&coupon=not%20a%20code')).toEqual({ plan: 'dev', coupon: null })
expect(sale('/pay/?plan=dev')).toEqual({ plan: 'dev', coupon: null })
// Decided on the path the browser resolves, not on the letters.
for (const other of [undefined, null, '', '/chat', '/payroll', '/pay/../chat?coupon=X', '/onboarding?next=%2Fpay', 'https://evil.example/pay', '//evil.example/pay'])
expect(sale(other), String(other)).toBeNull()
})
/** EARLY run against an address: whether it moved the page, and whether it marked <html> as held. */
function early(href: string): { moved: string | null; marked: boolean } {
const url = new URL(href)
let moved: string | null = null
const marks = new Set<string>()
const location = { search: url.search, hash: url.hash, pathname: url.pathname, origin: url.origin, replace: (to: string) => (moved = to) }
const document = { documentElement: { setAttribute: (name: string) => marks.add(name) } }
new Function('location', 'document', EARLY)(location, document)
return { moved, marked: marks.has(MARK) }
}
test('before paint: every checkout marks, nothing moves — /login stays the sign-in, coupon or not — and nothing else is touched', () => {
const coupon = encodeURIComponent('/pay/confirm/card?plan=dev&quantity=1&coupon=50OFFFOUNDER&utm_campaign=again-welcome')
expect(early(`https://hanzo.ai/login?next=${coupon}&ref=x#top`)).toEqual({ moved: null, marked: true })
expect(early(`https://hanzo.ai/login/?next=${coupon}`)).toEqual({ moved: null, marked: true })
expect(early(`https://hanzo.ai/signup?next=${coupon}`)).toEqual({ moved: null, marked: true })
expect(early(`https://hanzo.ai/login?redirect=${coupon}`), 'redirect is read as next is').toEqual({ moved: null, marked: true })
expect(early(`https://hanzo.ai/login?next=${encodeURIComponent('/pay/confirm/card?plan=dev')}`)).toEqual({ moved: null, marked: true })
for (const next of ['/chat', '//evil.example/pay', '/\\evil.example/pay', '/\t/evil.example/pay?coupon=X', '/payroll?coupon=X', '/pay/../login?coupon=X', 'https://evil.example/pay?coupon=X'])
expect(early(`https://hanzo.ai/login?next=${encodeURIComponent(next)}`), next).toEqual({ moved: null, marked: false })
expect(early('https://hanzo.ai/login')).toEqual({ moved: null, marked: false })
})