diff options
| author | Frank <[email protected]> | 2026-02-23 23:01:23 -0500 |
|---|---|---|
| committer | Frank <[email protected]> | 2026-02-24 04:45:41 -0500 |
| commit | cda2af2589ddef9265ca2db379ecd4ab556f6be8 (patch) | |
| tree | 40c02796f63186cf1bb27b5a80146bef7685fa16 /packages/console/app/src/routes/black/subscribe | |
| parent | eda71373b0f37e56ca07921d13b3faf566824d04 (diff) | |
| download | opencode-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].tsx | 477 |
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> - </> - ) -} |
