From c4ea11fef3dc3ac6bd2e3c55d1c8179457eace5d Mon Sep 17 00:00:00 2001
From: Frank
Date: Wed, 25 Feb 2026 23:06:16 -0500
Subject: wip: zen
---
.../app/src/routes/black/_subscribe/[plan].tsx | 477 --------------------
packages/console/app/src/routes/black/index.tsx | 13 +-
.../app/src/routes/black/subscribe/[plan].tsx | 484 +++++++++++++++++++++
3 files changed, 493 insertions(+), 481 deletions(-)
delete mode 100644 packages/console/app/src/routes/black/_subscribe/[plan].tsx
create mode 100644 packages/console/app/src/routes/black/subscribe/[plan].tsx
(limited to 'packages/console/app/src')
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
-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 (
-
-
- {i18n.t("black.subscribe.failurePrefix")} {props.message}
-
-
- )
-}
-
-function Success(props: SuccessData) {
- const i18n = useI18n()
-
- return (
-
-
{i18n.t("black.subscribe.success.title")}
-
-
-
- {i18n.t("black.subscribe.success.subscriptionPlan")}
- - {i18n.t("black.subscribe.success.planName", { plan: props.plan })}
-
-
-
- {i18n.t("black.subscribe.success.amount")}
- - {i18n.t("black.subscribe.success.amountValue", { plan: props.plan })}
-
-
-
- {i18n.t("black.subscribe.success.paymentMethod")}
- -
- {props.paymentMethodType}}>
-
- {props.paymentMethodType} - {props.paymentMethodLast4}
-
-
-
-
-
-
- {i18n.t("black.subscribe.success.dateJoined")}
- - {new Date().toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}
-
-
-
{i18n.t("black.subscribe.success.chargeNotice")}
-
- )
-}
-
-function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) {
- const i18n = useI18n()
- const stripe = useStripe()
- const elements = useElements()
- const [error, setError] = createSignal(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 (
-
- )
-}
-
-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(undefined)
- const [success, setSuccess] = createSignal(undefined)
- const [failure, setFailure] = createSignal(undefined)
- const [clientSecret, setClientSecret] = createSignal(undefined)
- const [stripe, setStripe] = createSignal(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 (
- <>
- {i18n.t("black.subscribe.title")}
-
-
-
- {(data) => }
- {(data) => }
-
- <>
-
-
{i18n.t("black.subscribe.title")}
-
- ${planData.id}{" "}
- {i18n.t("black.price.perMonth")}
-
- {(multiplier) => {i18n.t(multiplier())}}
-
-
-
-
- {i18n.t("black.subscribe.paymentMethod")}
-
-
-
- {selectedWorkspace()
- ? i18n.t("black.subscribe.loadingPaymentForm")
- : i18n.t("black.subscribe.selectWorkspaceToContinue")}
-
-
- }
- >
-
-
-
-
- >
-
-
-
-
- {/* Workspace picker modal */}
- {}} title={i18n.t("black.workspace.selectPlan")}>
-
-
{
- if (e.key === "Enter" && active()) {
- handleSelectWorkspace(active()!)
- } else {
- onKeyDown(e)
- }
- }}
- >
-
- {(workspace) => (
- - setActive(workspace.id)}
- onClick={() => handleSelectWorkspace(workspace.id)}
- >
- [*]
- {workspace.name || workspace.slug}
-
- )}
-
-
-
-
-
- {i18n.t("black.finePrint.beforeTerms")} ·{" "}
- {i18n.t("black.finePrint.terms")}
-
-
- >
- )
-}
diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx
index 382832e8f..8bce3cd46 100644
--- a/packages/console/app/src/routes/black/index.tsx
+++ b/packages/console/app/src/routes/black/index.tsx
@@ -1,16 +1,21 @@
-import { A, useSearchParams } from "@solidjs/router"
+import { A, createAsync, query, useSearchParams } from "@solidjs/router"
import { Title } from "@solidjs/meta"
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js"
import { PlanIcon, plans } from "./common"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
+import { Resource } from "@opencode-ai/console-resource"
-const paused = true
+const getPaused = query(async () => {
+ "use server"
+ return Resource.App.stage === "production"
+}, "black.paused")
export default function Black() {
const [params] = useSearchParams()
const i18n = useI18n()
const language = useLanguage()
+ const paused = createAsync(() => getPaused())
const [selected, setSelected] = createSignal((params.plan as string) || null)
const [mounted, setMounted] = createSignal(false)
const selectedPlan = createMemo(() => plans.find((p) => p.id === selected()))
@@ -44,7 +49,7 @@ export default function Black() {
<>
{i18n.t("black.title")}
- {i18n.t("black.paused")}
}>
+ {i18n.t("black.paused")}}>
@@ -108,7 +113,7 @@ export default function Black() {
-
+
{i18n.t("black.finePrint.beforeTerms")} ·{" "}
{i18n.t("black.finePrint.terms")}
diff --git a/packages/console/app/src/routes/black/subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx
new file mode 100644
index 000000000..19b56eabe
--- /dev/null
+++ b/packages/console/app/src/routes/black/subscribe/[plan].tsx
@@ -0,0 +1,484 @@
+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"
+import { Resource } from "@opencode-ai/console-resource"
+
+const getEnabled = query(async () => {
+ "use server"
+ return Resource.App.stage !== "production"
+}, "black.subscribe.enabled")
+
+const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record
+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 (
+
+
+ {i18n.t("black.subscribe.failurePrefix")} {props.message}
+
+
+ )
+}
+
+function Success(props: SuccessData) {
+ const i18n = useI18n()
+
+ return (
+
+
{i18n.t("black.subscribe.success.title")}
+
+
+
- {i18n.t("black.subscribe.success.subscriptionPlan")}
+ - {i18n.t("black.subscribe.success.planName", { plan: props.plan })}
+
+
+
- {i18n.t("black.subscribe.success.amount")}
+ - {i18n.t("black.subscribe.success.amountValue", { plan: props.plan })}
+
+
+
- {i18n.t("black.subscribe.success.paymentMethod")}
+ -
+ {props.paymentMethodType}}>
+
+ {props.paymentMethodType} - {props.paymentMethodLast4}
+
+
+
+
+
+
- {i18n.t("black.subscribe.success.dateJoined")}
+ - {new Date().toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}
+
+
+
{i18n.t("black.subscribe.success.chargeNotice")}
+
+ )
+}
+
+function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) {
+ const i18n = useI18n()
+ const stripe = useStripe()
+ const elements = useElements()
+ const [error, setError] = createSignal(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 (
+
+ )
+}
+
+export default function BlackSubscribe() {
+ const params = useParams()
+ const i18n = useI18n()
+ const language = useLanguage()
+ const enabled = createAsync(() => getEnabled())
+ const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
+ const plan = planData.id
+
+ const workspaces = createAsync(() => getWorkspaces(plan))
+ const [selectedWorkspace, setSelectedWorkspace] = createSignal(undefined)
+ const [success, setSuccess] = createSignal(undefined)
+ const [failure, setFailure] = createSignal(undefined)
+ const [clientSecret, setClientSecret] = createSignal(undefined)
+ const [stripe, setStripe] = createSignal(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 (
+
+ {i18n.t("black.subscribe.title")}
+
+
+
+ {(data) => }
+ {(data) => }
+
+ <>
+
+
{i18n.t("black.subscribe.title")}
+
+ ${planData.id}{" "}
+ {i18n.t("black.price.perMonth")}
+
+ {(multiplier) => {i18n.t(multiplier())}}
+
+
+
+
+ {i18n.t("black.subscribe.paymentMethod")}
+
+
+
+ {selectedWorkspace()
+ ? i18n.t("black.subscribe.loadingPaymentForm")
+ : i18n.t("black.subscribe.selectWorkspaceToContinue")}
+
+
+ }
+ >
+
+
+
+
+ >
+
+
+
+
+ {/* Workspace picker modal */}
+ {}} title={i18n.t("black.workspace.selectPlan")}>
+
+
{
+ if (e.key === "Enter" && active()) {
+ handleSelectWorkspace(active()!)
+ } else {
+ onKeyDown(e)
+ }
+ }}
+ >
+
+ {(workspace) => (
+ - setActive(workspace.id)}
+ onClick={() => handleSelectWorkspace(workspace.id)}
+ >
+ [*]
+ {workspace.name || workspace.slug}
+
+ )}
+
+
+
+
+
+ {i18n.t("black.finePrint.beforeTerms")} ·{" "}
+ {i18n.t("black.finePrint.terms")}
+
+
+
+ )
+}
--
cgit v1.2.3