@@ -16,6 +16,7 @@ import {
1616 WarningIcon ,
1717} from '@phosphor-icons/react' ;
1818import type { Product } from 'autumn-js' ;
19+ import dayjs from 'dayjs' ;
1920import React , { memo , useMemo } from 'react' ;
2021import { useBilling } from '@/app/(main)/billing/hooks/use-billing' ;
2122import { Badge } from '@/components/ui/badge' ;
@@ -38,10 +39,14 @@ const UsageCard = memo(function UsageCardComponent({
3839 feature,
3940 onUpgrade,
4041} : UsageCardProps ) {
41- const percentage =
42- feature . limit > 0 ? Math . min ( ( feature . used / feature . limit ) * 100 , 100 ) : 0 ;
43- const isNearLimit = ! feature . unlimited && percentage > 80 ;
44- const isOverLimit = ! feature . unlimited && percentage >= 100 ;
42+ const percentage = feature . unlimited
43+ ? 0
44+ : feature . limit > 0
45+ ? Math . min ( ( feature . used / feature . limit ) * 100 , 100 )
46+ : 0 ;
47+
48+ const isNearLimit = ! feature . unlimited && ( percentage > 80 || feature . balance < feature . limit * 0.2 ) ;
49+ const isOverLimit = ! feature . unlimited && ( percentage >= 100 || feature . balance <= 0 ) ;
4550
4651 const getIcon = ( ) => {
4752 if ( feature . name . toLowerCase ( ) . includes ( 'event' ) ) {
@@ -60,19 +65,17 @@ const UsageCard = memo(function UsageCardComponent({
6065 } ;
6166
6267 const getIntervalText = ( ) => {
63- if ( ! feature . interval ) {
64- return `Resets ${ feature . nextReset } ` ;
65- }
66- switch ( feature . interval ) {
67- case 'day' :
68- return 'Resets daily' ;
69- case 'month' :
70- return 'Resets monthly' ;
71- case 'year' :
72- return 'Resets yearly' ;
73- default :
74- return `Resets ${ feature . nextReset } ` ;
68+ const intervals : Record < string , string > = {
69+ day : 'Resets daily' ,
70+ month : 'Resets monthly' ,
71+ year : 'Resets yearly' ,
72+ } ;
73+
74+ if ( feature . interval && intervals [ feature . interval ] ) {
75+ return intervals [ feature . interval ] ;
7576 }
77+
78+ return feature . nextReset ? `Resets ${ feature . nextReset } ` : 'No reset scheduled' ;
7679 } ;
7780
7881 const getUsageTextColor = ( ) => {
@@ -189,61 +192,45 @@ const PlanStatusCard = memo(function PlanStatusCardComponent({
189192 if ( isCanceled ) {
190193 return (
191194 < Badge variant = "destructive" >
192- < WarningIcon
193- className = "mr-1 font-bold not-dark:text-primary"
194- size = { 12 }
195- weight = "duotone"
196- />
195+ < WarningIcon className = "mr-1" size = { 12 } weight = "duotone" />
197196 Cancelled
198197 </ Badge >
199198 ) ;
200199 }
201200 if ( isScheduled ) {
202201 return (
203202 < Badge variant = "secondary" >
204- < CalendarIcon
205- className = "mr-1 font-bold not-dark:text-primary"
206- size = { 12 }
207- weight = "duotone"
208- />
203+ < CalendarIcon className = "mr-1" size = { 12 } weight = "duotone" />
209204 Scheduled
210205 </ Badge >
211206 ) ;
212207 }
213208 return (
214209 < Badge >
215- < CheckIcon
216- className = "mr-1 text-white dark:text-black"
217- size = { 12 }
218- weight = "bold"
219- />
210+ < CheckIcon className = "mr-1" size = { 12 } weight = "bold" />
220211 Active
221212 </ Badge >
222213 ) ;
223214 } ;
224215
225216 const getFeatureText = ( item : Product [ 'items' ] [ 0 ] ) => {
226- let mainText = item . display ?. primary_text || '' ;
217+ let text = item . display ?. primary_text ?? '' ;
218+ const intervals : Record < string , string > = {
219+ day : ' per day' ,
220+ month : ' per month' ,
221+ year : ' per year' ,
222+ } ;
223+
227224 if (
228225 item . interval &&
229- ! mainText . toLowerCase ( ) . includes ( 'per ' ) &&
230- ! mainText . toLowerCase ( ) . includes ( '/' )
226+ intervals [ item . interval ] &&
227+ ! text . toLowerCase ( ) . includes ( 'per ' ) &&
228+ ! text . toLowerCase ( ) . includes ( '/' )
231229 ) {
232- switch ( item . interval ) {
233- case 'day' :
234- mainText += ' per day' ;
235- break ;
236- case 'month' :
237- mainText += ' per month' ;
238- break ;
239- case 'year' :
240- mainText += ' per year' ;
241- break ;
242- default :
243- break ;
244- }
230+ text += intervals [ item . interval ] ;
245231 }
246- return mainText ;
232+
233+ return text ;
247234 } ;
248235
249236 return (
@@ -279,16 +266,16 @@ const PlanStatusCard = memo(function PlanStatusCardComponent({
279266 </ div >
280267 </ div >
281268
282- < div className = "flex-shrink-0 text-right" >
283- < div className = "font-bold text-2xl sm:text-3xl" >
284- { isFree
285- ? 'Free'
286- : plan ?. items [ 0 ] ?. display ?. primary_text || 'Free' }
287- </ div >
288- < div className = "text-muted-foreground text-sm" >
289- { ! isFree && plan ?. items [ 0 ] ?. display ?. secondary_text }
269+ < div className = "flex-shrink-0 text-right" >
270+ < div className = "font-bold text-2xl sm:text-3xl" >
271+ { isFree
272+ ? 'Free'
273+ : plan ?. items [ 0 ] ?. display ?. primary_text || 'Free' }
274+ </ div >
275+ < div className = "text-muted-foreground text-sm" >
276+ { ! isFree && plan ?. items [ 0 ] ?. display ?. secondary_text }
277+ </ div >
290278 </ div >
291- </ div >
292279 </ div >
293280 </ CardHeader >
294281
@@ -391,7 +378,7 @@ interface OverviewTabProps {
391378export const OverviewTab = memo ( function OverviewTabComponent ( {
392379 onNavigateToPlans,
393380} : OverviewTabProps ) {
394- const { products, usage, customer, isLoading, refetch } = useBillingData ( ) ;
381+ const { products, usage, customer, isLoading, error , refetch } = useBillingData ( ) ;
395382 const {
396383 onCancelClick,
397384 onCancelConfirm,
@@ -405,44 +392,36 @@ export const OverviewTab = memo(function OverviewTabComponent({
405392 } = useBilling ( refetch ) ;
406393
407394 const { currentPlan, usageStats, statusDetails } = useMemo ( ( ) => {
408- const activePlan = products ?. find (
409- ( p : Product ) =>
410- p . scenario !== 'upgrade' &&
411- p . scenario !== 'downgrade' &&
412- p . scenario !== 'new'
413- ) ;
414- const featureUsage = usage ?. features || [ ] ;
395+ const activeCustomerProduct = customer ?. products ?. find ( ( p ) => {
396+ if ( p . canceled_at && p . current_period_end ) {
397+ return dayjs ( p . current_period_end ) . isAfter ( dayjs ( ) ) ;
398+ }
399+ return ! p . canceled_at || p . status === 'scheduled' ;
400+ } ) ;
415401
416- const customerProduct = activePlan
417- ? customer ?. products ?. find ( ( p ) => p . id === activePlan . id )
418- : undefined ;
402+ const activePlan = activeCustomerProduct
403+ ? products ?. find ( ( p : Product ) => p . id === activeCustomerProduct . id )
404+ : products ?. find ( ( p : Product ) => ! p . scenario || ( p . scenario !== 'upgrade' && p . scenario !== 'downgrade' ) ) ;
419405
420- const planStatusDetails = customerProduct
406+ const planStatusDetails = activeCustomerProduct
421407 ? getSubscriptionStatusDetails (
422- customerProduct as unknown as Parameters <
408+ activeCustomerProduct as unknown as Parameters <
423409 typeof getSubscriptionStatusDetails
424410 > [ 0 ]
425411 )
426412 : '' ;
427413
428414 return {
429415 currentPlan : activePlan ,
430- usageStats : featureUsage ,
416+ usageStats : usage ?. features ?? [ ] ,
431417 statusDetails : planStatusDetails ,
432418 } ;
433- } , [
434- products ,
435- usage ?. features ,
436- customer ?. products ,
437- getSubscriptionStatusDetails ,
438- ] ) ;
419+ } , [ products , usage ?. features , customer ?. products , getSubscriptionStatusDetails ] ) ;
439420
440421 if ( isLoading ) {
441422 return (
442423 < div className = "space-y-8" >
443- { /* Header Section Skeleton */ }
444424 < div className = "grid gap-8 lg:grid-cols-3" >
445- { /* Usage Overview Header Skeleton */ }
446425 < div className = "lg:col-span-2" >
447426 < div className = "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between" >
448427 < div className = "space-y-2" >
@@ -453,7 +432,6 @@ export const OverviewTab = memo(function OverviewTabComponent({
453432 </ div >
454433 </ div >
455434
456- { /* Current Plan Header Skeleton */ }
457435 < div className = "lg:col-span-1" >
458436 < div className = "space-y-2" >
459437 < Skeleton className = "h-8 w-32" />
@@ -462,14 +440,11 @@ export const OverviewTab = memo(function OverviewTabComponent({
462440 </ div >
463441 </ div >
464442
465- { /* Main Content Grid Skeleton */ }
466443 < div className = "grid gap-8 lg:grid-cols-3" >
467- { /* Usage Overview Section Skeleton */ }
468444 < div className = "space-y-6 lg:col-span-2" >
469445 < Skeleton className = "h-96 w-full" />
470446 </ div >
471447
472- { /* Current Plan Section Skeleton */ }
473448 < div className = "space-y-6 lg:col-span-1" >
474449 < Skeleton className = "h-96 w-full" />
475450 </ div >
@@ -478,6 +453,29 @@ export const OverviewTab = memo(function OverviewTabComponent({
478453 ) ;
479454 }
480455
456+ if ( error ) {
457+ return (
458+ < Card className = "h-full" >
459+ < CardContent className = "flex h-full flex-col items-center justify-center py-16" >
460+ < div className = "mb-6 flex h-16 w-16 items-center justify-center rounded border bg-destructive/10" >
461+ < WarningIcon
462+ className = "text-destructive"
463+ size = { 32 }
464+ weight = "duotone"
465+ />
466+ </ div >
467+ < h3 className = "mb-2 font-semibold text-xl" > Error Loading Billing Data</ h3 >
468+ < p className = "mb-4 max-w-sm text-center text-muted-foreground" >
469+ { error instanceof Error ? error . message : 'Failed to load customer data. Please try again.' }
470+ </ p >
471+ < Button onClick = { ( ) => refetch ( ) } size = "lg" type = "button" >
472+ Retry
473+ </ Button >
474+ </ CardContent >
475+ </ Card >
476+ ) ;
477+ }
478+
481479 return (
482480 < >
483481 < NoPaymentMethodDialog
@@ -496,9 +494,7 @@ export const OverviewTab = memo(function OverviewTabComponent({
496494 />
497495
498496 < div className = "space-y-8" >
499- { /* Header Section */ }
500497 < div className = "grid gap-8 lg:grid-cols-3" >
501- { /* Usage Overview Header */ }
502498 < div className = "lg:col-span-2" >
503499 < div className = "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between" >
504500 < div >
@@ -516,7 +512,6 @@ export const OverviewTab = memo(function OverviewTabComponent({
516512 </ div >
517513 </ div >
518514
519- { /* Current Plan Header */ }
520515 < div className = "lg:col-span-1" >
521516 < div >
522517 < h2 className = "font-bold text-2xl tracking-tight" >
@@ -529,9 +524,7 @@ export const OverviewTab = memo(function OverviewTabComponent({
529524 </ div >
530525 </ div >
531526
532- { /* Main Content Grid */ }
533527 < div className = "grid gap-8 lg:grid-cols-3" >
534- { /* Usage Overview Section */ }
535528 < div className = "space-y-6 lg:col-span-2" >
536529 { usageStats . length === 0 ? (
537530 < Card className = "h-full" >
@@ -562,7 +555,6 @@ export const OverviewTab = memo(function OverviewTabComponent({
562555 ) }
563556 </ div >
564557
565- { /* Current Plan Section */ }
566558 < div className = "space-y-6 lg:col-span-1" >
567559 < div className = "h-full" >
568560 < PlanStatusCard
0 commit comments