"use client"; import type { WorkspacePlan } from "@openstatus/db/src/schema"; import type { Addons, BillingInterval, } from "@openstatus/db/src/schema/plan/schema"; import { getAddonPackSize, getAddonPriceConfig, getPriceConfig, } from "@openstatus/db/src/schema/plan/utils"; import { Check } from "@openstatus/icons"; import { Badge } from "@openstatus/ui/components/ui/badge"; import { Button } from "@openstatus/ui/components/ui/button"; import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "@openstatus/ui/components/ui/table"; import { Tabs, TabsList, TabsTrigger } from "@openstatus/ui/components/ui/tabs"; import { useCookieState } from "@openstatus/ui/hooks/use-cookie-state"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { isTRPCClientError } from "@trpc/client"; import { useRouter } from "next/navigation"; import { Fragment, useState, useTransition } from "react"; import { toast } from "sonner"; import { config as featureGroups, plans } from "@/data/plans"; import { getStripe } from "@/lib/stripe"; import { useTRPC } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; const BASE_URL = process.env.NODE_ENV === "production" ? "https://app.openstatus.dev" : "http://localhost:3000"; function getQuantitySuffix(addon: keyof Addons) { const packSize = getAddonPackSize(addon); return packSize > 1 ? `/mo./${packSize}` : "/mo./each"; } export function DataTable({ restrictTo }: { restrictTo?: WorkspacePlan[] }) { const [interval, setInterval] = useState("monthly"); const [currency] = useCookieState("x-currency", "USD"); const trpc = useTRPC(); const router = useRouter(); const queryClient = useQueryClient(); const [isPending, startTransition] = useTransition(); const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); const checkoutSessionMutation = useMutation( trpc.stripeRouter.getCheckoutSession.mutationOptions({ onSuccess: async (data) => { if (!data) return; // An existing subscriber has the plan swapped on the subscription they // already have, so there is no checkout to redirect to — only the // refreshed workspace to pick up. if (data.type === "updated") { await queryClient.invalidateQueries({ queryKey: trpc.workspace.get.queryKey(), }); toast.success("Your plan has been updated"); return; } const stripe = await getStripe(); stripe?.redirectToCheckout({ sessionId: data.session.id }); }, }), ); const customerPortalMutation = useMutation( trpc.stripeRouter.getUserCustomerPortal.mutationOptions({ onSuccess: (url) => { if (!url) return; router.push(url); }, }), ); if (!workspace) return null; const filteredPlans = Object.values(plans).filter((plan) => restrictTo ? restrictTo.includes(plan.id) : true, ); return (
setInterval(v as BillingInterval)} > Monthly Yearly 2 months free A list to compare the different features by plan. Features comparison {filteredPlans.map(({ id, ...plan }) => { const isCurrentPlan = workspace.plan === id; const price = getPriceConfig(id, currency, interval); const isFreePlan = id === "free"; return (

{plan.title}

{plan.description}

{isFreePlan ? (

Free

) : (

{new Intl.NumberFormat(price.locale, { style: "currency", currency: price.currency, }).format(price.value)} {interval === "yearly" ? "/year" : "/month"}

)}
); })}
{Object.entries(featureGroups).map( ([groupKey, { label, features }]) => ( {label} {features.map( ({ value, label: featureLabel, monthly, badge }) => (
{featureLabel}{" "} {badge ? ( {badge} ) : null}
{filteredPlans.map((plan) => { const limitValue = plan.limits[value as keyof typeof plan.limits]; const isAddon = value in plan.addons; function renderContent() { if (isAddon) { const price = getAddonPriceConfig( plan.id, value as keyof typeof plan.addons, currency, ); if (!price) return null; const isNumber = typeof limitValue === "number"; return (
{isNumber ? new Intl.NumberFormat("us") .format(limitValue) .toString() : null} {isNumber ? " + " : ""} {new Intl.NumberFormat(price.locale, { style: "currency", currency: price.currency, }).format(price.value)} {isNumber ? getQuantitySuffix( value as keyof typeof plan.addons, ) : "/mo."}
); } if (typeof limitValue === "boolean") { return limitValue ? ( ) : ( ‐ ); } if (typeof limitValue === "number") { return new Intl.NumberFormat("us") .format(limitValue) .toString(); } // TODO: create a format function for this in @data/plans if ( value === "regions" && Array.isArray(limitValue) ) { return limitValue?.length ?? 0; } if ( Array.isArray(limitValue) && limitValue.length > 0 ) { return limitValue[0]; } return limitValue; } return ( {renderContent()} {monthly ? "/mo." : ""} ); })}
), )}
), )}
); }