fix: enforce 32-digit numeric EID validation with inline user feedback

EID input now strips non-numeric characters, shows a digit counter warning
while typing, and enforces exactly 32 digits via Zod schemas across domain,
order configurations, and order selections layers. Also installs missing
input-otp dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Temuulen Ankhbayar 2026-03-07 11:16:58 +09:00
parent 18b4c515a4
commit 9ae3d5e9c7
3 changed files with 30 additions and 12 deletions

View File

@ -138,6 +138,12 @@ function PhysicalSimOption({
); );
} }
function getEidWarning(eid: string): string | undefined {
if (!eid) return undefined;
if (eid.length < 32) return `EID must be 32 digits (${eid.length}/32)`;
return undefined;
}
function EidInput({ function EidInput({
simType, simType,
eid, eid,
@ -150,6 +156,8 @@ function EidInput({
errors: Record<string, string | undefined>; errors: Record<string, string | undefined>;
}) { }) {
const [showEidInfo, setShowEidInfo] = useState(false); const [showEidInfo, setShowEidInfo] = useState(false);
const warning = getEidWarning(eid);
const hasError = Boolean(errors["eid"]);
return ( return (
<div <div
@ -176,16 +184,18 @@ function EidInput({
</label> </label>
<input <input
type="text" type="text"
inputMode="numeric"
id="eid" id="eid"
value={eid} value={eid}
onChange={e => onEidChange(e.target.value)} onChange={e => onEidChange(e.target.value.replace(/\D/g, "").slice(0, 32))}
className={`w-full px-4 py-3 bg-card border rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors ${ className={`w-full px-4 py-3 bg-card border rounded-lg text-foreground font-mono tracking-wider placeholder:text-muted-foreground placeholder:font-sans placeholder:tracking-normal focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors ${
errors["eid"] ? "border-destructive" : "border-border" hasError ? "border-destructive" : warning ? "border-warning" : "border-border"
}`} }`}
placeholder="32-digit EID number" placeholder="32-digit EID number (numbers only)"
maxLength={32} maxLength={32}
/> />
{errors["eid"] && <p className="text-destructive text-sm mt-2">{errors["eid"]}</p>} {hasError && <p className="text-destructive text-sm mt-2">{errors["eid"]}</p>}
{!hasError && warning && <p className="text-warning text-sm mt-2">{warning}</p>}
<button <button
type="button" type="button"

View File

@ -131,7 +131,10 @@ export const orderConfigurationsSchema = z.object({
scheduledAt: z.string().optional(), scheduledAt: z.string().optional(),
accessMode: z.enum(ACCESS_MODE_VALUES).optional(), accessMode: z.enum(ACCESS_MODE_VALUES).optional(),
simType: z.enum(SIM_TYPE_VALUES).optional(), simType: z.enum(SIM_TYPE_VALUES).optional(),
eid: z.string().optional(), eid: z
.string()
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
.optional(),
isMnp: z.string().optional(), isMnp: z.string().optional(),
mnpNumber: z.string().optional(), mnpNumber: z.string().optional(),
mnpExpiry: z.string().optional(), mnpExpiry: z.string().optional(),
@ -164,7 +167,10 @@ export const orderSelectionsSchema = z
activationType: z.enum(ACTIVATION_TYPE_VALUES).optional(), activationType: z.enum(ACTIVATION_TYPE_VALUES).optional(),
scheduledAt: z.string().optional(), scheduledAt: z.string().optional(),
simType: z.enum(SIM_TYPE_VALUES).optional(), simType: z.enum(SIM_TYPE_VALUES).optional(),
eid: z.string().optional(), eid: z
.string()
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
.optional(),
isMnp: z.string().optional(), isMnp: z.string().optional(),
mnpNumber: z.string().optional(), mnpNumber: z.string().optional(),
mnpExpiry: z.string().optional(), mnpExpiry: z.string().optional(),

View File

@ -426,8 +426,7 @@ export const simConfigureFormSchema = z
simType: simCardTypeSchema, simType: simCardTypeSchema,
eid: z eid: z
.string() .string()
.min(15, "EID must be at least 15 characters") .regex(/^\d{32}$/, "EID must be exactly 32 digits")
.max(32, "EID must be at most 32 characters")
.optional(), .optional(),
selectedAddons: z.array(z.string()).default([]), selectedAddons: z.array(z.string()).default([]),
activationType: simActivationTypeSchema, activationType: simActivationTypeSchema,
@ -436,11 +435,11 @@ export const simConfigureFormSchema = z
mnpData: simMnpFormSchema.optional(), mnpData: simMnpFormSchema.optional(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.simType === "eSIM" && (!data.eid || data.eid.trim().length < 15)) { if (data.simType === "eSIM" && (!data.eid || !/^\d{32}$/.test(data.eid.trim()))) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ["eid"], path: ["eid"],
message: "EID is required for eSIM configuration", message: "EID must be exactly 32 digits",
}); });
} }
@ -515,7 +514,10 @@ export const simOrderActivationRequestSchema = z
.object({ .object({
planSku: z.string().min(1, "Plan SKU is required"), planSku: z.string().min(1, "Plan SKU is required"),
simType: simCardTypeSchema, simType: simCardTypeSchema,
eid: z.string().min(15, "EID must be at least 15 characters").optional(), eid: z
.string()
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
.optional(),
activationType: simActivationTypeSchema, activationType: simActivationTypeSchema,
scheduledAt: z.string().regex(YYYYMMDD_REGEX, MSG_SCHEDULED_DATE_FORMAT).optional(), scheduledAt: z.string().regex(YYYYMMDD_REGEX, MSG_SCHEDULED_DATE_FORMAT).optional(),
addons: simOrderActivationAddonsSchema.optional(), addons: simOrderActivationAddonsSchema.optional(),