| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- import { useFlag, useParams } from 'common'
- import { useState } from 'react'
- import { toast } from 'sonner'
- import { Button, cn, Modal, TextArea } from 'ui'
- import { Admonition } from 'ui-patterns/admonition'
- import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip'
- import { CANCELLATION_REASONS } from '@/components/interfaces/Billing/Billing.constants'
- import { useSendDowngradeFeedbackMutation } from '@/data/feedback/exit-survey-send'
- import { getComputeSize, OrgProject } from '@/data/projects/org-projects-infinite-query'
- import { useOrgSubscriptionUpdateMutation } from '@/data/subscriptions/org-subscription-update-mutation'
- export interface ExitSurveyModalProps {
- visible: boolean
- projects: OrgProject[]
- onClose: (success?: boolean) => void
- }
- // [Joshen] For context - Exit survey is only when going to Free Plan from a paid plan
- export const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) => {
- const { slug } = useParams()
- const [message, setMessage] = useState('')
- const [selectedReason, setSelectedReason] = useState<string[]>([])
- const subscriptionUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
- const { mutate: updateOrgSubscription, isPending: isUpdating } = useOrgSubscriptionUpdateMutation(
- {
- onError: (error) => {
- toast.error(`Failed to downgrade project: ${error.message}`)
- },
- }
- )
- const { mutateAsync: sendExitSurvey, isPending: isSubmittingFeedback } =
- useSendDowngradeFeedbackMutation()
- const isSubmitting = isUpdating || isSubmittingFeedback
- const projectsWithComputeDowngrade = projects.filter((project) => {
- const computeSize = getComputeSize(project)
- return computeSize !== 'nano'
- })
- const hasProjectsWithComputeDowngrade = projectsWithComputeDowngrade.length > 0
- const [shuffledReasons] = useState(() => [
- ...CANCELLATION_REASONS.sort(() => Math.random() - 0.5),
- { value: 'None of the above' },
- ])
- const onSelectCancellationReason = (reason: string) => {
- setSelectedReason([reason])
- }
- // Helper to get label for selected reason
- const getReasonLabel = (reason: string | undefined) => {
- const found = CANCELLATION_REASONS.find((r) => r.value === reason)
- return found?.label || 'What can we improve on?'
- }
- const textareaLabel = getReasonLabel(selectedReason[0])
- const onSubmit = async () => {
- if (selectedReason.length === 0) {
- return toast.error('Please select a reason for canceling your subscription')
- }
- await downgradeOrganization()
- }
- const downgradeOrganization = async () => {
- // Update the subscription first, followed by posting the exit survey if successful
- // If compute instance is present within the existing subscription, then a restart will be triggered
- if (!slug) return console.error('Slug is required')
- updateOrgSubscription(
- { slug, tier: 'tier_free' },
- {
- onSuccess: async () => {
- try {
- await sendExitSurvey({
- orgSlug: slug,
- reasons: selectedReason.reduce((a, b) => `${a}- ${b}\n`, ''),
- message,
- exitAction: 'downgrade',
- })
- } catch (error) {
- // [Joshen] In this case we don't raise any errors if the exit survey fails to send since it shouldn't block the user
- } finally {
- toast.success(
- hasProjectsWithComputeDowngrade
- ? 'Successfully downgraded organization to the Free Plan. Your projects are currently restarting to update their compute instances.'
- : 'Successfully downgraded organization to the Free Plan',
- { duration: hasProjectsWithComputeDowngrade ? 8000 : 4000 }
- )
- onClose(true)
- window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
- }
- },
- }
- )
- }
- return (
- <Modal hideFooter size="xlarge" visible={visible} onCancel={onClose} header="Help us improve">
- <Modal.Content>
- <div className="space-y-4">
- <p className="text-sm text-foreground-light">
- What made you decide to downgrade your plan?
- </p>
- <div className="space-y-8 mt-6">
- <div className="flex flex-wrap gap-2" data-toggle="buttons">
- {shuffledReasons.map((option) => {
- const active = selectedReason[0] === option.value
- return (
- <label
- key={option.value}
- className={cn(
- 'flex cursor-pointer items-center space-x-2 rounded-md py-1',
- 'pl-2 pr-3 text-center text-sm',
- 'shadow-xs transition-all duration-100',
- active
- ? `bg-foreground text-background opacity-100 hover:bg-foreground/75`
- : `bg-border-strong text-foreground opacity-75 hover:opacity-100`
- )}
- >
- <input
- type="radio"
- name="options"
- value={option.value}
- className="hidden"
- checked={active}
- onChange={() => onSelectCancellationReason(option.value)}
- />
- <div>{option.value}</div>
- </label>
- )
- })}
- </div>
- <div className="text-area-text-sm flex flex-col gap-y-2">
- <label htmlFor="message" className="text-sm whitespace-pre-line wrap-break-word">
- {textareaLabel}
- </label>
- <TextArea
- id="message"
- name="message"
- value={message}
- onChange={(event: any) => setMessage(event.target.value)}
- rows={3}
- />
- </div>
- </div>
- {hasProjectsWithComputeDowngrade && (
- <Admonition
- type="warning"
- layout="horizontal"
- title={`${projectsWithComputeDowngrade.length} of your projects will be restarted upon clicking confirm,`}
- description={
- <>
- This is due to changes in compute instances from the downgrade. Affected projects
- include {projectsWithComputeDowngrade.map((project) => project.name).join(', ')}.
- </>
- }
- />
- )}
- </div>
- </Modal.Content>
- <div className="flex items-center justify-between border-t px-4 py-4">
- <p className="text-xs text-foreground-lighter">
- The unused amount for the remaining time of your billing cycle will be refunded as credits
- </p>
- <div className="flex items-center space-x-2">
- <Button type="default" onClick={() => onClose()}>
- Cancel
- </Button>
- <ProjectUpdateDisabledTooltip projectUpdateDisabled={subscriptionUpdateDisabled}>
- <Button
- type="danger"
- className="pointer-events-auto"
- loading={isSubmitting}
- disabled={subscriptionUpdateDisabled || isSubmitting}
- onClick={onSubmit}
- >
- Confirm downgrade
- </Button>
- </ProjectUpdateDisabledTooltip>
- </div>
- </div>
- </Modal>
- )
- }
|