- Added new models and request types for enhanced SIM cancellation and plan change functionalities. - Implemented full cancellation flow with email notifications and confirmation handling. - Introduced enhanced plan change request with Salesforce product mapping and scheduling. - Updated UI components for better user experience during SIM management actions. - Improved error handling and validation for cancellation and plan change requests.
407 lines
17 KiB
TypeScript
407 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { useEffect, useState, type ReactNode } from "react";
|
|
import { simActionsService, type CancellationPreview } from "@/features/subscriptions/services/sim-actions.service";
|
|
|
|
type Step = 1 | 2 | 3;
|
|
|
|
function Notice({ title, children }: { title: string; children: ReactNode }) {
|
|
return (
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded p-4">
|
|
<div className="text-sm font-semibold text-yellow-900 mb-2">{title}</div>
|
|
<div className="text-sm text-yellow-800 leading-relaxed">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div>
|
|
<div className="text-xs text-gray-500">{label}</div>
|
|
<div className="text-sm font-medium text-gray-900">{value}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SimCancelContainer() {
|
|
const params = useParams();
|
|
const router = useRouter();
|
|
const subscriptionId = params.id as string;
|
|
|
|
const [step, setStep] = useState<Step>(1);
|
|
const [loading, setLoading] = useState(false);
|
|
const [preview, setPreview] = useState<CancellationPreview | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const [acceptTerms, setAcceptTerms] = useState(false);
|
|
const [confirmMonthEnd, setConfirmMonthEnd] = useState(false);
|
|
const [selectedMonth, setSelectedMonth] = useState<string>("");
|
|
const [alternativeEmail, setAlternativeEmail] = useState<string>("");
|
|
const [alternativeEmail2, setAlternativeEmail2] = useState<string>("");
|
|
const [comments, setComments] = useState<string>("");
|
|
const [loadingPreview, setLoadingPreview] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchPreview = async () => {
|
|
try {
|
|
const data = await simActionsService.getCancellationPreview(subscriptionId);
|
|
setPreview(data);
|
|
} catch (e: unknown) {
|
|
setError(e instanceof Error ? e.message : "Failed to load cancellation information");
|
|
} finally {
|
|
setLoadingPreview(false);
|
|
}
|
|
};
|
|
void fetchPreview();
|
|
}, [subscriptionId]);
|
|
|
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
const emailProvided = alternativeEmail.trim().length > 0 || alternativeEmail2.trim().length > 0;
|
|
const emailValid =
|
|
!emailProvided ||
|
|
(emailPattern.test(alternativeEmail.trim()) && emailPattern.test(alternativeEmail2.trim()));
|
|
const emailsMatch = !emailProvided || alternativeEmail.trim() === alternativeEmail2.trim();
|
|
const canProceedStep2 = !!preview && !!selectedMonth;
|
|
const canProceedStep3 = acceptTerms && confirmMonthEnd && emailValid && emailsMatch;
|
|
|
|
const selectedMonthInfo = preview?.availableMonths.find(m => m.value === selectedMonth);
|
|
|
|
const submit = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
setMessage(null);
|
|
|
|
if (!selectedMonth) {
|
|
setError("Please select a cancellation month before submitting.");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await simActionsService.cancelFull(subscriptionId, {
|
|
cancellationMonth: selectedMonth,
|
|
confirmRead: acceptTerms,
|
|
confirmCancel: confirmMonthEnd,
|
|
alternativeEmail: alternativeEmail.trim() || undefined,
|
|
comments: comments.trim() || undefined,
|
|
});
|
|
setMessage("Cancellation request submitted. You will receive a confirmation email.");
|
|
setTimeout(() => router.push(`/subscriptions/${subscriptionId}#sim-management`), 2000);
|
|
} catch (e: unknown) {
|
|
setError(e instanceof Error ? e.message : "Failed to submit cancellation");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loadingPreview) {
|
|
return (
|
|
<div className="max-w-3xl mx-auto p-6">
|
|
<div className="animate-pulse space-y-4">
|
|
<div className="h-8 bg-gray-200 rounded w-1/3"></div>
|
|
<div className="h-64 bg-gray-200 rounded"></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-3xl mx-auto p-6">
|
|
<div className="mb-4">
|
|
<Link
|
|
href={`/subscriptions/${subscriptionId}#sim-management`}
|
|
className="text-blue-600 hover:text-blue-700"
|
|
>
|
|
← Back to SIM Management
|
|
</Link>
|
|
<div className="flex items-center gap-2 mt-2">
|
|
{[1, 2, 3].map(s => (
|
|
<div
|
|
key={s}
|
|
className={`h-2 flex-1 rounded-full ${
|
|
s <= step ? "bg-blue-600" : "bg-gray-200"
|
|
}`}
|
|
></div>
|
|
))}
|
|
</div>
|
|
<div className="text-sm text-gray-500 mt-1">Step {step} of 3</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-red-700 bg-red-50 border border-red-200 rounded p-4 mb-4">{error}</div>
|
|
)}
|
|
{message && (
|
|
<div className="text-green-700 bg-green-50 border border-green-200 rounded p-4 mb-4">
|
|
{message}
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h1 className="text-xl font-semibold text-gray-900 mb-2">Cancel SIM</h1>
|
|
<p className="text-sm text-gray-600 mb-6">
|
|
Cancel your SIM subscription. Please read all the information carefully before proceeding.
|
|
</p>
|
|
|
|
{/* Minimum Contract Warning */}
|
|
{preview?.isWithinMinimumTerm && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
<div className="text-sm font-semibold text-red-900 mb-1">Minimum Contract Term Warning</div>
|
|
<div className="text-sm text-red-800">
|
|
Your subscription is still within the minimum contract period (ends {preview.minimumContractEndDate}).
|
|
Early cancellation may result in additional charges for the remaining months.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 1 && (
|
|
<div className="space-y-6">
|
|
{/* SIM Info */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg">
|
|
<InfoRow label="SIM Number" value={preview?.simNumber || "—"} />
|
|
<InfoRow label="Serial #" value={preview?.serialNumber || "—"} />
|
|
<InfoRow label="Start Date" value={preview?.startDate || "—"} />
|
|
</div>
|
|
|
|
{/* Month Selection */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Select Cancellation Month
|
|
</label>
|
|
<select
|
|
value={selectedMonth}
|
|
onChange={e => {
|
|
setSelectedMonth(e.target.value);
|
|
setConfirmMonthEnd(false);
|
|
}}
|
|
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="">Select month…</option>
|
|
{preview?.availableMonths.map(month => (
|
|
<option key={month.value} value={month.value}>
|
|
{month.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Your subscription will be cancelled at the end of the selected month.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<button
|
|
disabled={!canProceedStep2}
|
|
onClick={() => setStep(2)}
|
|
className="px-6 py-2 rounded-md bg-blue-600 text-white text-sm font-medium disabled:opacity-50 hover:bg-blue-700 transition-colors"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<div className="space-y-6">
|
|
<div className="space-y-4">
|
|
<Notice title="[Cancellation Procedure]">
|
|
Online cancellations must be made from this website by the 25th of the desired
|
|
cancellation month. Once a request of a cancellation of the SONIXNET SIM is accepted
|
|
from this online form, a confirmation email containing details of the SIM plan will
|
|
be sent to the registered email address. The SIM card is a rental piece of hardware
|
|
and must be returned to Assist Solutions upon cancellation. The cancellation request
|
|
through this website retains to your SIM subscriptions only. To cancel any other
|
|
services with Assist Solutions (home internet etc.) please contact Assist Solutions
|
|
at info@asolutions.co.jp
|
|
</Notice>
|
|
|
|
<Notice title="[Minimum Contract Term]">
|
|
The SONIXNET SIM has a minimum contract term agreement of three months (sign-up
|
|
month is not included in the minimum term of three months; ie. sign-up in January =
|
|
minimum term is February, March, April). If the minimum contract term is not
|
|
fulfilled, the monthly fees of the remaining months will be charged upon
|
|
cancellation.
|
|
</Notice>
|
|
|
|
<Notice title="[Cancellation of Option Services (for Data+SMS/Voice Plan)]">
|
|
Cancellation of option services only (Voice Mail, Call Waiting) while keeping the
|
|
base plan active is not possible from this online form. Please contact Assist
|
|
Solutions Customer Support (info@asolutions.co.jp) for more information. Upon
|
|
cancelling the base plan, all additional options associated with the requested SIM
|
|
plan will be cancelled.
|
|
</Notice>
|
|
|
|
<Notice title="[MNP Transfer (for Data+SMS/Voice Plan)]">
|
|
Upon cancellation the SIM phone number will be lost. In order to keep the phone
|
|
number active to be used with a different cellular provider, a request for an MNP
|
|
transfer (administrative fee ¥1,000+tax) is necessary. The MNP cannot be requested
|
|
from this online form. Please contact Assist Solutions Customer Support
|
|
(info@asolutions.co.jp) for more information.
|
|
</Notice>
|
|
</div>
|
|
|
|
<div className="space-y-3 bg-gray-50 rounded-lg p-4">
|
|
<div className="flex items-start gap-3">
|
|
<input
|
|
id="acceptTerms"
|
|
type="checkbox"
|
|
checked={acceptTerms}
|
|
onChange={e => setAcceptTerms(e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 border-gray-300 rounded mt-0.5"
|
|
/>
|
|
<label htmlFor="acceptTerms" className="text-sm text-gray-700">
|
|
I have read and accepted the conditions above.
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-3">
|
|
<input
|
|
id="confirmMonthEnd"
|
|
type="checkbox"
|
|
checked={confirmMonthEnd}
|
|
onChange={e => setConfirmMonthEnd(e.target.checked)}
|
|
disabled={!selectedMonth}
|
|
className="h-4 w-4 text-blue-600 border-gray-300 rounded mt-0.5"
|
|
/>
|
|
<label htmlFor="confirmMonthEnd" className="text-sm text-gray-700">
|
|
I would like to cancel my SonixNet SIM subscription at the end of{" "}
|
|
<strong>{selectedMonthInfo?.label || "the selected month"}</strong>.
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between">
|
|
<button
|
|
onClick={() => setStep(1)}
|
|
className="px-6 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50 transition-colors"
|
|
>
|
|
Back
|
|
</button>
|
|
<button
|
|
disabled={!canProceedStep3}
|
|
onClick={() => setStep(3)}
|
|
className="px-6 py-2 rounded-md bg-blue-600 text-white text-sm font-medium disabled:opacity-50 hover:bg-blue-700 transition-colors"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<div className="space-y-6">
|
|
{/* Voice SIM Notice */}
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<div className="text-sm font-semibold text-blue-900 mb-2">
|
|
For Voice-enabled SIM subscriptions:
|
|
</div>
|
|
<div className="text-sm text-blue-800">
|
|
Calling charges are post payment. Your bill for the final month's calling charges
|
|
will be charged on your credit card on file during the first week of the second month
|
|
after the cancellation.
|
|
</div>
|
|
<div className="text-sm text-blue-800 mt-2">
|
|
If you would like to make the payment with a different credit card, please contact
|
|
Assist Solutions at info@asolutions.co.jp
|
|
</div>
|
|
</div>
|
|
|
|
{/* Registered Email */}
|
|
<div className="text-sm text-gray-800">
|
|
Your registered email address is:{" "}
|
|
<span className="font-medium">{preview?.customerEmail || "—"}</span>
|
|
</div>
|
|
<div className="text-sm text-gray-600">
|
|
You will receive a cancellation confirmation email. If you would like to receive this
|
|
email on a different address, please enter the address below.
|
|
</div>
|
|
|
|
{/* Alternative Email */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Email address:
|
|
</label>
|
|
<input
|
|
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
|
|
value={alternativeEmail}
|
|
onChange={e => setAlternativeEmail(e.target.value)}
|
|
placeholder="you@example.com"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">(Confirm):</label>
|
|
<input
|
|
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
|
|
value={alternativeEmail2}
|
|
onChange={e => setAlternativeEmail2(e.target.value)}
|
|
placeholder="you@example.com"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{emailProvided && !emailValid && (
|
|
<div className="text-xs text-red-600">Please enter a valid email address in both fields.</div>
|
|
)}
|
|
{emailProvided && emailValid && !emailsMatch && (
|
|
<div className="text-xs text-red-600">Email addresses do not match.</div>
|
|
)}
|
|
|
|
{/* Comments */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
If you have any other questions/comments/requests regarding your cancellation, please
|
|
note them below and an Assist Solutions staff will contact you shortly.
|
|
</label>
|
|
<textarea
|
|
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500"
|
|
rows={4}
|
|
value={comments}
|
|
onChange={e => setComments(e.target.value)}
|
|
placeholder="Optional: Enter any questions or requests here."
|
|
/>
|
|
</div>
|
|
|
|
{/* Final Warning */}
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div className="text-sm font-semibold text-red-900 mb-1">
|
|
Your cancellation request is not confirmed yet.
|
|
</div>
|
|
<div className="text-sm text-red-800">
|
|
This is the final page. To finalize your cancellation request please proceed from
|
|
REQUEST CANCELLATION below.
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between">
|
|
<button
|
|
onClick={() => setStep(2)}
|
|
className="px-6 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50 transition-colors"
|
|
>
|
|
Back
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (
|
|
window.confirm(
|
|
`Are you sure you want to cancel your SIM subscription? This will take effect at the end of ${selectedMonthInfo?.label || selectedMonth}.`
|
|
)
|
|
) {
|
|
void submit();
|
|
}
|
|
}}
|
|
disabled={loading || !canProceedStep3}
|
|
className="px-6 py-2 rounded-md bg-red-600 text-white text-sm font-medium disabled:opacity-50 hover:bg-red-700 transition-colors"
|
|
>
|
|
{loading ? "Processing…" : "REQUEST CANCELLATION"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default SimCancelContainer;
|