summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src/routes/black/subscribe
diff options
context:
space:
mode:
authorFrank <[email protected]>2026-02-23 23:01:23 -0500
committerFrank <[email protected]>2026-02-24 04:45:41 -0500
commitcda2af2589ddef9265ca2db379ecd4ab556f6be8 (patch)
tree40c02796f63186cf1bb27b5a80146bef7685fa16 /packages/console/app/src/routes/black/subscribe
parenteda71373b0f37e56ca07921d13b3faf566824d04 (diff)
downloadopencode-cda2af2589ddef9265ca2db379ecd4ab556f6be8.tar.gz
opencode-cda2af2589ddef9265ca2db379ecd4ab556f6be8.zip
wip: zen lite
Diffstat (limited to 'packages/console/app/src/routes/black/subscribe')
-rw-r--r--packages/console/app/src/routes/black/subscribe/[plan].tsx477
1 files changed, 0 insertions, 477 deletions
diff --git a/packages/console/app/src/routes/black/subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx
deleted file mode 100644
index 644d87d9b..000000000
--- a/packages/console/app/src/routes/black/subscribe/[plan].tsx
+++ /dev/null
@@ -1,477 +0,0 @@
-import { A, createAsync, query, redirect, useParams } from "@solidjs/router"
-import { Title } from "@solidjs/meta"
-import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js"
-import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js"
-import { Elements, PaymentElement, useStripe, useElements, AddressElement } from "solid-stripe"
-import { PlanID, plans } from "../common"
-import { getActor, useAuthSession } from "~/context/auth"
-import { withActor } from "~/context/auth.withActor"
-import { Actor } from "@opencode-ai/console-core/actor.js"
-import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
-import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
-import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
-import { createList } from "solid-list"
-import { Modal } from "~/component/modal"
-import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
-import { Billing } from "@opencode-ai/console-core/billing.js"
-import { useI18n } from "~/context/i18n"
-import { useLanguage } from "~/context/language"
-import { formError } from "~/lib/form-error"
-
-const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
-const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!)
-
-const getWorkspaces = query(async (plan: string) => {
- "use server"
- const actor = await getActor()
- if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe/" + plan)
- return withActor(async () => {
- return Database.use((tx) =>
- tx
- .select({
- id: WorkspaceTable.id,
- name: WorkspaceTable.name,
- slug: WorkspaceTable.slug,
- billing: {
- customerID: BillingTable.customerID,
- paymentMethodID: BillingTable.paymentMethodID,
- paymentMethodType: BillingTable.paymentMethodType,
- paymentMethodLast4: BillingTable.paymentMethodLast4,
- subscriptionID: BillingTable.subscriptionID,
- timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
- },
- })
- .from(UserTable)
- .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
- .innerJoin(BillingTable, eq(WorkspaceTable.id, BillingTable.workspaceID))
- .where(
- and(
- eq(UserTable.accountID, Actor.account()),
- isNull(WorkspaceTable.timeDeleted),
- isNull(UserTable.timeDeleted),
- ),
- ),
- )
- })
-}, "black.subscribe.workspaces")
-
-const createSetupIntent = async (input: { plan: string; workspaceID: string }) => {
- "use server"
- const { plan, workspaceID } = input
-
- if (!plan || !["20", "100", "200"].includes(plan)) return { error: formError.invalidPlan }
- if (!workspaceID) return { error: formError.workspaceRequired }
-
- return withActor(async () => {
- const session = await useAuthSession()
- const account = session.data.account?.[session.data.current ?? ""]
- const email = account?.email
-
- const customer = await Database.use((tx) =>
- tx
- .select({
- customerID: BillingTable.customerID,
- subscriptionID: BillingTable.subscriptionID,
- })
- .from(BillingTable)
- .where(eq(BillingTable.workspaceID, workspaceID))
- .then((rows) => rows[0]),
- )
- if (customer?.subscriptionID) {
- return { error: formError.alreadySubscribed }
- }
-
- let customerID = customer?.customerID
- if (!customerID) {
- const customer = await Billing.stripe().customers.create({
- email,
- metadata: {
- workspaceID,
- },
- })
- customerID = customer.id
- await Database.use((tx) =>
- tx
- .update(BillingTable)
- .set({
- customerID,
- })
- .where(eq(BillingTable.workspaceID, workspaceID)),
- )
- }
-
- const intent = await Billing.stripe().setupIntents.create({
- customer: customerID,
- payment_method_types: ["card"],
- metadata: {
- workspaceID,
- },
- })
-
- return { clientSecret: intent.client_secret ?? undefined }
- }, workspaceID)
-}
-
-const bookSubscription = async (input: {
- workspaceID: string
- plan: PlanID
- paymentMethodID: string
- paymentMethodType: string
- paymentMethodLast4?: string
-}) => {
- "use server"
- return withActor(
- () =>
- Database.use((tx) =>
- tx
- .update(BillingTable)
- .set({
- paymentMethodID: input.paymentMethodID,
- paymentMethodType: input.paymentMethodType,
- paymentMethodLast4: input.paymentMethodLast4,
- subscriptionPlan: input.plan,
- timeSubscriptionBooked: new Date(),
- })
- .where(eq(BillingTable.workspaceID, input.workspaceID)),
- ),
- input.workspaceID,
- )
-}
-
-interface SuccessData {
- plan: string
- paymentMethodType: string
- paymentMethodLast4?: string
-}
-
-function Failure(props: { message: string }) {
- const i18n = useI18n()
-
- return (
- <div data-slot="failure">
- <p data-slot="message">
- {i18n.t("black.subscribe.failurePrefix")} {props.message}
- </p>
- </div>
- )
-}
-
-function Success(props: SuccessData) {
- const i18n = useI18n()
-
- return (
- <div data-slot="success">
- <p data-slot="title">{i18n.t("black.subscribe.success.title")}</p>
- <dl data-slot="details">
- <div>
- <dt>{i18n.t("black.subscribe.success.subscriptionPlan")}</dt>
- <dd>{i18n.t("black.subscribe.success.planName", { plan: props.plan })}</dd>
- </div>
- <div>
- <dt>{i18n.t("black.subscribe.success.amount")}</dt>
- <dd>{i18n.t("black.subscribe.success.amountValue", { plan: props.plan })}</dd>
- </div>
- <div>
- <dt>{i18n.t("black.subscribe.success.paymentMethod")}</dt>
- <dd>
- <Show when={props.paymentMethodLast4} fallback={<span>{props.paymentMethodType}</span>}>
- <span>
- {props.paymentMethodType} - {props.paymentMethodLast4}
- </span>
- </Show>
- </dd>
- </div>
- <div>
- <dt>{i18n.t("black.subscribe.success.dateJoined")}</dt>
- <dd>{new Date().toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}</dd>
- </div>
- </dl>
- <p data-slot="charge-notice">{i18n.t("black.subscribe.success.chargeNotice")}</p>
- </div>
- )
-}
-
-function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) {
- const i18n = useI18n()
- const stripe = useStripe()
- const elements = useElements()
- const [error, setError] = createSignal<string | undefined>(undefined)
- const [loading, setLoading] = createSignal(false)
-
- const handleSubmit = async (e: Event) => {
- e.preventDefault()
- if (!stripe() || !elements()) return
-
- setLoading(true)
- setError(undefined)
-
- const result = await elements()!.submit()
- if (result.error) {
- setError(result.error.message ?? i18n.t("black.subscribe.error.generic"))
- setLoading(false)
- return
- }
-
- const { error: confirmError, setupIntent } = await stripe()!.confirmSetup({
- elements: elements()!,
- confirmParams: {
- expand: ["payment_method"],
- payment_method_data: {
- allow_redisplay: "always",
- },
- },
- redirect: "if_required",
- })
-
- if (confirmError) {
- setError(confirmError.message ?? i18n.t("black.subscribe.error.generic"))
- setLoading(false)
- return
- }
-
- if (setupIntent?.status === "succeeded") {
- const pm = setupIntent.payment_method as PaymentMethod
-
- await bookSubscription({
- workspaceID: props.workspaceID,
- plan: props.plan,
- paymentMethodID: pm.id,
- paymentMethodType: pm.type,
- paymentMethodLast4: pm.card?.last4,
- })
-
- props.onSuccess({
- plan: props.plan,
- paymentMethodType: pm.type,
- paymentMethodLast4: pm.card?.last4,
- })
- }
-
- setLoading(false)
- }
-
- return (
- <form onSubmit={handleSubmit} data-slot="checkout-form">
- <PaymentElement />
- <AddressElement options={{ mode: "billing" }} />
- <Show when={error()}>
- <p data-slot="error">{error()}</p>
- </Show>
- <button type="submit" disabled={loading() || !stripe() || !elements()} data-slot="submit-button">
- {loading() ? i18n.t("black.subscribe.processing") : i18n.t("black.subscribe.submit", { plan: props.plan })}
- </button>
- <p data-slot="charge-notice">{i18n.t("black.subscribe.form.chargeNotice")}</p>
- </form>
- )
-}
-
-export default function BlackSubscribe() {
- const params = useParams()
- const i18n = useI18n()
- const language = useLanguage()
- const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
- const plan = planData.id
-
- const workspaces = createAsync(() => getWorkspaces(plan))
- const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | undefined>(undefined)
- const [success, setSuccess] = createSignal<SuccessData | undefined>(undefined)
- const [failure, setFailure] = createSignal<string | undefined>(undefined)
- const [clientSecret, setClientSecret] = createSignal<string | undefined>(undefined)
- const [stripe, setStripe] = createSignal<Stripe | undefined>(undefined)
-
- const formatError = (error: string) => {
- if (error === formError.invalidPlan) return i18n.t("black.subscribe.error.invalidPlan")
- if (error === formError.workspaceRequired) return i18n.t("black.subscribe.error.workspaceRequired")
- if (error === formError.alreadySubscribed) return i18n.t("black.subscribe.error.alreadySubscribed")
- if (error === "Invalid plan") return i18n.t("black.subscribe.error.invalidPlan")
- if (error === "Workspace ID is required") return i18n.t("black.subscribe.error.workspaceRequired")
- if (error === "This workspace already has a subscription") return i18n.t("black.subscribe.error.alreadySubscribed")
- return error
- }
-
- // Resolve stripe promise once
- createEffect(() => {
- stripePromise.then((s) => {
- if (s) setStripe(s)
- })
- })
-
- // Auto-select if only one workspace
- createEffect(() => {
- const ws = workspaces()
- if (ws?.length === 1 && !selectedWorkspace()) {
- setSelectedWorkspace(ws[0].id)
- }
- })
-
- // Fetch setup intent when workspace is selected (unless workspace already has payment method)
- createEffect(async () => {
- const id = selectedWorkspace()
- if (!id) return
-
- const ws = workspaces()?.find((w) => w.id === id)
- if (ws?.billing?.subscriptionID) {
- setFailure(i18n.t("black.subscribe.error.alreadySubscribed"))
- return
- }
- if (ws?.billing?.paymentMethodID) {
- if (!ws?.billing?.timeSubscriptionBooked) {
- await bookSubscription({
- workspaceID: id,
- plan: planData.id,
- paymentMethodID: ws.billing.paymentMethodID!,
- paymentMethodType: ws.billing.paymentMethodType!,
- paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
- })
- }
- setSuccess({
- plan: planData.id,
- paymentMethodType: ws.billing.paymentMethodType!,
- paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
- })
- return
- }
-
- const result = await createSetupIntent({ plan, workspaceID: id })
- if (result.error) {
- setFailure(formatError(result.error))
- } else if ("clientSecret" in result) {
- setClientSecret(result.clientSecret)
- }
- })
-
- // Keyboard navigation for workspace picker
- const { active, setActive, onKeyDown } = createList({
- items: () => workspaces()?.map((w) => w.id) ?? [],
- initialActive: null,
- })
-
- const handleSelectWorkspace = (id: string) => {
- setSelectedWorkspace(id)
- }
-
- let listRef: HTMLUListElement | undefined
-
- // Show workspace picker if multiple workspaces and none selected
- const showWorkspacePicker = () => {
- const ws = workspaces()
- return ws && ws.length > 1 && !selectedWorkspace()
- }
-
- return (
- <>
- <Title>{i18n.t("black.subscribe.title")}</Title>
- <section data-slot="subscribe-form">
- <div data-slot="form-card">
- <Switch>
- <Match when={success()}>{(data) => <Success {...data()} />}</Match>
- <Match when={failure()}>{(data) => <Failure message={data()} />}</Match>
- <Match when={true}>
- <>
- <div data-slot="plan-header">
- <p data-slot="title">{i18n.t("black.subscribe.title")}</p>
- <p data-slot="price">
- <span data-slot="amount">${planData.id}</span>{" "}
- <span data-slot="period">{i18n.t("black.price.perMonth")}</span>
- <Show when={planData.multiplier}>
- {(multiplier) => <span data-slot="multiplier">{i18n.t(multiplier())}</span>}
- </Show>
- </p>
- </div>
- <div data-slot="divider" />
- <p data-slot="section-title">{i18n.t("black.subscribe.paymentMethod")}</p>
-
- <Show
- when={clientSecret() && selectedWorkspace() && stripe()}
- fallback={
- <div data-slot="loading">
- <p>
- {selectedWorkspace()
- ? i18n.t("black.subscribe.loadingPaymentForm")
- : i18n.t("black.subscribe.selectWorkspaceToContinue")}
- </p>
- </div>
- }
- >
- <Elements
- stripe={stripe()!}
- options={{
- clientSecret: clientSecret()!,
- appearance: {
- theme: "night",
- variables: {
- colorPrimary: "#ffffff",
- colorBackground: "#1a1a1a",
- colorText: "#ffffff",
- colorTextSecondary: "#999999",
- colorDanger: "#ff6b6b",
- fontFamily: "JetBrains Mono, monospace",
- borderRadius: "4px",
- spacingUnit: "4px",
- },
- rules: {
- ".Input": {
- backgroundColor: "#1a1a1a",
- border: "1px solid rgba(255, 255, 255, 0.17)",
- color: "#ffffff",
- },
- ".Input:focus": {
- borderColor: "rgba(255, 255, 255, 0.35)",
- boxShadow: "none",
- },
- ".Label": {
- color: "rgba(255, 255, 255, 0.59)",
- fontSize: "14px",
- marginBottom: "8px",
- },
- },
- },
- }}
- >
- <IntentForm plan={plan} workspaceID={selectedWorkspace()!} onSuccess={setSuccess} />
- </Elements>
- </Show>
- </>
- </Match>
- </Switch>
- </div>
-
- {/* Workspace picker modal */}
- <Modal open={showWorkspacePicker() ?? false} onClose={() => {}} title={i18n.t("black.workspace.selectPlan")}>
- <div data-slot="workspace-picker">
- <ul
- ref={listRef}
- data-slot="workspace-list"
- tabIndex={0}
- onKeyDown={(e) => {
- if (e.key === "Enter" && active()) {
- handleSelectWorkspace(active()!)
- } else {
- onKeyDown(e)
- }
- }}
- >
- <For each={workspaces()}>
- {(workspace) => (
- <li
- data-slot="workspace-item"
- data-active={active() === workspace.id}
- onMouseEnter={() => setActive(workspace.id)}
- onClick={() => handleSelectWorkspace(workspace.id)}
- >
- <span data-slot="selected-icon">[*]</span>
- <span>{workspace.name || workspace.slug}</span>
- </li>
- )}
- </For>
- </ul>
- </div>
- </Modal>
- <p data-slot="fine-print">
- {i18n.t("black.finePrint.beforeTerms")} ยท{" "}
- <A href={language.route("/legal/terms-of-service")}>{i18n.t("black.finePrint.terms")}</A>
- </p>
- </section>
- </>
- )
-}