From 76822982f3f10390a944d2d386ce6a2a236d8610 Mon Sep 17 00:00:00 2001 From: jnsdls Date: Mon, 14 Apr 2025 14:19:42 +0000 Subject: [PATCH] Add billing warning and open invoice filtering (#6718) # Add Warning for Teams with Invalid Payment Methods This PR adds a warning message for teams with invalid payment methods, preventing them from upgrading plans until they resolve outstanding invoices. ## Changes: - Added a warning component that displays when a team has an `invalidPayment` billing status - Added a filter to view only open invoices in the invoices page - Updated the `getTeamInvoices` function to accept an optional `status` parameter - Disabled checkout buttons for teams with invalid payment methods - Added a warning in the cancel plan modal for teams with unpaid invoices - Added a link to direct users to pay their outstanding invoices These changes help users understand why they can't upgrade their plan and guides them to resolve payment issues. --- .../dashboard/src/@/actions/stripe-actions.ts | 4 +- apps/dashboard/src/@/components/billing.tsx | 87 +++++++++++++------ .../src/@/components/blocks/pricing-card.tsx | 3 + .../team-onboarding/InviteTeamMembers.tsx | 6 ++ .../billing/components/PlanInfoCard.tsx | 2 + .../invoices/components/billing-filter.tsx | 46 ++++++++++ .../invoices/components/billing-history.tsx | 9 ++ .../(team)/~/settings/invoices/page.tsx | 26 ++++-- .../~/settings/invoices/search-params.ts | 3 +- .../CancelPlanModal/CancelPlanModal.tsx | 28 +++++- .../settings/Account/Billing/Pricing.tsx | 4 + 11 files changed, 181 insertions(+), 37 deletions(-) create mode 100644 apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/invoices/components/billing-filter.tsx 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 */}