mirror of
https://github.com/hanzoai/billing.git
synced 2026-10-30 18:46:54 +00:00
`?plan=<slug>` is what every "Get started" on the marketing site links to, and the grid now continues that choice through the same door a click uses: the tab the plan lives on, its price from the catalog, and the hop to the Pay card form. It cannot charge on its own. A click may charge a card already on file in one step; a plan carried in on a link stops at the card form, because arriving somewhere is not consent to be charged. Sign-in keeps it too — startLogin remembers the query and hash to come back to, and the callback lands there instead of at the bare page. Only a suffix is ever stored, so there is no path or host in it to point somewhere else. The CTA on the commerce page names its own tier rather than dropping it, and `make test` runs the suite the repo actually has.
59 lines
1.8 KiB
TypeScript
59 lines
1.8 KiB
TypeScript
/**
|
|
* Sign-in resumes where it started, and nowhere else.
|
|
*
|
|
* The plan a buyer picked is in the URL when they sign in, so the round trip
|
|
* has to hand it back or they choose again — and the observed second choice is
|
|
* the cheapest tier. What is handed back is only ever a query and a hash: the
|
|
* app is one page, so a suffix is the whole destination, and anything naming a
|
|
* path or a host is refused rather than followed.
|
|
*/
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { resumed } from './iam-auth'
|
|
|
|
/** A localStorage the test controls. */
|
|
function stored(at: string | null) {
|
|
const store = new Map(at === null ? [] : [['hanzo_billing_resume', at]])
|
|
vi.stubGlobal('localStorage', {
|
|
getItem: (k: string) => store.get(k) ?? null,
|
|
setItem: (k: string, v: string) => void store.set(k, v),
|
|
removeItem: (k: string) => void store.delete(k),
|
|
})
|
|
return store
|
|
}
|
|
|
|
afterEach(() => vi.unstubAllGlobals())
|
|
|
|
describe('resumed', () => {
|
|
it('hands back the plan the buyer signed in with', () => {
|
|
stored('?plan=pro#pricing')
|
|
expect(resumed()).toBe('?plan=pro#pricing')
|
|
})
|
|
|
|
it('is consumed by reading, so a later sign-in starts clean', () => {
|
|
const store = stored('?plan=pro#pricing')
|
|
expect(resumed()).toBe('?plan=pro#pricing')
|
|
expect(store.has('hanzo_billing_resume')).toBe(false)
|
|
expect(resumed()).toBe('')
|
|
})
|
|
|
|
it('has nothing to say when sign-in started from the plain page', () => {
|
|
stored(null)
|
|
expect(resumed()).toBe('')
|
|
})
|
|
|
|
// A suffix cannot name a host, so these are the shapes that would have to be
|
|
// refused if one ever reached the key.
|
|
it.each([
|
|
'//evil.example',
|
|
'/\\evil.example',
|
|
'https://evil.example',
|
|
'/overview',
|
|
'plan=pro',
|
|
])('refuses %s, which is not a query or a hash', (at) => {
|
|
stored(at)
|
|
expect(resumed()).toBe('')
|
|
})
|
|
})
|