Files
050337c69c sign-in: a new account goes straight to the plans, and every sign-in until one is chosen (#93)
After sign-in, an account that has chosen no plan (none kept as
onboarding.plan, Free included) and pays for none is sent straight to
pay's plans (/pay/cart?welcome=1), after the terms if it owes them. The
Personal or Business step is gone for it: the plans page has both tabs.
`welcome` draws a Back to hanzo.ai there until a plan is chosen
(hanzo-inc/pay#10). Every later sign-in opens the plans again until one
is; an earlier Personal/Business answer (`use`) is not a plan. A
SuperAdmin, a support session, an invited member, a checkout chosen
before sign-in and Free chosen on /pricing go on as before, and an IAM
read that fails is read as a plan chosen, so it never holds anyone there.

Sign-in and sign-up no longer offer the phone or the wallet (OFFERED in
components/auth/panel.tsx); their flows stay, to offer again.

Testing: pnpm typecheck clean (0 errors); oxlint shows no new warnings
in the three files; no e2e spec covers these screens.

Risk: medium. Accounts that never chose a plan, including older ones,
now land on the plans at sign-in until they pick one (Free is one tap).

How to revert: revert this commit.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-27 13:02:03 -07:00

77 lines
3.0 KiB
TypeScript

/**
* pay's two addresses on this site: its plans, where a plan is chosen, and its
* card step, the one address a plan is bought at.
*
* hanzo-inc/pay is mounted at /pay on this origin. `/pay/cart` is every plan the
* catalog sells, one card each, priced by commerce and with a link's coupon
* quoted on the plans it covers; its close control and a finished purchase come
* back to `returnUrl`. It takes the organization billed, the coupon and the
* campaign the link carried.
*
* `/pay/confirm/card` names the plan and its price and asks for the card, with
* no picker before it. It takes the plan's catalog slug, the term, the seats, a
* first-month coupon commerce quoted for the plan (lib/coupon), the
* organization billed, and the hanzo.ai address to return to once paid. A
* month and one seat are pay's defaults and are not written. The price never
* rides along: commerce reads it.
*/
import type { Interval } from '@hanzo/app/lib/plans'
/** What pay's card step is opened on. */
export interface Order {
/** The catalog slug. */
plan: string
interval?: Interval
/** Seats, for a per-seat plan. */
quantity?: number
coupon?: string
/** The organization the purchase bills. */
org?: string
/** The campaign the link carried (lib/campaign), kept on every hop inside pay. */
campaign?: [string, string][]
/** Where pay sends the buyer once paid: an absolute hanzo.ai address. */
returnUrl: string
}
/** The path of pay's card step for `order`. */
export function checkout({ plan, interval = 'month', quantity = 1, coupon, org, campaign = [], returnUrl }: Order): string {
const q = new URLSearchParams({ plan })
if (interval === 'year') q.set('interval', 'year')
if (quantity > 1) q.set('quantity', String(quantity))
if (coupon) q.set('coupon', coupon)
if (org) q.set('org', org)
for (const [k, v] of campaign) q.append(k, v)
q.set('returnUrl', returnUrl)
return `/pay/confirm/card?${q}`
}
/** What pay's plans are opened on. */
export interface Browse {
/** The organization a purchase bills. */
org?: string
/** A coupon the link carried: pay quotes it on each plan it covers and says when it covers none. */
coupon?: string
/** The campaign the link carried (lib/campaign), kept on every hop inside pay. */
campaign?: [string, string][]
/** Where the close control and a finished purchase return to: an absolute hanzo.ai address. */
returnUrl: string
/**
* An account that has chosen no plan yet, sent here by its sign-in
* (components/onboarding/flow): pay draws a Back to the site above the plans
* until one is chosen, Free included (hanzo-inc/pay routes/cart).
*/
welcome?: boolean
}
/** The path of pay's plans for `browse`. */
export function plans({ org, coupon, campaign = [], returnUrl, welcome }: Browse): string {
const q = new URLSearchParams()
if (org) q.set('org', org)
if (coupon) q.set('coupon', coupon)
for (const [k, v] of campaign) q.append(k, v)
q.set('returnUrl', returnUrl)
if (welcome) q.set('welcome', '1')
return `/pay/cart?${q}`
}