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 ( -
- - - -

{error()}

-
- -

{i18n.t("black.subscribe.form.chargeNotice")}

- - ) -} - -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 ( +
+ + + +

{error()}

+
+ +

{i18n.t("black.subscribe.form.chargeNotice")}

+ + ) +} + +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