Skip to content

Commit 7e456e5

Browse files
committed
fix: better feature gate
1 parent 884be18 commit 7e456e5

File tree

2 files changed

+80
-57
lines changed

2 files changed

+80
-57
lines changed

apps/dashboard/app/(main)/websites/[id]/errors/_components/errors-page-content.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ErrorSummaryStats } from "./error-summary-stats";
1313
import { ErrorTrendsChart } from "./error-trends-chart";
1414
import { RecentErrorsTable } from "./recent-errors-table";
1515
import { TopErrorCard } from "./top-error-card";
16+
import { FeatureGate } from "@/components/feature-gate";
1617
import type {
1718
ErrorByPage,
1819
ErrorChartData,
@@ -21,6 +22,7 @@ import type {
2122
ProcessedChartData,
2223
RecentError,
2324
} from "./types";
25+
import { GATED_FEATURES } from "@/components/providers/billing-provider";
2426

2527
interface ErrorsPageContentProps {
2628
params: Promise<{ id: string }>;
@@ -104,6 +106,7 @@ export const ErrorsPageContent = ({ params }: ErrorsPageContentProps) => {
104106
}
105107

106108
return (
109+
<FeatureGate feature={GATED_FEATURES.ERROR_TRACKING}>
107110
<div className="space-y-4 p-4">
108111
{isLoading ? (
109112
<ErrorsLoadingSkeleton />
@@ -132,9 +135,10 @@ export const ErrorsPageContent = ({ params }: ErrorsPageContentProps) => {
132135
errors_by_page: errorsByPage,
133136
}}
134137
/>
135-
</div>
136-
)}
137-
</div>
138+
</div>
139+
)}
140+
</div>
141+
</FeatureGate>
138142
);
139143
};
140144

Lines changed: 73 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,51 @@
11
"use client";
22

3-
import { LockIcon, RocketLaunchIcon } from "@phosphor-icons/react";
3+
import {
4+
ArrowRightIcon,
5+
CrownIcon,
6+
LockSimpleIcon,
7+
RocketLaunchIcon,
8+
SparkleIcon,
9+
StarIcon,
10+
} from "@phosphor-icons/react";
411
import Link from "next/link";
512
import type { ReactNode } from "react";
613
import {
714
useBillingContext,
815
type GatedFeatureId,
916
} from "@/components/providers/billing-provider";
1017
import { Button } from "@/components/ui/button";
18+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
19+
import { cn } from "@/lib/utils";
1120
import { FEATURE_METADATA, PLAN_IDS } from "@/types/features";
1221

13-
const PLAN_NAMES: Record<string, string> = {
14-
[PLAN_IDS.FREE]: "Free",
15-
[PLAN_IDS.HOBBY]: "Hobby",
16-
[PLAN_IDS.PRO]: "Pro",
17-
[PLAN_IDS.SCALE]: "Scale",
22+
const PLAN_CONFIG: Record<
23+
string,
24+
{ name: string; icon: typeof StarIcon; color: string }
25+
> = {
26+
[PLAN_IDS.FREE]: { name: "Free", icon: SparkleIcon, color: "text-muted-foreground" },
27+
[PLAN_IDS.HOBBY]: { name: "Hobby", icon: RocketLaunchIcon, color: "text-success" },
28+
[PLAN_IDS.PRO]: { name: "Pro", icon: StarIcon, color: "text-primary" },
29+
[PLAN_IDS.SCALE]: { name: "Scale", icon: CrownIcon, color: "text-amber-500" },
1830
};
1931

2032
interface FeatureGateProps {
2133
feature: GatedFeatureId;
2234
children: ReactNode;
2335
title?: string;
2436
description?: string;
25-
/** Block rendering while checking access (default: false, shows content optimistically) */
2637
blockWhileLoading?: boolean;
2738
}
2839

29-
/**
30-
* Wraps content requiring a specific feature.
31-
* Shows upgrade prompt when feature is unavailable.
32-
*/
3340
export function FeatureGate({
3441
feature,
3542
children,
3643
title,
3744
description,
3845
blockWhileLoading = false,
3946
}: FeatureGateProps) {
40-
const { isFeatureEnabled, currentPlanId, isFree, isLoading } =
41-
useBillingContext();
47+
const { isFeatureEnabled, currentPlanId, isLoading } = useBillingContext();
4248

43-
// Optimistic: show content while loading
4449
if (isLoading && !blockWhileLoading) {
4550
return <>{children}</>;
4651
}
@@ -50,67 +55,81 @@ export function FeatureGate({
5055
}
5156

5257
const metadata = FEATURE_METADATA[feature];
53-
const planName = metadata?.minPlan ? PLAN_NAMES[metadata.minPlan] : "a paid";
58+
const requiredPlan = metadata?.minPlan ?? PLAN_IDS.HOBBY;
59+
const planConfig = PLAN_CONFIG[requiredPlan] ?? PLAN_CONFIG[PLAN_IDS.HOBBY];
60+
const currentConfig = PLAN_CONFIG[currentPlanId ?? PLAN_IDS.FREE] ?? PLAN_CONFIG[PLAN_IDS.FREE];
61+
const PlanIcon = planConfig.icon;
62+
const CurrentIcon = currentConfig.icon;
5463

5564
return (
56-
<div className="flex h-full min-h-[400px] flex-col items-center justify-center p-8">
57-
<div className="flex max-w-md flex-col items-center text-center">
58-
<div className="mb-6 flex size-16 items-center justify-center rounded-full bg-secondary">
59-
<LockIcon
60-
className="size-8 text-muted-foreground"
61-
weight="duotone"
62-
/>
63-
</div>
65+
<div className="flex h-full min-h-[400px] items-center justify-center p-4">
66+
<Card className="w-full max-w-md overflow-hidden pt-0">
67+
<CardHeader className="dotted-bg flex flex-col items-center gap-4 border-b bg-accent py-8">
68+
<div className="flex size-14 items-center justify-center rounded border bg-card">
69+
<LockSimpleIcon
70+
className="size-7 text-muted-foreground"
71+
weight="duotone"
72+
/>
73+
</div>
74+
<div className="text-center">
75+
<h2 className="font-semibold text-lg tracking-tight">
76+
{title ?? `Unlock ${metadata?.name ?? "this feature"}`}
77+
</h2>
78+
<p className="mt-1 text-muted-foreground text-sm">
79+
{description ?? metadata?.description ?? "Upgrade to access this feature."}
80+
</p>
81+
</div>
82+
</CardHeader>
6483

65-
<h2 className="mb-2 font-semibold text-xl">
66-
{title ??
67-
`${metadata?.name ?? "This feature"} requires ${planName} plan`}
68-
</h2>
84+
<CardContent className="space-y-4 p-4">
85+
{/* Required plan */}
86+
<div className="flex items-center justify-between rounded border bg-accent/50 px-3 py-2.5">
87+
<span className="text-muted-foreground text-sm">Required plan</span>
88+
<div className="flex items-center gap-1.5">
89+
<PlanIcon className={cn("size-4", planConfig.color)} weight="duotone" />
90+
<span className={cn("font-semibold text-sm", planConfig.color)}>
91+
{planConfig.name}
92+
</span>
93+
</div>
94+
</div>
6995

70-
<p className="mb-6 text-muted-foreground">
71-
{description ??
72-
metadata?.description ??
73-
"Upgrade your plan to access this feature."}
74-
</p>
96+
{/* Current plan */}
97+
<div className="flex items-center justify-between rounded border px-3 py-2.5">
98+
<span className="text-muted-foreground text-sm">Your plan</span>
99+
<div className="flex items-center gap-1.5">
100+
<CurrentIcon className={cn("size-4", currentConfig.color)} weight="duotone" />
101+
<span className="font-medium text-foreground text-sm">
102+
{currentConfig.name}
103+
</span>
104+
</div>
105+
</div>
75106

76-
<div className="flex flex-col gap-3 sm:flex-row">
77-
<Button asChild>
107+
{/* CTA */}
108+
<Button asChild className="group w-full gap-2" size="lg">
78109
<Link href="/billing/plans">
79-
<RocketLaunchIcon className="mr-2 size-4" weight="duotone" />
80-
Upgrade to {planName}
110+
<RocketLaunchIcon className="size-5" weight="duotone" />
111+
Upgrade to {planConfig.name}
112+
<ArrowRightIcon className="size-4 transition-transform group-hover:translate-x-0.5" />
81113
</Link>
82114
</Button>
83-
<Button asChild variant="outline">
84-
<Link href="/billing">View Current Plan</Link>
85-
</Button>
86-
</div>
87-
88-
{isFree && (
89-
<p className="mt-6 text-muted-foreground text-sm">
90-
You&apos;re on the{" "}
91-
<span className="font-medium">
92-
{PLAN_NAMES[currentPlanId ?? PLAN_IDS.FREE]}
93-
</span>{" "}
94-
plan
95-
</p>
96-
)}
97-
</div>
115+
</CardContent>
116+
</Card>
98117
</div>
99118
);
100119
}
101120

102121
export function useFeatureGate(feature: GatedFeatureId) {
103-
const { isFeatureEnabled, getGatedFeatureAccess, isLoading } =
104-
useBillingContext();
122+
const { isFeatureEnabled, getGatedFeatureAccess, isLoading } = useBillingContext();
105123

106124
const access = getGatedFeatureAccess(feature);
107125
const metadata = FEATURE_METADATA[feature];
126+
const planConfig = metadata?.minPlan ? PLAN_CONFIG[metadata.minPlan] : null;
108127

109128
return {
110129
isEnabled: isFeatureEnabled(feature),
111130
isLoading,
112131
...access,
113-
planName: metadata?.minPlan ? PLAN_NAMES[metadata.minPlan] : null,
132+
planName: planConfig?.name ?? null,
114133
featureName: metadata?.name ?? feature,
115134
};
116135
}

0 commit comments

Comments
 (0)