11"use client" ;
22
3- import { CalendarIcon , LightningIcon } from "@phosphor-icons/react" ;
3+ import {
4+ CalendarIcon ,
5+ CircleNotchIcon ,
6+ LightningIcon ,
7+ WarningCircleIcon ,
8+ } from "@phosphor-icons/react" ;
9+ import dayjs from "dayjs" ;
10+ import { useState } from "react" ;
11+ import { Badge } from "@/components/ui/badge" ;
412import { Button } from "@/components/ui/button" ;
513import {
614 Dialog ,
@@ -20,6 +28,8 @@ interface CancelSubscriptionDialogProps {
2028 isLoading : boolean ;
2129}
2230
31+ type CancelOption = "end_of_period" | "immediate" | null ;
32+
2333export function CancelSubscriptionDialog ( {
2434 open,
2535 onOpenChange,
@@ -28,82 +38,135 @@ export function CancelSubscriptionDialog({
2838 currentPeriodEnd,
2939 isLoading,
3040} : CancelSubscriptionDialogProps ) {
41+ const [ selected , setSelected ] = useState < CancelOption > ( null ) ;
42+ const [ confirming , setConfirming ] = useState ( false ) ;
43+
3144 const periodEndDate = currentPeriodEnd
32- ? new Date ( currentPeriodEnd ) . toLocaleDateString ( )
45+ ? dayjs ( currentPeriodEnd ) . format ( "MMMM D, YYYY" )
3346 : null ;
3447
48+ const handleConfirm = async ( ) => {
49+ if ( ! selected ) return ;
50+ setConfirming ( true ) ;
51+ await onCancel ( selected === "immediate" ) ;
52+ setConfirming ( false ) ;
53+ onOpenChange ( false ) ;
54+ setSelected ( null ) ;
55+ } ;
56+
57+ const handleClose = ( ) => {
58+ onOpenChange ( false ) ;
59+ setSelected ( null ) ;
60+ } ;
61+
3562 return (
36- < Dialog onOpenChange = { onOpenChange } open = { open } >
37- < DialogContent className = "max-w-lg " >
38- < DialogHeader className = "space-y-3" >
39- < DialogTitle className = "text-xl" > Cancel { planName } </ DialogTitle >
63+ < Dialog onOpenChange = { handleClose } open = { open } >
64+ < DialogContent className = "w-[95vw] max-w-md sm:w-full " >
65+ < DialogHeader >
66+ < DialogTitle > Cancel { planName } </ DialogTitle >
4067 < DialogDescription >
41- Choose how you'd like to cancel your subscription
68+ Choose when you'd like to cancel your subscription
4269 </ DialogDescription >
4370 </ DialogHeader >
4471
45- < div className = "space-y-3 py-4" >
72+ < div className = "space-y-2" >
73+ { /* End of period option */ }
4674 < button
47- className = "w-full cursor-pointer rounded-lg border p-4 text-left transition-colors hover:bg-muted/50 disabled:cursor-default disabled:opacity-50"
48- disabled = { isLoading }
49- onClick = { ( ) => {
50- onCancel ( false ) ;
51- onOpenChange ( false ) ;
52- } }
75+ className = { `w-full rounded border p-4 text-left transition-all ${
76+ selected === "end_of_period"
77+ ? "border-primary bg-primary/5 ring-1 ring-primary"
78+ : "hover:bg-accent/50"
79+ } disabled:cursor-not-allowed disabled:opacity-50`}
80+ disabled = { isLoading || confirming }
81+ onClick = { ( ) => setSelected ( "end_of_period" ) }
5382 type = "button"
5483 >
55- < div className = "mb-2 flex items-center gap-3" >
56- < div className = "flex size-8 items-center justify-center rounded-full bg-blue-100" >
57- < CalendarIcon className = "text-foreground" size = { 16 } />
84+ < div className = "flex items-start gap-3" >
85+ < div className = "flex size-10 shrink-0 items-center justify-center rounded border bg-accent" >
86+ < CalendarIcon
87+ className = "text-accent-foreground"
88+ size = { 20 }
89+ weight = "duotone"
90+ />
5891 </ div >
59- < div >
60- < div className = "font-medium" > Cancel at period end</ div >
61- < div className = "text-muted-foreground text-sm" > Recommended</ div >
92+ < div className = "flex-1" >
93+ < div className = "flex items-center gap-2" >
94+ < span className = "font-medium" > Cancel at period end</ span >
95+ < Badge variant = "secondary" > Recommended</ Badge >
96+ </ div >
97+ < p className = "mt-1 text-muted-foreground text-sm" >
98+ { periodEndDate
99+ ? `Keep access until ${ periodEndDate } `
100+ : "Keep access until your billing period ends" }
101+ </ p >
62102 </ div >
63103 </ div >
64- < p className = "ml-11 text-muted-foreground text-sm" >
65- { periodEndDate
66- ? `Keep access until ${ periodEndDate } . No additional charges.`
67- : "Keep access until your current billing period ends. No additional charges." }
68- </ p >
69104 </ button >
70105
106+ { /* Immediate option */ }
71107 < button
72- className = "w-full cursor-pointer rounded-lg border p-4 text-left transition-colors hover:bg-muted/50 disabled:cursor-default disabled:opacity-50"
73- disabled = { isLoading }
74- onClick = { ( ) => {
75- onCancel ( true ) ;
76- onOpenChange ( false ) ;
77- } }
108+ className = { `w-full rounded border p-4 text-left transition-all ${
109+ selected === "immediate"
110+ ? "border-destructive bg-destructive/5 ring-1 ring-destructive"
111+ : "hover:bg-accent/50"
112+ } disabled:cursor-not-allowed disabled:opacity-50`}
113+ disabled = { isLoading || confirming }
114+ onClick = { ( ) => setSelected ( "immediate" ) }
78115 type = "button"
79116 >
80- < div className = "mb-2 flex items-center gap-3" >
81- < div className = "flex size-8 items-center justify-center rounded-full bg-orange-100" >
82- < LightningIcon className = "text-orange-600" size = { 16 } />
117+ < div className = "flex items-start gap-3" >
118+ < div className = "flex size-10 shrink-0 items-center justify-center rounded border border-destructive/20 bg-destructive/10" >
119+ < LightningIcon
120+ className = "text-destructive"
121+ size = { 20 }
122+ weight = "duotone"
123+ />
83124 </ div >
84- < div >
85- < div className = "font-medium" > Cancel immediately</ div >
86- < div className = "text-muted-foreground text-sm" >
87- Lose access now
88- </ div >
125+ < div className = "flex-1" >
126+ < span className = "font-medium" > Cancel immediately</ span >
127+ < p className = "mt-1 text-muted-foreground text-sm" >
128+ Lose access now. Any pending usage will be invoiced.
129+ </ p >
89130 </ div >
90131 </ div >
91- < p className = "ml-11 text-muted-foreground text-sm" >
92- Access ends immediately. You'll be invoiced for any pending usage
93- charges.
94- </ p >
95132 </ button >
96133 </ div >
97134
98- < DialogFooter >
135+ { /* Warning for immediate cancellation */ }
136+ { selected === "immediate" && (
137+ < div className = "flex items-start gap-2 rounded border border-destructive/20 bg-destructive/5 p-3 text-sm" >
138+ < WarningCircleIcon
139+ className = "mt-0.5 shrink-0 text-destructive"
140+ size = { 16 }
141+ weight = "fill"
142+ />
143+ < span className = "text-destructive" >
144+ This action cannot be undone. You will lose access to all{ " " }
145+ { planName } features immediately.
146+ </ span >
147+ </ div >
148+ ) }
149+
150+ < DialogFooter className = "flex-col gap-2 sm:flex-row" >
99151 < Button
100- className = "cursor-pointer "
101- disabled = { isLoading }
102- onClick = { ( ) => onOpenChange ( false ) }
152+ className = "w-full sm:w-auto "
153+ disabled = { isLoading || confirming }
154+ onClick = { handleClose }
103155 variant = "outline"
104156 >
105157 Keep subscription
106158 </ Button >
159+ < Button
160+ className = "w-full sm:w-auto"
161+ disabled = { ! selected || isLoading || confirming }
162+ onClick = { handleConfirm }
163+ variant = { selected === "immediate" ? "destructive" : "default" }
164+ >
165+ { confirming && (
166+ < CircleNotchIcon className = "mr-2 size-4 animate-spin" />
167+ ) }
168+ { selected === "immediate" ? "Cancel now" : "Confirm cancellation" }
169+ </ Button >
107170 </ DialogFooter >
108171 </ DialogContent >
109172 </ Dialog >
0 commit comments