353 lines
14 KiB
TypeScript

"use client";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
import { useAuthStore } from "@/features/auth/services/auth.store";
import type { SimDetails } from "@/features/sim-management/components/SimDetailsCard";
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-3">
<div className="text-sm font-medium text-yellow-900 mb-1">{title}</div>
<div className="text-sm text-yellow-800">{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 [details, setDetails] = useState<SimDetails | 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 [cancelMonth, setCancelMonth] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [email2, setEmail2] = useState<string>("");
const [notes, setNotes] = useState<string>("");
const [registeredEmail, setRegisteredEmail] = useState<string | null>(null);
useEffect(() => {
const fetchDetails = async () => {
try {
const info = await simActionsService.getSimInfo<SimDetails, unknown>(subscriptionId);
setDetails(info?.details || null);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to load SIM details");
}
};
void fetchDetails();
}, [subscriptionId]);
useEffect(() => {
const fetchEmail = () => {
try {
const emailFromStore = useAuthStore.getState().user?.email;
if (emailFromStore) {
setRegisteredEmail(emailFromStore);
return;
}
} catch {
// ignore
}
};
fetchEmail();
}, []);
const monthOptions = useMemo(() => {
const opts: { value: string; label: string }[] = [];
const now = new Date();
for (let i = 1; i <= 12; i++) {
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + i, 1));
const y = d.getUTCFullYear();
const m = String(d.getUTCMonth() + 1).padStart(2, "0");
opts.push({ value: `${y}${m}`, label: `${y} / ${m}` });
}
return opts;
}, []);
const canProceedStep2 = !!details;
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const emailProvided = email.trim().length > 0 || email2.trim().length > 0;
const emailValid =
!emailProvided || (emailPattern.test(email.trim()) && emailPattern.test(email2.trim()));
const emailsMatch = !emailProvided || email.trim() === email2.trim();
const canProceedStep3 =
acceptTerms && !!cancelMonth && confirmMonthEnd && emailValid && emailsMatch;
const runDate = cancelMonth ? `${cancelMonth}01` : null;
const submit = async () => {
setLoading(true);
setError(null);
setMessage(null);
if (!runDate) {
setError("Please select a cancellation month before submitting.");
setLoading(false);
return;
}
try {
await simActionsService.cancel(subscriptionId, { scheduledAt: runDate });
setMessage("Cancellation request submitted. You will receive a confirmation email.");
setTimeout(() => router.push(`/subscriptions/${subscriptionId}#sim-management`), 1500);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to submit cancellation");
} finally {
setLoading(false);
}
};
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="text-sm text-gray-500">Step {step} of 3</div>
</div>
{error && (
<div className="text-red-700 bg-red-50 border border-red-200 rounded p-3">{error}</div>
)}
{message && (
<div className="text-green-700 bg-green-50 border border-green-200 rounded p-3">
{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 SIM: Permanently cancel your SIM service. This action cannot be undone and will
terminate your service immediately.
</p>
{step === 1 && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
<InfoRow label="SIM" value={details?.msisdn || "—"} />
<InfoRow label="Start Date" value={details?.startDate || "—"} />
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Cancellation Month
</label>
<select
value={cancelMonth}
onChange={e => {
setCancelMonth(e.target.value);
setConfirmMonthEnd(false);
}}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
>
<option value="">Select month</option>
{monthOptions.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">
Cancellation takes effect at the start of the selected month.
</p>
</div>
</div>
<div className="flex justify-end">
<button
disabled={!canProceedStep2}
onClick={() => setStep(2)}
className="px-4 py-2 rounded-md bg-blue-600 text-white text-sm disabled:opacity-50"
>
Next
</button>
</div>
</div>
)}
{step === 2 && (
<div className="space-y-6">
<div className="space-y-3">
<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="Option Services">
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 (Voice Plans)">
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,000yen+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="flex items-center gap-2">
<input
id="acceptTerms"
type="checkbox"
checked={acceptTerms}
onChange={e => setAcceptTerms(e.target.checked)}
/>
<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-2">
<input
id="confirmMonthEnd"
type="checkbox"
checked={confirmMonthEnd}
onChange={e => setConfirmMonthEnd(e.target.checked)}
disabled={!cancelMonth}
/>
<label htmlFor="confirmMonthEnd" className="text-sm text-gray-700">
I would like to cancel my SonixNet SIM subscription at the end of the selected month
above.
</label>
</div>
<div className="flex justify-between">
<button
onClick={() => setStep(1)}
className="px-4 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50"
>
Back
</button>
<button
disabled={!canProceedStep3}
onClick={() => setStep(3)}
className="px-4 py-2 rounded-md bg-blue-600 text-white text-sm disabled:opacity-50"
>
Next
</button>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-6">
{registeredEmail && (
<div className="text-sm text-gray-800">
Your registered email address is:{" "}
<span className="font-medium">{registeredEmail}</span>
</div>
)}
<div className="text-sm text-gray-700">
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>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">Email address</label>
<input
className="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="you@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">(Confirm)</label>
<input
className="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
value={email2}
onChange={e => setEmail2(e.target.value)}
placeholder="you@example.com"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">
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="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
rows={4}
value={notes}
onChange={e => setNotes(e.target.value)}
placeholder="If you have any questions or requests, note them here."
/>
</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>
)}
<div className="text-sm text-gray-700">
Your cancellation request is not confirmed yet. This is the final page. To finalize
your cancellation request please proceed from REQUEST CANCELLATION below.
</div>
<div className="flex justify-between">
<button
onClick={() => setStep(2)}
className="px-4 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50"
>
Back
</button>
<button
onClick={() => {
if (
window.confirm(
"Request cancellation now? This will schedule the cancellation for " +
(runDate || "") +
"."
)
) {
void submit();
}
}}
disabled={loading || !runDate || !canProceedStep3}
className="px-4 py-2 rounded-md bg-red-600 text-white text-sm disabled:opacity-50"
>
{loading ? "Processing…" : "Request Cancellation"}
</button>
</div>
</div>
)}
</div>
</div>
);
}
export default SimCancelContainer;