fix: enhance OTP sending and error handling in registration flow

This commit is contained in:
Thanh Quy - wolf
2026-04-20 14:42:19 +07:00
parent 4eaed6c973
commit 4a43299969
2 changed files with 110 additions and 26 deletions
+68 -16
View File
@@ -6,7 +6,7 @@ import { SHOP_INFO } from "@/lib/constants";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { FormEvent, useState } from "react";
import { FormEvent, useEffect, useState } from "react";
// Static OTP for demo (in production, this would be sent via SMS)
const DEMO_OTP = "123456";
@@ -20,6 +20,34 @@ export default function RegisterPage() {
const [otp, setOtp] = useState("");
const [errors, setErrors] = useState({ phone: "", otp: "" });
const [isLoading, setIsLoading] = useState(false);
const [otpSending, setOtpSending] = useState(false);
const [otpSendError, setOtpSendError] = useState("");
const sendOtp = async () => {
setOtpSending(true);
setOtpSendError("");
try {
const res = await fetch("/api/sms_otp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone }),
});
if (!res.ok) {
setOtpSendError("Không thể gửi OTP, vui lòng thử lại");
}
} catch {
setOtpSendError("Không thể kết nối, vui lòng thử lại");
} finally {
setOtpSending(false);
}
};
useEffect(() => {
if (step === "otp") {
sendOtp();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [step]);
// Validate Vietnamese phone number
const validatePhone = (phoneNumber: string): boolean => {
@@ -58,11 +86,12 @@ export default function RegisterPage() {
if (res.ok) {
setStep("otp");
} else {
const data = await res.json().catch(() => ({}));
const msg =
data?.error === "ExistedUser"
? "Số điện thoại đã được đăng ký"
: "Có lỗi xảy ra, vui lòng thử lại";
const errorCode = (await res.text().catch(() => "")).trim();
const phoneErrorMap: Record<string, string> = {
ExistedUser: "Số điện thoại đã được đăng ký",
InvalidPhoneNumber: "Số điện thoại không hợp lệ",
};
const msg = phoneErrorMap[errorCode] ?? "Đã có lỗi xảy ra, vui lòng thử lại";
setErrors({ phone: msg, otp: "" });
}
} catch {
@@ -96,11 +125,11 @@ export default function RegisterPage() {
localStorage.setItem("coffee-shop-user", JSON.stringify(userData));
router.push("/");
} else {
const data = await res.json().catch(() => ({}));
const errorCode = (await res.text().catch(() => "")).trim();
const msg =
data?.error === "InvalidOTP"
? "Mã OTP không đúng"
: "Có lỗi xảy ra, vui lòng thử lại";
errorCode === "InvalidOTP"
? "Mã OTP không đúng hoặc đã hết hạn"
: "Đã có lỗi xảy ra, vui lòng thử lại";
setErrors({ phone: "", otp: msg });
}
} catch {
@@ -240,7 +269,16 @@ export default function RegisterPage() {
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
<p className="mb-2 text-sm text-blue-800">
<i className="fa-solid fa-circle-info mr-2"></i>
OTP đã đưc gửi đến số <strong>{phone}</strong>
{otpSending ? (
<>
<i className="fa-solid fa-spinner fa-spin mr-1"></i>
Đang gửi OTP đến <strong>{phone}</strong>...
</>
) : (
<>
OTP đã đưc gửi đến số <strong>{phone}</strong>
</>
)}
</p>
<p className="text-xs text-blue-600">
Demo OTP:{" "}
@@ -249,6 +287,12 @@ export default function RegisterPage() {
</code>
</p>
</div>
{otpSendError && (
<p className="flex items-center gap-1 text-xs text-red-500">
<i className="fa-solid fa-circle-exclamation"></i>
{otpSendError}
</p>
)}
{/* OTP Input */}
<div>
@@ -290,7 +334,7 @@ export default function RegisterPage() {
type="submit"
style="login"
size="lg"
disabled={isLoading}
disabled={isLoading || otpSending}
>
{isLoading ? (
<>
@@ -314,14 +358,22 @@ export default function RegisterPage() {
</Button>
</div>
{/* Resend OTP (disabled in demo) */}
{/* Resend OTP */}
<div className="text-center">
<button
type="button"
disabled
className="cursor-not-allowed text-sm text-(--color-text-muted)"
onClick={sendOtp}
disabled={otpSending || isLoading}
className="text-sm text-(--color-primary) underline disabled:cursor-not-allowed disabled:opacity-50 disabled:no-underline"
>
Gửi lại OTP (60s)
{otpSending ? (
<>
<i className="fa-solid fa-spinner fa-spin mr-1"></i>
Đang gửi lại...
</>
) : (
"Gửi lại mã OTP"
)}
</button>
</div>
</form>
+42 -10
View File
@@ -6,12 +6,18 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { SubmitEvent, useState } from "react";
const LOGIN_ERROR_MAP: Record<string, { field: "username" | "password" | "general"; msg: string }> = {
InvalidPhoneNumber: { field: "username", msg: "Số điện thoại không hợp lệ" },
InvalidPassword: { field: "password", msg: "Mật khẩu không đúng" },
};
export default function LoginForm() {
const router = useRouter();
const { login } = useAuth();
const { setUser } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState({
username: "",
password: "",
@@ -44,16 +50,34 @@ export default function LoginForm() {
if (!validate()) return;
const success = await login(username, password);
setIsLoading(true);
setErrors({ username: "", password: "", general: "" });
if (success) {
router.push("/");
} else {
setErrors({
username: "",
password: "",
general: "Tên đăng nhập hoặc mật khẩu không đúng",
try {
const res = await fetch("/api/customer/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone: username, password }),
});
if (res.ok) {
const userData = await res.json();
setUser(userData);
localStorage.setItem("coffee-shop-user", JSON.stringify(userData));
router.push("/");
} else {
const errorCode = (await res.text().catch(() => "")).trim();
const mapped = LOGIN_ERROR_MAP[errorCode];
if (mapped) {
setErrors({ username: "", password: "", general: "", [mapped.field]: mapped.msg });
} else {
setErrors({ username: "", password: "", general: "Đã có lỗi xảy ra, vui lòng thử lại" });
}
}
} catch {
setErrors({ username: "", password: "", general: "Không thể kết nối, vui lòng thử lại" });
} finally {
setIsLoading(false);
}
};
@@ -107,8 +131,16 @@ export default function LoginForm() {
type="submit"
style="login"
size="lg"
disabled={isLoading}
>
Đăng nhập
{isLoading ? (
<>
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
Đang xử ...
</>
) : (
"Đăng nhập"
)}
</Button>
{/* Register Button */}