Skip to content

Commit 3240ae5

Browse files
authored
[TOOL-3526] Dashboard: Add billing status alert banners (#6341)
1 parent 9f69cc2 commit 3240ae5

File tree

4 files changed

+167
-1
lines changed

4 files changed

+167
-1
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import {
3+
PastDueBannerUI,
4+
ServiceCutOffBannerUI,
5+
} from "./BillingAlertBannersUI";
6+
7+
const meta = {
8+
title: "Banners/Billing Alerts",
9+
parameters: {
10+
layout: "centered",
11+
},
12+
} satisfies Meta;
13+
14+
export default meta;
15+
16+
type Story = StoryObj<typeof meta>;
17+
18+
export const PaymentAlerts: Story = {
19+
render: () => (
20+
<div className="space-y-10">
21+
<PastDueBannerUI
22+
teamSlug="foo"
23+
redirectToBillingPortal={() => Promise.resolve({ status: 200 })}
24+
/>
25+
26+
<ServiceCutOffBannerUI
27+
teamSlug="foo"
28+
redirectToBillingPortal={() => Promise.resolve({ status: 200 })}
29+
/>
30+
</div>
31+
),
32+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
3+
import { redirectToBillingPortal } from "@/actions/billing";
4+
import {
5+
PastDueBannerUI,
6+
ServiceCutOffBannerUI,
7+
} from "./BillingAlertBannersUI";
8+
9+
export function PastDueBanner(props: { teamSlug: string }) {
10+
return (
11+
<PastDueBannerUI
12+
redirectToBillingPortal={redirectToBillingPortal}
13+
teamSlug={props.teamSlug}
14+
/>
15+
);
16+
}
17+
18+
export function ServiceCutOffBanner(props: { teamSlug: string }) {
19+
return (
20+
<ServiceCutOffBannerUI
21+
redirectToBillingPortal={redirectToBillingPortal}
22+
teamSlug={props.teamSlug}
23+
/>
24+
);
25+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"use client";
2+
3+
import type { BillingBillingPortalAction } from "@/actions/billing";
4+
import { BillingPortalButton } from "@/components/billing";
5+
import { Spinner } from "@/components/ui/Spinner/Spinner";
6+
import { cn } from "@/lib/utils";
7+
import { useState } from "react";
8+
9+
function BillingAlertBanner(props: {
10+
title: string;
11+
description: React.ReactNode;
12+
teamSlug: string;
13+
variant: "error" | "warning";
14+
ctaLabel: string;
15+
redirectToBillingPortal: BillingBillingPortalAction;
16+
}) {
17+
const [isRouteLoading, setIsRouteLoading] = useState(false);
18+
19+
return (
20+
<div
21+
className={cn(
22+
"flex flex-col border-b bg-card px-4 py-6 lg:items-center lg:text-center",
23+
props.variant === "warning" &&
24+
"border-yellow-600 bg-yellow-50 text-yellow-800 dark:border-yellow-700 dark:bg-yellow-950 dark:text-yellow-100",
25+
props.variant === "error" &&
26+
"border-red-600 bg-red-50 text-red-800 dark:border-red-700 dark:bg-red-950 dark:text-red-100",
27+
)}
28+
>
29+
<h3 className="font-semibold text-xl tracking-tight">{props.title}</h3>
30+
<p className="mt-1 mb-4 text-sm">{props.description}</p>
31+
<BillingPortalButton
32+
className={cn(
33+
"gap-2",
34+
props.variant === "warning" &&
35+
"border border-yellow-600 bg-yellow-100 text-yellow-800 hover:bg-yellow-200 dark:border-yellow-700 dark:bg-yellow-900 dark:text-yellow-100 dark:hover:bg-yellow-800",
36+
props.variant === "error" &&
37+
"border border-red-600 bg-red-100 text-red-800 hover:bg-red-200 dark:border-red-700 dark:bg-red-900 dark:text-red-100 dark:hover:bg-red-800",
38+
)}
39+
size="sm"
40+
teamSlug={props.teamSlug}
41+
redirectPath={`/team/${props.teamSlug}`}
42+
redirectToBillingPortal={props.redirectToBillingPortal}
43+
onClick={() => {
44+
setIsRouteLoading(true);
45+
}}
46+
>
47+
{props.ctaLabel}
48+
{isRouteLoading ? <Spinner className="size-4" /> : null}
49+
</BillingPortalButton>
50+
</div>
51+
);
52+
}
53+
54+
export function PastDueBannerUI(props: {
55+
teamSlug: string;
56+
redirectToBillingPortal: BillingBillingPortalAction;
57+
}) {
58+
return (
59+
<BillingAlertBanner
60+
ctaLabel="View Invoices"
61+
variant="warning"
62+
title="Unpaid Invoices"
63+
redirectToBillingPortal={props.redirectToBillingPortal}
64+
description={
65+
<>
66+
You have unpaid invoices. Service may be suspended if not paid
67+
promptly.
68+
</>
69+
}
70+
teamSlug={props.teamSlug}
71+
/>
72+
);
73+
}
74+
75+
export function ServiceCutOffBannerUI(props: {
76+
teamSlug: string;
77+
redirectToBillingPortal: BillingBillingPortalAction;
78+
}) {
79+
return (
80+
<BillingAlertBanner
81+
ctaLabel="Pay Now"
82+
variant="error"
83+
title="Service Suspended"
84+
redirectToBillingPortal={props.redirectToBillingPortal}
85+
description={
86+
<>
87+
Your service has been suspended due to unpaid invoices. Pay now to
88+
resume service.
89+
</>
90+
}
91+
teamSlug={props.teamSlug}
92+
/>
93+
);
94+
}

apps/dashboard/src/app/team/[team_slug]/layout.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { AppFooter } from "@/components/blocks/app-footer";
33
import { redirect } from "next/navigation";
44
import { TWAutoConnect } from "../../components/autoconnect";
55
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
6+
import {
7+
PastDueBanner,
8+
ServiceCutOffBanner,
9+
} from "./(team)/_components/BillingAlertBanners";
610

711
export default async function RootTeamLayout(props: {
812
children: React.ReactNode;
@@ -17,8 +21,19 @@ export default async function RootTeamLayout(props: {
1721

1822
return (
1923
<div className="flex min-h-dvh flex-col">
20-
<div className="flex grow flex-col">{props.children}</div>
24+
<div className="flex grow flex-col">
25+
{team.billingStatus === "pastDue" && (
26+
<PastDueBanner teamSlug={team_slug} />
27+
)}
28+
29+
{team.billingStatus === "invalidPayment" && (
30+
<ServiceCutOffBanner teamSlug={team_slug} />
31+
)}
32+
33+
{props.children}
34+
</div>
2135
<TWAutoConnect />
36+
2237
<AppFooter />
2338
<SaveLastVisitedTeamPage />
2439
</div>

0 commit comments

Comments
 (0)