Files
Monica KhouryandJoshen Lim 9e17c41771 Chore: Link Delete Project doc from the Delete Project confirmation m… (#47637)
Fixes FE-3801. 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Updated the project deletion confirmation dialog so the “learn more /
documentation” text and external link are always shown consistently.
* Paid projects now include the full warning (“All project data will be
lost, and cannot be undone.”) alongside the documentation link, while
free projects show the simplified message without the additional
warning.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-07-09 11:41:16 +03:00

212 lines
7.6 KiB
TypeScript

import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { TextArea } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { CANCELLATION_REASONS } from '@/components/interfaces/Billing/Billing.constants'
import { LogicalBackupCliInstructions } from '@/components/layouts/ProjectLayout/LogicalBackupCliInstructions'
import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
import { useSendDowngradeFeedbackMutation } from '@/data/feedback/exit-survey-send'
import type { OrgProject } from '@/data/projects/org-projects-infinite-query'
import { useProjectDeleteMutation } from '@/data/projects/project-delete-mutation'
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import type { Organization } from '@/types'
export const DeleteProjectModal = ({
visible,
onClose,
project: projectProp,
organization: organizationProp,
}: {
visible: boolean
onClose: () => void
project?: OrgProject
organization?: Organization
}) => {
const router = useRouter()
const { data: projectFromQuery } = useSelectedProjectQuery()
const { data: organizationFromQuery } = useSelectedOrganizationQuery()
// Use props if provided, otherwise fall back to hooks
const project = projectProp || projectFromQuery
const organization = organizationProp || organizationFromQuery
const { lastVisitedOrganization } = useLastVisitedOrganization()
const projectRef = project?.ref
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
const projectPlan = subscription?.plan?.id ?? 'free'
const isFree = projectPlan === 'free'
const [message, setMessage] = useState<string>('')
const [selectedReason, setSelectedReason] = useState<string[]>([])
// Single select for cancellation reason
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 [shuffledReasons] = useState(() => [
...CANCELLATION_REASONS.sort(() => Math.random() - 0.5),
{ value: 'None of the above' },
])
const { mutate: deleteProject, isPending: isDeleting } = useProjectDeleteMutation({
onSuccess: async () => {
if (!isFree) {
try {
await sendExitSurvey({
orgSlug: organization?.slug,
projectRef,
message,
reasons: selectedReason.reduce((a, b) => `${a}- ${b}\n`, ''),
exitAction: 'delete',
})
} 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
}
}
toast.success(`Successfully deleted ${project?.name}`)
// Only redirect if still viewing the deleted project
if (router.asPath.startsWith(`/project/${projectRef}`)) {
if (lastVisitedOrganization) {
router.push(`/org/${lastVisitedOrganization}`)
} else {
router.push('/organizations')
}
}
},
})
const { mutateAsync: sendExitSurvey, isPending: isSending } = useSendDowngradeFeedbackMutation()
const isSubmitting = isDeleting || isSending
async function handleDeleteProject() {
if (project === undefined) return
if (!isFree && selectedReason.length === 0) {
return toast.error('Please select a reason for deleting your project')
}
deleteProject({ projectRef: project.ref, organizationSlug: organization?.slug })
}
useEffect(() => {
if (visible) {
setSelectedReason([])
setMessage('')
}
}, [visible])
return (
<TextConfirmModal
visible={visible}
loading={isSubmitting}
size={isFree ? 'medium' : 'xlarge'}
title={`Confirm deletion of ${project?.name}`}
variant="destructive"
alert={{
title: isFree
? 'This action cannot be undone.'
: `This will permanently delete the ${project?.name}`,
description: (
<>
{!isFree && 'All project data will be lost, and cannot be undone. '}
Read the{' '}
<a
href="https://supabase.com/docs/guides/platform/delete-project"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
documentation
</a>{' '}
for prerequisites, implications, and recovery information.
</>
),
}}
text={
isFree
? `This will permanently delete the ${project?.name} project and all of its data.`
: undefined
}
confirmPlaceholder="Type the project name in here"
confirmString={project?.name || ''}
confirmLabel="I understand, delete this project"
onConfirm={handleDeleteProject}
onCancel={() => {
if (!isSubmitting) onClose()
}}
>
<div className="space-y-6">
<LogicalBackupCliInstructions enabled={visible} showResetPassword={false} />
{/*
[Joshen] This is basically ExitSurvey.tsx, ideally we have one shared component but the one
in ExitSurvey has a Form wrapped around it already. Will probably need some effort to refactor
but leaving that for the future.
*/}
{!isFree && (
<div className="flex flex-col gap-y-6">
<FormItemLayout
isReactForm={false}
label="What made you decide to delete your project?"
>
<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={[
'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-50 hover:opacity-75`
}`,
].join(' ')}
>
<input
type="radio"
name="options"
value={option.value}
className="hidden"
checked={active}
onChange={() => onSelectCancellationReason(option.value)}
/>
<div>{option.value}</div>
</label>
)
})}
</div>
</FormItemLayout>
<FormItemLayout isReactForm={false} label={textareaLabel}>
<TextArea
name="message"
rows={3}
value={message}
onChange={(event) => setMessage(event.target.value)}
/>
</FormItemLayout>
</div>
)}
</div>
</TextConfirmModal>
)
}