diff options
| author | Jay V <[email protected]> | 2025-09-16 16:16:30 -0400 |
|---|---|---|
| committer | Jay V <[email protected]> | 2025-09-16 16:16:30 -0400 |
| commit | 4b1eca73eb64c62ebdf668eb18d587510066bd9d (patch) | |
| tree | 19be45b8df61b5db1514887e4307c2bc17f6b19d /cloud/app/src/component/workspace/billing-section.tsx | |
| parent | fffcf69cd45ce98cee37b7bf455a81a69f0fe83c (diff) | |
| download | opencode-4b1eca73eb64c62ebdf668eb18d587510066bd9d.tar.gz opencode-4b1eca73eb64c62ebdf668eb18d587510066bd9d.zip | |
ignore: zen
Diffstat (limited to 'cloud/app/src/component/workspace/billing-section.tsx')
| -rw-r--r-- | cloud/app/src/component/workspace/billing-section.tsx | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/cloud/app/src/component/workspace/billing-section.tsx b/cloud/app/src/component/workspace/billing-section.tsx new file mode 100644 index 000000000..4bc4d4225 --- /dev/null +++ b/cloud/app/src/component/workspace/billing-section.tsx @@ -0,0 +1,153 @@ +import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router" +import { createEffect, createMemo, createSignal, For, Show } from "solid-js" +import { Billing } from "@opencode/cloud-core/billing.js" +import { withActor } from "~/context/auth.withActor" +import { IconCreditCard } from "~/component/icon" + +const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { + "use server" + return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID) +}, "checkoutUrl") + +const reload = action(async (form: FormData) => { + "use server" + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json(await withActor(() => Billing.reload(), workspaceID), { revalidate: getBillingInfo.key }) +}, "billing.reload") + +const disableReload = action(async (form: FormData) => { + "use server" + const workspaceID = form.get("workspaceID")?.toString() + if (!workspaceID) return { error: "Workspace ID is required" } + return json(await withActor(() => Billing.disableReload(), workspaceID), { revalidate: getBillingInfo.key }) +}, "billing.disableReload") + +const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => { + "use server" + return withActor(() => Billing.generateSessionUrl({ returnUrl }), workspaceID) +}, "sessionUrl") + +const getBillingInfo = query(async (workspaceID: string) => { + "use server" + return withActor(async () => { + return await Billing.get() + }, workspaceID) +}, "billing.get") + +export function BillingSection() { + const params = useParams() + const balanceInfo = createAsync(() => getBillingInfo(params.id)) + const createCheckoutUrlAction = useAction(createCheckoutUrl) + const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl) + const createSessionUrlAction = useAction(createSessionUrl) + const createSessionUrlSubmission = useSubmission(createSessionUrl) + const disableReloadSubmission = useSubmission(disableReload) + const reloadSubmission = useSubmission(reload) + + const balanceAmount = createMemo(() => { + return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2) + }) + + return ( + <section data-component="billing-section"> + <div data-slot="section-title"> + <h2>Billing</h2> + <p> + Manage payments methods. <a href="mailto:[email protected]">Contact us</a> if you have any questions. + </p> + </div> + <div data-slot="section-content"> + <Show when={balanceInfo()?.reloadError}> + <div data-slot="reload-error"> + <p> + Reload failed at{" "} + {balanceInfo()?.timeReloadError!.toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + })} + . Reason: {balanceInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try + again. + </p> + <form action={reload} method="post" data-slot="create-form"> + <input type="hidden" name="workspaceID" value={params.id} /> + <button data-color="primary" type="submit" disabled={reloadSubmission.pending}> + {reloadSubmission.pending ? "Reloading..." : "Reload"} + </button> + </form> + </div> + </Show> + <div data-slot="payment"> + <div data-slot="credit-card"> + <div data-slot="card-icon"> + <IconCreditCard style={{ width: "32px", height: "32px" }} /> + </div> + <div data-slot="card-details"> + <Show when={balanceInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}> + <span data-slot="secret">••••</span> + <span data-slot="number">{balanceInfo()?.paymentMethodLast4}</span> + </Show> + </div> + </div> + <div data-slot="button-row"> + <Show + when={balanceInfo()?.reload} + fallback={ + <button + data-color="primary" + disabled={createCheckoutUrlSubmission.pending} + onClick={async () => { + const baseUrl = window.location.href + const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) + if (checkoutUrl) { + window.location.href = checkoutUrl + } + }} + > + {createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"} + </button> + } + > + <button + data-color="primary" + disabled={createSessionUrlSubmission.pending} + onClick={async () => { + const baseUrl = window.location.href + const sessionUrl = await createSessionUrlAction(params.id, baseUrl) + if (sessionUrl) { + window.location.href = sessionUrl + } + }} + > + {createSessionUrlSubmission.pending ? "Loading..." : "Manage Payment Methods"} + </button> + <form action={disableReload} method="post" data-slot="create-form"> + <input type="hidden" name="workspaceID" value={params.id} /> + <button data-color="ghost" type="submit" disabled={disableReloadSubmission.pending}> + {disableReloadSubmission.pending ? "Disabling..." : "Disable"} + </button> + </form> + </Show> + </div> + </div> + <div data-slot="usage"> + <Show when={!balanceInfo()?.reload && !(balanceAmount() === "0.00" || balanceAmount() === "-0.00")}> + <p> + You have <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b> remaining in + your account. You can continue using the API with your remaining balance. + </p> + </Show> + <Show when={balanceInfo()?.reload && !balanceInfo()?.reloadError}> + <p> + Your current balance is <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b> + . We'll automatically reload <b>$20</b> (+$1.23 processing fee) when it reaches <b>$5</b>. + </p> + </Show> + </div> + </div> + </section> + ) +} |
