- Updated multiple components to use consistent conditional rendering syntax by adding parentheses around conditions. - Enhanced readability and maintainability of the code in components such as OtpInput, AddressCard, and others. - Improved overall code quality and developer experience through uniformity in the codebase.
349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
import { cancellationService } from "@/features/subscriptions/api";
|
|
import type { CancellationPreview } from "@customer-portal/domain/subscriptions";
|
|
import { GlobeAltIcon, DevicePhoneMobileIcon, ClockIcon } from "@heroicons/react/24/outline";
|
|
import { PageLayout } from "@/components/templates/PageLayout";
|
|
import { Button } from "@/components/atoms";
|
|
import Link from "next/link";
|
|
import {
|
|
CancellationFlow,
|
|
Notice,
|
|
InfoNotice,
|
|
ServiceInfoGrid,
|
|
ServiceInfoItem,
|
|
CancellationSummary,
|
|
MinimumContractWarning,
|
|
} from "@/features/subscriptions/components/CancellationFlow";
|
|
import { useAuthStore } from "@/features/auth/stores/auth.store";
|
|
import { devErrorMessage, formatAddressLabel } from "@/shared/utils";
|
|
|
|
const SUBSCRIPTIONS_HREF = "/account/subscriptions";
|
|
|
|
// ============================================================================
|
|
// Pending Cancellation View (when Opportunity is already in △Cancelling)
|
|
// ============================================================================
|
|
|
|
function CancellationPendingView({
|
|
subscriptionId,
|
|
preview,
|
|
}: {
|
|
subscriptionId: string;
|
|
preview: CancellationPreview;
|
|
}) {
|
|
const icon = preview.serviceType === "internet" ? <GlobeAltIcon /> : <DevicePhoneMobileIcon />;
|
|
const title =
|
|
preview.serviceType === "internet"
|
|
? "Internet Cancellation Pending"
|
|
: "SIM Cancellation Pending";
|
|
|
|
return (
|
|
<PageLayout
|
|
icon={icon}
|
|
title={title}
|
|
description={preview.serviceName}
|
|
breadcrumbs={[
|
|
{ label: "Subscriptions", href: SUBSCRIPTIONS_HREF },
|
|
{ label: preview.serviceName, href: `/account/subscriptions/${subscriptionId}` },
|
|
{ label: "Cancellation Status" },
|
|
]}
|
|
>
|
|
<div className="max-w-2xl mx-auto">
|
|
<div className="bg-card border border-border rounded-xl p-6 sm:p-8">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="w-12 h-12 bg-warning-soft rounded-full flex items-center justify-center">
|
|
<ClockIcon className="w-6 h-6 text-warning" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-foreground">Cancellation In Progress</h2>
|
|
<p className="text-sm text-muted-foreground">
|
|
Your cancellation request is being processed.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="p-4 bg-muted/50 rounded-lg">
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-muted-foreground">Service:</span>
|
|
<div className="font-medium text-foreground">{preview.serviceName}</div>
|
|
</div>
|
|
{preview.cancellationStatus?.scheduledEndDate && (
|
|
<div>
|
|
<span className="text-muted-foreground">Scheduled End:</span>
|
|
<div className="font-medium text-foreground">
|
|
{new Date(preview.cancellationStatus.scheduledEndDate).toLocaleDateString(
|
|
"en-US",
|
|
{ month: "long", year: "numeric" }
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{preview.serviceType === "internet" &&
|
|
preview.cancellationStatus?.rentalReturnStatus && (
|
|
<div className="p-4 bg-info-soft/50 border-l-4 border-info rounded-r-lg">
|
|
<div className="text-sm font-medium text-foreground mb-1">
|
|
Equipment Return Status
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{preview.cancellationStatus.rentalReturnStatus}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
You will receive an email confirmation when the cancellation is complete. If you have
|
|
questions, please contact our support team.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mt-6 pt-6 border-t border-border">
|
|
<Link href={`/account/subscriptions/${subscriptionId}`}>
|
|
<Button variant="outline">Back to Subscription</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Component
|
|
// ============================================================================
|
|
|
|
function getServiceIcon(serviceType: string) {
|
|
return serviceType === "internet" ? <GlobeAltIcon /> : <DevicePhoneMobileIcon />;
|
|
}
|
|
|
|
function getServiceTitle(serviceType: string) {
|
|
return serviceType === "internet" ? "Cancel Internet Service" : "Cancel SIM Service";
|
|
}
|
|
|
|
function getConfirmMessage(serviceType: string) {
|
|
return serviceType === "internet"
|
|
? "Are you sure you want to cancel your Internet service? This will take effect at the end of {month}."
|
|
: "Are you sure you want to cancel your SIM subscription? This will take effect at the end of {month}.";
|
|
}
|
|
|
|
function useCancellationState(subscriptionId: string) {
|
|
const router = useRouter();
|
|
const [loading, setLoading] = useState(true);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [preview, setPreview] = useState<CancellationPreview | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [formError, setFormError] = useState<string | null>(null);
|
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
const [selectedMonthLabel, setSelectedMonthLabel] = useState<string>("");
|
|
|
|
useEffect(() => {
|
|
const fetchPreview = async () => {
|
|
try {
|
|
const data = await cancellationService.getPreview(subscriptionId);
|
|
setPreview(data);
|
|
} catch (e: unknown) {
|
|
setError(
|
|
devErrorMessage(
|
|
e,
|
|
"Failed to load cancellation information",
|
|
"Unable to load cancellation information right now. Please try again."
|
|
)
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
void fetchPreview();
|
|
}, [subscriptionId]);
|
|
|
|
const handleSubmit = async (data: {
|
|
cancellationMonth: string;
|
|
confirmRead: boolean;
|
|
confirmCancel: boolean;
|
|
comments?: string;
|
|
}) => {
|
|
setSubmitting(true);
|
|
setFormError(null);
|
|
|
|
const monthInfo = preview?.availableMonths.find(m => m.value === data.cancellationMonth);
|
|
setSelectedMonthLabel(monthInfo?.label || data.cancellationMonth);
|
|
|
|
try {
|
|
await cancellationService.submit(subscriptionId, {
|
|
cancellationMonth: data.cancellationMonth,
|
|
confirmRead: data.confirmRead,
|
|
confirmCancel: data.confirmCancel,
|
|
comments: data.comments,
|
|
});
|
|
setSuccessMessage("Cancellation request submitted. You will receive a confirmation email.");
|
|
setTimeout(() => router.push(`/account/subscriptions/${subscriptionId}`), 2000);
|
|
} catch (e: unknown) {
|
|
setFormError(
|
|
devErrorMessage(
|
|
e,
|
|
"Failed to submit cancellation",
|
|
"Unable to submit your cancellation right now. Please try again."
|
|
)
|
|
);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
loading,
|
|
submitting,
|
|
preview,
|
|
error,
|
|
formError,
|
|
successMessage,
|
|
selectedMonthLabel,
|
|
handleSubmit,
|
|
};
|
|
}
|
|
|
|
function CancellationFlowView({
|
|
subscriptionId,
|
|
preview,
|
|
formError,
|
|
successMessage,
|
|
submitting,
|
|
selectedMonthLabel,
|
|
onSubmit,
|
|
}: {
|
|
subscriptionId: string;
|
|
preview: CancellationPreview;
|
|
formError: string | null;
|
|
successMessage: string | null;
|
|
submitting: boolean;
|
|
selectedMonthLabel: string;
|
|
onSubmit: (data: {
|
|
cancellationMonth: string;
|
|
confirmRead: boolean;
|
|
confirmCancel: boolean;
|
|
comments?: string;
|
|
}) => Promise<void>;
|
|
}) {
|
|
const user = useAuthStore(state => state.user);
|
|
|
|
return (
|
|
<CancellationFlow
|
|
icon={getServiceIcon(preview.serviceType)}
|
|
title={getServiceTitle(preview.serviceType)}
|
|
description={preview.serviceName}
|
|
breadcrumbs={[
|
|
{ label: "Subscriptions", href: SUBSCRIPTIONS_HREF },
|
|
{ label: preview.serviceName, href: `/account/subscriptions/${subscriptionId}` },
|
|
{ label: "Cancel" },
|
|
]}
|
|
backHref={`/account/subscriptions/${subscriptionId}`}
|
|
backLabel="Back to Subscription"
|
|
availableMonths={preview.availableMonths}
|
|
customerEmail={preview.customerEmail}
|
|
loading={false}
|
|
error={null}
|
|
formError={formError}
|
|
successMessage={successMessage}
|
|
submitting={submitting}
|
|
confirmMessage={getConfirmMessage(preview.serviceType)}
|
|
confirmationDetails={[
|
|
{ label: "Service", value: preview.serviceName },
|
|
{ label: "Customer", value: preview.customerName },
|
|
{ label: "Email", value: preview.customerEmail },
|
|
{ label: "Service Address", value: formatAddressLabel(user?.address) || "—" },
|
|
]}
|
|
onSubmit={onSubmit}
|
|
warningBanner={
|
|
preview.isWithinMinimumTerm && preview.minimumContractEndDate ? (
|
|
<MinimumContractWarning endDate={preview.minimumContractEndDate} />
|
|
) : undefined
|
|
}
|
|
serviceInfo={
|
|
<ServiceInfoGrid>
|
|
{preview.serviceInfo.map((info, idx) => (
|
|
<ServiceInfoItem
|
|
key={idx}
|
|
label={info.label}
|
|
value={info.value}
|
|
mono={info.mono ?? false}
|
|
/>
|
|
))}
|
|
</ServiceInfoGrid>
|
|
}
|
|
termsContent={
|
|
<div className="space-y-3">
|
|
{preview.terms.map((term, idx) => (
|
|
<Notice key={idx} title={term.title}>
|
|
{term.content}
|
|
</Notice>
|
|
))}
|
|
</div>
|
|
}
|
|
summaryContent={
|
|
<>
|
|
<CancellationSummary
|
|
items={preview.serviceInfo.map(info => ({ label: info.label, value: info.value }))}
|
|
selectedMonth={selectedMonthLabel || "the selected month"}
|
|
/>
|
|
{preview.step3Notices && preview.step3Notices.length > 0 && (
|
|
<div className="space-y-3 mt-4">
|
|
{preview.step3Notices.map((notice, idx) => (
|
|
<InfoNotice key={idx} title={notice.title}>
|
|
{notice.content}
|
|
</InfoNotice>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function CancelSubscriptionContainer() {
|
|
const params = useParams();
|
|
const subscriptionId = params["id"] as string;
|
|
const state = useCancellationState(subscriptionId);
|
|
|
|
if (state.loading || state.error) {
|
|
return (
|
|
<PageLayout
|
|
icon={<GlobeAltIcon />}
|
|
title="Cancel Subscription"
|
|
description="Loading cancellation information..."
|
|
breadcrumbs={[{ label: "Subscriptions", href: SUBSCRIPTIONS_HREF }, { label: "Cancel" }]}
|
|
loading={state.loading}
|
|
error={state.error}
|
|
>
|
|
<></>
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
if (!state.preview) return null;
|
|
|
|
if (state.preview.cancellationStatus?.stage === "△Cancelling") {
|
|
return <CancellationPendingView subscriptionId={subscriptionId} preview={state.preview} />;
|
|
}
|
|
|
|
return (
|
|
<CancellationFlowView
|
|
subscriptionId={subscriptionId}
|
|
preview={state.preview}
|
|
formError={state.formError}
|
|
successMessage={state.successMessage}
|
|
submitting={state.submitting}
|
|
selectedMonthLabel={state.selectedMonthLabel}
|
|
onSubmit={state.handleSubmit}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default CancelSubscriptionContainer;
|