fix(auth): improve OTP autofill DOM binding, event listeners, and WebOTP compatibility
All checks were successful
Deploy Canina / deploy (push) Successful in 1m31s

This commit is contained in:
parsa aghaei 2026-08-16 16:58:51 +03:30
parent 8d1b786a24
commit 2e217ebe3d

View File

@ -53,6 +53,11 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
const [showPassword, setShowPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const otpInputRef = useRef<HTMLInputElement>(null);
const phoneNumberRef = useRef(phoneNumber);
useEffect(() => {
phoneNumberRef.current = phoneNumber;
}, [phoneNumber]);
useEffect(() => {
if (view === "otp") {
@ -93,8 +98,8 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
const triggerVerifyOtp = useCallback(
async (codeToVerify: string) => {
const cleanCode = toEnglishDigits(codeToVerify).trim();
const cleanPhone = toEnglishDigits(phoneNumber).trim();
const cleanCode = toEnglishDigits(codeToVerify).replace(/[^0-9]/g, "").slice(0, 5);
const cleanPhone = toEnglishDigits(phoneNumberRef.current).replace(/[^0-9]/g, "");
if (cleanCode.length !== 5 || !cleanPhone) return;
setIsLoading(true);
@ -112,38 +117,74 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
setIsLoading(false);
}
},
[phoneNumber, fetchProfile, onClose],
[fetchProfile, onClose],
);
// WebOTP API SMS Auto-read
useEffect(() => {
if (view === "otp" && typeof window !== "undefined" && "OTPCredential" in window) {
const ac = new AbortController();
if (view !== "otp") return;
let isMounted = true;
const ac = new AbortController();
if (typeof window !== "undefined" && "OTPCredential" in window) {
(navigator as unknown as { credentials: { get: (opts: unknown) => Promise<{ code?: string }> } }).credentials
?.get({
otp: { transport: ["sms"] },
signal: ac.signal,
})
.then((otp) => {
if (otp?.code) {
if (!isMounted) return;
if (otp && typeof otp.code === "string") {
const clean = toEnglishDigits(otp.code).replace(/[^0-9]/g, "").slice(0, 5);
setOtpCode(clean);
if (clean.length === 5) {
triggerVerifyOtp(clean);
if (clean) {
setOtpCode(clean);
if (otpInputRef.current) {
otpInputRef.current.value = clean;
}
if (clean.length === 5) {
triggerVerifyOtp(clean);
}
}
}
})
.catch(() => {});
return () => {
ac.abort();
};
}
// Direct DOM input / change listener for mobile keyboard autofill compatibility
const inputEl = otpInputRef.current;
const handleDomAutofill = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target && target.value) {
const clean = toEnglishDigits(target.value).replace(/[^0-9]/g, "").slice(0, 5);
setOtpCode(clean);
if (clean.length === 5) {
triggerVerifyOtp(clean);
}
}
};
if (inputEl) {
inputEl.addEventListener("input", handleDomAutofill);
inputEl.addEventListener("change", handleDomAutofill);
}
return () => {
isMounted = false;
ac.abort();
if (inputEl) {
inputEl.removeEventListener("input", handleDomAutofill);
inputEl.removeEventListener("change", handleDomAutofill);
}
};
}, [view, triggerVerifyOtp]);
const handleOtpChange = (val: string) => {
const clean = toEnglishDigits(val).replace(/[^0-9]/g, "").slice(0, 5);
setOtpCode(clean);
if (otpInputRef.current) {
otpInputRef.current.value = clean;
}
if (clean.length === 5) {
triggerVerifyOtp(clean);
}
@ -332,7 +373,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
<div className="p-6 pb-0 flex justify-between items-center">
<div className="flex items-center gap-2 text-canina-blue font-black text-sm">
<ShieldCheck className="w-5 h-5" />
<span>ورود امن به کانینو</span>
<span>ورود امن به کنینا</span>
</div>
<button
onClick={onClose}
@ -734,13 +775,15 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
<Key className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-medical-gray-400 pointer-events-none" />
<input
ref={otpInputRef}
id="otp-code-input"
name="one-time-code"
type="text"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={5}
placeholder="کد ۵ رقمی"
value={otpCode}
onChange={(e) => handleOtpChange(e.target.value)}
onInput={(e) => handleOtpChange((e.target as HTMLInputElement).value)}
disabled={isLoading}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pr-12 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold text-center text-2xl font-mono tracking-[0.5em]"
dir="ltr"
@ -849,11 +892,10 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
type="text"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={5}
required
placeholder="کد ۵ رقمی"
value={otpCode}
onChange={(e) => setOtpCode(toEnglishDigits(e.target.value).replace(/[^0-9]/g, "").slice(0, 5))}
onInput={(e) => setOtpCode(toEnglishDigits((e.target as HTMLInputElement).value).replace(/[^0-9]/g, "").slice(0, 5))}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-center font-mono font-bold text-xl tracking-[0.4em]"
dir="ltr"
/>