Skip to content

Commit 54a3553

Browse files
committed
cleanup plans
1 parent 8445d51 commit 54a3553

File tree

2 files changed

+104
-79
lines changed

2 files changed

+104
-79
lines changed

apps/dashboard/components/autumn/pricing-table.tsx

Lines changed: 101 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ import {
3232
} from "@/components/ui/dialog";
3333
import { getPricingTableContent } from "@/lib/autumn/pricing-table-content";
3434
import { cn } from "@/lib/utils";
35+
import {
36+
FEATURE_METADATA,
37+
PLAN_FEATURES,
38+
PLAN_IDS,
39+
type GatedFeatureId,
40+
type PlanId,
41+
} from "@/types/features";
3542

36-
// Plan icons - matches billing page
3743
const PLAN_ICONS: Record<string, typeof CrownIcon> = {
3844
free: SparkleIcon,
3945
hobby: RocketLaunchIcon,
@@ -46,7 +52,35 @@ function getPlanIcon(planId: string) {
4652
return PLAN_ICONS[planId] || CrownIcon;
4753
}
4854

49-
// Skeleton - matches billing design
55+
/** Get gated features that are NEW in this plan (not inherited from lower tiers) */
56+
function getNewFeaturesForPlan(planId: string): GatedFeatureId[] {
57+
const plan = planId as PlanId;
58+
const planFeatures = PLAN_FEATURES[plan];
59+
if (!planFeatures) return [];
60+
61+
// For free plan, return all enabled features
62+
if (plan === PLAN_IDS.FREE) {
63+
return Object.entries(planFeatures)
64+
.filter(([, enabled]) => enabled)
65+
.map(([feature]) => feature as GatedFeatureId);
66+
}
67+
68+
// For other plans, find features that weren't enabled in the previous tier
69+
const tierOrder: PlanId[] = [
70+
PLAN_IDS.FREE,
71+
PLAN_IDS.HOBBY,
72+
PLAN_IDS.PRO,
73+
PLAN_IDS.SCALE,
74+
];
75+
const currentIndex = tierOrder.indexOf(plan);
76+
const previousPlan = tierOrder[currentIndex - 1];
77+
const previousFeatures = PLAN_FEATURES[previousPlan] ?? {};
78+
79+
return Object.entries(planFeatures)
80+
.filter(([feature, enabled]) => enabled && !previousFeatures[feature as GatedFeatureId])
81+
.map(([feature]) => feature as GatedFeatureId);
82+
}
83+
5084
function PricingTableSkeleton() {
5185
return (
5286
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -80,7 +114,6 @@ function PricingTableSkeleton() {
80114
);
81115
}
82116

83-
// Context
84117
const PricingTableContext = createContext<{
85118
products: Product[];
86119
selectedPlan?: string | null;
@@ -90,7 +123,6 @@ function usePricingTableCtx() {
90123
return useContext(PricingTableContext);
91124
}
92125

93-
// Main component
94126
export default function PricingTable({
95127
productDetails,
96128
selectedPlan,
@@ -126,54 +158,42 @@ export default function PricingTable({
126158
);
127159
}
128160

129-
const intervalFilter = (product: Product) => {
130-
if (!product.properties?.interval_group) {
131-
return true;
132-
}
133-
return true;
134-
};
135-
136161
const filteredProducts =
137162
products?.filter(
138163
(p) =>
139164
p.id !== "free" &&
140165
p.id !== "verification_fee" &&
141-
!(p as Product & { is_add_on?: boolean }).is_add_on &&
142-
intervalFilter(p)
166+
!(p as Product & { is_add_on?: boolean }).is_add_on
143167
) ?? [];
144168

145169
return (
146-
<div>
147-
{/* Cards Grid */}
148-
<PricingTableContext.Provider
149-
value={{ products: products ?? [], selectedPlan }}
150-
>
151-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
152-
{filteredProducts.map((plan) => (
153-
<PricingCard
154-
buttonProps={{
155-
disabled:
156-
plan.scenario === "active" || plan.scenario === "scheduled",
157-
onClick: async () => {
158-
await attach({
159-
productId: plan.id,
160-
dialog: AttachDialog,
161-
...(plan.id === "hobby" && { reward: "SAVE80" }),
162-
});
163-
},
164-
}}
165-
isSelected={selectedPlan === plan.id}
166-
key={plan.id}
167-
productId={plan.id}
168-
/>
169-
))}
170-
</div>
171-
</PricingTableContext.Provider>
172-
</div>
170+
<PricingTableContext.Provider
171+
value={{ products: products ?? [], selectedPlan }}
172+
>
173+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
174+
{filteredProducts.map((plan) => (
175+
<PricingCard
176+
buttonProps={{
177+
disabled:
178+
plan.scenario === "active" || plan.scenario === "scheduled",
179+
onClick: async () => {
180+
await attach({
181+
productId: plan.id,
182+
dialog: AttachDialog,
183+
...(plan.id === "hobby" && { reward: "SAVE80" }),
184+
});
185+
},
186+
}}
187+
isSelected={selectedPlan === plan.id}
188+
key={plan.id}
189+
productId={plan.id}
190+
/>
191+
))}
192+
</div>
193+
</PricingTableContext.Provider>
173194
);
174195
}
175196

176-
// Downgrade Confirm Dialog
177197
function DowngradeConfirmDialog({
178198
isOpen,
179199
onClose,
@@ -238,7 +258,6 @@ function DowngradeConfirmDialog({
238258
);
239259
}
240260

241-
// Pricing Card
242261
function PricingCard({
243262
productId,
244263
className,
@@ -255,17 +274,14 @@ function PricingCard({
255274
const [showDowngradeDialog, setShowDowngradeDialog] = useState(false);
256275
const product = products.find((p) => p.id === productId);
257276

258-
if (!product) {
259-
return null;
260-
}
277+
if (!product) return null;
261278

262279
const { name, display: productDisplay } = product;
263280
const { buttonText: defaultButtonText } = getPricingTableContent(product);
264281
const isRecommended = !!productDisplay?.recommend_text;
265282
const Icon = getPlanIcon(product.id);
266283
const isDowngrade = product.scenario === "downgrade";
267284

268-
// Find current active product
269285
const currentProduct = products.find(
270286
(p) => p.scenario === "active" || p.scenario === "scheduled"
271287
);
@@ -283,7 +299,6 @@ function PricingCard({
283299
? { primary_text: "Free", secondary_text: "forever" }
284300
: product.items[0]?.display;
285301

286-
// Support levels
287302
const supportLevels: Record<string, string> = {
288303
free: "Community Support",
289304
hobby: "Email Support",
@@ -303,12 +318,16 @@ function PricingCard({
303318
? { display: { primary_text: supportLevels[product.id] } }
304319
: null;
305320

306-
const featureItems = [
321+
// Autumn billing features (usage limits, etc.)
322+
const billingItems = [
307323
...(product.properties?.is_free ? product.items : product.items.slice(1)),
308324
...extraFeatures,
309325
...(supportItem ? [supportItem] : []),
310326
];
311327

328+
// Gated features new to this plan
329+
const newGatedFeatures = getNewFeaturesForPlan(product.id);
330+
312331
return (
313332
<div
314333
className={cn(
@@ -318,7 +337,6 @@ function PricingCard({
318337
className
319338
)}
320339
>
321-
{/* Recommended Badge */}
322340
{isRecommended && (
323341
<Badge className="absolute top-3 right-3 bg-primary text-primary-foreground">
324342
<StarIcon className="mr-1" size={12} weight="fill" />
@@ -349,7 +367,6 @@ function PricingCard({
349367
</div>
350368
</div>
351369

352-
{/* Price */}
353370
<div className="dotted-bg border-y bg-accent px-5 py-4">
354371
{product.id === "hobby" ? (
355372
<div className="flex items-baseline gap-2">
@@ -372,21 +389,39 @@ function PricingCard({
372389
)}
373390
</div>
374391

375-
{/* Features */}
376392
<div className="flex-1 p-5">
377393
{product.display?.everything_from && (
378394
<p className="mb-3 text-muted-foreground text-sm">
379395
Everything from {product.display.everything_from}, plus:
380396
</p>
381397
)}
382-
<div className="space-y-3">
383-
{featureItems.map((item) => (
398+
399+
{/* Billing features (usage limits) */}
400+
<div className="space-y-2.5">
401+
{billingItems.map((item) => (
384402
<FeatureItem item={item} key={item.display?.primary_text} />
385403
))}
386404
</div>
405+
406+
{/* Gated features new to this plan */}
407+
{newGatedFeatures.length > 0 && (
408+
<div className="mt-4 space-y-2.5 border-t pt-4">
409+
<span className="text-muted-foreground text-xs uppercase">
410+
Features Included
411+
</span>
412+
{newGatedFeatures.map((featureId) => {
413+
const meta = FEATURE_METADATA[featureId];
414+
return (
415+
<GatedFeatureItem
416+
key={featureId}
417+
name={meta?.name ?? featureId}
418+
/>
419+
);
420+
})}
421+
</div>
422+
)}
387423
</div>
388424

389-
{/* Button */}
390425
<div className="p-5 pt-0">
391426
<PricingCardButton
392427
disabled={buttonProps?.disabled}
@@ -421,7 +456,6 @@ function PricingCard({
421456
);
422457
}
423458

424-
// Feature Item
425459
function FeatureItem({ item }: { item: ProductItem }) {
426460
const featureItem = item as ProductItem & {
427461
tiers?: { to: number | "inf"; amount: number }[];
@@ -459,7 +493,18 @@ function FeatureItem({ item }: { item: ProductItem }) {
459493
);
460494
}
461495

462-
// Button
496+
function GatedFeatureItem({ name }: { name: string }) {
497+
return (
498+
<div className="flex items-center gap-2 text-sm">
499+
<CheckIcon
500+
className="size-4 shrink-0 text-accent-foreground"
501+
weight="bold"
502+
/>
503+
<span>{name}</span>
504+
</div>
505+
);
506+
}
507+
463508
function PricingCardButton({
464509
recommended,
465510
children,
@@ -498,5 +543,4 @@ function PricingCardButton({
498543
);
499544
}
500545

501-
// Exports for external use
502546
export { PricingCard, FeatureItem as PricingFeatureItem };

0 commit comments

Comments
 (0)