diff --git a/apps/dashboard/src/@/actions/stripe-actions.ts b/apps/dashboard/src/@/actions/stripe-actions.ts index ed88ae191b5..8009cbc2386 100644 --- a/apps/dashboard/src/@/actions/stripe-actions.ts +++ b/apps/dashboard/src/@/actions/stripe-actions.ts @@ -23,7 +23,7 @@ function getStripe() { export async function getTeamInvoices( team: Team, - options?: { cursor?: string }, + options?: { cursor?: string; status?: "open" }, ) { try { const customerId = team.stripeCustomerId; @@ -37,6 +37,8 @@ export async function getTeamInvoices( customer: customerId, limit: 10, starting_after: options?.cursor, + // Only return open invoices if the status is open + status: options?.status, }); return invoices; diff --git a/apps/dashboard/src/@/components/billing.tsx b/apps/dashboard/src/@/components/billing.tsx index 8aa011b7d2a..d0137111d78 100644 --- a/apps/dashboard/src/@/components/billing.tsx +++ b/apps/dashboard/src/@/components/billing.tsx @@ -1,6 +1,8 @@ "use client"; import { useMutation } from "@tanstack/react-query"; +import { AlertTriangleIcon } from "lucide-react"; +import Link from "next/link"; import { toast } from "sonner"; import type { GetBillingCheckoutUrlAction, @@ -8,6 +10,7 @@ import type { GetBillingPortalUrlAction, GetBillingPortalUrlOptions, } from "../actions/billing"; +import type { Team } from "../api/team"; import { cn } from "../lib/utils"; import { Spinner } from "./ui/Spinner/Spinner"; import { Button, type ButtonProps } from "./ui/button"; @@ -16,6 +19,7 @@ type CheckoutButtonProps = Omit & { getBillingCheckoutUrl: GetBillingCheckoutUrlAction; buttonProps?: Omit; children: React.ReactNode; + billingStatus: Team["billingStatus"]; }; export function CheckoutButton({ @@ -25,6 +29,7 @@ export function CheckoutButton({ getBillingCheckoutUrl, children, buttonProps, + billingStatus, }: CheckoutButtonProps) { const getUrlMutation = useMutation({ mutationFn: async () => { @@ -40,35 +45,65 @@ export function CheckoutButton({ const errorMessage = "Failed to open checkout page"; return ( - + }, + }); + }} + > + {getUrlMutation.isPending && } + {children} + + + ); +} + +function BillingWarning({ teamSlug }: { teamSlug: string }) { + return ( +
+ +

+ You have outstanding invoices. Please{" "} + + pay them + {" "} + to continue. +

+
); } diff --git a/apps/dashboard/src/@/components/blocks/pricing-card.tsx b/apps/dashboard/src/@/components/blocks/pricing-card.tsx index b517441ea2a..0ef29a9949f 100644 --- a/apps/dashboard/src/@/components/blocks/pricing-card.tsx +++ b/apps/dashboard/src/@/components/blocks/pricing-card.tsx @@ -30,6 +30,7 @@ type PricingCardCta = { type PricingCardProps = { teamSlug: string; + billingStatus: Team["billingStatus"]; billingPlan: keyof typeof TEAM_PLANS; cta?: PricingCardCta; ctaHint?: string; @@ -41,6 +42,7 @@ type PricingCardProps = { export const PricingCard: React.FC = ({ teamSlug, + billingStatus, billingPlan, cta, highlighted = false, @@ -131,6 +133,7 @@ export const PricingCard: React.FC = ({
{billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && ( void; }) { @@ -154,6 +156,7 @@ function InviteModalContent(props: { const starterPlan = ( { + setStates({ + cursor: null, + // only set the status if it's "open", otherwise clear it + status: v === "open" ? "open" : null, + }); + }} + > + + + + + All Invoices + Open Invoices + + + ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx index ecade336024..d282f84d01e 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/components/billing-history.tsx @@ -25,6 +25,7 @@ import { searchParams } from "../search-params"; export function BillingHistory(props: { invoices: Stripe.Invoice[]; + status: "all" | "past_due" | "open"; hasMore: boolean; }) { const [isLoading, startTransition] = useTransition(); @@ -74,6 +75,14 @@ export function BillingHistory(props: { }; if (props.invoices.length === 0) { + if (props.status === "open") { + return ( +
+ +

No open invoices

+
+ ); + } return (
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/page.tsx index 9e7621817b7..2d11eb68344 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/page.tsx @@ -3,6 +3,7 @@ import { getTeamBySlug } from "@/api/team"; import { redirect } from "next/navigation"; import type { SearchParams } from "nuqs/server"; import { getValidAccount } from "../../../../../../account/settings/getAccount"; +import { BillingFilter } from "./components/billing-filter"; import { BillingHistory } from "./components/billing-history"; import { searchParamLoader } from "./search-params"; @@ -31,19 +32,28 @@ export default async function Page(props: { const invoices = await getTeamInvoices(team, { cursor: searchParams.cursor ?? undefined, + status: searchParams.status ?? undefined, }); return (
-
-

- Invoice History -

-

- View your past invoices and payment history -

+
+
+

+ Invoice History +

+

+ View your past invoices and payment history +

+
+
- +
); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/search-params.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/search-params.ts index c43f7b8cb26..bdaca3e8f34 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/search-params.ts +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/search-params.ts @@ -1,7 +1,8 @@ -import { createLoader, parseAsString } from "nuqs/server"; +import { createLoader, parseAsString, parseAsStringEnum } from "nuqs/server"; export const searchParams = { cursor: parseAsString, + status: parseAsStringEnum(["open"]), }; export const searchParamLoader = createLoader(searchParams); diff --git a/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx b/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx index 104a49e9a76..dbf288de489 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/CancelPlanModal/CancelPlanModal.tsx @@ -83,8 +83,10 @@ const cancelReasons: Array<{ ]; export function CancelPlanButton(props: { + teamSlug: string; cancelPlan: CancelPlan; currentPlan: Team["billingPlan"]; + billingStatus: Team["billingStatus"]; getTeam: () => Promise; }) { return ( @@ -102,7 +104,9 @@ export function CancelPlanButton(props: { - {props.currentPlan === "pro" ? ( + {props.billingStatus === "invalidPayment" ? ( + + ) : props.currentPlan === "pro" ? ( ) : ( +

+ Cancel Plan +

+

+ You have unpaid invoices. Please pay them before cancelling your plan. +

+ + +
+ ); +} + function ProPlanCancelPlanSheetContent() { return (
diff --git a/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx b/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx index 7a8e5592054..a7543ee2da1 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx @@ -91,6 +91,7 @@ export const BillingPricing: React.FC = ({ {/* Starter */} = ({ {/* Growth */} = ({ {/* Accelerate */} = ({ {/* Scale */}