diff options
| author | Jay V <[email protected]> | 2025-09-15 19:02:47 -0400 |
|---|---|---|
| committer | Jay V <[email protected]> | 2025-09-15 19:02:51 -0400 |
| commit | 74f9fcea8829d1fa7cb7c1f87230718605e54358 (patch) | |
| tree | 9753510129a2e1dd9225b25634e7c8964bf07489 /cloud | |
| parent | bc213e1a619027b4cd91bda267b0c55f80ff453a (diff) | |
| download | opencode-74f9fcea8829d1fa7cb7c1f87230718605e54358.tar.gz opencode-74f9fcea8829d1fa7cb7c1f87230718605e54358.zip | |
ignore: zen
Diffstat (limited to 'cloud')
| -rw-r--r-- | cloud/app/src/routes/workspace/[id].css | 181 | ||||
| -rw-r--r-- | cloud/app/src/routes/workspace/[id].tsx | 291 |
2 files changed, 306 insertions, 166 deletions
diff --git a/cloud/app/src/routes/workspace/[id].css b/cloud/app/src/routes/workspace/[id].css index 3f4db713e..b08ae24d7 100644 --- a/cloud/app/src/routes/workspace/[id].css +++ b/cloud/app/src/routes/workspace/[id].css @@ -269,9 +269,39 @@ } } - /* Balance Section */ - [data-component="balance-section"] { - [data-slot="balance"] { + /* Billing Section */ + [data-component="billing-section"] { + [data-slot="section-content"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + } + + [data-slot="reload-error"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + + p { + color: var(--color-danger); + font-size: var(--font-size-sm); + line-height: 1.4; + margin: 0; + flex: 1; + } + + [data-slot="create-form"] { + display: flex; + gap: var(--space-2); + margin: 0; + flex-shrink: 0; + } + } + [data-slot="payment"] { display: flex; flex-direction: column; gap: var(--space-3); @@ -281,23 +311,93 @@ min-width: 14.5rem; width: fit-content; - [data-slot="amount"] { + [data-slot="credit-card"] { padding: var(--space-3-5) var(--space-4); background-color: var(--color-bg-surface); border-radius: var(--border-radius-sm); display: flex; - align-items: baseline; - gap: var(--space-1); - justify-content: flex-end; + align-items: center; + justify-content: space-between; - &[data-state="danger"] { - [data-slot="value"] { - color: var(--color-danger); + [data-slot="card-icon"] { + display: flex; + align-items: center; + } + + [data-slot="card-details"] { + display: flex; + align-items: baseline; + gap: var(--space-1); + + [data-slot="secret"] { + position: relative; + bottom: 2px; + font-size: var(--font-size-lg); + color: var(--color-text-muted); + font-weight: 400; } - [data-slot="currency"] { - color: var(--color-danger); + + [data-slot="number"] { + font-size: var(--font-size-3xl); + font-weight: 500; + color: var(--color-text); } } + } + + [data-slot="button-row"] { + display: flex; + gap: var(--space-2); + align-items: center; + + [data-slot="create-form"] { + margin: 0; + } + + /* Make Enable Billing button full width when it's the only button */ + > button { + flex: 1; + } + } + } + [data-slot="usage"] { + p { + font-size: var(--font-size-sm); + line-height: 1.5; + color: var(--color-text-secondary); + b { + font-weight: 600; + } + } + } + } + + /* Monthly Limit Section */ + [data-component="monthly-limit-section"] { + [data-slot="section-content"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + } + + [data-slot="balance"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + padding: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + min-width: 15rem; + width: fit-content; + + [data-slot="amount"] { + padding: var(--space-3-5) var(--space-4); + background-color: var(--color-bg-surface); + border-radius: var(--border-radius-sm); + display: flex; + align-items: baseline; + gap: var(--space-1); + justify-content: flex-end; [data-slot="currency"] { position: relative; @@ -313,6 +413,63 @@ color: var(--color-text); } } + + [data-slot="create-form"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + margin-top: var(--space-1); + + [data-slot="input-container"] { + display: flex; + flex-direction: column; + gap: var(--space-1); + } + + @media (max-width: 30rem) { + gap: var(--space-2); + } + + input { + flex: 1; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-mono); + + &:focus { + outline: none; + border-color: var(--color-accent); + } + + &::placeholder { + color: var(--color-text-disabled); + } + } + + [data-slot="form-actions"] { + display: flex; + gap: var(--space-2); + justify-content: flex-end; + } + + [data-slot="form-error"] { + color: var(--color-danger); + font-size: var(--font-size-sm); + margin-top: var(--space-1); + line-height: 1.4; + } + } + } + + [data-slot="usage-status"] { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + margin: 0; + line-height: 1.4; } } diff --git a/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx index 5844f1af4..4d525b33d 100644 --- a/cloud/app/src/routes/workspace/[id].tsx +++ b/cloud/app/src/routes/workspace/[id].tsx @@ -278,140 +278,122 @@ function KeyCreateForm() { ) } -function BalanceSection() { +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="balance-section"> + <section data-component="billing-section"> <div data-slot="section-title"> - <h2>Balance</h2> - <p>Add credits to your account.</p> + <h2>Billing</h2> + <p>Manage the payment method for your account.</p> </div> - <div data-slot="balance"> - <div - data-slot="amount" - data-state={(() => { - const balanceStr = ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2) - return balanceStr === "0.00" || balanceStr === "-0.00" ? "danger" : undefined - })()} - > - <span data-slot="currency">$</span> - <span data-slot="value"> - {(() => { - const balanceStr = ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2) - return balanceStr === "-0.00" ? "0.00" : balanceStr - })()} - </span> - </div> - <Show - when={balanceInfo()?.reload} - fallback={ - <> + <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={createCheckoutUrlSubmission.pending} + disabled={createSessionUrlSubmission.pending} onClick={async () => { const baseUrl = window.location.href - const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl) - if (checkoutUrl) { - window.location.href = checkoutUrl + const sessionUrl = await createSessionUrlAction(params.id, baseUrl) + if (sessionUrl) { + window.location.href = sessionUrl } }} > - {createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"} + {createSessionUrlSubmission.pending ? "Loading..." : "Manage Payment Methods"} </button> - <p>You can continue using the API with the remaining credits.</p> - </> - } - > - <> - <div> - <p> - You will be automatically reloading <b>$20</b> (+$1.23 processing fee) when your balance reaches{" "} - <b>$5</b>. - </p> - <p>You will be able to continue using the API with the remaining credits after disabling billing.</p> <form action={disableReload} method="post" data-slot="create-form"> <input type="hidden" name="workspaceID" value={params.id} /> - <button data-color="primary" type="submit" disabled={disableReloadSubmission.pending}> - {disableReloadSubmission.pending ? "Disabling..." : "Disable Billing"} + <button data-color="ghost" type="submit" disabled={disableReloadSubmission.pending}> + {disableReloadSubmission.pending ? "Disabling..." : "Disable"} </button> </form> - <Show when={balanceInfo()?.reloadError}> - <> - <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..." : "Retry Reload"} - </button> - </form> - </> - </Show> - </div> - </> - </Show> - </div> - <Show when={balanceInfo()?.reload}> - <BalancePaymentForm /> - <BalanceLimitForm /> - </Show> - </section> - ) -} - -function BalancePaymentForm() { - const params = useParams() - const createSessionUrlAction = useAction(createSessionUrl) - const createSessionUrlSubmission = useSubmission(createSessionUrl) - const balanceInfo = createAsync(() => getBillingInfo(params.id)) - - return ( - <> - <div data-slot="section-title"> - <h2>Payment Method</h2> - </div> - <div data-slot="balance"> - <div data-slot="amount"> - <IconCreditCard style={{ width: "32px", height: "32px" }} /> - <span data-slot="currency">••••</span> - <span data-slot="value">{balanceInfo()?.paymentMethodLast4}</span> + </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> + . It'll be reloaded to <b>$20</b> (+$1.23 processing fee) when it reaches <b>$5</b>. + </p> + </Show> </div> - <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> </div> - </> + </section> ) } -function BalanceLimitForm() { +function MonthlyLimitSection() { const params = useParams() const submission = useSubmission(setMonthlyLimit) const [store, setStore] = createStore({ show: false }) @@ -444,19 +426,47 @@ function BalanceLimitForm() { } return ( - <> + <section data-component="monthly-limit-section"> <div data-slot="section-title"> <h2>Monthly Limit</h2> + <p>Set a monthly spending limit for your account.</p> </div> - <div data-slot="balance"> - <div data-slot="amount"> - <span data-slot="currency">$</span> - <span data-slot="value">{balanceInfo()?.monthlyLimit ?? "-"}</span> + <div data-slot="section-content"> + <div data-slot="balance"> + <div data-slot="amount"> + {balanceInfo()?.monthlyLimit ? <span data-slot="currency">$</span> : null} + <span data-slot="value">{balanceInfo()?.monthlyLimit ?? "-"}</span> + </div> + <Show + when={!store.show} + fallback={ + <form action={setMonthlyLimit} method="post" data-slot="create-form"> + <div data-slot="input-container"> + <input ref={(r) => (input = r)} data-component="input" name="limit" type="number" placeholder="50" /> + <Show when={submission.result && submission.result.error}> + {(err) => <div data-slot="form-error">{err()}</div>} + </Show> + </div> + <input type="hidden" name="workspaceID" value={params.id} /> + <div data-slot="form-actions"> + <button type="reset" data-color="ghost" onClick={() => hide()}> + Cancel + </button> + <button type="submit" data-color="primary" disabled={submission.pending}> + {submission.pending ? "Setting..." : "Set"} + </button> + </div> + </form> + } + > + <button data-color="primary" onClick={() => show()}> + {balanceInfo()?.monthlyLimit ? "Edit Limit" : "Set Limit"} + </button> + </Show> </div> - <Show when={balanceInfo()?.monthlyLimit} fallback={<p>No spending limit set.</p>}> - <p> - Current usage for the month of {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })}{" "} - is $ + <Show when={balanceInfo()?.monthlyLimit} fallback={<p data-slot="usage-status">No spending limit set.</p>}> + <p data-slot="usage-status"> + Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $ {(() => { const dateLastUsed = balanceInfo()?.timeMonthlyUsageUpdated if (!dateLastUsed) return "0" @@ -474,42 +484,11 @@ function BalanceLimitForm() { if (current !== lastUsed) return "0" return ((balanceInfo()?.monthlyUsage ?? 0) / 100000000).toFixed(2) })()} + . </p> </Show> </div> - <Show - when={store.show} - fallback={ - <button data-color="primary" onClick={() => show()}> - {balanceInfo()?.monthlyLimit ? "Edit Spending Limit" : "Set Spending Limit"} - </button> - } - > - <form action={setMonthlyLimit} method="post" data-slot="create-form"> - <div data-slot="input-container"> - <input - ref={(r) => (input = r)} - data-component="input" - name="limit" - type="number" - placeholder="Enter limit" - /> - <Show when={submission.result && submission.result.error}> - {(err) => <div data-slot="form-error">{err()}</div>} - </Show> - </div> - <input type="hidden" name="workspaceID" value={params.id} /> - <div data-slot="form-actions"> - <button type="reset" data-color="ghost" onClick={() => hide()}> - Cancel - </button> - <button type="submit" data-color="primary" disabled={submission.pending}> - {submission.pending ? "Setting..." : "Set"} - </button> - </div> - </form> - </Show> - </> + </section> ) } @@ -570,7 +549,6 @@ function UsageSection() { function PaymentSection() { const params = useParams() const payments = createAsync(() => getPaymentsInfo(params.id)) - console.log("!#!@", payments()) return ( payments() && @@ -675,13 +653,13 @@ function NewUserSection() { <div data-component="next-steps"> <ol> + <li>Enable billing</li> <li> Run <code>opencode auth login</code> and select opencode </li> <li>Paste your API key</li> - <li>Start opencode</li> <li> - Run <code>/models</code> to see available models + Start opencode and run <code>/models</code> to select a model </li> </ol> </div> @@ -690,7 +668,9 @@ function NewUserSection() { ) } -export default function () { +export default function() { + const params = useParams() + return ( <div data-page="workspace-[id]"> <section data-component="title-section"> @@ -707,7 +687,10 @@ export default function () { <div data-slot="sections"> <NewUserSection /> <KeySection /> - <BalanceSection /> + <BillingSection /> + <Show when={createAsync(() => getBillingInfo(params.id))()?.reload}> + <MonthlyLimitSection /> + </Show> <UsageSection /> <PaymentSection /> </div> |
