From bd44489ada70cf908b69466f623ca74e800b3fc7 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 19 Mar 2026 18:44:21 -0400 Subject: go: upi payment --- packages/console/app/src/component/icon.tsx | 13 +++ packages/console/app/src/component/modal.css | 1 + packages/console/app/src/routes/stripe/webhook.ts | 23 ++-- .../workspace/[id]/billing/billing-section.tsx | 5 +- .../workspace/[id]/billing/payment-section.tsx | 14 ++- .../workspace/[id]/go/lite-section.module.css | 39 ++++++- .../src/routes/workspace/[id]/go/lite-section.tsx | 127 +++++++++++++++------ 7 files changed, 173 insertions(+), 49 deletions(-) (limited to 'packages/console/app/src') diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index df7e067c2..0aaa302b3 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -76,6 +76,19 @@ export function IconAlipay(props: JSX.SvgSVGAttributes) { ) } +export function IconUpi(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} + export function IconWechat(props: JSX.SvgSVGAttributes) { return ( diff --git a/packages/console/app/src/component/modal.css b/packages/console/app/src/component/modal.css index 1f47f395d..e71fd1a19 100644 --- a/packages/console/app/src/component/modal.css +++ b/packages/console/app/src/component/modal.css @@ -62,5 +62,6 @@ font-size: var(--font-size-lg); font-weight: 600; color: var(--color-text); + text-align: center; } } diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts index 95cd9da21..47fee05cf 100644 --- a/packages/console/app/src/routes/stripe/webhook.ts +++ b/packages/console/app/src/routes/stripe/webhook.ts @@ -244,6 +244,7 @@ export async function POST(input: APIEvent) { customerID, enrichment: { type: productID === LiteData.productID() ? "lite" : "subscription", + currency: body.data.object.currency === "inr" ? "inr" : undefined, couponID, }, }), @@ -331,16 +332,17 @@ export async function POST(input: APIEvent) { ) if (!workspaceID) throw new Error("Workspace ID not found") - const amount = await Database.use((tx) => + const payment = await Database.use((tx) => tx .select({ amount: PaymentTable.amount, + enrichment: PaymentTable.enrichment, }) .from(PaymentTable) .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID))) - .then((rows) => rows[0]?.amount), + .then((rows) => rows[0]), ) - if (!amount) throw new Error("Payment not found") + if (!payment) throw new Error("Payment not found") await Database.transaction(async (tx) => { await tx @@ -350,12 +352,15 @@ export async function POST(input: APIEvent) { }) .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID))) - await tx - .update(BillingTable) - .set({ - balance: sql`${BillingTable.balance} - ${amount}`, - }) - .where(eq(BillingTable.workspaceID, workspaceID)) + // deduct balance only for top up + if (!payment.enrichment?.type) { + await tx + .update(BillingTable) + .set({ + balance: sql`${BillingTable.balance} - ${payment.amount}`, + }) + .where(eq(BillingTable.workspaceID, workspaceID)) + } }) } })() 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 50e30585b..4d9b0cabd 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 @@ -3,7 +3,7 @@ import { createMemo, Match, Show, Switch, createEffect } from "solid-js" import { createStore } from "solid-js/store" import { Billing } from "@opencode-ai/console-core/billing.js" import { withActor } from "~/context/auth.withActor" -import { IconAlipay, IconCreditCard, IconStripe, IconWechat } from "~/component/icon" +import { IconAlipay, IconCreditCard, IconStripe, IconUpi, IconWechat } from "~/component/icon" import styles from "./billing-section.module.css" import { createCheckoutUrl, formatBalance, queryBillingInfo } from "../../common" import { useI18n } from "~/context/i18n" @@ -211,6 +211,9 @@ export function BillingSection() { + + +
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx index 2311be321..6da5c42ed 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx @@ -6,6 +6,14 @@ import { formatDateUTC, formatDateForTable } from "../../common" import styles from "./payment-section.module.css" import { useI18n } from "~/context/i18n" +function money(amount: number, currency?: string) { + const formatter = + currency === "inr" + ? new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" }) + : new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }) + return formatter.format(amount / 100_000_000) +} + const getPaymentsInfo = query(async (workspaceID: string) => { "use server" return withActor(async () => { @@ -81,6 +89,10 @@ export function PaymentSection() { const date = new Date(payment.timeCreated) const amount = payment.enrichment?.type === "subscription" && payment.enrichment.couponID ? 0 : payment.amount + const currency = + payment.enrichment?.type === "subscription" || payment.enrichment?.type === "lite" + ? payment.enrichment.currency + : undefined return ( @@ -88,7 +100,7 @@ export function PaymentSection() { {payment.id} - ${((amount ?? 0) / 100000000).toFixed(2)} + {money(amount, currency)} {" "} diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css b/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css index a760753d0..05daf43b7 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.module.css @@ -188,8 +188,45 @@ line-height: 1.4; } + [data-slot="subscribe-actions"] { + display: flex; + align-items: center; + gap: var(--space-4); + margin-top: var(--space-4); + } + [data-slot="subscribe-button"] { - align-self: flex-start; + align-self: stretch; + } + + [data-slot="other-methods"] { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + } + + [data-slot="other-methods-icons"] { + display: inline-flex; + align-items: center; + gap: 4px; + } + + [data-slot="modal-actions"] { + display: flex; + gap: var(--space-3); margin-top: var(--space-4); + + button { + flex: 1; + } + } + + [data-slot="method-button"] { + display: flex; + align-items: center; + justify-content: flex-start; + gap: var(--space-2); + height: 48px; } } diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index ccdda5b45..4a64eb1b2 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -1,6 +1,7 @@ import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router" import { createStore } from "solid-js/store" import { createMemo, For, Show } from "solid-js" +import { Modal } from "~/component/modal" import { Billing } from "@opencode-ai/console-core/billing.js" import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js" import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js" @@ -14,6 +15,8 @@ import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" import { formError } from "~/lib/form-error" +import { IconAlipay, IconUpi } from "~/component/icon" + const queryLiteSubscription = query(async (workspaceID: string) => { "use server" return withActor(async () => { @@ -78,22 +81,25 @@ function formatResetTime(seconds: number, i18n: ReturnType) { return `${minutes} ${minutes === 1 ? i18n.t("workspace.lite.time.minute") : i18n.t("workspace.lite.time.minutes")}` } -const createLiteCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => { - "use server" - return json( - await withActor( - () => - Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl }) - .then((data) => ({ error: undefined, data })) - .catch((e) => ({ - error: e.message as string, - data: undefined, - })), - workspaceID, - ), - { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, - ) -}, "liteCheckoutUrl") +const createLiteCheckoutUrl = action( + async (workspaceID: string, successUrl: string, cancelUrl: string, method?: "alipay" | "upi") => { + "use server" + return json( + await withActor( + () => + Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl, method }) + .then((data) => ({ error: undefined, data })) + .catch((e) => ({ + error: e.message as string, + data: undefined, + })), + workspaceID, + ), + { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, + ) + }, + "liteCheckoutUrl", +) const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => { "use server" @@ -147,23 +153,30 @@ export function LiteSection() { const checkoutSubmission = useSubmission(createLiteCheckoutUrl) const useBalanceSubmission = useSubmission(setLiteUseBalance) const [store, setStore] = createStore({ - redirecting: false, + loading: undefined as undefined | "session" | "checkout" | "alipay" | "upi", + showModal: false, }) + const busy = createMemo(() => !!store.loading) + async function onClickSession() { + setStore("loading", "session") const result = await sessionAction(params.id!, window.location.href) if (result.data) { - setStore("redirecting", true) window.location.href = result.data + return } + setStore("loading", undefined) } - async function onClickSubscribe() { - const result = await checkoutAction(params.id!, window.location.href, window.location.href) + async function onClickSubscribe(method?: "alipay" | "upi") { + setStore("loading", method ?? "checkout") + const result = await checkoutAction(params.id!, window.location.href, window.location.href, method) if (result.data) { - setStore("redirecting", true) window.location.href = result.data + return } + setStore("loading", undefined) } return ( @@ -179,12 +192,8 @@ export function LiteSection() {

{i18n.t("workspace.lite.subscription.message")}

- @@ -282,16 +291,60 @@ export function LiteSection() {
  • MiniMax M2.7
  • {i18n.t("workspace.lite.promo.footer")}

    - +
    + + +
    + setStore("showModal", false)} title="Select payment method"> +
    + + +
    +
    -- cgit v1.2.3