diff options
| author | Frank <[email protected]> | 2025-10-16 14:59:44 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-10-16 14:59:46 -0400 |
| commit | 7ec5e49e19122f53d99836bdda0d7a98eb61c868 (patch) | |
| tree | 6ee0dceb9ff90aa580491607fd65ee783985cc5a /packages/console/app/src | |
| parent | 1c1380d3c8163393c7133981d43c9ec5abf4da43 (diff) | |
| download | opencode-7ec5e49e19122f53d99836bdda0d7a98eb61c868.tar.gz opencode-7ec5e49e19122f53d99836bdda0d7a98eb61c868.zip | |
zen: support stripe link
Diffstat (limited to 'packages/console/app/src')
4 files changed, 54 insertions, 9 deletions
diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index fc27ef3b4..0395cad52 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -100,6 +100,17 @@ export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) { ) } +export function IconStripe(props: JSX.SvgSVGAttributes<SVGSVGElement>) { + return ( + <svg {...props} viewBox="0 0 24 24"> + <path + fill="currentColor" + d="M15.827 12.506c0 .672-.31 1.175-.771 1.175-.293 0-.468-.106-.589-.237l-.007-1.855c.13-.143.31-.247.596-.247.456-.001.771.51.771 1.164zm3.36-1.253c-.312 0-.659.236-.659.798h1.291c0-.562-.325-.798-.632-.798zm4.813-5.253v12c0 1.104-.896 2-2 2h-20c-1.104 0-2-.896-2-2v-12c0-1.104.896-2 2-2h20c1.104 0 2 .896 2 2zm-17.829 7.372c0-1.489-1.909-1.222-1.909-1.784 0-.195.162-.271.424-.271.38 0 .862.116 1.242.321v-1.176c-.414-.165-.827-.228-1.241-.228-1.012.001-1.687.53-1.687 1.414 0 1.382 1.898 1.158 1.898 1.754 0 .231-.201.305-.479.305-.414 0-.947-.171-1.366-.399v1.192c.464.2.935.283 1.365.283 1.038.001 1.753-.512 1.753-1.411zm2.422-3.054h-.949l-.001-1.084-1.219.259-.005 4.006c0 .739.556 1.285 1.297 1.285.408 0 .71-.074.876-.165v-1.016c-.16.064-.948.293-.948-.443v-1.776h.948v-1.066zm2.596 0c-.166-.06-.75-.169-1.042.369l-.078-.369h-1.079v4.377h1.248v-2.967c.295-.388.793-.313.952-.262v-1.148zm1.554 0h-1.253v4.377h1.253v-4.377zm0-1.664l-1.253.266v1.017l1.253-.266v-1.017zm4.314 3.824c0-1.454-.826-2.244-1.703-2.243-.489 0-.805.23-.978.392l-.065-.309h-1.099v5.828l1.249-.265.003-1.413c.179.131.446.316.883.316.893 0 1.71-.719 1.71-2.306zm3.943.045c0-1.279-.619-2.288-1.805-2.288-1.188 0-1.911 1.01-1.911 2.281 0 1.506.852 2.267 2.068 2.267.597 0 1.045-.136 1.384-.324v-1.006c-.34.172-.731.276-1.227.276-.487 0-.915-.172-.971-.758h2.444l.018-.448z" + /> + </svg> + ) +} + export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) { return ( <svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg"> diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts index c12f47afc..f1fa73c91 100644 --- a/packages/console/app/src/routes/stripe/webhook.ts +++ b/packages/console/app/src/routes/stripe/webhook.ts @@ -32,7 +32,8 @@ export async function POST(input: APIEvent) { .update(BillingTable) .set({ paymentMethodID, - paymentMethodLast4: paymentMethod.card!.last4, + paymentMethodLast4: paymentMethod.card?.last4 ?? null, + paymentMethodType: paymentMethod.type, }) .where(eq(BillingTable.customerID, customerID)) }) @@ -77,7 +78,8 @@ export async function POST(input: APIEvent) { balance: sql`${BillingTable.balance} + ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`, customerID, paymentMethodID: paymentMethod.id, - paymentMethodLast4: paymentMethod.card!.last4, + paymentMethodLast4: paymentMethod.card?.last4 ?? null, + paymentMethodType: paymentMethod.type, reload: true, reloadError: null, timeReloadError: null, diff --git a/packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css index 123bb1c86..b562dcd45 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.module.css @@ -70,6 +70,12 @@ font-weight: 500; color: var(--color-text); } + + [data-slot="type"] { + font-size: var(--font-size-sm); + font-weight: 400; + color: var(--color-text-muted); + } } } diff --git a/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx index f9084bbf1..af4a47e48 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx @@ -1,8 +1,8 @@ import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router" -import { createMemo, Show } from "solid-js" +import { createMemo, Match, Show, Switch } from "solid-js" import { Billing } from "@opencode-ai/console-core/billing.js" import { withActor } from "~/context/auth.withActor" -import { IconCreditCard } from "~/component/icon" +import { IconCreditCard, IconStripe } from "~/component/icon" import styles from "./billing-section.module.css" import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js" import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js" @@ -61,6 +61,7 @@ export function BillingSection() { // Scenario 1: User has not added billing details and has no balance // const balanceInfo = () => ({ // balance: 0, + // paymentMethodType: null as string | null, // paymentMethodLast4: null as string | null, // reload: false, // reloadError: null as string | null, @@ -70,6 +71,7 @@ export function BillingSection() { // Scenario 2: User has not added billing details but has a balance // const balanceInfo = () => ({ // balance: 1500000000, // $15.00 + // paymentMethodType: null as string | null, // paymentMethodLast4: null as string | null, // reload: false, // reloadError: null as string | null, @@ -79,6 +81,7 @@ export function BillingSection() { // Scenario 3: User has added billing details (reload enabled) // const balanceInfo = () => ({ // balance: 750000000, // $7.50 + // paymentMethodType: "card", // paymentMethodLast4: "4242", // reload: true, // reloadError: null as string | null, @@ -88,12 +91,23 @@ export function BillingSection() { // Scenario 4: User has billing details but reload failed // const balanceInfo = () => ({ // balance: 250000000, // $2.50 + // paymentMethodType: "card", // paymentMethodLast4: "4242", // reload: true, // reloadError: "Your card was declined." as string, // timeReloadError: new Date(Date.now() - 3600000) as Date // 1 hour ago // }) + // Scenario 5: User has Link payment method + // const balanceInfo = () => ({ + // balance: 500000000, // $5.00 + // paymentMethodType: "link", + // paymentMethodLast4: null as string | null, + // reload: true, + // reloadError: null as string | null, + // timeReloadError: null as Date | null + // }) + const balanceAmount = createMemo(() => { return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2) }) @@ -136,13 +150,25 @@ export function BillingSection() { <div data-slot="payment"> <div data-slot="credit-card"> <div data-slot="card-icon"> - <IconCreditCard style={{ width: "32px", height: "32px" }} /> + <Switch fallback={<IconCreditCard style={{ width: "32px", height: "32px" }} />}> + <Match when={balanceInfo()?.paymentMethodType === "link"}> + <IconStripe style={{ width: "32px", height: "32px" }} /> + </Match> + </Switch> </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> + <Switch + fallback={ + <Show when={balanceInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}> + <span data-slot="secret">••••</span> + <span data-slot="number">{balanceInfo()?.paymentMethodLast4}</span> + </Show> + } + > + <Match when={balanceInfo()?.paymentMethodType === "link"}> + <span data-slot="type">Linked to Stripe</span> + </Match> + </Switch> </div> </div> <div data-slot="button-row"> |
