'use client'; import { useCallback, useState } from 'react'; import { Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Textarea } from '@/components/ui/textarea'; import { CANCELLATION_NOTE_MAX_LENGTH, CANCELLATION_REASONS, type CancellationReason, } from '@/lib/cancellation-reasons'; interface CancelSubscriptionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; /** When access ends if the cancellation goes through, or null when unknown. */ periodEnd: string | null; /** True for a subscription that is still inside its Stripe trial. */ isTrial: boolean; /** Unpaid subscriptions end now; cancellation does not extend access. */ canceledImmediately?: boolean; /** Resolves true once the cancellation went through; false keeps the dialog and its answer. */ onConfirm: (input: { reason: CancellationReason | null; note: string | null; }) => Promise; } /** * One question, five answers, no default, all of it skippable. * * The answer is the whole reason this dialog exists instead of a plain confirm, * and the way to get honest answers is to make them cheap: one click, no * required field, and a cancel button that works with nothing selected. A * free-text box appears only under the two answers where the detail is worth * more than the category. */ export function CancelSubscriptionDialog({ open, onOpenChange, periodEnd, isTrial, canceledImmediately = false, onConfirm, }: CancelSubscriptionDialogProps) { const [reason, setReason] = useState(null); const [note, setNote] = useState(''); const [submitting, setSubmitting] = useState(false); const selected = CANCELLATION_REASONS.find((entry) => entry.value === reason) ?? null; const showNote = selected?.askForDetail ?? false; const handleOpenChange = useCallback( (next: boolean) => { if (submitting) return; if (!next) { setReason(null); setNote(''); } onOpenChange(next); }, [onOpenChange, submitting] ); const handleConfirm = useCallback(async () => { setSubmitting(true); try { const trimmed = note.trim(); const done = await onConfirm({ reason, note: showNote && trimmed.length > 0 ? trimmed : null, }); // A failed request keeps the answer on screen. Wiping a typed note // because Stripe timed out is the fastest way to never get it back. if (done) { setReason(null); setNote(''); } } finally { setSubmitting(false); } }, [note, onConfirm, reason, showNote]); const endsOn = periodEnd ? new Date(periodEnd).toLocaleDateString() : null; return ( Cancel your {isTrial ? 'trial' : 'subscription'}? {canceledImmediately ? 'This subscription ends immediately. Canceling does not extend access to your workspaces. Automatic collection stops for its open invoices. Eligible current-period subscription invoices are canceled; charges for prior service and other items may still be owed.' : endsOn ? `Everything stays on until ${endsOn}. Nothing is deleted before then, and you will not be charged again.` : 'Everything stays on until the end of the current period. Nothing is deleted before then, and you will not be charged again.'}

What is the main reason?{' '} (optional)

setReason(value as CancellationReason)} disabled={submitting} > {CANCELLATION_REASONS.map((entry) => ( ))} {showNote ? (