Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { StripeRedirectErrorPage } from "../../../_components/StripeRedirectErro
import {
getBillingCheckoutUrl,
getCryptoTopupUrl,
getInvoicePaymentUrl,
} from "../../../utils/billing";

export default async function CheckoutPage(props: {
Expand All @@ -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 <StripeRedirectErrorPage errorMessage="Invalid amount" />;
}
const topupUrl = await getCryptoTopupUrl({
teamSlug: params.team_slug,
amountUSD,
});
if (!topupUrl) {
// TODO: make a better error page
return (
<StripeRedirectErrorPage errorMessage="Failed to load topup page" />
switch (params.sku) {
case "topup": {
const amountUSD = Number.parseInt(
(await props.searchParams).amount || "10",
);
if (Number.isNaN(amountUSD)) {
return <StripeRedirectErrorPage errorMessage="Invalid amount" />;
}
const topupUrl = await getCryptoTopupUrl({
teamSlug: params.team_slug,
amountUSD,
});
if (!topupUrl) {
// TODO: make a better error page
return (
<StripeRedirectErrorPage errorMessage="Failed to load topup page" />
);
}
redirect(topupUrl);
break;
}
redirect(topupUrl);
return null;
}
case "invoice": {
const invoiceId = (await props.searchParams).invoice_id;
if (!invoiceId) {
return <StripeRedirectErrorPage errorMessage="Invalid invoice ID" />;
}
const invoice = await getInvoicePaymentUrl({
teamSlug: params.team_slug,
invoiceId,
});
if (!invoice) {
return (
<StripeRedirectErrorPage errorMessage="Failed to load invoice payment page" />
);
}
redirect(invoice);
break;
}
default: {
const billingUrl = await getBillingCheckoutUrl({
teamSlug: params.team_slug,
sku: decodeURIComponent(params.sku) as Exclude<ProductSKU, null>,
});

const billingUrl = await getBillingCheckoutUrl({
teamSlug: params.team_slug,
sku: decodeURIComponent(params.sku) as Exclude<ProductSKU, null>,
});
if (!billingUrl) {
return (
<StripeRedirectErrorPage errorMessage="Failed to load checkout page" />
);
}

if (!billingUrl) {
return (
<StripeRedirectErrorPage errorMessage="Failed to load checkout page" />
);
redirect(billingUrl);
break;
}
}

redirect(billingUrl);

return null;
}
43 changes: 42 additions & 1 deletion apps/dashboard/src/app/(app)/(stripe)/utils/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -156,3 +159,41 @@ export async function getCryptoTopupUrl(options: {

return json.result as string;
}

export async function getInvoicePaymentUrl(options: {
teamSlug: string;
invoiceId: string;
}): Promise<string | undefined> {
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -119,7 +120,8 @@ export function CreditBalanceSection({
prefetch={false}
target="_blank"
>
Top Up Credits
<ThirdwebMiniLogo className="mr-2 h-4 w-4" />
Top Up With Crypto
<ArrowRightIcon className="ml-2 h-4 w-4" />
</Link>
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -71,7 +70,7 @@ export default async function Page(props: {
</div>

{/* Credit Balance Section */}
{searchParams.showCreditBalance === "true" && team.stripeCustomerId && (
{team.stripeCustomerId && (
<CreditBalanceSection
teamSlug={team.slug}
balancePromise={getStripeBalance(team.stripeCustomerId)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ import {
DownloadIcon,
ReceiptIcon,
} from "lucide-react";
import Link from "next/link";
import { useQueryState } from "nuqs";
import { useTransition } from "react";
import type Stripe from "stripe";
import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo";
import { searchParams } from "../search-params";

export function BillingHistory(props: {
teamSlug: string;
invoices: Stripe.Invoice[];
status: "all" | "past_due" | "open";
hasMore: boolean;
Expand Down Expand Up @@ -61,7 +64,7 @@ export function BillingHistory(props: {
// we treate "uncollectible" as unpaid
case "uncollectible": {
// if the invoice due date is in the past, we want to display it as past due
if (invoice.due_date && invoice.due_date < Date.now()) {
if (invoice.due_date && invoice.due_date * 1000 < Date.now()) {
return <Badge variant="destructive">Past Due</Badge>;
}
return <Badge variant="outline">Open</Badge>;
Expand Down Expand Up @@ -122,19 +125,33 @@ export function BillingHistory(props: {
<TableCell>{getStatusBadge(invoice)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
{invoice.status === "open" &&
invoice.hosted_invoice_url && (
{invoice.status === "open" && (
<>
{/* always show the crypto payment button */}
<Button variant="default" size="sm" asChild>
<a
href={invoice.hosted_invoice_url}
<Link
target="_blank"
rel="noopener noreferrer"
href={`/checkout/${props.teamSlug}/invoice?invoice_id=${invoice.id}`}
>
<CreditCardIcon className="mr-2 h-4 w-4 text-muted-foreground" />
Pay Now
</a>
<ThirdwebMiniLogo className="mr-2 h-4 w-4" />
Pay with crypto
</Link>
</Button>
)}
{/* if we have a hosted invoice url, show that */}
{invoice.hosted_invoice_url && (
<Button variant="outline" size="sm" asChild>
<a
href={invoice.hosted_invoice_url}
target="_blank"
rel="noopener noreferrer"
>
<CreditCardIcon className="mr-2 h-4 w-4" />
Pay with Card
</a>
</Button>
)}
</>
)}

{invoice.invoice_pdf && (
<Button variant="ghost" size="sm" asChild>
Expand All @@ -143,7 +160,7 @@ export function BillingHistory(props: {
target="_blank"
rel="noopener noreferrer"
>
<DownloadIcon className="mr-2 h-4 w-4 text-muted-foreground" />
<DownloadIcon className="mr-2 h-4 w-4 " />
PDF
</a>
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default async function Page(props: {
<BillingFilter />
</div>
<BillingHistory
teamSlug={params.team_slug}
invoices={invoices.data}
hasMore={invoices.has_more}
// fall back to "all" if the status is not set
Expand Down
Loading