Home / Function/ ExitSurveyModal() — supabase Function Reference

ExitSurveyModal() — supabase Function Reference

Architecture documentation for the ExitSurveyModal() function in ExitSurveyModal.tsx from the supabase codebase.

Entity Profile

Relationship Graph

Source Code

apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx lines 19–186

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">
            Share with us why you're downgrading 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-sm transition-all duration-100',
                      active
                        ? `bg-foreground text-background opacity-100 hover:bg-opacity-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 className="text-sm whitespace-pre-line break-words">{textareaLabel}</label>
              <Input.TextArea
                id="message"
                name="message"
                value={message}
                onChange={(event: any) => setMessage(event.target.value)}
                rows={3}
              />
            </div>
          </div>
          {hasProjectsWithComputeDowngrade && (
            <Alert
              withIcon
              variant="warning"
              title={`${projectsWithComputeDowngrade.length} of your projects will be restarted upon clicking confirm,`}
            >
              This is due to changes in compute instances from the downgrade. Affected projects
              include {projectsWithComputeDowngrade.map((project) => project.name).join(', ')}.
            </Alert>
          )}
        </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>
  )
}

Subdomains

Analyze Your Own Codebase

Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.

Try Supermodel Free