diff --git a/apps/portal/src/features/subscriptions/components/CancellationFlow/CancellationFlow.tsx b/apps/portal/src/features/subscriptions/components/CancellationFlow/CancellationFlow.tsx new file mode 100644 index 00000000..9997e1ed --- /dev/null +++ b/apps/portal/src/features/subscriptions/components/CancellationFlow/CancellationFlow.tsx @@ -0,0 +1,478 @@ +"use client"; + +import Link from "next/link"; +import { useState, type ReactNode } from "react"; +import { PageLayout, type BreadcrumbItem } from "@/components/templates/PageLayout"; +import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; +import { Button } from "@/components/atoms"; +import { ArrowLeftIcon, CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline"; + +// ============================================================================ +// Types +// ============================================================================ + +export type Step = 1 | 2 | 3; + +export interface CancellationMonth { + value: string; + label: string; +} + +export interface CancellationFlowProps { + /** Page icon */ + icon: ReactNode; + /** Page title */ + title: string; + /** Page description / subtitle */ + description: string; + /** Breadcrumb items */ + breadcrumbs: BreadcrumbItem[]; + /** Back link URL */ + backHref: string; + /** Back link label */ + backLabel: string; + /** Available cancellation months */ + availableMonths: CancellationMonth[]; + /** Customer email for confirmation */ + customerEmail: string; + /** Loading state */ + loading?: boolean; + /** Page-level blocking error */ + error?: string | null; + /** Success message after submission */ + successMessage?: string | null; + /** Non-blocking error message */ + formError?: string | null; + /** Submitting state */ + submitting?: boolean; + /** Service info to display in step 1 */ + serviceInfo: ReactNode; + /** Notice/terms content for step 2 */ + termsContent: ReactNode; + /** Summary content for step 3 (before email and comments) */ + summaryContent: ReactNode; + /** Optional extra content for step 3 (after summary, before email) */ + step3ExtraContent?: ReactNode; + /** Optional warning banner (e.g., minimum contract term) */ + warningBanner?: ReactNode; + /** Confirmation message shown in the browser confirm dialog */ + confirmMessage: string; + /** Called when user submits the cancellation */ + onSubmit: (data: { + cancellationMonth: string; + confirmRead: boolean; + confirmCancel: boolean; + comments?: string; + }) => void | Promise; +} + +// ============================================================================ +// Shared Components +// ============================================================================ + +const STEP_LABELS = ["Select Date", "Review Terms", "Confirm"] as const; + +export function Notice({ title, children }: { title: string; children: ReactNode }) { + return ( +
+
{title}
+
{children}
+
+ ); +} + +export function InfoNotice({ title, children }: { title: string; children: ReactNode }) { + return ( +
+
{title}
+
{children}
+
+ ); +} + +function StepIndicator({ currentStep }: { currentStep: Step }) { + return ( +
+ {STEP_LABELS.map((label, idx) => { + const stepNum = (idx + 1) as Step; + const isCompleted = stepNum < currentStep; + const isCurrent = stepNum === currentStep; + + return ( +
+
+
+ {isCompleted ? : stepNum} +
+ +
+ {stepNum < 3 && ( +
+ )} +
+ ); + })} +
+ ); +} + +// ============================================================================ +// Main Component +// ============================================================================ + +export function CancellationFlow({ + icon, + title, + description, + breadcrumbs, + backHref, + backLabel, + availableMonths, + customerEmail, + loading = false, + error = null, + successMessage = null, + formError = null, + submitting = false, + serviceInfo, + termsContent, + summaryContent, + step3ExtraContent, + warningBanner, + confirmMessage, + onSubmit, +}: CancellationFlowProps) { + const [step, setStep] = useState(1); + const [selectedMonth, setSelectedMonth] = useState(""); + const [acceptTerms, setAcceptTerms] = useState(false); + const [confirmMonthEnd, setConfirmMonthEnd] = useState(false); + const [comments, setComments] = useState(""); + + const selectedMonthInfo = availableMonths.find(m => m.value === selectedMonth); + const canProceedStep2 = !!selectedMonth; + const canProceedStep3 = acceptTerms && confirmMonthEnd; + + const handleSubmit = () => { + const message = confirmMessage.replace("{month}", selectedMonthInfo?.label || selectedMonth); + if (window.confirm(message)) { + void onSubmit({ + cancellationMonth: selectedMonth, + confirmRead: acceptTerms, + confirmCancel: confirmMonthEnd, + comments: comments.trim() || undefined, + }); + } + }; + + const headerActions = step === 3 && canProceedStep3 && ( + + ); + + return ( + +
+ {/* Back link */} + + + {backLabel} + + + {/* Warning banner (e.g., minimum contract) */} + {warningBanner} + + {/* Alerts */} + {formError && ( +
+ + {formError} + +
+ )} + {successMessage && ( +
+ + {successMessage} + +
+ )} + + {/* Step indicator */} + + + {/* Step content */} +
+ {step === 1 && ( +
+
+

+ Select Cancellation Date +

+

+ Choose when you would like your service to end. +

+
+ + {/* Service info (passed from parent) */} + {serviceInfo} + + {/* Month selection */} +
+ + +

+ Service will end at the end of the selected month. +

+
+ +
+ +
+
+ )} + + {step === 2 && ( +
+
+

+ Review Cancellation Terms +

+

+ Please read the following information carefully. +

+
+ + {/* Terms content (passed from parent) */} + {termsContent} + +
+ + + +
+ +
+ + +
+
+ )} + + {step === 3 && ( +
+
+

Confirm Your Request

+

+ Review your cancellation details and submit. +

+
+ + {/* Summary content (passed from parent) */} + {summaryContent} + + {/* Extra content (e.g., Voice SIM notice) */} + {step3ExtraContent} + + {/* Email confirmation */} +
+
Confirmation will be sent to:
+
{customerEmail || "—"}
+
+ + {/* Comments */} +
+ +