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
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
PastDueBannerUI,
ServiceCutOffBannerUI,
} from "./BillingAlertBannersUI";

const meta = {
title: "Banners/Billing Alerts",
parameters: {
layout: "centered",
},
} satisfies Meta;

export default meta;

type Story = StoryObj<typeof meta>;

export const PaymentAlerts: Story = {
render: () => (
<div className="space-y-10">
<PastDueBannerUI
teamSlug="foo"
redirectToBillingPortal={() => Promise.resolve({ status: 200 })}
/>

<ServiceCutOffBannerUI
teamSlug="foo"
redirectToBillingPortal={() => Promise.resolve({ status: 200 })}
/>
</div>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { redirectToBillingPortal } from "@/actions/billing";
import {
PastDueBannerUI,
ServiceCutOffBannerUI,
} from "./BillingAlertBannersUI";

export function PastDueBanner(props: { teamSlug: string }) {
return (
<PastDueBannerUI
redirectToBillingPortal={redirectToBillingPortal}
teamSlug={props.teamSlug}
/>
);
}

export function ServiceCutOffBanner(props: { teamSlug: string }) {
return (
<ServiceCutOffBannerUI
redirectToBillingPortal={redirectToBillingPortal}
teamSlug={props.teamSlug}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"use client";

import type { BillingBillingPortalAction } from "@/actions/billing";
import { BillingPortalButton } from "@/components/billing";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { cn } from "@/lib/utils";
import { useState } from "react";

function BillingAlertBanner(props: {
title: string;
description: React.ReactNode;
teamSlug: string;
variant: "error" | "warning";
ctaLabel: string;
redirectToBillingPortal: BillingBillingPortalAction;
}) {
const [isRouteLoading, setIsRouteLoading] = useState(false);

return (
<div
className={cn(
"flex flex-col border-b bg-card px-4 py-6 lg:items-center lg:text-center",
props.variant === "warning" &&
"border-yellow-600 bg-yellow-50 text-yellow-800 dark:border-yellow-700 dark:bg-yellow-950 dark:text-yellow-100",
props.variant === "error" &&
"border-red-600 bg-red-50 text-red-800 dark:border-red-700 dark:bg-red-950 dark:text-red-100",
)}
>
<h3 className="font-semibold text-xl tracking-tight">{props.title}</h3>
<p className="mt-1 mb-4 text-sm">{props.description}</p>
<BillingPortalButton
className={cn(
"gap-2",
props.variant === "warning" &&
"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",
props.variant === "error" &&
"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",
)}
size="sm"
teamSlug={props.teamSlug}
redirectPath={`/team/${props.teamSlug}`}
redirectToBillingPortal={props.redirectToBillingPortal}
onClick={() => {
setIsRouteLoading(true);
}}
>
{props.ctaLabel}
{isRouteLoading ? <Spinner className="size-4" /> : null}
</BillingPortalButton>
</div>
);
}

export function PastDueBannerUI(props: {
teamSlug: string;
redirectToBillingPortal: BillingBillingPortalAction;
}) {
return (
<BillingAlertBanner
ctaLabel="View Invoices"
variant="warning"
title="Unpaid Invoices"
redirectToBillingPortal={props.redirectToBillingPortal}
description={
<>
You have unpaid invoices. Service may be suspended if not paid
promptly.
</>
}
teamSlug={props.teamSlug}
/>
);
}

export function ServiceCutOffBannerUI(props: {
teamSlug: string;
redirectToBillingPortal: BillingBillingPortalAction;
}) {
return (
<BillingAlertBanner
ctaLabel="Pay Now"
variant="error"
title="Service Suspended"
redirectToBillingPortal={props.redirectToBillingPortal}
description={
<>
Your service has been suspended due to unpaid invoices. Pay now to
resume service.
</>
}
teamSlug={props.teamSlug}
/>
);
}
17 changes: 16 additions & 1 deletion apps/dashboard/src/app/team/[team_slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { AppFooter } from "@/components/blocks/app-footer";
import { redirect } from "next/navigation";
import { TWAutoConnect } from "../../components/autoconnect";
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
import {
PastDueBanner,
ServiceCutOffBanner,
} from "./(team)/_components/BillingAlertBanners";

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

return (
<div className="flex min-h-dvh flex-col">
<div className="flex grow flex-col">{props.children}</div>
<div className="flex grow flex-col">
{team.billingStatus === "pastDue" && (
<PastDueBanner teamSlug={team_slug} />
)}

{team.billingStatus === "invalidPayment" && (
<ServiceCutOffBanner teamSlug={team_slug} />
)}

{props.children}
</div>
<TWAutoConnect />

<AppFooter />
<SaveLastVisitedTeamPage />
</div>
Expand Down
Loading