@@ -32,8 +32,14 @@ import {
3232} from "@/components/ui/dialog" ;
3333import { getPricingTableContent } from "@/lib/autumn/pricing-table-content" ;
3434import { 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
3743const 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+
5084function 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
84117const PricingTableContext = createContext < {
85118 products : Product [ ] ;
86119 selectedPlan ?: string | null ;
@@ -90,7 +123,6 @@ function usePricingTableCtx() {
90123 return useContext ( PricingTableContext ) ;
91124}
92125
93- // Main component
94126export 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
177197function DowngradeConfirmDialog ( {
178198 isOpen,
179199 onClose,
@@ -238,7 +258,6 @@ function DowngradeConfirmDialog({
238258 ) ;
239259}
240260
241- // Pricing Card
242261function 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
425459function 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+
463508function PricingCardButton ( {
464509 recommended,
465510 children,
@@ -498,5 +543,4 @@ function PricingCardButton({
498543 ) ;
499544}
500545
501- // Exports for external use
502546export { PricingCard , FeatureItem as PricingFeatureItem } ;
0 commit comments