mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +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]>
48 lines
3.3 KiB
TypeScript
48 lines
3.3 KiB
TypeScript
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 })
|
||
})
|