1- import { includes , without } from 'lodash'
2- import { useReducer , useState } from 'react'
1+ import { useState } from 'react'
32import { toast } from 'sonner'
43
54import { useParams } from 'common'
5+ import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants'
66import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send'
7+ import { ProjectInfo } from 'data/projects/projects-query'
78import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation'
89import { useFlag } from 'hooks/ui/useFlag'
9- import { Alert , Button , Input , Modal } from 'ui'
10- import type { ProjectInfo } from '../../../../../data/projects/projects-query'
11- import { CANCELLATION_REASONS } from '../BillingSettings.constants'
10+ import { Alert , Button , cn , Input , Modal } from 'ui'
1211import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip'
1312
1413export interface ExitSurveyModalProps {
@@ -18,11 +17,11 @@ export interface ExitSurveyModalProps {
1817}
1918
2019// [Joshen] For context - Exit survey is only when going to Free Plan from a paid plan
21- const ExitSurveyModal = ( { visible, projects, onClose } : ExitSurveyModalProps ) => {
20+ export const ExitSurveyModal = ( { visible, projects, onClose } : ExitSurveyModalProps ) => {
2221 const { slug } = useParams ( )
2322
2423 const [ message , setMessage ] = useState ( '' )
25- const [ selectedReasons , dispatchSelectedReasons ] = useReducer ( reducer , [ ] )
24+ const [ selectedReason , setSelectedReason ] = useState < string [ ] > ( [ ] )
2625
2726 const subscriptionUpdateDisabled = useFlag ( 'disableProjectCreationAndUpdate' )
2827 const { mutate : updateOrgSubscription , isLoading : isUpdating } = useOrgSubscriptionUpdateMutation (
@@ -42,17 +41,26 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) =
4241
4342 const hasProjectsWithComputeDowngrade = projectsWithComputeDowngrade . length > 0
4443
45- function reducer ( state : any , action : any ) {
46- if ( includes ( state , action . target . value ) ) {
47- return without ( state , action . target . value )
48- } else {
49- return [ ...state , action . target . value ]
50- }
44+ const [ shuffledReasons ] = useState ( ( ) => [
45+ ...CANCELLATION_REASONS . sort ( ( ) => Math . random ( ) - 0.5 ) ,
46+ { value : 'None of the above' } ,
47+ ] )
48+
49+ const onSelectCancellationReason = ( reason : string ) => {
50+ setSelectedReason ( [ reason ] )
51+ }
52+
53+ // Helper to get label for selected reason
54+ const getReasonLabel = ( reason : string | undefined ) => {
55+ const found = CANCELLATION_REASONS . find ( ( r ) => r . value === reason )
56+ return found ?. label || 'What can we improve on?'
5157 }
5258
59+ const textareaLabel = getReasonLabel ( selectedReason [ 0 ] )
60+
5361 const onSubmit = async ( ) => {
54- if ( selectedReasons . length === 0 ) {
55- return toast . error ( 'Please select at least one reason for canceling your subscription' )
62+ if ( selectedReason . length === 0 ) {
63+ return toast . error ( 'Please select a reason for canceling your subscription' )
5664 }
5765
5866 await downgradeOrganization ( )
@@ -70,7 +78,7 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) =
7078 try {
7179 await sendExitSurvey ( {
7280 orgSlug : slug ,
73- reasons : selectedReasons . reduce ( ( a , b ) => `${ a } - ${ b } \n` , '' ) ,
81+ reasons : selectedReason . reduce ( ( a , b ) => `${ a } - ${ b } \n` , '' ) ,
7482 message,
7583 exitAction : 'downgrade' ,
7684 } )
@@ -92,99 +100,87 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) =
92100 }
93101
94102 return (
95- < >
96- < Modal
97- hideFooter
98- size = "xlarge"
99- visible = { visible }
100- onCancel = { onClose }
101- header = "Help us improve."
102- >
103- < Modal . Content >
104- < div className = "space-y-4" >
105- < p className = "text-sm text-foreground-light" >
106- We always strive to improve Supabase as much as we can. Please let us know the reasons
107- you are canceling your subscription so that we can improve in the future.
108- </ p >
109- < div className = "space-y-8 mt-6" >
110- < div className = "flex flex-wrap gap-2" data-toggle = "buttons" >
111- { CANCELLATION_REASONS . map ( ( option ) => {
112- const active = selectedReasons . find ( ( x ) => x === option )
113- return (
114- < label
115- key = { option }
116- className = { `
117- flex cursor-pointer items-center space-x-2 rounded-md py-1
118- pl-2 pr-3 text-center text-sm
119- shadow-sm transition-all duration-100
120- ${
121- active
122- ? ` bg-foreground text-background opacity-100 hover:bg-opacity-75`
123- : ` bg-border-strong text-foreground opacity-25 hover:opacity-50`
124- }
125- ` }
126- >
127- < input
128- type = "checkbox"
129- name = "options"
130- value = { option }
131- className = "hidden"
132- onClick = { dispatchSelectedReasons }
133- />
134- < div > { option } </ div >
135- </ label >
136- )
137- } ) }
138- </ div >
139- < div className = "text-area-text-sm" >
140- < Input . TextArea
141- id = "message"
142- name = "message"
143- value = { message }
144- onChange = { ( event : any ) => setMessage ( event . target . value ) }
145- label = "Anything else that we can improve on?"
146- />
147- </ div >
103+ < Modal hideFooter size = "xlarge" visible = { visible } onCancel = { onClose } header = "Help us improve" >
104+ < Modal . Content >
105+ < div className = "space-y-4" >
106+ < p className = "text-sm text-foreground-light" >
107+ Share with us why you're downgrading your plan.
108+ </ p >
109+ < div className = "space-y-8 mt-6" >
110+ < div className = "flex flex-wrap gap-2" data-toggle = "buttons" >
111+ { shuffledReasons . map ( ( option ) => {
112+ const active = selectedReason [ 0 ] === option . value
113+ return (
114+ < label
115+ key = { option . value }
116+ className = { cn (
117+ 'flex cursor-pointer items-center space-x-2 rounded-md py-1' ,
118+ 'pl-2 pr-3 text-center text-sm' ,
119+ 'shadow-sm transition-all duration-100' ,
120+ active
121+ ? `bg-foreground text-background opacity-100 hover:bg-opacity-75`
122+ : `bg-border-strong text-foreground opacity-75 hover:opacity-100`
123+ ) }
124+ >
125+ < input
126+ type = "radio"
127+ name = "options"
128+ value = { option . value }
129+ className = "hidden"
130+ checked = { active }
131+ onChange = { ( ) => onSelectCancellationReason ( option . value ) }
132+ />
133+ < div > { option . value } </ div >
134+ </ label >
135+ )
136+ } ) }
137+ </ div >
138+ < div className = "text-area-text-sm flex flex-col gap-y-2" >
139+ < label className = "text-sm whitespace-pre-line break-words" > { textareaLabel } </ label >
140+ < Input . TextArea
141+ id = "message"
142+ name = "message"
143+ value = { message }
144+ onChange = { ( event : any ) => setMessage ( event . target . value ) }
145+ rows = { 3 }
146+ />
148147 </ div >
149- { hasProjectsWithComputeDowngrade && (
150- < Alert
151- withIcon
152- variant = "warning"
153- title = { `${ projectsWithComputeDowngrade . length } of your projects will be restarted upon clicking confirm,` }
154- >
155- This is due to changes in compute instances from the downgrade. Affected projects
156- include { projectsWithComputeDowngrade . map ( ( project ) => project . name ) . join ( ', ' ) } .
157- </ Alert >
158- ) }
159148 </ div >
160- </ Modal . Content >
161-
162- < div className = "flex items-center justify-between border-t px-4 py-4" >
163- < p className = "text-xs text-foreground-lighter" >
164- The unused amount for the remaining time of your billing cycle will be refunded as
165- credits
166- </ p >
167-
168- < div className = "flex items-center space-x-2" >
169- < Button type = "default" onClick = { ( ) => onClose ( ) } >
170- Cancel
149+ { hasProjectsWithComputeDowngrade && (
150+ < Alert
151+ withIcon
152+ variant = "warning"
153+ title = { `${ projectsWithComputeDowngrade . length } of your projects will be restarted upon clicking confirm,` }
154+ >
155+ This is due to changes in compute instances from the downgrade. Affected projects
156+ include { projectsWithComputeDowngrade . map ( ( project ) => project . name ) . join ( ', ' ) } .
157+ </ Alert >
158+ ) }
159+ </ div >
160+ </ Modal . Content >
161+
162+ < div className = "flex items-center justify-between border-t px-4 py-4" >
163+ < p className = "text-xs text-foreground-lighter" >
164+ The unused amount for the remaining time of your billing cycle will be refunded as credits
165+ </ p >
166+
167+ < div className = "flex items-center space-x-2" >
168+ < Button type = "default" onClick = { ( ) => onClose ( ) } >
169+ Cancel
170+ </ Button >
171+ < ProjectUpdateDisabledTooltip projectUpdateDisabled = { subscriptionUpdateDisabled } >
172+ < Button
173+ type = "danger"
174+ className = "pointer-events-auto"
175+ loading = { isSubmitting }
176+ disabled = { subscriptionUpdateDisabled || isSubmitting }
177+ onClick = { onSubmit }
178+ >
179+ Confirm downgrade
171180 </ Button >
172- < ProjectUpdateDisabledTooltip projectUpdateDisabled = { subscriptionUpdateDisabled } >
173- < Button
174- type = "danger"
175- className = "pointer-events-auto"
176- loading = { isSubmitting }
177- disabled = { subscriptionUpdateDisabled || isSubmitting }
178- onClick = { onSubmit }
179- >
180- Confirm downgrade
181- </ Button >
182- </ ProjectUpdateDisabledTooltip >
183- </ div >
181+ </ ProjectUpdateDisabledTooltip >
184182 </ div >
185- </ Modal >
186- </ >
183+ </ div >
184+ </ Modal >
187185 )
188186}
189-
190- export default ExitSurveyModal
0 commit comments