From 5f3ab9395fc8f8543724dcda914d38fba0809049 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 22 Jan 2026 16:59:32 -0500 Subject: wip: zen black --- .../workspace/[id]/billing/black-section.tsx | 103 +++++++++++++++++++-- .../[id]/billing/black-waitlist-section.module.css | 15 +++ .../[id]/billing/black-waitlist-section.tsx | 57 ------------ .../src/routes/workspace/[id]/billing/index.tsx | 6 +- .../app/src/routes/workspace/[id]/index.tsx | 20 ++-- .../console/app/src/routes/workspace/common.tsx | 1 + 6 files changed, 123 insertions(+), 79 deletions(-) delete mode 100644 packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.tsx (limited to 'packages/console/app/src/routes/workspace') diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx index 98d4debbb..03c75387a 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx @@ -9,6 +9,7 @@ import { Black } from "@opencode-ai/console-core/black.js" import { withActor } from "~/context/auth.withActor" import { queryBillingInfo } from "../../common" import styles from "./black-section.module.css" +import waitlistStyles from "./black-waitlist-section.module.css" const querySubscription = query(async (workspaceID: string) => { "use server" @@ -27,7 +28,7 @@ const querySubscription = query(async (workspaceID: string) => { .where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted))) .then((r) => r[0]), ) - if (!row.subscription) return null + if (!row?.subscription) return null return { plan: row.subscription.plan, @@ -58,6 +59,37 @@ function formatResetTime(seconds: number) { return `${minutes} ${minutes === 1 ? "minute" : "minutes"}` } +const cancelWaitlist = action(async (workspaceID: string) => { + "use server" + return json( + await withActor(async () => { + await Database.use((tx) => + tx + .update(BillingTable) + .set({ + subscriptionPlan: null, + timeSubscriptionBooked: null, + timeSubscriptionSelected: null, + }) + .where(eq(BillingTable.workspaceID, workspaceID)), + ) + return { error: undefined } + }, workspaceID).catch((e) => ({ error: e.message as string })), + { revalidate: [queryBillingInfo.key, querySubscription.key] }, + ) +}, "cancelWaitlist") + +const enroll = action(async (workspaceID: string) => { + "use server" + return json( + await withActor(async () => { + await Billing.subscribe({ seats: 1 }) + return { error: undefined } + }, workspaceID).catch((e) => ({ error: e.message as string })), + { revalidate: [queryBillingInfo.key, querySubscription.key] }, + ) +}, "enroll") + const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => { "use server" return json( @@ -71,17 +103,24 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) = })), workspaceID, ), - { revalidate: queryBillingInfo.key }, + { revalidate: [queryBillingInfo.key, querySubscription.key] }, ) }, "sessionUrl") export function BlackSection() { const params = useParams() + const billing = createAsync(() => queryBillingInfo(params.id!)) + const subscription = createAsync(() => querySubscription(params.id!)) const sessionAction = useAction(createSessionUrl) const sessionSubmission = useSubmission(createSessionUrl) - const subscription = createAsync(() => querySubscription(params.id!)) + const cancelAction = useAction(cancelWaitlist) + const cancelSubmission = useSubmission(cancelWaitlist) + const enrollAction = useAction(enroll) + const enrollSubmission = useSubmission(enroll) const [store, setStore] = createStore({ sessionRedirecting: false, + cancelled: false, + enrolled: false, }) async function onClickSession() { @@ -92,11 +131,25 @@ export function BlackSection() { } } + async function onClickCancel() { + const result = await cancelAction(params.id!) + if (!result.error) { + setStore("cancelled", true) + } + } + + async function onClickEnroll() { + const result = await enrollAction(params.id!) + if (!result.error) { + setStore("enrolled", true) + } + } + return ( -
+ <> {(sub) => ( - <> +

Subscription

@@ -132,9 +185,45 @@ export function BlackSection() { Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}
- +
)}
-
+ +
+
+

Waitlist

+
+

+ {billing()?.timeSubscriptionSelected + ? `We're ready to enroll you into the $${billing()?.subscriptionPlan} per month OpenCode Black plan.` + : `You are on the waitlist for the $${billing()?.subscriptionPlan} per month OpenCode Black plan.`} +

+ +
+
+ +
+ +

+ When you click Enroll, your subscription starts immediately and your card will be charged. +

+
+
+
+
+ ) } diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.module.css index c189f0d64..685d62c93 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.module.css @@ -5,4 +5,19 @@ align-items: center; gap: var(--space-4); } + + [data-slot="enroll-section"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + } + + [data-slot="enroll-button"] { + align-self: flex-start; + } + + [data-slot="enroll-note"] { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } } diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.tsx deleted file mode 100644 index 3ec9be395..000000000 --- a/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { action, useParams, useAction, useSubmission, json, createAsync } from "@solidjs/router" -import { createStore } from "solid-js/store" -import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js" -import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js" -import { withActor } from "~/context/auth.withActor" -import { queryBillingInfo } from "../../common" -import styles from "./black-waitlist-section.module.css" - -const cancelWaitlist = action(async (workspaceID: string) => { - "use server" - return json( - await withActor(async () => { - await Database.use((tx) => - tx - .update(BillingTable) - .set({ - subscriptionPlan: null, - timeSubscriptionBooked: null, - }) - .where(eq(BillingTable.workspaceID, workspaceID)), - ) - return { error: undefined } - }, workspaceID).catch((e) => ({ error: e.message as string })), - { revalidate: queryBillingInfo.key }, - ) -}, "cancelWaitlist") - -export function BlackWaitlistSection() { - const params = useParams() - const billingInfo = createAsync(() => queryBillingInfo(params.id!)) - const cancelAction = useAction(cancelWaitlist) - const cancelSubmission = useSubmission(cancelWaitlist) - const [store, setStore] = createStore({ - cancelled: false, - }) - - async function onClickCancel() { - const result = await cancelAction(params.id!) - if (!result.error) { - setStore("cancelled", true) - } - } - - return ( -
-
-

Waitlist

-
-

You are on the waitlist for the ${billingInfo()?.subscriptionPlan} per month OpenCode Black plan.

- -
-
-
- ) -} diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx index 7fe4092be..a252a0234 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -3,7 +3,6 @@ import { BillingSection } from "./billing-section" import { ReloadSection } from "./reload-section" import { PaymentSection } from "./payment-section" import { BlackSection } from "./black-section" -import { BlackWaitlistSection } from "./black-waitlist-section" import { Show } from "solid-js" import { createAsync, useParams } from "@solidjs/router" import { queryBillingInfo, querySessionInfo } from "../../common" @@ -17,12 +16,9 @@ export default function () {
- + - - - diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx index e25e09645..fe308dbb0 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/index.tsx @@ -1,4 +1,4 @@ -import { Show, createMemo } from "solid-js" +import { Match, Show, Switch, createMemo } from "solid-js" import { createStore } from "solid-js/store" import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router" import { NewUserSection } from "./new-user-section" @@ -43,9 +43,8 @@ export default function () { - + - } - > - - Current balance ${balance()} - - + + + + Current balance ${balance()} + + +

diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx index ddd3fd3e6..b5a90f749 100644 --- a/packages/console/app/src/routes/workspace/common.tsx +++ b/packages/console/app/src/routes/workspace/common.tsx @@ -113,6 +113,7 @@ export const queryBillingInfo = query(async (workspaceID: string) => { subscriptionID: billing.subscriptionID, subscriptionPlan: billing.subscriptionPlan, timeSubscriptionBooked: billing.timeSubscriptionBooked, + timeSubscriptionSelected: billing.timeSubscriptionSelected, } }, workspaceID) }, "billing.get") -- cgit v1.2.3