Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/web/src/page/onboarding/bill-through-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Html, HtmlBuilder } from "foldkit/html"
import { AppRoute, hrefOf } from "../../route"

/** Under the plan picker: an org another org pays for links itself in billing settings instead. */
export const billThroughAnotherOrgLink = <M>(h: HtmlBuilder<M>): Html =>
h.p(
[h.Class("text-center text-xs text-muted-foreground")],
[
"Billed through another organization? ",
h.a(
[
h.Href(hrefOf(AppRoute.Settings({ search: { tab: "billing" } }))),
h.Class("text-foreground underline-offset-4 hover:underline"),
],
["Link it in billing settings"],
),
],
)
30 changes: 25 additions & 5 deletions apps/web/src/page/onboarding/plan-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,32 @@ describe("plan gate", () => {

test("an org that never held a plan is sent to onboarding with its destination", () => {
expect(gate(home, customerWith(null)).redirect).toEqual(
Option.some({ url: hrefOf(AppRoute.QuickStart({ search: { redirect_url: "/" } })), isReplace: true }),
Option.some({
url: hrefOf(AppRoute.QuickStart({ search: { redirect_url: "/" } })),
isReplace: true,
}),
)
})

test("an org with no plan reaches billing settings, where it links to the org that pays", () => {
const billing = AppRoute.Settings({ search: { tab: "billing" } })
expect(gate(billing, customerWith(null)).redirect).toEqual(Option.none())
expect(gate(billing, loading).isSplash).toBe(false)
expect(
Option.isSome(
gate(AppRoute.Settings({ search: { tab: "members" } }), customerWith(null)).redirect,
),
).toBe(true)
})

test("a subscriber finishes the onboarding steps after the plan, and leaves the plan picker", () => {
expect(gate(AppRoute.QuickStart({ search: {} }), customerWith("active")).redirect).toEqual(Option.none())
expect(gate(AppRoute.SelectPlan({ search: { redirect_url: "/traces" } }), customerWith("active")).redirect).toEqual(
Option.some({ url: "/traces", isReplace: true }),
expect(gate(AppRoute.QuickStart({ search: {} }), customerWith("active")).redirect).toEqual(
Option.none(),
)
expect(
gate(AppRoute.SelectPlan({ search: { redirect_url: "/traces" } }), customerWith("active"))
.redirect,
).toEqual(Option.some({ url: "/traces", isReplace: true }))
})

test("a failed customer read fails open", () => {
Expand Down Expand Up @@ -78,7 +95,10 @@ describe("plan gate", () => {
expect(at(elsewhere, AppRoute.CliLogin({ search: {} })).wrongRegion).toBe("eu")
const open: ClerkOrganization = { ...elsewhere, isRegionOpen: true }
expect(at(open, home).redirect).toEqual(
Option.some({ url: hrefOf(AppRoute.QuickStart({ search: { redirect_url: "/" } })), isReplace: true }),
Option.some({
url: hrefOf(AppRoute.QuickStart({ search: { redirect_url: "/" } })),
isReplace: true,
}),
)
expect(at(open, home).wrongRegion).toBeNull()
})
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/page/onboarding/plan-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ export const saveSelectedPlanKnown = (orgId: string, mayRenderApp: boolean): Eff
// GATE

/** Routes that render their own onboarding or billing UI: never sent away for want of a plan. */
const isAllowedWithoutPlan = AppRoute.isAnyOf(["SelectPlan", "QuickStart", "CliLogin", "McpAuthorize"])
const isPlanFreeRoute = AppRoute.isAnyOf(["SelectPlan", "QuickStart", "CliLogin", "McpAuthorize"])

// Billing settings too: an org with no plan links itself to the org that pays for it there.
const isAllowedWithoutPlan = (route: AppRoute): boolean =>
isPlanFreeRoute(route) || (route._tag === "Settings" && route.search.tab === "billing")

/** Public paths that never read org data (`isOrgIndependentPath`). */
const isOrgIndependent = AppRoute.isAnyOf(["Unsubscribe"])
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/page/onboarding/quick-start-team-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { skeleton } from "../../ui/skeleton"
import { icon, type IconName } from "../../ui/icon"
import { input } from "../../ui/input"
import { label } from "../../ui/label"
import { billThroughAnotherOrgLink } from "./bill-through-link"
import { Message } from "./quick-start"
import { STEP_BODY_CLASS, STEP_FOOTER_CLASS } from "./step-layout"
import { isAlreadyInvited, type Team, TeamMessage } from "./quick-start-team"
Expand Down Expand Up @@ -299,6 +300,7 @@ export const planStep = (h: H, isCustomerLoading: boolean, pricing: Html): Html
],
),
isCustomerLoading ? pricingSkeleton(h) : pricing,
h.div([h.Class("mt-8")], [billThroughAnotherOrgLink(h)]),
],
),
],
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/page/onboarding/select-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { defineMessageUnion } from "foldkit/message"
import type { Resources } from "../../platform/resources"
import { badge } from "../../ui/display"
import { icon } from "../../ui/icon"
import { billThroughAnotherOrgLink } from "./bill-through-link"
import * as PricingCards from "./pricing-cards"
import * as PricingCardsView from "./pricing-cards-view"

Expand Down Expand Up @@ -142,6 +143,7 @@ export const view = (h: H, model: Model, customer: BillingCustomer | undefined):
}),
],
),
billThroughAnotherOrgLink(h),
],
),
],
Expand Down
Loading