fix: enhance OTP sending and error handling in registration flow
This commit is contained in:
@@ -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>
|
||||
Mã 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>...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Mã 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 mã 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>
|
||||
|
||||
@@ -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ử lý...
|
||||
</>
|
||||
) : (
|
||||
"Đăng nhập"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Register Button */}
|
||||
|
||||
Reference in New Issue
Block a user