353 lines
14 KiB
TypeScript
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;
|