Files
Darkhorse7starsandGitHub ff026c9ad6 sign-in: straight on to where it was headed; /onboarding is not shown (#102)
After signup the reader was sent through /onboarding, which could forward on
to pay's plans. The owner's call (2026-10-29): don't show it, keep its files.

- lib/auth/arrive: a finished sign-in goes to where it was headed. A checkout
  asked for (`?next=/pay…`) opens as asked, billed to the organization worked
  in, with the campaign on pay's plans and card step.
- The callback and a signed-in /login or /signup use it, in place of
  @hanzo/app `picker`.
- /onboarding only forwards to its `next` (or `/`), keeping a coupon link's
  code in the tab. components/onboarding/flow.tsx is kept, unrendered.
- Gates: arrive.spec holds the forwarding. onboarding.spec, coupon.spec and
  the sale and focus tests that pass the drawn page are skipped until it
  returns; /pricing signed in is the checkout itself.

Dropped with the page: terms acceptance for a provider-made (Google/GitHub)
account, an offer link opening the plans, Free from /pricing recorded on the
account, and a tab's kept coupon added to a checkout that names none.
2026-10-29 07:31:29 -04:00

146 lines
7.9 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 { expect, test, type Page } from '@playwright/test'
import { serveExport } from './export'
import { signIn } from '../session'
import { AUP_VERSION, TERMS_VERSION } from '@hanzo/app/lib/terms'
test.skip(true, '/onboarding is not shown (owner, 2026-10-29): a sign-in goes straight on (lib/auth/arrive), and the page only forwards. Its flow is kept, unrendered, and these return with it.')
/**
* AN OFFER LINK OPENS PAY'S PLANS WITH ITS CODE AND ITS CAMPAIGN.
*
* /onboarding?coupon= keeps the code in the tab and, for a stranger, sends the
* reader through the sign-up and back carrying it and the campaign. Signed in,
* past the terms where IAM says they are owed, a new account is asked its one
* question under the offer, with Continue with Free beside it, and either
* answer opens pay's plans (/pay/cart) with the code, the campaign, the
* organization worked in and the way back to the app; an account past its first
* run opens the plans at once. With no code nothing is quoted.
*/
let base: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
base = served.url
stop = served.close
})
test.afterAll(async () => stop())
const NOW = () => new Date(Date.now() - 60_000).toISOString()
const CAMPAIGN = { utm_source: 'hanzo', utm_medium: 'email', utm_campaign: 'poolparty' }
interface Heard {
/** Every catalog read, by its query. */
quotes: string[]
/** The organizations billing was asked about. */
tiers: string[]
}
/** The platform as a spec wants it: an account IAM made `made`, with the terms on record unless `owed`, on Free. */
async function platform(page: Page, { made = NOW(), owed = false }: { made?: string; owed?: boolean } = {}): Promise<Heard> {
const heard: Heard = { quotes: [], tiers: [] }
await page.route(
(u) => u.pathname.startsWith('/v1/') && !u.pathname.startsWith('/v1/iam/oauth/'),
(route) => {
const req = route.request()
const url = new URL(req.url())
const path = url.pathname
if (path === '/v1/billing/tier') {
heard.tiers.push(req.headers()['x-org-id'] ?? '-')
return route.fulfill({
json: {
user: 'gate',
plan: '',
tier: { name: 'free', displayName: 'free', dailyCreditsCents: 0, maxAgents: 1, unlimitedAgents: false },
balance: { currency: 'usd', prepaidAvailable: 0, creditsRemaining: 0, dailyRemaining: 0, effectiveAvailable: 0 },
},
})
}
if (path === '/v1/billing/balance') return route.fulfill({ json: { balance: 0, holds: 0, available: 0 } })
if (path === '/v1/iam/preferences') return route.fulfill({ json: owed ? {} : { terms: { terms: TERMS_VERSION, aup: AUP_VERSION } } })
if (path === '/v1/iam/terms') return route.fulfill({ json: { terms: TERMS_VERSION, aup: AUP_VERSION } })
if (path === '/v1/billing/plans') {
heard.quotes.push(url.search)
const coupon = url.searchParams.get('coupon')
return route.fulfill({
json: coupon ? [{ slug: 'dev', name: 'Pro', price: 2000, couponCode: coupon, couponPercent: 50, couponFirstCents: 1000 }] : [],
})
}
if (path === '/v1/iam/account') return route.fulfill({ json: { name: 'gate', displayName: '', email: '[email protected]', createdTime: made } })
if (path.endsWith('/stream')) return route.fulfill({ status: 200, contentType: 'text/event-stream', body: '' })
return route.fulfill({ json: {} })
},
)
return heard
}
/** pay's plans, once the page has left for them: the query, read as one record. */
async function plans(page: Page): Promise<Record<string, string>> {
await page.waitForURL((u) => u.pathname === '/pay/cart', { timeout: 30_000 })
return Object.fromEntries(new URL(page.url()).searchParams)
}
// A stranger is asked the question too, under the offer, and signs in on pay's
// plans (b2de8afa): the tab keeps the code, and an answer opens the plans with
// it, the campaign and the way back.
test('a stranger with a coupon and a next is asked the question, and Personal opens pay\'s plans carrying the code and the way back', async ({ page }) => {
await platform(page)
await page.goto(`${base}/onboarding?${new URLSearchParams({ next: '/dev', coupon: '50OFF' })}`)
await expect(page.getByRole('heading', { name: 'How will you use Hanzo?' })).toBeVisible({ timeout: 30_000 })
await page.getByRole('link', { name: /^Personal/ }).click()
expect(await plans(page)).toEqual({ coupon: '50OFF', returnUrl: `${base}/dev`, tab: 'individual' })
})
test('a stranger with a coupon keeps it and the campaign, and Business opens pay\'s plans with both', async ({ page }) => {
await platform(page)
await page.setViewportSize({ width: 390, height: 844 })
await page.goto(`${base}/onboarding?${new URLSearchParams({ coupon: '50OFF', ...CAMPAIGN })}`)
await expect(page.getByRole('heading', { name: 'How will you use Hanzo?' })).toBeVisible({ timeout: 30_000 })
await expect.poll(() => page.evaluate(() => sessionStorage.getItem('checkout.coupon')), { message: 'the tab keeps the code' }).toBe('50OFF')
await page.getByRole('link', { name: /^Business/ }).click()
expect(await plans(page)).toEqual({ coupon: '50OFF', ...CAMPAIGN, returnUrl: `${base}/chat`, tab: 'team' })
})
for (const width of [1440, 390]) {
test(`at ${width}px a new account with an offer link is asked its question under the offer, Free beside it, and an answer opens pay's plans with the code and campaign`, async ({ page }) => {
await signIn(page, { orgs: [{ org: 'gate-org', role: 'owner' }] })
await platform(page)
await page.setViewportSize({ width, height: 900 })
await page.goto(`${base}/onboarding?${new URLSearchParams({ coupon: '50OFF', ...CAMPAIGN })}`)
await expect(page.getByRole('heading', { name: 'How will you use Hanzo?' })).toBeVisible({ timeout: 30_000 })
await expect(page.getByRole('status').filter({ hasText: '50% off your first month of Pro' }), 'the offer, said as pay says it').toBeVisible()
// Free is chosen on pay's plans, which come back to the app (1aa7f96a); this page offers no Free button.
await expect(page.getByRole('button', { name: 'Continue with Free' })).toHaveCount(0)
await page.getByRole('link', { name: /^Personal/ }).click()
expect(await plans(page)).toEqual({ org: 'gate-org', coupon: '50OFF', ...CAMPAIGN, returnUrl: `${base}/chat`, tab: 'individual' })
})
}
test('a returning account with an offer link opens pay\'s plans with it at once', async ({ page }) => {
await signIn(page, { orgs: [{ org: 'gate-org', role: 'owner' }] })
await platform(page, { made: '2024-01-01T00:00:00Z' })
await page.goto(`${base}/onboarding?coupon=50OFF`)
expect(await plans(page)).toEqual({ org: 'gate-org', coupon: '50OFF', returnUrl: `${base}/chat` })
})
test('a new account a provider made agrees to the terms before the offer’s question', async ({ page }) => {
await signIn(page, { orgs: [{ org: 'gate-org', role: 'owner' }] })
await platform(page, { owed: true })
await page.setViewportSize({ width: 390, height: 844 })
await page.goto(`${base}/onboarding?coupon=50OFF`)
await page.getByRole('checkbox', { name: /^I agree to Hanzo's Terms/ }).click({ timeout: 30_000 })
await page.getByRole('button', { name: 'Create account' }).click()
await page.getByRole('link', { name: /^Business/ }).click({ timeout: 15_000 })
expect(await plans(page)).toEqual({ org: 'gate-org', coupon: '50OFF', returnUrl: `${base}/chat`, tab: 'team' })
})
test('with no code nothing is quoted and the plans open plain', async ({ page }) => {
await signIn(page, { orgs: [{ org: 'gate-org', role: 'owner' }] })
const heard = await platform(page)
await page.setViewportSize({ width: 1440, height: 900 })
await page.goto(`${base}/onboarding?next=%2Fchat`)
await page.getByRole('link', { name: /^Personal/ }).click({ timeout: 30_000 })
expect(await plans(page)).toEqual({ org: 'gate-org', returnUrl: `${base}/chat`, tab: 'individual' })
expect(heard.quotes, 'no quote is asked for').toEqual([])
})