classify web
This commit is contained in:
249
components/Auth/DeleteAccountVerifyOtpModal.jsx
Normal file
249
components/Auth/DeleteAccountVerifyOtpModal.jsx
Normal file
@@ -0,0 +1,249 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import "react-phone-input-2/lib/style.css";
|
||||
import { handleFirebaseAuthError, t } from "@/utils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { logoutSuccess, userSignUpData } from "@/redux/reducer/authSlice";
|
||||
import { Label } from "../ui/label";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import useAutoFocus from "../Common/useAutoFocus";
|
||||
import {
|
||||
deleteUser,
|
||||
getAuth,
|
||||
RecaptchaVerifier,
|
||||
signInWithPhoneNumber,
|
||||
} from "firebase/auth";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { deleteUserApi } from "@/utils/api";
|
||||
|
||||
const DeleteAccountVerifyOtpModal = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
pathname,
|
||||
navigate,
|
||||
}) => {
|
||||
const userData = useSelector(userSignUpData);
|
||||
const auth = getAuth();
|
||||
|
||||
const countryCode = userData?.country_code;
|
||||
const formattedNumber = userData?.mobile;
|
||||
|
||||
const otpInputRef = useAutoFocus();
|
||||
const [showLoader, setShowLoader] = useState(false);
|
||||
const [otp, setOtp] = useState("");
|
||||
const [resendOtpLoader, setResendOtpLoader] = useState(false);
|
||||
const [resendTimer, setResendTimer] = useState(0);
|
||||
const [confirmationResult, setConfirmationResult] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const timer = setTimeout(() => {
|
||||
sendOTP();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId;
|
||||
if (resendTimer > 0) {
|
||||
intervalId = setInterval(() => {
|
||||
setResendTimer((prevTimer) => prevTimer - 1);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
};
|
||||
}, [resendTimer]);
|
||||
|
||||
const generateRecaptcha = async () => {
|
||||
// Reuse existing verifier if it's still valid
|
||||
if (window.recaptchaVerifier && !window.recaptchaVerifier.destroyed) {
|
||||
return window.recaptchaVerifier;
|
||||
}
|
||||
const recaptchaContainer = document.getElementById("recaptcha-container");
|
||||
if (!recaptchaContainer) {
|
||||
console.error("Container element 'recaptcha-container' not found.");
|
||||
return null;
|
||||
}
|
||||
// Clear container and reset reference
|
||||
recaptchaContainer.innerHTML = "";
|
||||
window.recaptchaVerifier = undefined;
|
||||
try {
|
||||
window.recaptchaVerifier = new RecaptchaVerifier(
|
||||
auth,
|
||||
recaptchaContainer,
|
||||
{ size: "invisible" }
|
||||
);
|
||||
return window.recaptchaVerifier;
|
||||
} catch (error) {
|
||||
console.error("Error initializing RecaptchaVerifier:", error.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
recaptchaClear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const recaptchaClear = async () => {
|
||||
if (window.recaptchaVerifier && !window.recaptchaVerifier.destroyed) {
|
||||
try {
|
||||
await window.recaptchaVerifier.clear();
|
||||
} catch (error) {
|
||||
// Ignore errors - verifier might already be cleared
|
||||
}
|
||||
}
|
||||
window.recaptchaVerifier = undefined;
|
||||
const recaptchaContainer = document.getElementById("recaptcha-container");
|
||||
if (recaptchaContainer) {
|
||||
recaptchaContainer.innerHTML = "";
|
||||
}
|
||||
};
|
||||
|
||||
const sendOTP = async () => {
|
||||
try {
|
||||
const PhoneNumber = `${countryCode}${formattedNumber}`;
|
||||
setShowLoader(true);
|
||||
const appVerifier = await generateRecaptcha();
|
||||
const confirmation = await signInWithPhoneNumber(
|
||||
auth,
|
||||
PhoneNumber,
|
||||
appVerifier
|
||||
);
|
||||
setConfirmationResult(confirmation);
|
||||
toast.success(t("otpSentSuccess"));
|
||||
setResendTimer(60);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
handleFirebaseAuthError(error.code);
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyOTPWithFirebase = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setShowLoader(true);
|
||||
const result = await confirmationResult.confirm(otp);
|
||||
const user = result.user;
|
||||
await deleteUser(user);
|
||||
await deleteUserApi.deleteUser();
|
||||
logoutSuccess();
|
||||
toast.success(t("userDeleteSuccess"));
|
||||
setIsOpen(false);
|
||||
if (pathname !== "/") {
|
||||
navigate("/");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const errorCode = error?.code;
|
||||
handleFirebaseAuthError(errorCode);
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resendOtp = async () => {
|
||||
try {
|
||||
setResendOtpLoader(true);
|
||||
const PhoneNumber = `${countryCode}${formattedNumber}`;
|
||||
const appVerifier = await generateRecaptcha();
|
||||
const confirmation = await signInWithPhoneNumber(
|
||||
auth,
|
||||
PhoneNumber,
|
||||
appVerifier
|
||||
);
|
||||
setConfirmationResult(confirmation);
|
||||
toast.success(t("otpSentSuccess"));
|
||||
} catch (error) {
|
||||
const errorCode = error.code;
|
||||
handleFirebaseAuthError(errorCode);
|
||||
} finally {
|
||||
setResendOtpLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
className="px-[40px] sm:py-[50px] sm:px-[90px]"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-3xl sm:text-4xl font-light">
|
||||
{t("verifyOtp")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-base text-black font-light">
|
||||
{t("sentTo")} {`${countryCode} ${formattedNumber}`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
onSubmit={verifyOTPWithFirebase}
|
||||
>
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("otp")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("enterOtp")}
|
||||
id="otp"
|
||||
name="otp"
|
||||
value={otp}
|
||||
maxLength={6}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
ref={otpInputRef}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={showLoader}
|
||||
className="text-xl text-white font-light px-4 py-2"
|
||||
size="big"
|
||||
>
|
||||
{showLoader ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
t("verify")
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="text-lg text-black font-light bg-transparent"
|
||||
size="big"
|
||||
onClick={resendOtp}
|
||||
disabled={resendOtpLoader || showLoader || resendTimer > 0}
|
||||
>
|
||||
{resendOtpLoader ? (
|
||||
<Loader2 className="size-6 animate-spin" />
|
||||
) : resendTimer > 0 ? (
|
||||
`${t("resendOtp")} ${resendTimer}s`
|
||||
) : (
|
||||
t("resendOtp")
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
<div id="recaptcha-container" style={{ display: "none" }}></div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteAccountVerifyOtpModal;
|
||||
382
components/Auth/LoginModal.jsx
Normal file
382
components/Auth/LoginModal.jsx
Normal file
@@ -0,0 +1,382 @@
|
||||
"use client";
|
||||
import { formatPhoneNumber, handleFirebaseAuthError, t } from "@/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
Fcmtoken,
|
||||
getIsDemoMode,
|
||||
getOtpServiceProvider,
|
||||
settingsData,
|
||||
} from "@/redux/reducer/settingSlice";
|
||||
import "react-phone-input-2/lib/style.css";
|
||||
import { Button } from "../ui/button";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import { MdOutlineEmail, MdOutlineLocalPhone } from "react-icons/md";
|
||||
import {
|
||||
getAuth,
|
||||
GoogleAuthProvider,
|
||||
RecaptchaVerifier,
|
||||
signInWithPhoneNumber,
|
||||
signInWithPopup,
|
||||
} from "firebase/auth";
|
||||
import { toast } from "sonner";
|
||||
import { getOtpApi, userSignUpApi } from "@/utils/api";
|
||||
import { loadUpdateData } from "@/redux/reducer/authSlice";
|
||||
import LoginWithEmailForm from "./LoginWithEmailForm";
|
||||
import LoginWithMobileForm from "./LoginWithMobileForm";
|
||||
import OtpScreen from "./OtpScreen";
|
||||
import TermsAndPrivacyLinks from "./TermsAndPrivacyLinks";
|
||||
import { setIsLoginOpen } from "@/redux/reducer/globalStateSlice";
|
||||
import ResetPasswordScreen from "./ResetPasswordScreen";
|
||||
|
||||
const LoginModal = ({ IsLoginOpen, setIsRegisterModalOpen }) => {
|
||||
const settings = useSelector(settingsData);
|
||||
const auth = getAuth();
|
||||
const fetchFCM = useSelector(Fcmtoken);
|
||||
const isDemoMode = useSelector(getIsDemoMode);
|
||||
const [IsOTPScreen, setIsOTPScreen] = useState(null);
|
||||
const [resendTimer, setResendTimer] = useState(0);
|
||||
const [loginStates, setLoginStates] = useState({
|
||||
number: isDemoMode ? "919876598765" : "",
|
||||
countryCode: isDemoMode ? "+91" : '',
|
||||
showLoader: false,
|
||||
regionCode: "",
|
||||
password: isDemoMode ? "123456" : "",
|
||||
});
|
||||
|
||||
const [confirmationResult, setConfirmationResult] = useState(null);
|
||||
const [FirebaseId, setFirebaseId] = useState("");
|
||||
const { number, countryCode } = loginStates;
|
||||
const formattedNumber = formatPhoneNumber(number, countryCode);
|
||||
|
||||
const otp_service_provider = useSelector(getOtpServiceProvider);
|
||||
|
||||
// Active authentication methods
|
||||
const mobile_authentication = Number(settings?.mobile_authentication);
|
||||
const google_authentication = Number(settings?.google_authentication);
|
||||
const email_authentication = Number(settings?.email_authentication);
|
||||
|
||||
const [IsLoginWithEmail, setIsLoginWithEmail] = useState(
|
||||
mobile_authentication === 0 && email_authentication === 1 ? true : false
|
||||
);
|
||||
|
||||
const IsShowOrSignIn =
|
||||
!(
|
||||
mobile_authentication === 0 &&
|
||||
email_authentication === 0 &&
|
||||
google_authentication === 1
|
||||
) && google_authentication === 1;
|
||||
|
||||
const OnHide = async () => {
|
||||
setIsOTPScreen(null);
|
||||
setIsLoginOpen(false);
|
||||
setConfirmationResult(null);
|
||||
setResendTimer(0);
|
||||
};
|
||||
|
||||
const generateRecaptcha = async () => {
|
||||
// Reuse existing verifier if it's still valid
|
||||
if (window.recaptchaVerifier && !window.recaptchaVerifier.destroyed) {
|
||||
return window.recaptchaVerifier;
|
||||
}
|
||||
const recaptchaContainer = document.getElementById("recaptcha-container");
|
||||
if (!recaptchaContainer) {
|
||||
console.error("Container element 'recaptcha-container' not found.");
|
||||
return null;
|
||||
}
|
||||
// Clear container and reset reference
|
||||
recaptchaContainer.innerHTML = "";
|
||||
window.recaptchaVerifier = undefined;
|
||||
try {
|
||||
window.recaptchaVerifier = new RecaptchaVerifier(
|
||||
auth,
|
||||
recaptchaContainer,
|
||||
{ size: "invisible" }
|
||||
);
|
||||
return window.recaptchaVerifier;
|
||||
} catch (error) {
|
||||
console.error("Error initializing RecaptchaVerifier:", error.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
recaptchaClear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const recaptchaClear = async () => {
|
||||
if (window.recaptchaVerifier && !window.recaptchaVerifier.destroyed) {
|
||||
try {
|
||||
await window.recaptchaVerifier.clear();
|
||||
} catch (error) {
|
||||
// Ignore errors - verifier might already be cleared
|
||||
}
|
||||
}
|
||||
window.recaptchaVerifier = undefined;
|
||||
const recaptchaContainer = document.getElementById("recaptcha-container");
|
||||
if (recaptchaContainer) {
|
||||
recaptchaContainer.innerHTML = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleSignup = async () => {
|
||||
const provider = new GoogleAuthProvider();
|
||||
try {
|
||||
const res = await signInWithPopup(auth, provider);
|
||||
const user = res.user;
|
||||
try {
|
||||
const response = await userSignUpApi.userSignup({
|
||||
name: user.displayName ? user.displayName : "",
|
||||
email: user?.email,
|
||||
firebase_id: user?.uid, // Accessing UID directly from the user object
|
||||
fcm_id: fetchFCM ? fetchFCM : "",
|
||||
type: "google",
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
if (data.error === true) {
|
||||
toast.error(data.message);
|
||||
} else {
|
||||
loadUpdateData(data);
|
||||
toast.success(data.message);
|
||||
}
|
||||
OnHide();
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
toast.error("Failed to complete signup");
|
||||
}
|
||||
} catch (error) {
|
||||
const errorCode = error.code;
|
||||
handleFirebaseAuthError(errorCode);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateAnAccount = () => {
|
||||
OnHide();
|
||||
setIsRegisterModalOpen(true);
|
||||
};
|
||||
|
||||
// Handle forgot password - send OTP and show OTP screen
|
||||
const handleForgotPassword = async () => {
|
||||
const PhoneNumber = `${loginStates.countryCode}${formattedNumber}`;
|
||||
if (otp_service_provider === "twilio") {
|
||||
try {
|
||||
const response = await getOtpApi.getOtp({ number: formattedNumber, country_code: countryCode });
|
||||
if (response?.data?.error === false) {
|
||||
toast.success(t("otpSentSuccess"));
|
||||
setResendTimer(60);
|
||||
setIsOTPScreen("otp");
|
||||
} else {
|
||||
toast.error(t("failedToSendOtp"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const appVerifier = await generateRecaptcha();
|
||||
const confirmation = await signInWithPhoneNumber(
|
||||
auth,
|
||||
PhoneNumber,
|
||||
appVerifier
|
||||
);
|
||||
setConfirmationResult(confirmation);
|
||||
toast.success(t("otpSentSuccess"));
|
||||
setResendTimer(60);
|
||||
setIsOTPScreen("otp");
|
||||
} catch (error) {
|
||||
|
||||
console.log(error)
|
||||
|
||||
handleFirebaseAuthError(error.code);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle OTP verification success - move to reset password screen
|
||||
const handleForgotPasswordOtpVerified = (firebase_id) => {
|
||||
setFirebaseId(firebase_id);
|
||||
setIsOTPScreen("reset");
|
||||
toast.success(t("otpVerified"));
|
||||
};
|
||||
|
||||
// Handle successful password reset - go back to login
|
||||
const handleResetPasswordSuccess = () => {
|
||||
setIsOTPScreen(null);
|
||||
setConfirmationResult(null);
|
||||
setResendTimer(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={IsLoginOpen} onOpenChange={setIsLoginOpen}>
|
||||
<DialogContent
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
className="px-[40px] sm:py-[50px] sm:px-[90px]"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-3xl sm:text-4xl font-light">
|
||||
{IsOTPScreen === "otp" ? (
|
||||
t("verifyOtp")
|
||||
) : IsOTPScreen === "reset" ? (
|
||||
t("resetYourPassword")
|
||||
) : (
|
||||
<>
|
||||
{t("loginTo")}{" "}
|
||||
<span className="text-primary">{settings?.company_name}</span>
|
||||
</>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-base text-black font-light">
|
||||
{IsOTPScreen === "otp" ? (
|
||||
<>
|
||||
{t("sentTo")} {`${countryCode}${formattedNumber}`}{" "}
|
||||
<span
|
||||
onClick={() => setIsOTPScreen(false)}
|
||||
className="text-primary underline cursor-pointer"
|
||||
>
|
||||
{t("change")}
|
||||
</span>
|
||||
</>
|
||||
) : IsOTPScreen === "reset" ? (
|
||||
t("enterNewPassword")
|
||||
) : (
|
||||
<>
|
||||
{t("newto")} {settings?.company_name}?{" "}
|
||||
<span
|
||||
className="text-primary cursor-pointer underline"
|
||||
onClick={handleCreateAnAccount}
|
||||
>
|
||||
{t("createAccount")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{IsOTPScreen === "otp" ? (
|
||||
<OtpScreen
|
||||
OnHide={OnHide}
|
||||
generateRecaptcha={generateRecaptcha}
|
||||
countryCode={countryCode}
|
||||
formattedNumber={formattedNumber}
|
||||
confirmationResult={confirmationResult}
|
||||
setConfirmationResult={setConfirmationResult}
|
||||
resendTimer={resendTimer}
|
||||
setResendTimer={setResendTimer}
|
||||
regionCode={loginStates.regionCode}
|
||||
isDemoMode={isDemoMode}
|
||||
onOtpVerified={handleForgotPasswordOtpVerified}
|
||||
key="forgot-password-otp"
|
||||
/>
|
||||
) : IsOTPScreen === "reset" ? (
|
||||
<ResetPasswordScreen
|
||||
FirebaseId={FirebaseId}
|
||||
formattedNumber={formattedNumber}
|
||||
countryCode={loginStates.countryCode}
|
||||
onSuccess={handleResetPasswordSuccess}
|
||||
onCancel={() => setIsOTPScreen(null)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-[30px] mt-3.5">
|
||||
{!(
|
||||
mobile_authentication === 0 &&
|
||||
email_authentication === 0 &&
|
||||
google_authentication === 1
|
||||
) &&
|
||||
mobile_authentication === 1 &&
|
||||
email_authentication === 1 &&
|
||||
(IsLoginWithEmail ? (
|
||||
<LoginWithEmailForm OnHide={OnHide} key={IsLoginWithEmail} />
|
||||
) : (
|
||||
<LoginWithMobileForm
|
||||
formattedNumber={formattedNumber}
|
||||
loginStates={loginStates}
|
||||
setLoginStates={setLoginStates}
|
||||
onForgotPassword={handleForgotPassword}
|
||||
OnHide={OnHide}
|
||||
key={IsLoginWithEmail}
|
||||
/>
|
||||
))}
|
||||
|
||||
{email_authentication === 1 && mobile_authentication === 0 && (
|
||||
<LoginWithEmailForm OnHide={OnHide} key={IsLoginWithEmail} />
|
||||
)}
|
||||
|
||||
{mobile_authentication === 1 && email_authentication === 0 && (
|
||||
<LoginWithMobileForm
|
||||
OnHide={OnHide}
|
||||
formattedNumber={formattedNumber}
|
||||
loginStates={loginStates}
|
||||
setLoginStates={setLoginStates}
|
||||
onForgotPassword={handleForgotPassword}
|
||||
key={IsLoginWithEmail}
|
||||
/>
|
||||
)}
|
||||
|
||||
{IsShowOrSignIn && (
|
||||
<div className="flex items-center gap-2">
|
||||
<hr className="w-full" />
|
||||
<p className="text-nowrap text-sm">{t("orSignInWith")}</p>
|
||||
<hr className="w-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{google_authentication === 1 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="big"
|
||||
className="flex items-center justify-center py-4 text-base"
|
||||
onClick={handleGoogleSignup}
|
||||
>
|
||||
<FcGoogle className="!size-6" />
|
||||
<span>{t("continueWithGoogle")}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{IsLoginWithEmail && mobile_authentication === 1 ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="big"
|
||||
className="flex items-center justify-center py-4 text-base h-auto"
|
||||
onClick={() => setIsLoginWithEmail(false)}
|
||||
>
|
||||
<MdOutlineLocalPhone className="!size-6" />
|
||||
{t("continueWithMobile")}
|
||||
</Button>
|
||||
) : (
|
||||
!IsLoginWithEmail &&
|
||||
email_authentication === 1 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="big"
|
||||
className="flex items-center justify-center py-4 text-base h-auto"
|
||||
onClick={() => setIsLoginWithEmail(true)}
|
||||
>
|
||||
<MdOutlineEmail className="!size-6" />
|
||||
{t("continueWithEmail")}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<TermsAndPrivacyLinks t={t} settings={settings} OnHide={OnHide} />
|
||||
</div>
|
||||
)}
|
||||
<div id="recaptcha-container" style={{ display: "none" }}></div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginModal;
|
||||
189
components/Auth/LoginWithEmailForm.jsx
Normal file
189
components/Auth/LoginWithEmailForm.jsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { FaRegEye, FaRegEyeSlash } from "react-icons/fa";
|
||||
import { Label } from "../ui/label";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import useAutoFocus from "../Common/useAutoFocus";
|
||||
import { toast } from "sonner";
|
||||
import { handleFirebaseAuthError, t } from "@/utils";
|
||||
import {
|
||||
getAuth,
|
||||
sendPasswordResetEmail,
|
||||
signInWithEmailAndPassword,
|
||||
} from "firebase/auth";
|
||||
import { userSignUpApi } from "@/utils/api";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Fcmtoken } from "@/redux/reducer/settingSlice";
|
||||
import { loadUpdateData } from "@/redux/reducer/authSlice";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const LoginWithEmailForm = ({ OnHide }) => {
|
||||
const emailRef = useAutoFocus();
|
||||
const auth = getAuth();
|
||||
const fetchFCM = useSelector(Fcmtoken);
|
||||
const [loginStates, setLoginStates] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
IsPasswordVisible: false,
|
||||
showLoader: false,
|
||||
});
|
||||
|
||||
const { email, password, IsPasswordVisible, showLoader } = loginStates;
|
||||
|
||||
const signin = async (email, password) => {
|
||||
try {
|
||||
const userCredential = await signInWithEmailAndPassword(
|
||||
auth,
|
||||
email,
|
||||
password
|
||||
);
|
||||
if (!userCredential?.user) {
|
||||
toast.error(t("userNotFound"));
|
||||
return null;
|
||||
}
|
||||
return userCredential;
|
||||
} catch (error) {
|
||||
console.error("Error signing in:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const Signin = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email) {
|
||||
toast.error(t("emailRequired"));
|
||||
return;
|
||||
} else if (!/\S+@\S+\.\S+/.test(email)) {
|
||||
toast.error(t("emailInvalid"));
|
||||
return;
|
||||
} else if (!password) {
|
||||
toast.error(t("passwordRequired"));
|
||||
return;
|
||||
} else if (password.length < 6) {
|
||||
toast.error(t("passwordTooShort"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoginStates((prev) => ({ ...prev, showLoader: true }));
|
||||
const userCredential = await signin(email, password);
|
||||
const user = userCredential.user;
|
||||
if (user.emailVerified) {
|
||||
try {
|
||||
const response = await userSignUpApi.userSignup({
|
||||
name: user?.displayName || "",
|
||||
email: user?.email,
|
||||
firebase_id: user?.uid,
|
||||
fcm_id: fetchFCM ? fetchFCM : "",
|
||||
type: "email",
|
||||
is_login: 1,
|
||||
});
|
||||
const data = response.data;
|
||||
if (data.error === false) {
|
||||
loadUpdateData(data);
|
||||
toast.success(data.message);
|
||||
OnHide();
|
||||
} else {
|
||||
toast.error(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
// Add your logic here for verified users
|
||||
} else {
|
||||
toast.error(t("verifyEmailFirst"));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorCode = error.code;
|
||||
console.log("Error code:", errorCode);
|
||||
handleFirebaseAuthError(errorCode);
|
||||
} finally {
|
||||
setLoginStates((prev) => ({ ...prev, showLoader: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotModal = async (e) => {
|
||||
e.preventDefault();
|
||||
await sendPasswordResetEmail(auth, email)
|
||||
.then(() => {
|
||||
toast.success(t("resetPassword"));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error", error);
|
||||
handleFirebaseAuthError(error?.code);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form className="flex flex-col gap-6" onSubmit={Signin}>
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("email")}</Label>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder={t("enterEmail")}
|
||||
value={email}
|
||||
onChange={(e) =>
|
||||
setLoginStates((prev) => ({ ...prev, email: e.target.value }))
|
||||
}
|
||||
ref={emailRef}
|
||||
/>
|
||||
</div>
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("password")}</Label>
|
||||
<div className="flex items-center relative">
|
||||
<Input
|
||||
type={IsPasswordVisible ? "text" : "password"}
|
||||
placeholder={t("enterPassword")}
|
||||
className="ltr:pr-9 rtl:pl-9"
|
||||
value={password}
|
||||
onChange={(e) =>
|
||||
setLoginStates((prev) => ({
|
||||
...prev,
|
||||
password: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute ltr:right-3 rtl:left-3 cursor-pointer"
|
||||
onClick={() =>
|
||||
setLoginStates((prev) => ({
|
||||
...prev,
|
||||
IsPasswordVisible: !prev.IsPasswordVisible,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{IsPasswordVisible ? (
|
||||
<FaRegEye size={20} />
|
||||
) : (
|
||||
<FaRegEyeSlash size={20} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="text-right font-semibold text-primary"
|
||||
onClick={handleForgotModal}
|
||||
type="button"
|
||||
>
|
||||
{t("forgtPassword")}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
className="text-xl text-white font-light px-4 py-2"
|
||||
size="big"
|
||||
disabled={showLoader}
|
||||
>
|
||||
{showLoader ? (
|
||||
<Loader2 className="size-6 animate-spin" />
|
||||
) : (
|
||||
t("signIn")
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginWithEmailForm;
|
||||
194
components/Auth/LoginWithMobileForm.jsx
Normal file
194
components/Auth/LoginWithMobileForm.jsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import PhoneInput from "react-phone-input-2";
|
||||
import { Label } from "../ui/label";
|
||||
import { Button } from "../ui/button";
|
||||
import useAutoFocus from "../Common/useAutoFocus";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js/max";
|
||||
import { toast } from "sonner";
|
||||
import { t } from "@/utils";
|
||||
import { Input } from "../ui/input";
|
||||
import { useState } from "react";
|
||||
import { FaRegEye, FaRegEyeSlash } from "react-icons/fa";
|
||||
import { getUserExistsApi, userSignUpApi } from "@/utils/api";
|
||||
import { Fcmtoken } from "@/redux/reducer/settingSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
import { loadUpdateData } from "@/redux/reducer/authSlice";
|
||||
|
||||
const LoginWithMobileForm = ({
|
||||
loginStates,
|
||||
setLoginStates,
|
||||
formattedNumber,
|
||||
onForgotPassword,
|
||||
OnHide,
|
||||
}) => {
|
||||
const numberInputRef = useAutoFocus();
|
||||
const { number, countryCode, showLoader } = loginStates;
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const fcm_id = useSelector(Fcmtoken);
|
||||
const [forgotPasswordLoading, setForgotPasswordLoading] = useState(false);
|
||||
|
||||
const handleInputChange = (value, data) => {
|
||||
setLoginStates((prev) => ({
|
||||
...prev,
|
||||
number: value,
|
||||
countryCode: "+" + (data?.dialCode || ""),
|
||||
regionCode: data?.countryCode.toLowerCase() || "",
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCountryChange = (code) => {
|
||||
setLoginStates((prev) => ({
|
||||
...prev,
|
||||
countryCode: code,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleLoginWithMobile = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
if (!isValidPhoneNumber(`${countryCode}${formattedNumber}`)) {
|
||||
toast.error(t("invalidPhoneNumber"));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoginStates((prev) => ({
|
||||
...prev,
|
||||
showLoader: true,
|
||||
}));
|
||||
const params = {
|
||||
mobile: formattedNumber,
|
||||
password: loginStates.password,
|
||||
country_code: countryCode,
|
||||
type: "phone",
|
||||
fcm_id: fcm_id ? fcm_id : "",
|
||||
is_login: 1,
|
||||
};
|
||||
const response = await userSignUpApi.userSignup(params);
|
||||
if (response?.data?.error === false) {
|
||||
toast.success(response?.data?.message);
|
||||
loadUpdateData(response?.data);
|
||||
OnHide();
|
||||
} else {
|
||||
toast.error(response?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoginStates((prev) => ({
|
||||
...prev,
|
||||
showLoader: false,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const checkIfUserExistsOrNot = async () => {
|
||||
try {
|
||||
const res = await getUserExistsApi.getUserExists({
|
||||
mobile: formattedNumber,
|
||||
country_code: countryCode,
|
||||
forgot_password: 1
|
||||
})
|
||||
if (res?.data?.error === false) {
|
||||
return true
|
||||
} else {
|
||||
toast.error(res?.data?.message)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Handle forgot password with loading state
|
||||
const handleForgotPasswordClick = async () => {
|
||||
if (!isValidPhoneNumber(`${countryCode}${formattedNumber}`)) {
|
||||
toast.error(t("invalidPhoneNumber"));
|
||||
return;
|
||||
}
|
||||
setForgotPasswordLoading(true);
|
||||
const isUserExists = await checkIfUserExistsOrNot()
|
||||
if (!isUserExists) {
|
||||
setForgotPasswordLoading(false);
|
||||
return;
|
||||
}
|
||||
await onForgotPassword();
|
||||
setForgotPasswordLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-6" onSubmit={handleLoginWithMobile}>
|
||||
<div className="labelInputCont">
|
||||
<Label className="font-semibold after:content-['*'] after:text-red-500">
|
||||
{t("mobileNumber")}
|
||||
</Label>
|
||||
<PhoneInput
|
||||
country={process.env.NEXT_PUBLIC_DEFAULT_COUNTRY}
|
||||
value={number}
|
||||
onChange={(phone, data) => handleInputChange(phone, data)}
|
||||
onCountryChange={handleCountryChange}
|
||||
inputProps={{
|
||||
name: "phone",
|
||||
required: true,
|
||||
ref: numberInputRef,
|
||||
}}
|
||||
enableLongNumbers
|
||||
/>
|
||||
</div>
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("password")}</Label>
|
||||
<div className="flex items-center relative">
|
||||
<Input
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
placeholder={t("enterPassword")}
|
||||
className="ltr:pr-9 rtl:pl-9"
|
||||
value={loginStates.password}
|
||||
onChange={(e) =>
|
||||
setLoginStates((prev) => ({ ...prev, password: e.target.value }))
|
||||
}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute ltr:right-3 rtl:left-3 cursor-pointer"
|
||||
onClick={() => setIsPasswordVisible((prev) => !prev)}
|
||||
>
|
||||
{isPasswordVisible ? (
|
||||
<FaRegEye size={20} />
|
||||
) : (
|
||||
<FaRegEyeSlash size={20} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="text-right font-semibold text-primary w-fit self-end"
|
||||
onClick={handleForgotPasswordClick}
|
||||
type="button"
|
||||
disabled={forgotPasswordLoading}
|
||||
>
|
||||
{forgotPasswordLoading ? (
|
||||
<>
|
||||
<span className="flex items-center gap-2 justify-end">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span>{t("loading")}</span>
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
t("forgtPassword")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={showLoader}
|
||||
className="text-xl text-white font-light px-4 py-2"
|
||||
size="big"
|
||||
>
|
||||
{showLoader ? <Loader2 className="size-6 animate-spin" /> : t("login")}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginWithMobileForm;
|
||||
36
components/Auth/MailSentSuccessModal.jsx
Normal file
36
components/Auth/MailSentSuccessModal.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import mainSentImg from "../../public/assets/Mail Verification.svg";
|
||||
import { t } from "@/utils";
|
||||
import CustomImage from "../Common/CustomImage";
|
||||
|
||||
const MailSentSuccessModal = ({ IsMailSentSuccess, setIsMailSentSuccess }) => {
|
||||
return (
|
||||
<Dialog open={IsMailSentSuccess} onOpenChange={setIsMailSentSuccess}>
|
||||
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="sr-only"></DialogTitle>
|
||||
<DialogDescription className="sr-only"></DialogDescription>
|
||||
<div className="flex flex-col gap-3 items-center justify-center">
|
||||
<CustomImage
|
||||
src={mainSentImg}
|
||||
alt="Verification Mail sent"
|
||||
width={300}
|
||||
height={195}
|
||||
className="aspect-[300/195] object-contain"
|
||||
/>
|
||||
<h1 className="text-2xl font-medium">{t("youveGotMail")}</h1>
|
||||
<p className="opacity-65">{t("verifyAccount")}</p>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default MailSentSuccessModal;
|
||||
244
components/Auth/OtpScreen.jsx
Normal file
244
components/Auth/OtpScreen.jsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import { getAuth, signInWithPhoneNumber } from "firebase/auth";
|
||||
import useAutoFocus from "../Common/useAutoFocus";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { toast } from "sonner";
|
||||
import { handleFirebaseAuthError, t } from "@/utils";
|
||||
import { getOtpApi, userSignUpApi, verifyOtpApi } from "@/utils/api";
|
||||
import { loadUpdateData } from "@/redux/reducer/authSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Fcmtoken, getOtpServiceProvider } from "@/redux/reducer/settingSlice";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "../Common/useNavigate";
|
||||
|
||||
const OtpScreen = ({
|
||||
generateRecaptcha,
|
||||
countryCode,
|
||||
formattedNumber,
|
||||
confirmationResult,
|
||||
setConfirmationResult,
|
||||
OnHide,
|
||||
resendTimer,
|
||||
setResendTimer,
|
||||
regionCode,
|
||||
isDemoMode,
|
||||
isRegister = false,
|
||||
onOtpVerified,
|
||||
password
|
||||
}) => {
|
||||
const { navigate } = useNavigate();
|
||||
const otpInputRef = useAutoFocus();
|
||||
const fetchFCM = useSelector(Fcmtoken);
|
||||
const auth = getAuth();
|
||||
const [resendOtpLoader, setResendOtpLoader] = useState(false);
|
||||
const [showLoader, setShowLoader] = useState(false);
|
||||
const [otp, setOtp] = useState(isDemoMode && !isRegister ? "123456" : "");
|
||||
const otp_service_provider = useSelector(getOtpServiceProvider);
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId;
|
||||
if (resendTimer > 0) {
|
||||
intervalId = setInterval(() => {
|
||||
setResendTimer((prevTimer) => prevTimer - 1);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
};
|
||||
}, [resendTimer]);
|
||||
|
||||
const verifyOTPWithTwillio = async () => {
|
||||
try {
|
||||
|
||||
const payload = {
|
||||
number: formattedNumber,
|
||||
country_code: countryCode,
|
||||
otp: otp,
|
||||
}
|
||||
|
||||
if (isRegister && password) {
|
||||
payload.password = password;
|
||||
}
|
||||
|
||||
const response = await verifyOtpApi.verifyOtp(payload);
|
||||
if (response?.data?.error === false) {
|
||||
// If callback provided, use it (for forgot password)
|
||||
if (onOtpVerified) {
|
||||
onOtpVerified();
|
||||
return;
|
||||
}
|
||||
// Otherwise, do normal login
|
||||
loadUpdateData(response?.data);
|
||||
toast.success(response?.data?.message);
|
||||
if (
|
||||
response?.data?.data?.email === "" ||
|
||||
response?.data?.data?.name === ""
|
||||
) {
|
||||
navigate("/profile");
|
||||
}
|
||||
OnHide();
|
||||
} else {
|
||||
toast.error(response?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyOTPWithFirebase = async () => {
|
||||
try {
|
||||
const result = await confirmationResult.confirm(otp);
|
||||
// Access user information from the result
|
||||
const user = result.user;
|
||||
|
||||
const firebase_id = user?.uid;
|
||||
|
||||
// If callback provided, use it (for forgot password)
|
||||
if (onOtpVerified) {
|
||||
onOtpVerified(firebase_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
mobile: formattedNumber,
|
||||
firebase_id: user.uid, // Accessing UID directly from the user object
|
||||
fcm_id: fetchFCM ? fetchFCM : "",
|
||||
country_code: countryCode,
|
||||
type: "phone",
|
||||
region_code: regionCode?.toUpperCase() || "",
|
||||
}
|
||||
|
||||
if (isRegister && password) {
|
||||
payload.password = password;
|
||||
}
|
||||
|
||||
// Otherwise, do normal login
|
||||
const response = await userSignUpApi.userSignup(payload);
|
||||
const data = response.data;
|
||||
loadUpdateData(data);
|
||||
toast.success(data.message);
|
||||
OnHide();
|
||||
if (data?.data?.email === "" || data?.data?.name === "") {
|
||||
navigate("/profile");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const errorCode = error?.code;
|
||||
handleFirebaseAuthError(errorCode);
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyOTP = async (e) => {
|
||||
e.preventDefault();
|
||||
if (otp === "") {
|
||||
toast.error(t("otpmissing"));
|
||||
return;
|
||||
}
|
||||
setShowLoader(true);
|
||||
if (otp_service_provider === "twilio") {
|
||||
await verifyOTPWithTwillio();
|
||||
} else {
|
||||
await verifyOTPWithFirebase();
|
||||
}
|
||||
};
|
||||
|
||||
const resendOtpWithTwillio = async () => {
|
||||
try {
|
||||
const response = await getOtpApi.getOtp({ number: formattedNumber, country_code: countryCode });
|
||||
if (response?.data?.error === false) {
|
||||
toast.success(t("otpSentSuccess"));
|
||||
setResendTimer(60); // Start the 60-second timer
|
||||
} else {
|
||||
toast.error(t("failedToSendOtp"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setResendOtpLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resendOtpWithFirebase = async (PhoneNumber) => {
|
||||
try {
|
||||
const appVerifier = await generateRecaptcha();
|
||||
const confirmation = await signInWithPhoneNumber(
|
||||
auth,
|
||||
PhoneNumber,
|
||||
appVerifier
|
||||
);
|
||||
setConfirmationResult(confirmation);
|
||||
toast.success(t("otpSentSuccess"));
|
||||
} catch (error) {
|
||||
const errorCode = error.code;
|
||||
handleFirebaseAuthError(errorCode);
|
||||
} finally {
|
||||
setResendOtpLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resendOtp = async (e) => {
|
||||
e.preventDefault();
|
||||
setResendOtpLoader(true);
|
||||
const PhoneNumber = `${countryCode}${formattedNumber}`;
|
||||
if (otp_service_provider === "twilio") {
|
||||
await resendOtpWithTwillio();
|
||||
} else {
|
||||
await resendOtpWithFirebase(PhoneNumber);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-6" onSubmit={verifyOTP}>
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("otp")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("enterOtp")}
|
||||
id="otp"
|
||||
name="otp"
|
||||
value={otp}
|
||||
maxLength={6}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
ref={otpInputRef}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={showLoader}
|
||||
className="text-xl text-white font-light px-4 py-2"
|
||||
size="big"
|
||||
>
|
||||
{showLoader ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
t("verify")
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="text-lg text-black font-light bg-transparent"
|
||||
size="big"
|
||||
onClick={resendOtp}
|
||||
disabled={resendOtpLoader || showLoader || resendTimer > 0}
|
||||
>
|
||||
{resendOtpLoader ? (
|
||||
<Loader2 className="size-6 animate-spin" />
|
||||
) : resendTimer > 0 ? (
|
||||
`${t("resendOtp")} ${resendTimer}s`
|
||||
) : (
|
||||
t("resendOtp")
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default OtpScreen;
|
||||
180
components/Auth/RegisterModal.jsx
Normal file
180
components/Auth/RegisterModal.jsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { t } from "@/utils";
|
||||
import { settingsData } from "@/redux/reducer/settingSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
import TermsAndPrivacyLinks from "./TermsAndPrivacyLinks";
|
||||
import { setIsLoginOpen } from "@/redux/reducer/globalStateSlice";
|
||||
import { MdOutlineEmail, MdOutlineLocalPhone } from "react-icons/md";
|
||||
import RegisterWithEmailForm from "./RegisterWithEmailForm";
|
||||
import RegisterWithMobileForm from "./RegisterWithMobileForm";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
const RegisterModal = ({
|
||||
setIsMailSentSuccess,
|
||||
IsRegisterModalOpen,
|
||||
setIsRegisterModalOpen,
|
||||
}) => {
|
||||
// Get Global data
|
||||
const settings = useSelector(settingsData);
|
||||
const [descriptionState, setDescriptionState] = useState({
|
||||
type: "register", // "register" | "otp"
|
||||
phoneNumber: "",
|
||||
});
|
||||
const [isOTPScreen, setIsOTPScreen] = useState(false);
|
||||
|
||||
// Active authentication methods
|
||||
const mobile_authentication = Number(settings?.mobile_authentication);
|
||||
const email_authentication = Number(settings?.email_authentication);
|
||||
|
||||
// Toggle between email and mobile registration
|
||||
const [IsRegisterWithEmail, setIsRegisterWithEmail] = useState(
|
||||
!mobile_authentication == 1
|
||||
);
|
||||
|
||||
const OnHide = () => {
|
||||
setIsRegisterModalOpen(false);
|
||||
};
|
||||
|
||||
const handleLoginClick = () => {
|
||||
OnHide();
|
||||
setIsLoginOpen(true);
|
||||
};
|
||||
|
||||
const handleChangeClick = () => {
|
||||
setIsOTPScreen(false);
|
||||
setDescriptionState({ type: "register", phoneNumber: "" });
|
||||
};
|
||||
|
||||
// Show divider when alternative auth methods (email/mobile toggle) are available
|
||||
const showOrSignInWith =
|
||||
!isOTPScreen &&
|
||||
((IsRegisterWithEmail && mobile_authentication == 1) ||
|
||||
(!IsRegisterWithEmail && email_authentication == 1));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={IsRegisterModalOpen} onOpenChange={setIsRegisterModalOpen}>
|
||||
<DialogContent
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
className="px-[40px] sm:py-[50px] sm:px-[90px]"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-3xl sm:text-4xl font-light">
|
||||
{descriptionState.type === "otp" ? (
|
||||
t("verifyOtp")
|
||||
) : (
|
||||
<>
|
||||
{t("welcomeTo")}{" "}
|
||||
<span className="text-primary">{settings?.company_name}</span>
|
||||
</>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-base text-black font-light">
|
||||
{descriptionState.type === "otp" ? (
|
||||
<>
|
||||
{t("sentTo")} {descriptionState.phoneNumber}{" "}
|
||||
<span
|
||||
className="text-primary cursor-pointer underline"
|
||||
onClick={handleChangeClick}
|
||||
>
|
||||
{t("change")}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t("haveAccount")}{" "}
|
||||
<span
|
||||
className="text-primary cursor-pointer underline"
|
||||
onClick={handleLoginClick}
|
||||
>
|
||||
{t("logIn")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-[30px] mt-3.5">
|
||||
{/* Show RegisterWithEmailForm when email auth is enabled */}
|
||||
{email_authentication === 1 &&
|
||||
(mobile_authentication == 0 || IsRegisterWithEmail) && (
|
||||
<RegisterWithEmailForm
|
||||
OnHide={OnHide}
|
||||
setIsMailSentSuccess={setIsMailSentSuccess}
|
||||
key={IsRegisterWithEmail}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Show RegisterWithMobileForm when mobile auth is enabled */}
|
||||
{mobile_authentication == 1 &&
|
||||
(email_authentication == 0 || !IsRegisterWithEmail) && (
|
||||
<RegisterWithMobileForm
|
||||
OnHide={OnHide}
|
||||
setDescriptionState={setDescriptionState}
|
||||
key={IsRegisterWithEmail}
|
||||
isOTPScreen={isOTPScreen}
|
||||
setIsOTPScreen={setIsOTPScreen}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Show divider when alternative auth methods are available */}
|
||||
{showOrSignInWith && (
|
||||
<div className="flex items-center gap-2">
|
||||
<hr className="w-full" />
|
||||
<p className="text-nowrap text-sm">{t("orSignUpWith")}</p>
|
||||
<hr className="w-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toggle buttons for switching between email and mobile */}
|
||||
|
||||
{showOrSignInWith && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Show "Continue with Mobile" button when email is selected and mobile is enabled */}
|
||||
{IsRegisterWithEmail && mobile_authentication == 1 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="big"
|
||||
className="flex items-center justify-center py-4 text-base h-auto"
|
||||
onClick={() => setIsRegisterWithEmail(false)}
|
||||
>
|
||||
<MdOutlineLocalPhone className="!size-6" />
|
||||
{t("continueWithMobile")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Show "Continue with Email" button when mobile is selected and email is enabled */}
|
||||
{!IsRegisterWithEmail && email_authentication === 1 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="big"
|
||||
className="flex items-center justify-center py-4 text-base h-auto"
|
||||
onClick={() => setIsRegisterWithEmail(true)}
|
||||
>
|
||||
<MdOutlineEmail className="!size-6" />
|
||||
{t("continueWithEmail")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Terms and Privacy Links */}
|
||||
{!isOTPScreen && (
|
||||
<TermsAndPrivacyLinks t={t} settings={settings} OnHide={OnHide} />
|
||||
)}
|
||||
</div>
|
||||
<div id="recaptcha-container" style={{ display: "none" }}></div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterModal;
|
||||
183
components/Auth/RegisterWithEmailForm.jsx
Normal file
183
components/Auth/RegisterWithEmailForm.jsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useState } from "react";
|
||||
import { FaRegEye, FaRegEyeSlash } from "react-icons/fa";
|
||||
import { Label } from "../ui/label";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { handleFirebaseAuthError, t } from "@/utils";
|
||||
import {
|
||||
createUserWithEmailAndPassword,
|
||||
getAuth,
|
||||
sendEmailVerification,
|
||||
} from "firebase/auth";
|
||||
import { userSignUpApi } from "@/utils/api";
|
||||
import useAutoFocus from "../Common/useAutoFocus";
|
||||
|
||||
const RegisterWithEmailForm = ({ OnHide, setIsMailSentSuccess }) => {
|
||||
const auth = getAuth();
|
||||
const emailRef = useAutoFocus();
|
||||
|
||||
// Form state management
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
username: "",
|
||||
password: "",
|
||||
IsPasswordVisible: false,
|
||||
showLoader: false,
|
||||
});
|
||||
|
||||
const { email, username, password, IsPasswordVisible, showLoader } = formData;
|
||||
|
||||
// Handle input changes
|
||||
const handleInputChange = (field, value) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// Toggle password visibility
|
||||
const togglePasswordVisibility = () => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
IsPasswordVisible: !prev.IsPasswordVisible,
|
||||
}));
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleSignup = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate email
|
||||
if (!email) {
|
||||
toast.error(t("emailRequired"));
|
||||
return;
|
||||
} else if (!/\S+@\S+\.\S+/.test(email)) {
|
||||
toast.error(t("emailInvalid"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate username
|
||||
if (username?.trim() === "") {
|
||||
toast.error(t("usernameRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if (!password) {
|
||||
toast.error(t("passwordRequired"));
|
||||
return;
|
||||
} else if (password.length < 6) {
|
||||
toast.error(t("passwordTooShort"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setFormData((prev) => ({ ...prev, showLoader: true }));
|
||||
|
||||
// Create user with email and password
|
||||
const userCredential = await createUserWithEmailAndPassword(
|
||||
auth,
|
||||
email,
|
||||
password
|
||||
);
|
||||
const user = userCredential.user;
|
||||
|
||||
// Send email verification
|
||||
await sendEmailVerification(user);
|
||||
|
||||
// Register user in backend
|
||||
try {
|
||||
const response = await userSignUpApi.userSignup({
|
||||
name: username ? username : "",
|
||||
email: email ? email : "",
|
||||
firebase_id: user?.uid,
|
||||
type: "email",
|
||||
registration: true,
|
||||
});
|
||||
|
||||
// Close modal and show success message
|
||||
OnHide();
|
||||
setIsMailSentSuccess(true);
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
toast.error(t("registrationFailed"));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(error);
|
||||
handleFirebaseAuthError(errorCode);
|
||||
} finally {
|
||||
setFormData((prev) => ({ ...prev, showLoader: false }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-6" onSubmit={handleSignup}>
|
||||
{/* Email Input */}
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("email")}</Label>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder={t("enterEmail")}
|
||||
value={email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
ref={emailRef}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Username Input */}
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("username")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("typeUsername")}
|
||||
value={username}
|
||||
onChange={(e) => handleInputChange("username", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password Input */}
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("password")}</Label>
|
||||
<div className="flex items-center relative">
|
||||
<Input
|
||||
type={IsPasswordVisible ? "text" : "password"}
|
||||
placeholder={t("enterPassword")}
|
||||
className="ltr:pr-9 rtl:pl-9"
|
||||
value={password}
|
||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute ltr:right-3 rtl:left-3 cursor-pointer"
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
{IsPasswordVisible ? (
|
||||
<FaRegEye size={20} />
|
||||
) : (
|
||||
<FaRegEyeSlash size={20} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={showLoader}
|
||||
className="text-xl text-white font-light px-4 py-2"
|
||||
size="big"
|
||||
>
|
||||
{showLoader ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
t("verifyEmail")
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWithEmailForm;
|
||||
310
components/Auth/RegisterWithMobileForm.jsx
Normal file
310
components/Auth/RegisterWithMobileForm.jsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import PhoneInput from "react-phone-input-2";
|
||||
import "react-phone-input-2/lib/style.css";
|
||||
import { Label } from "../ui/label";
|
||||
import { Button } from "../ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js/max";
|
||||
import { toast } from "sonner";
|
||||
import { handleFirebaseAuthError, t } from "@/utils";
|
||||
import {
|
||||
getAuth,
|
||||
RecaptchaVerifier,
|
||||
signInWithPhoneNumber,
|
||||
} from "firebase/auth";
|
||||
import { getOtpApi, getUserExistsApi } from "@/utils/api";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
getOtpServiceProvider,
|
||||
settingsData,
|
||||
} from "@/redux/reducer/settingSlice";
|
||||
import useAutoFocus from "../Common/useAutoFocus";
|
||||
import OtpScreen from "./OtpScreen";
|
||||
import { Input } from "../ui/input";
|
||||
import { FaRegEye, FaRegEyeSlash } from "react-icons/fa";
|
||||
|
||||
const RegisterWithMobileForm = ({
|
||||
OnHide,
|
||||
setDescriptionState,
|
||||
isOTPScreen,
|
||||
setIsOTPScreen,
|
||||
}) => {
|
||||
const auth = getAuth();
|
||||
const settings = useSelector(settingsData);
|
||||
const isDemoMode = settings?.demo_mode;
|
||||
const otp_service_provider = useSelector(getOtpServiceProvider);
|
||||
const phoneInputRef = useAutoFocus();
|
||||
|
||||
// Mobile registration states
|
||||
const [number, setNumber] = useState(isDemoMode ? "919876598765" : "");
|
||||
const [countryCode, setCountryCode] = useState("");
|
||||
const [regionCode, setRegionCode] = useState("");
|
||||
const [confirmationResult, setConfirmationResult] = useState(null);
|
||||
const [showLoader, setShowLoader] = useState(false);
|
||||
const [resendTimer, setResendTimer] = useState(0);
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
// Remove any non-digit characters from the country code
|
||||
const countryCodeDigitsOnly = countryCode.replace(/\D/g, "");
|
||||
|
||||
// Check if the entered number starts with the selected country code
|
||||
const startsWithCountryCode = number.startsWith(countryCodeDigitsOnly);
|
||||
|
||||
// If the number starts with the country code, remove it
|
||||
const formattedNumber = startsWithCountryCode
|
||||
? number.substring(countryCodeDigitsOnly.length)
|
||||
: number;
|
||||
|
||||
// Generate reCAPTCHA verifier
|
||||
const generateRecaptcha = async () => {
|
||||
// Reuse existing verifier if it's still valid
|
||||
if (window.recaptchaVerifier && !window.recaptchaVerifier.destroyed) {
|
||||
return window.recaptchaVerifier;
|
||||
}
|
||||
const recaptchaContainer = document.getElementById("recaptcha-container");
|
||||
if (!recaptchaContainer) {
|
||||
console.error("Container element 'recaptcha-container' not found.");
|
||||
return null;
|
||||
}
|
||||
// Clear container and reset reference
|
||||
recaptchaContainer.innerHTML = "";
|
||||
window.recaptchaVerifier = undefined;
|
||||
try {
|
||||
window.recaptchaVerifier = new RecaptchaVerifier(
|
||||
auth,
|
||||
recaptchaContainer,
|
||||
{ size: "invisible" }
|
||||
);
|
||||
return window.recaptchaVerifier;
|
||||
} catch (error) {
|
||||
console.error("Error initializing RecaptchaVerifier:", error.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
recaptchaClear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const recaptchaClear = async () => {
|
||||
if (window.recaptchaVerifier && !window.recaptchaVerifier.destroyed) {
|
||||
try {
|
||||
await window.recaptchaVerifier.clear();
|
||||
} catch (error) {
|
||||
// Ignore errors - verifier might already be cleared
|
||||
}
|
||||
}
|
||||
window.recaptchaVerifier = undefined;
|
||||
const recaptchaContainer = document.getElementById("recaptcha-container");
|
||||
if (recaptchaContainer) {
|
||||
recaptchaContainer.innerHTML = "";
|
||||
}
|
||||
};
|
||||
|
||||
// Send OTP with Twilio
|
||||
const sendOtpWithTwillio = async (PhoneNumber) => {
|
||||
try {
|
||||
const response = await getOtpApi.getOtp({
|
||||
number: formattedNumber,
|
||||
country_code: countryCode,
|
||||
});
|
||||
if (response?.data?.error === false) {
|
||||
toast.success(t("otpSentSuccess"));
|
||||
setIsOTPScreen(true);
|
||||
setResendTimer(60);
|
||||
setDescriptionState({
|
||||
type: "otp",
|
||||
phoneNumber: PhoneNumber,
|
||||
});
|
||||
} else {
|
||||
toast.error(t("failedToSendOtp"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Send OTP with Firebase
|
||||
const sendOtpWithFirebase = async (PhoneNumber) => {
|
||||
try {
|
||||
const appVerifier = await generateRecaptcha();
|
||||
const confirmation = await signInWithPhoneNumber(
|
||||
auth,
|
||||
PhoneNumber,
|
||||
appVerifier
|
||||
);
|
||||
setConfirmationResult(confirmation);
|
||||
toast.success(t("otpSentSuccess"));
|
||||
setIsOTPScreen(true);
|
||||
setResendTimer(60);
|
||||
setDescriptionState({
|
||||
type: "otp",
|
||||
phoneNumber: PhoneNumber,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const errorCode = error.code;
|
||||
handleFirebaseAuthError(errorCode);
|
||||
} finally {
|
||||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle phone input change
|
||||
const handleInputChange = (value, data) => {
|
||||
setNumber(value);
|
||||
setCountryCode("+" + (data?.dialCode || ""));
|
||||
setRegionCode(data?.countryCode.toLowerCase() || "");
|
||||
};
|
||||
|
||||
// Handle country change
|
||||
const handleCountryChange = (code) => {
|
||||
setCountryCode(code);
|
||||
};
|
||||
|
||||
const checkIfUserExistsOrNot = async () => {
|
||||
try {
|
||||
const res = await getUserExistsApi.getUserExists({
|
||||
mobile: formattedNumber,
|
||||
country_code: countryCode,
|
||||
});
|
||||
if (res?.data?.error === false) {
|
||||
toast.error(res?.data?.message);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleMobileSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate phone number
|
||||
const PhoneNumber = `${countryCode}${formattedNumber}`;
|
||||
|
||||
if (!isValidPhoneNumber(PhoneNumber)) {
|
||||
toast.error(t("invalidPhoneNumber"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if (!password) {
|
||||
toast.error(t("passwordRequired"));
|
||||
return;
|
||||
}
|
||||
if (password.length < 6) {
|
||||
toast.error(t("passwordTooShort"));
|
||||
return;
|
||||
}
|
||||
// Send OTP
|
||||
setShowLoader(true);
|
||||
const isUserExists = await checkIfUserExistsOrNot();
|
||||
if (isUserExists) {
|
||||
setShowLoader(false);
|
||||
return;
|
||||
}
|
||||
if (otp_service_provider === "twilio") {
|
||||
await sendOtpWithTwillio(PhoneNumber);
|
||||
} else {
|
||||
await sendOtpWithFirebase(PhoneNumber);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle password visibility
|
||||
const togglePasswordVisibility = () => {
|
||||
setIsPasswordVisible((prev) => !prev);
|
||||
};
|
||||
|
||||
// Show OTP screen if OTP was sent
|
||||
if (isOTPScreen) {
|
||||
return (
|
||||
<OtpScreen
|
||||
OnHide={OnHide}
|
||||
generateRecaptcha={generateRecaptcha}
|
||||
countryCode={countryCode}
|
||||
formattedNumber={formattedNumber}
|
||||
confirmationResult={confirmationResult}
|
||||
setConfirmationResult={setConfirmationResult}
|
||||
setResendTimer={setResendTimer}
|
||||
resendTimer={resendTimer}
|
||||
regionCode={regionCode}
|
||||
password={password}
|
||||
isDemoMode={isDemoMode}
|
||||
isRegister={true}
|
||||
key="register-otp"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Show mobile registration form
|
||||
return (
|
||||
<form className="flex flex-col gap-6" onSubmit={handleMobileSubmit}>
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("phoneNumber")}</Label>
|
||||
<PhoneInput
|
||||
country={process.env.NEXT_PUBLIC_DEFAULT_COUNTRY}
|
||||
value={number}
|
||||
onChange={(phone, data) => handleInputChange(phone, data)}
|
||||
onCountryChange={handleCountryChange}
|
||||
inputProps={{
|
||||
name: "phone",
|
||||
required: true,
|
||||
ref: phoneInputRef,
|
||||
}}
|
||||
enableLongNumbers
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password Input */}
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("password")}</Label>
|
||||
<div className="flex items-center relative">
|
||||
<Input
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
placeholder={t("enterPassword")}
|
||||
className="ltr:pr-9 rtl:pl-9"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute ltr:right-3 rtl:left-3 cursor-pointer"
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
{isPasswordVisible ? (
|
||||
<FaRegEye size={20} />
|
||||
) : (
|
||||
<FaRegEyeSlash size={20} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={showLoader}
|
||||
className="text-xl text-white font-light px-4 py-2"
|
||||
size="big"
|
||||
>
|
||||
{showLoader ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
t("continue")
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterWithMobileForm;
|
||||
127
components/Auth/ResetPasswordScreen.jsx
Normal file
127
components/Auth/ResetPasswordScreen.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { FaRegEye, FaRegEyeSlash } from "react-icons/fa";
|
||||
import { toast } from "sonner";
|
||||
import { t } from "@/utils";
|
||||
import { resetPasswordApi, userSignUpApi } from "@/utils/api";
|
||||
import { useState } from "react";
|
||||
|
||||
const ResetPasswordScreen = ({
|
||||
formattedNumber,
|
||||
countryCode,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
FirebaseId,
|
||||
}) => {
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const [resetPasswordLoader, setResetPasswordLoader] = useState(false);
|
||||
|
||||
const handleResetPassword = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!newPassword) {
|
||||
toast.error(t("passwordRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
toast.error(t("passwordTooShort"));
|
||||
return;
|
||||
}
|
||||
|
||||
setResetPasswordLoader(true);
|
||||
try {
|
||||
// Step 1: Get token by calling userSignUpApi
|
||||
const loginResponse = await userSignUpApi.userSignup({
|
||||
mobile: formattedNumber,
|
||||
country_code: countryCode,
|
||||
type: "phone",
|
||||
firebase_id: FirebaseId,
|
||||
});
|
||||
|
||||
// Extract token from response
|
||||
const token = loginResponse?.data?.token;
|
||||
|
||||
if (!token) {
|
||||
toast.error(t("errorOccurred"));
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await resetPasswordApi.resetPassword({
|
||||
number: formattedNumber,
|
||||
country_code: countryCode,
|
||||
new_password: newPassword,
|
||||
token: token,
|
||||
});
|
||||
|
||||
if (response?.data?.error === false) {
|
||||
toast.success(response?.data?.message);
|
||||
onSuccess(); // Go back to login screen
|
||||
} else {
|
||||
toast.error(response?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(t("errorOccurred"));
|
||||
} finally {
|
||||
setResetPasswordLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-6 mt-3.5" onSubmit={handleResetPassword}>
|
||||
<div className="labelInputCont">
|
||||
<Label className="requiredInputLabel">{t("newPassword")}</Label>
|
||||
<div className="flex items-center relative">
|
||||
<Input
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
placeholder={t("enterNewPassword")}
|
||||
className="ltr:pr-9 rtl:pl-9"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute ltr:right-3 rtl:left-3 cursor-pointer"
|
||||
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||
>
|
||||
{isPasswordVisible ? (
|
||||
<FaRegEye size={20} />
|
||||
) : (
|
||||
<FaRegEyeSlash size={20} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={resetPasswordLoader}
|
||||
className="text-xl text-white font-light px-4 py-2"
|
||||
size="big"
|
||||
>
|
||||
{resetPasswordLoader ? (
|
||||
<Loader2 className="size-6 animate-spin" />
|
||||
) : (
|
||||
t("submitResetPassword")
|
||||
)}
|
||||
</Button>
|
||||
{onCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="text-lg text-black font-light px-4 py-2"
|
||||
size="big"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasswordScreen;
|
||||
26
components/Auth/TermsAndPrivacyLinks.jsx
Normal file
26
components/Auth/TermsAndPrivacyLinks.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import CustomLink from "@/components/Common/CustomLink";
|
||||
|
||||
const TermsAndPrivacyLinks = ({ t, settings, OnHide }) => {
|
||||
return (
|
||||
<div className="text-center">
|
||||
{t("agreeSignIn")} {settings?.company_name} <br />
|
||||
<CustomLink
|
||||
href="/terms-and-condition"
|
||||
className="text-primary underline"
|
||||
onClick={OnHide}
|
||||
>
|
||||
{t("termsService")}
|
||||
</CustomLink>{" "}
|
||||
{t("and")}{" "}
|
||||
<CustomLink
|
||||
href="/privacy-policy"
|
||||
className="text-primary underline"
|
||||
onClick={OnHide}
|
||||
>
|
||||
{t("privacyPolicy")}
|
||||
</CustomLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsAndPrivacyLinks;
|
||||
39
components/Auth/UnauthorizedModal.jsx
Normal file
39
components/Auth/UnauthorizedModal.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { getIsUnauthorized, setIsUnauthorized } from "@/redux/reducer/globalStateSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
const UnauthorizedModal = () => {
|
||||
const dispatch = useDispatch();
|
||||
const open = useSelector(getIsUnauthorized);
|
||||
|
||||
const handleOk = () => {
|
||||
dispatch(setIsUnauthorized(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={open}>
|
||||
<AlertDialogContent onInteractOutside={(e) => e.preventDefault()}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unauthorized</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You do not have permission to access this resource. Please log in or
|
||||
contact the administrator.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={handleOk}>OK</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnauthorizedModal;
|
||||
Reference in New Issue
Block a user