- Deleted migration file that removed cached profile fields from the users table, centralizing profile data retrieval from WHMCS. - Updated CsrfMiddleware to include new public authentication endpoints for password reset, setting password, and WHMCS account linking. - Enhanced error handling in password and WHMCS linking workflows to provide clearer feedback on missing mappings and improve user experience. - Adjusted user creation and update methods in UsersFacade to handle cases where WHMCS mappings are not yet available, ensuring smoother account setup.
222 lines
8.6 KiB
TypeScript
222 lines
8.6 KiB
TypeScript
"use client";
|
||
|
||
import React, { useMemo, useState } from "react";
|
||
import { ArrowPathIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
||
|
||
type SimKind = "physical" | "esim";
|
||
|
||
interface ReissueSimModalProps {
|
||
subscriptionId: number;
|
||
currentSimType: SimKind;
|
||
onClose: () => void;
|
||
onSuccess: () => void;
|
||
onError: (message: string) => void;
|
||
}
|
||
|
||
const IMPORTANT_POINTS: string[] = [
|
||
"The reissue request cannot be reversed.",
|
||
"Service to the existing SIM will be terminated with immediate effect.",
|
||
"A fee of 1,500 yen + tax will be incurred.",
|
||
"For physical SIM: allow approximately 3-5 business days for shipping.",
|
||
"For eSIM: activation typically completes within 30-60 minutes after processing.",
|
||
];
|
||
|
||
const EID_HELP = "Enter the 32-digit EID (numbers only). Leave blank to reuse Freebit's generated EID.";
|
||
|
||
export function ReissueSimModal({
|
||
subscriptionId,
|
||
currentSimType,
|
||
onClose,
|
||
onSuccess,
|
||
onError,
|
||
}: ReissueSimModalProps) {
|
||
const [selectedSimType, setSelectedSimType] = useState<SimKind>(currentSimType);
|
||
const [newEid, setNewEid] = useState("");
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [validationError, setValidationError] = useState<string | null>(null);
|
||
|
||
const isEsimSelected = selectedSimType === "esim";
|
||
const isPhysicalSelected = selectedSimType === "physical";
|
||
|
||
const disableSubmit = useMemo(() => {
|
||
if (isPhysicalSelected) {
|
||
return false; // Allow click to show guidance message
|
||
}
|
||
if (!isEsimSelected) {
|
||
return true;
|
||
}
|
||
if (!newEid) {
|
||
return false; // Optional – backend supports auto EID
|
||
}
|
||
return !/^\d{32}$/.test(newEid.trim());
|
||
}, [isPhysicalSelected, isEsimSelected, newEid]);
|
||
|
||
const handleSubmit = async () => {
|
||
if (isPhysicalSelected) {
|
||
setValidationError(
|
||
"Physical SIM reissue cannot be requested online yet. Please contact support for assistance."
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (isEsimSelected && newEid && !/^\d{32}$/.test(newEid.trim())) {
|
||
setValidationError("EID must be 32 digits.");
|
||
return;
|
||
}
|
||
|
||
setValidationError(null);
|
||
setSubmitting(true);
|
||
try {
|
||
await simActionsService.reissueEsim(String(subscriptionId), {
|
||
newEid: newEid.trim() || undefined,
|
||
});
|
||
onSuccess();
|
||
} catch (error: unknown) {
|
||
const message = error instanceof Error ? error.message : "Failed to submit reissue request";
|
||
onError(message);
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||
<div className="absolute inset-0 bg-gray-500 bg-opacity-75" aria-hidden="true" />
|
||
|
||
<div className="relative z-10 w-full max-w-2xl rounded-lg border border-gray-200 bg-white shadow-2xl">
|
||
<div className="px-6 pt-6 pb-4 sm:px-8 sm:pb-6">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-green-100">
|
||
<ArrowPathIcon className="h-6 w-6 text-green-600" />
|
||
</span>
|
||
<div>
|
||
<h3 className="text-lg font-semibold text-gray-900">Reissue SIM</h3>
|
||
<p className="text-sm text-gray-600">
|
||
Submit a reissue request for your SIM. Review the important information before continuing.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="text-gray-400 transition hover:text-gray-600"
|
||
aria-label="Close reissue SIM modal"
|
||
type="button"
|
||
>
|
||
<XMarkIcon className="h-5 w-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="mt-6 rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||
<h4 className="text-sm font-semibold text-amber-800">Important information</h4>
|
||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-amber-900">
|
||
{IMPORTANT_POINTS.map(point => (
|
||
<li key={point}>{point}</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="mt-6 grid gap-6 md:grid-cols-2">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">Select SIM type</label>
|
||
<div className="mt-3 space-y-2">
|
||
<label className="flex items-start gap-3 rounded-lg border border-gray-200 p-3">
|
||
<input
|
||
type="radio"
|
||
name="sim-type"
|
||
value="physical"
|
||
checked={selectedSimType === "physical"}
|
||
onChange={() => setSelectedSimType("physical")}
|
||
className="mt-1"
|
||
/>
|
||
<div>
|
||
<p className="text-sm font-medium text-gray-900">Physical SIM</p>
|
||
<p className="text-xs text-gray-500">
|
||
We’ll ship a replacement SIM card. Currently, online requests are not available; contact support to proceed.
|
||
</p>
|
||
</div>
|
||
</label>
|
||
|
||
<label className="flex items-start gap-3 rounded-lg border border-gray-200 p-3">
|
||
<input
|
||
type="radio"
|
||
name="sim-type"
|
||
value="esim"
|
||
checked={selectedSimType === "esim"}
|
||
onChange={() => setSelectedSimType("esim")}
|
||
className="mt-1"
|
||
/>
|
||
<div>
|
||
<p className="text-sm font-medium text-gray-900">eSIM</p>
|
||
<p className="text-xs text-gray-500">
|
||
Generate a new eSIM activation profile. You’ll receive new QR code details once processing completes.
|
||
</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-gray-200 p-4 text-sm text-gray-600">
|
||
<p>
|
||
Current SIM type: <strong className="uppercase">{currentSimType}</strong>
|
||
</p>
|
||
<p className="mt-2">
|
||
The selection above lets you specify which type of replacement you need. If you choose a physical SIM, a support agent will contact you to finalise the process.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{isEsimSelected && (
|
||
<div className="mt-6">
|
||
<label htmlFor="new-eid" className="block text-sm font-medium text-gray-700">
|
||
New EID (optional)
|
||
</label>
|
||
<input
|
||
id="new-eid"
|
||
type="text"
|
||
inputMode="numeric"
|
||
pattern="[0-9]*"
|
||
value={newEid}
|
||
onChange={event => {
|
||
setNewEid(event.target.value.replace(/\s+/g, ""));
|
||
setValidationError(null);
|
||
}}
|
||
placeholder="Enter 32-digit EID"
|
||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||
/>
|
||
<p className="mt-1 text-xs text-gray-500">{EID_HELP}</p>
|
||
</div>
|
||
)}
|
||
|
||
{validationError && (
|
||
<p className="mt-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||
{validationError}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-3 border-t border-gray-200 bg-gray-50 p-4 sm:flex-row sm:justify-end sm:px-6">
|
||
<button
|
||
type="button"
|
||
onClick={() => void handleSubmit()}
|
||
disabled={disableSubmit || submitting}
|
||
className="inline-flex justify-center rounded-md px-4 py-2 text-sm font-semibold text-white shadow-sm disabled:cursor-not-allowed disabled:opacity-70"
|
||
style={{ background: "linear-gradient(90deg, #16a34a, #15803d)" }}
|
||
>
|
||
{submitting ? "Submitting..." : isPhysicalSelected ? "Contact Support" : "Confirm Reissue"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
disabled={submitting}
|
||
className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|