From 3b3a225e2b839e5cf1cfdec7e065a4c385f71c9f Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Tue, 3 Jun 2025 23:52:07 -0700 Subject: [PATCH] [Dashboard] Add crypto payment option for invoices --- .../checkout/[team_slug]/[sku]/page.tsx | 80 ++++++++++++------- .../src/app/(app)/(stripe)/utils/billing.ts | 43 +++++++++- .../credit-balance-section.client.tsx | 4 +- .../(team)/~/settings/billing/page.tsx | 3 +- .../invoices/components/billing-history.tsx | 39 ++++++--- .../(team)/~/settings/invoices/page.tsx | 1 + 6 files changed, 126 insertions(+), 44 deletions(-) diff --git a/apps/dashboard/src/app/(app)/(stripe)/checkout/[team_slug]/[sku]/page.tsx b/apps/dashboard/src/app/(app)/(stripe)/checkout/[team_slug]/[sku]/page.tsx index 039f4844f66..45258c7c459 100644 --- a/apps/dashboard/src/app/(app)/(stripe)/checkout/[team_slug]/[sku]/page.tsx +++ b/apps/dashboard/src/app/(app)/(stripe)/checkout/[team_slug]/[sku]/page.tsx @@ -4,6 +4,7 @@ import { StripeRedirectErrorPage } from "../../../_components/StripeRedirectErro import { getBillingCheckoutUrl, getCryptoTopupUrl, + getInvoicePaymentUrl, } from "../../../utils/billing"; export default async function CheckoutPage(props: { @@ -13,44 +14,65 @@ export default async function CheckoutPage(props: { }>; searchParams: Promise<{ amount?: string; + invoice_id?: string; }>; }) { const params = await props.params; - // special case for crypto topup - if (params.sku === "topup") { - const amountUSD = Number.parseInt( - (await props.searchParams).amount || "10", - ); - if (Number.isNaN(amountUSD)) { - return ; - } - const topupUrl = await getCryptoTopupUrl({ - teamSlug: params.team_slug, - amountUSD, - }); - if (!topupUrl) { - // TODO: make a better error page - return ( - + switch (params.sku) { + case "topup": { + const amountUSD = Number.parseInt( + (await props.searchParams).amount || "10", ); + if (Number.isNaN(amountUSD)) { + return ; + } + const topupUrl = await getCryptoTopupUrl({ + teamSlug: params.team_slug, + amountUSD, + }); + if (!topupUrl) { + // TODO: make a better error page + return ( + + ); + } + redirect(topupUrl); + break; } - redirect(topupUrl); - return null; - } + case "invoice": { + const invoiceId = (await props.searchParams).invoice_id; + if (!invoiceId) { + return ; + } + const invoice = await getInvoicePaymentUrl({ + teamSlug: params.team_slug, + invoiceId, + }); + if (!invoice) { + return ( + + ); + } + redirect(invoice); + break; + } + default: { + const billingUrl = await getBillingCheckoutUrl({ + teamSlug: params.team_slug, + sku: decodeURIComponent(params.sku) as Exclude, + }); - const billingUrl = await getBillingCheckoutUrl({ - teamSlug: params.team_slug, - sku: decodeURIComponent(params.sku) as Exclude, - }); + if (!billingUrl) { + return ( + + ); + } - if (!billingUrl) { - return ( - - ); + redirect(billingUrl); + break; + } } - redirect(billingUrl); - return null; } diff --git a/apps/dashboard/src/app/(app)/(stripe)/utils/billing.ts b/apps/dashboard/src/app/(app)/(stripe)/utils/billing.ts index 35ffc479e62..a3df8501353 100644 --- a/apps/dashboard/src/app/(app)/(stripe)/utils/billing.ts +++ b/apps/dashboard/src/app/(app)/(stripe)/utils/billing.ts @@ -131,7 +131,10 @@ export async function getCryptoTopupUrl(options: { } const res = await fetch( - `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamSlug}/checkout/crypto-top-up`, + new URL( + `/v1/teams/${options.teamSlug}/checkout/crypto-top-up`, + NEXT_PUBLIC_THIRDWEB_API_HOST, + ), { method: "POST", body: JSON.stringify({ @@ -156,3 +159,41 @@ export async function getCryptoTopupUrl(options: { return json.result as string; } + +export async function getInvoicePaymentUrl(options: { + teamSlug: string; + invoiceId: string; +}): Promise { + const token = await getAuthToken(); + if (!token) { + return undefined; + } + const res = await fetch( + new URL( + `/v1/teams/${options.teamSlug}/checkout/crypto-pay-invoice`, + NEXT_PUBLIC_THIRDWEB_API_HOST, + ), + { + method: "POST", + body: JSON.stringify({ + invoiceId: options.invoiceId, + }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }, + ); + + if (!res.ok) { + return undefined; + } + + const json = await res.json(); + + if (!json.result) { + return undefined; + } + + return json.result as string; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx index 2843273dbe3..bdd4b9d0328 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/components/credit-balance-section.client.tsx @@ -16,6 +16,7 @@ import { ArrowRightIcon, DollarSignIcon } from "lucide-react"; import Link from "next/link"; import { Suspense, use, useState } from "react"; import { ErrorBoundary } from "react-error-boundary"; +import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo"; const predefinedAmounts = [ { value: "25", label: "$25" }, @@ -119,7 +120,8 @@ export function CreditBalanceSection({ prefetch={false} target="_blank" > - Top Up Credits + + Top Up With Crypto diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx index 809069a4f84..148a361eda2 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/billing/page.tsx @@ -17,7 +17,6 @@ export default async function Page(props: { searchParams: Promise<{ showPlans?: string | string[]; highlight?: string | string[]; - showCreditBalance?: string | string[]; }>; }) { const [params, searchParams] = await Promise.all([ @@ -71,7 +70,7 @@ export default async function Page(props: { {/* Credit Balance Section */} - {searchParams.showCreditBalance === "true" && team.stripeCustomerId && ( + {team.stripeCustomerId && ( Past Due; } return Open; @@ -122,19 +125,33 @@ export function BillingHistory(props: { {getStatusBadge(invoice)}
- {invoice.status === "open" && - invoice.hosted_invoice_url && ( + {invoice.status === "open" && ( + <> + {/* always show the crypto payment button */} - )} + {/* if we have a hosted invoice url, show that */} + {invoice.hosted_invoice_url && ( + + )} + + )} {invoice.invoice_pdf && ( diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx index 2d11eb68344..d3ade359e52 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/invoices/page.tsx @@ -49,6 +49,7 @@ export default async function Page(props: {