@@ -52,7 +52,11 @@ const STATIC_PLANS: PlanConfig[] = (planConfigData.plans ?? []).map((plan) => ({
5252
5353function CheckIcon ( ) {
5454 return (
55- < svg viewBox = "0 0 24 24" aria-hidden = "true" className = "h-4 w-4 text-sky-600" >
55+ < svg
56+ viewBox = "0 0 24 24"
57+ aria-hidden = "true"
58+ className = "h-4 w-4 text-sky-600"
59+ >
5660 < path
5761 d = "M20.285 6.709a1 1 0 0 1 0 1.414l-9.192 9.192a1 1 0 0 1-1.414 0L3.715 11.55a1 1 0 0 1 1.414-1.415l5.136 5.136 8.485-8.485a1 1 0 0 1 1.535-.077z"
5862 fill = "currentColor"
@@ -111,6 +115,23 @@ export default function ChangePlansPage() {
111115 const [ errorMessage , setErrorMessage ] = useState < string | null > ( null ) ;
112116 const [ confirmOpen , setConfirmOpen ] = useState ( false ) ;
113117
118+ const getPlanByKey = ( planKey : string ) =>
119+ STATIC_PLANS . find ( ( plan ) => plan . key === planKey ) ?? null ;
120+
121+ const isDowngradePlanSelection = ( nextPlanKey : string ) => {
122+ const currentPlan = getPlanByKey ( currentPlanKey ) ;
123+ const nextPlan = getPlanByKey ( nextPlanKey ) ;
124+
125+ if ( ! currentPlan || ! nextPlan ) {
126+ return false ;
127+ }
128+
129+ return nextPlan . monthlyPriceUsdCents < currentPlan . monthlyPriceUsdCents ;
130+ } ;
131+
132+ const downgradePlanMessage =
133+ "You are currently on a higher plan. Please cancel your current subscription to switch plans at the end of the billing cycle." ;
134+
114135 useEffect ( ( ) => {
115136 const params = new URLSearchParams ( location . search ) ;
116137 const nextMode = params . get ( "mode" ) === "seats" ? "seats" : "plan" ;
@@ -159,7 +180,8 @@ export default function ChangePlansPage() {
159180 return ;
160181 }
161182
162- const billingData = ( billingRes . data ?? null ) as BillingAccessResponse | null ;
183+ const billingData = ( billingRes . data ??
184+ null ) as BillingAccessResponse | null ;
163185 setBilling ( billingData ) ;
164186
165187 const subscribedPaddlePlanKey = String (
@@ -174,7 +196,10 @@ export default function ChangePlansPage() {
174196 const normalizedPlanKey = matchedPlan ?. key ?? subscribedPaddlePlanKey ;
175197 const seats =
176198 Number (
177- billingData ?. subscription_seats ?? billingData ?. seats ?? billingData ?. quantity ?? MIN_SEATS ,
199+ billingData ?. subscription_seats ??
200+ billingData ?. seats ??
201+ billingData ?. quantity ??
202+ MIN_SEATS ,
178203 ) || MIN_SEATS ;
179204
180205 setCurrentPlanKey ( normalizedPlanKey ) ;
@@ -185,7 +210,11 @@ export default function ChangePlansPage() {
185210 } catch ( error : any ) {
186211 if ( mounted ) {
187212 setErrorMessage (
188- String ( error ?. response ?. data ?. detail ?? error ?. message ?? "Failed to load billing data." ) ,
213+ String (
214+ error ?. response ?. data ?. detail ??
215+ error ?. message ??
216+ "Failed to load billing data." ,
217+ ) ,
189218 ) ;
190219 }
191220 } finally {
@@ -209,7 +238,10 @@ export default function ChangePlansPage() {
209238 setPendingSeats ( ( prev ) => prev + 1 ) ;
210239 } ;
211240
212- function buildPreviewMessage ( previewData : any , nextBillingDate ?: string ) : string | null {
241+ function buildPreviewMessage (
242+ previewData : any ,
243+ nextBillingDate ?: string ,
244+ ) : string | null {
213245 if ( ! previewData ) return null ;
214246
215247 const hasImmediateCharge = Boolean ( previewData ?. immediate_transaction ) ;
@@ -221,7 +253,9 @@ export default function ChangePlansPage() {
221253
222254 if ( nextBillingDate ) {
223255 const date = new Date ( nextBillingDate ) ;
224- const formatted = date . toLocaleDateString ( undefined , { dateStyle : "medium" } ) ;
256+ const formatted = date . toLocaleDateString ( undefined , {
257+ dateStyle : "medium" ,
258+ } ) ;
225259 return `Your plan will change on ${ formatted } . No charge today.` ;
226260 }
227261
@@ -244,9 +278,16 @@ export default function ChangePlansPage() {
244278 return ;
245279 }
246280
281+ if ( isDowngradePlanSelection ( planKey ) ) {
282+ setSummaryMessage ( downgradePlanMessage ) ;
283+ return ;
284+ }
285+
247286 try {
248287 const selected = STATIC_PLANS . find ( ( plan ) => plan . key === planKey ) ;
249- const selectedPriceId = selected ? priceMap [ selected . paddlePlanKey ] : undefined ;
288+ const selectedPriceId = selected
289+ ? priceMap [ selected . paddlePlanKey ]
290+ : undefined ;
250291
251292 if ( ! selectedPriceId ) {
252293 throw new Error ( "Missing price id for selected plan." ) ;
@@ -257,17 +298,18 @@ export default function ChangePlansPage() {
257298 seats : currentSeats ,
258299 } ) ;
259300
260- const amount = extractPreviewAmount ( preview ) ;
261301 setPreviewData ( preview ) ;
262- setPreviewMessage (
263- buildPreviewMessage ( preview , billing ?. next_billed_at )
264- ) ;
302+ setPreviewMessage ( buildPreviewMessage ( preview , billing ?. next_billed_at ) ) ;
265303 setSummaryMessage (
266304 `Plan change: ${ currentPlanKey || "current" } → ${ planKey } (${ currentSeats } seats).` ,
267305 ) ;
268306 } catch ( error : any ) {
269307 setErrorMessage (
270- String ( error ?. response ?. data ?. detail ?? error ?. message ?? "Failed to preview plan change." ) ,
308+ String (
309+ error ?. response ?. data ?. detail ??
310+ error ?. message ??
311+ "Failed to preview plan change." ,
312+ ) ,
271313 ) ;
272314 }
273315 } ;
@@ -282,19 +324,29 @@ export default function ChangePlansPage() {
282324 try {
283325 if ( mode === "seats" ) {
284326 const preview = await previewChange ( { seats : pendingSeats } ) ;
285- const amount = extractPreviewAmount ( preview ) ;
286327 setPreviewData ( preview ) ;
287328 setPreviewMessage (
288- buildPreviewMessage ( preview , billing ?. next_billed_at )
329+ buildPreviewMessage ( preview , billing ?. next_billed_at ) ,
289330 ) ;
290331 } else {
291332 if ( selectedPlanKey === ENTERPRISE_PLAN_KEY ) {
292- setSummaryMessage ( "Please contact us to switch to the Enterprise plan." ) ;
333+ setSummaryMessage (
334+ "Please contact us to switch to the Enterprise plan." ,
335+ ) ;
293336 return ;
294337 }
295338
296- const selected = STATIC_PLANS . find ( ( plan ) => plan . key === selectedPlanKey ) ;
297- const selectedPriceId = selected ? priceMap [ selected . paddlePlanKey ] : undefined ;
339+ if ( isDowngradePlanSelection ( selectedPlanKey ) ) {
340+ setSummaryMessage ( downgradePlanMessage ) ;
341+ return ;
342+ }
343+
344+ const selected = STATIC_PLANS . find (
345+ ( plan ) => plan . key === selectedPlanKey ,
346+ ) ;
347+ const selectedPriceId = selected
348+ ? priceMap [ selected . paddlePlanKey ]
349+ : undefined ;
298350
299351 if ( ! selectedPriceId ) {
300352 throw new Error ( "Missing price id for selected plan." ) ;
@@ -305,10 +357,9 @@ export default function ChangePlansPage() {
305357 seats : currentSeats ,
306358 } ) ;
307359
308- const amount = extractPreviewAmount ( preview ) ;
309360 setPreviewData ( preview ) ;
310361 setPreviewMessage (
311- buildPreviewMessage ( preview , billing ?. next_billed_at )
362+ buildPreviewMessage ( preview , billing ?. next_billed_at ) ,
312363 ) ;
313364 setSummaryMessage (
314365 `Plan change: ${ currentPlanKey || "current" } → ${ selectedPlanKey } (${ currentSeats } seats).` ,
@@ -341,13 +392,19 @@ export default function ChangePlansPage() {
341392 setCurrentSeats ( pendingSeats ) ;
342393 } else {
343394 if ( selectedPlanKey === ENTERPRISE_PLAN_KEY ) {
344- setSummaryMessage ( "Please contact us to switch to the Enterprise plan." ) ;
395+ setSummaryMessage (
396+ "Please contact us to switch to the Enterprise plan." ,
397+ ) ;
345398 setConfirmOpen ( false ) ;
346399 return ;
347400 }
348401
349- const selected = STATIC_PLANS . find ( ( plan ) => plan . key === selectedPlanKey ) ;
350- const selectedPriceId = selected ? priceMap [ selected . paddlePlanKey ] : undefined ;
402+ const selected = STATIC_PLANS . find (
403+ ( plan ) => plan . key === selectedPlanKey ,
404+ ) ;
405+ const selectedPriceId = selected
406+ ? priceMap [ selected . paddlePlanKey ]
407+ : undefined ;
351408
352409 if ( ! selectedPriceId ) {
353410 throw new Error ( "Missing price id for selected plan." ) ;
@@ -364,20 +421,28 @@ export default function ChangePlansPage() {
364421 navigate ( "/settings/pricing-plans" , { replace : true } ) ;
365422 } catch ( error : any ) {
366423 setErrorMessage (
367- String ( error ?. response ?. data ?. detail ?? error ?. message ?? "Failed to apply billing change." ) ,
424+ String (
425+ error ?. response ?. data ?. detail ??
426+ error ?. message ??
427+ "Failed to apply billing change." ,
428+ ) ,
368429 ) ;
369430 } finally {
370431 setSubmitting ( false ) ;
371432 }
372433 } ;
373434
374435 const canSubmitSeatMode =
375- mode === "seats" && ! actionsBlocked && pendingSeats >= MIN_SEATS && pendingSeats !== currentSeats ;
436+ mode === "seats" &&
437+ ! actionsBlocked &&
438+ pendingSeats >= MIN_SEATS &&
439+ pendingSeats !== currentSeats ;
376440 const canSubmitPlanMode =
377441 mode === "plan" &&
378442 ! actionsBlocked &&
379443 Boolean ( selectedPlanKey ) &&
380444 selectedPlanKey !== currentPlanKey &&
445+ ! isDowngradePlanSelection ( selectedPlanKey ) &&
381446 selectedPlanKey !== ENTERPRISE_PLAN_KEY ;
382447
383448 return (
@@ -404,7 +469,8 @@ export default function ChangePlansPage() {
404469 < >
405470 { actionsBlocked && (
406471 < div className = "mt-6 rounded-xl border border-amber-300 bg-amber-50 p-3 text-sm text-amber-700" >
407- Actions are disabled because your subscription is cancelled or you are not an admin.
472+ Actions are disabled because your subscription is cancelled
473+ or you are not an admin.
408474 </ div >
409475 ) }
410476
@@ -430,24 +496,31 @@ export default function ChangePlansPage() {
430496 { STATIC_PLANS . map ( ( plan ) => {
431497 const isCurrentPlan = plan . key === currentPlanKey ;
432498 const isSelectedPlan = plan . key === selectedPlanKey ;
433- const planDisabledInSeatMode = mode === "seats" && ! isCurrentPlan ;
434- const isDisabledPlan = actionsBlocked || planDisabledInSeatMode ;
499+ const planDisabledInSeatMode =
500+ mode === "seats" && ! isCurrentPlan ;
501+ const isDisabledPlan =
502+ actionsBlocked || planDisabledInSeatMode ;
435503
436504 const borderClass = isCurrentPlan
437505 ? "border-emerald-500"
438506 : isSelectedPlan
439507 ? "border-sky-400"
440508 : "border-slate-200" ;
441509
442- const cardOpacity = mode === "seats" && ! isCurrentPlan ? "opacity-50" : "opacity-100" ;
443-
444- const seatsForCard = isCurrentPlan ? pendingSeats : currentSeats ;
445- const total = ( seatsForCard * plan . monthlyPriceUsdCents ) / 100 ;
446- const canEditSeats = mode === "seats" && isCurrentPlan && ! actionsBlocked ;
510+ const cardOpacity =
511+ mode === "seats" && ! isCurrentPlan
512+ ? "opacity-50"
513+ : "opacity-100" ;
514+
515+ const seatsForCard = isCurrentPlan
516+ ? pendingSeats
517+ : currentSeats ;
518+ const total =
519+ ( seatsForCard * plan . monthlyPriceUsdCents ) / 100 ;
520+ const canEditSeats =
521+ mode === "seats" && isCurrentPlan && ! actionsBlocked ;
447522 const buttonDisabled =
448- isDisabledPlan ||
449- mode === "seats" ||
450- isCurrentPlan ;
523+ isDisabledPlan || mode === "seats" || isCurrentPlan ;
451524
452525 return (
453526 < div
@@ -458,11 +531,14 @@ export default function ChangePlansPage() {
458531 { plan . name }
459532 </ div >
460533 < div className = "mt-2 text-3xl font-semibold text-slate-900" >
461- { ( plan . monthlyPriceUsdCents / 100 ) . toLocaleString ( "en-US" , {
462- style : "currency" ,
463- currency : "USD" ,
464- maximumFractionDigits : 0 ,
465- } ) }
534+ { ( plan . monthlyPriceUsdCents / 100 ) . toLocaleString (
535+ "en-US" ,
536+ {
537+ style : "currency" ,
538+ currency : "USD" ,
539+ maximumFractionDigits : 0 ,
540+ } ,
541+ ) }
466542 /seat/month
467543 </ div >
468544
@@ -506,15 +582,20 @@ export default function ChangePlansPage() {
506582 </ button >
507583 </ div >
508584
509- < div className = "mt-3 text-sm text-slate-500" > Estimated monthly total</ div >
585+ < div className = "mt-3 text-sm text-slate-500" >
586+ Estimated monthly total
587+ </ div >
510588 < div className = "text-2xl font-semibold text-slate-900" >
511589 { toCurrency ( total ) } USD
512590 </ div >
513591 </ div >
514592
515593 < ul className = "mt-5 space-y-3 text-sm text-slate-600" >
516594 { plan . features . map ( ( feature ) => (
517- < li key = { feature } className = "flex items-center gap-2" >
595+ < li
596+ key = { feature }
597+ className = "flex items-center gap-2"
598+ >
518599 < CheckIcon />
519600 { feature }
520601 </ li >
@@ -527,7 +608,9 @@ export default function ChangePlansPage() {
527608 disabled = { buttonDisabled }
528609 className = "mt-6 rounded-xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white transition enabled:hover:bg-slate-800 disabled:cursor-not-allowed disabled:bg-slate-300"
529610 >
530- { isCurrentPlan ? "Current Plan" : `Select ${ plan . name } ` }
611+ { isCurrentPlan
612+ ? "Current Plan"
613+ : `Select ${ plan . name } ` }
531614 </ button >
532615 </ div >
533616 ) ;
@@ -537,18 +620,24 @@ export default function ChangePlansPage() {
537620 < div className = "mt-8 flex flex-wrap justify-center gap-3" >
538621 < Button
539622 onClick = { ( ) => void handlePreviewBeforeConfirm ( ) }
540- disabled = { mode === "seats" ? ! canSubmitSeatMode : ! canSubmitPlanMode }
623+ disabled = {
624+ mode === "seats" ? ! canSubmitSeatMode : ! canSubmitPlanMode
625+ }
541626 >
542627 { mode === "seats" ? "Update Seats" : "Confirm Plan Change" }
543628 </ Button >
544- < Button variant = "outline" onClick = { ( ) => navigate ( "/settings/pricing-plans" ) } >
629+ < Button
630+ variant = "outline"
631+ onClick = { ( ) => navigate ( "/settings/pricing-plans" ) }
632+ >
545633 Back
546634 </ Button >
547635 </ div >
548636
549637 { previewData && (
550638 < div className = "mt-4 rounded-xl border border-slate-200 bg-slate-50 p-3 text-xs text-slate-600" >
551- Preview received. Charges shown above will be applied only after confirmation.
639+ Preview received. Charges shown above will be applied only
640+ after confirmation.
552641 </ div >
553642 ) }
554643 </ >
@@ -564,7 +653,8 @@ export default function ChangePlansPage() {
564653 { mode === "seats" ? "Confirm Seat Update" : "Confirm Plan Change" }
565654 </ DialogTitle >
566655 < DialogDescription >
567- { previewMessage ?? "A billing preview was generated for this change." }
656+ { previewMessage ??
657+ "A billing preview was generated for this change." }
568658 </ DialogDescription >
569659 </ DialogHeader >
570660 < DialogFooter >
0 commit comments