import React, { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { FirebaseApp, FirebaseError } from "@firebase/app"; import { ErrorView, FireCMSLogo, useModeController, useSnackbarController, } from "@firecms/core"; import { ArrowBackIcon, Button, CallIcon, CircularProgress, cls, IconButton, LoadingButton, MailIcon, PersonIcon, TextField, Typography, } from "@firecms/ui"; import { appleIcon, facebookIcon, githubIcon, googleIcon, microsoftIcon, twitterIcon } from "./social_icons"; import { getAuth, getMultiFactorResolver, PhoneAuthProvider, PhoneMultiFactorGenerator, RecaptchaVerifier } from "@firebase/auth"; import { FirebaseAuthController, FirebaseSignInOption, FirebaseSignInProvider, RECAPTCHA_CONTAINER_ID, useRecaptcha } from "../index"; /** * @category Firebase */ export interface FirebaseLoginViewProps { /** * Firebase app this login view is accessing */ firebaseApp: FirebaseApp; /** * Delegate holding the auth state */ authController: FirebaseAuthController; /** * Path to the logo displayed in the login screen */ logo?: string; /** * Enable the skip login button */ allowSkipLogin?: boolean; /** * Each of the sign in options that get a custom button */ signInOptions: Array; /** * Disable the login buttons */ disabled?: boolean; /** * Prevent users from creating new users in when the `signInOptions` value * is `password`. This does not apply to the rest of login providers. */ disableSignupScreen?: boolean; /** * Prevent users from resetting their password when the `signInOptions` value * is `password`. This does not apply to the rest of login providers. */ disableResetPassword?: boolean; /** * Display this component when no user is found a user tries to log in * when the `signInOptions` value is `password`. */ noUserComponent?: ReactNode; /** * Include additional components in the login view, on top of the login buttons. */ children?: ReactNode; /** * Display this component bellow the sign-in buttons. * Useful for adding checkboxes for privacy and terms and conditions. * You may want to use it in conjunction with the `disabled` prop. */ additionalComponent?: ReactNode; notAllowedError?: any; className?: string; } /** * Use this component to render a login view, that updates * the state of the {@link FirebaseAuthController} based on the result * @category Firebase */ export function FirebaseLoginView({ children, allowSkipLogin, logo, signInOptions, firebaseApp, authController, noUserComponent, disableSignupScreen = false, disableResetPassword = false, disabled = false, additionalComponent, notAllowedError, className }: FirebaseLoginViewProps) { const modeState = useModeController(); const [passwordLoginSelected, setPasswordLoginSelected] = useState(false); const [phoneLoginSelected, setPhoneLoginSelected] = useState(false); const [fadeIn, setFadeIn] = useState(false); useEffect(() => { // Trigger the fade-in effect on component mount const timer = setTimeout(() => { setFadeIn(true); }, 50); // Small delay to ensure transition works properly return () => clearTimeout(timer); }, []); const resolvedSignInOptions: FirebaseSignInProvider[] = signInOptions.map((o) => { if (typeof o === "object") { return o.provider; } else return o as FirebaseSignInProvider; }) const sendMFASms = useCallback(() => { const auth = getAuth(firebaseApp); const recaptchaVerifier = new RecaptchaVerifier(auth, "recaptcha", { size: "invisible" }); const resolver = getMultiFactorResolver(auth, authController.authProviderError); if (resolver.hints[0].factorId === PhoneMultiFactorGenerator.FACTOR_ID) { const phoneInfoOptions = { multiFactorHint: resolver.hints[0], session: resolver.session }; const phoneAuthProvider = new PhoneAuthProvider(auth); // Send SMS verification code phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier) .then(function (verificationId) { // Ask user for the SMS verification code. Then: const verificationCode = String(window.prompt("Please enter the verification " + "code that was sent to your mobile device.")); const cred = PhoneAuthProvider.credential(verificationId, verificationCode); const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred); // // Complete sign-in. return resolver.resolveSignIn(multiFactorAssertion); }) } else { // Unsupported second factor. console.warn("Unsupported second factor."); } }, [authController.authProviderError]); function buildErrorView() { let errorView: any; if (authController.user != null) return errorView; // if the user is logged in via MFA const ignoredCodes = ["auth/popup-closed-by-user", "auth/cancelled-popup-request"]; if (authController.authProviderError) { if (authController.authProviderError.code === "auth/operation-not-allowed" || authController.authProviderError.code === "auth/configuration-not-found") { errorView = <>
{firebaseApp &&
} ; } else if (authController.authProviderError.code === "auth/invalid-api-key") { errorView =
; } else if (authController.authProviderError.code === "auth/email-already-in-use") { errorView =
; } else if (authController.authProviderError.code === "auth/invalid-credential") { errorView =
; } else if (!ignoredCodes.includes(authController.authProviderError.code)) { if (authController.authProviderError.code === "auth/multi-factor-auth-required") { sendMFASms(); } errorView =
; } } return errorView; } let logoComponent; if (logo) { logoComponent = {"Logo"}/; } else { logoComponent = ; } let notAllowedMessage: string | undefined; if (notAllowedError) { if (typeof notAllowedError === "string") { notAllowedMessage = notAllowedError; } else if (notAllowedError instanceof Error) { notAllowedMessage = notAllowedError.message; } else { notAllowedMessage = "It looks like you don't have access to the CMS, based on the specified Authenticator configuration"; } } const fadeStyle = { opacity: fadeIn ? 1 : 0, transition: "opacity 0.6s ease-in-out" }; return (
{logoComponent}
{children} {notAllowedMessage &&
} {buildErrorView()} {(!passwordLoginSelected && !phoneLoginSelected) &&
{buildOauthLoginButtons(authController, resolvedSignInOptions, modeState.mode, disabled)} {resolvedSignInOptions.includes("password") && } onClick={() => setPasswordLoginSelected(true)}/>} {resolvedSignInOptions.includes("phone") && } onClick={() => setPhoneLoginSelected(true)}/>} {resolvedSignInOptions.includes("anonymous") && } onClick={authController.anonymousLogin}/>} {allowSkipLogin && }
} {passwordLoginSelected && setPasswordLoginSelected(false)} mode={modeState.mode} noUserComponent={noUserComponent} disableSignupScreen={disableSignupScreen} disableResetPassword={disableResetPassword} />} {phoneLoginSelected && setPhoneLoginSelected(false)} />} {!passwordLoginSelected && !phoneLoginSelected && additionalComponent}
); } export function LoginButton({ icon, onClick, text, disabled }: { icon: React.ReactNode, onClick: () => void, text: string, disabled?: boolean }) { return (
) } function PhoneLoginForm({ onClose, authController }: { onClose: () => void, authController: FirebaseAuthController, }) { useRecaptcha(); const [phone, setPhone] = useState(); const [code, setCode] = useState(); const [isInvalidCode, setIsInvalidCode] = useState(false); const handleSubmit = async (event: any) => { event.preventDefault(); if (code && authController.confirmationResult) { setIsInvalidCode(false); authController.confirmationResult.confirm(code).catch((e: FirebaseError) => { if (e.code === "auth/invalid-verification-code") { setIsInvalidCode(true) } }); } else { if (phone) { authController.phoneLogin(phone, window.recaptchaVerifier); } } } return (
{isInvalidCode &&
}
{"Please enter your phone number"}
setPhone(event.target.value)}/> {Boolean(phone && authController.confirmationResult) && <>
{"Please enter the confirmation code"}
setCode(event.target.value)}/> }
{authController.authLoading && }
); } type LoginFormMode = "email" | "password" | "registration"; function LoginForm({ onClose, authController, mode, noUserComponent, disableSignupScreen, disableResetPassword }: { onClose: () => void, authController: FirebaseAuthController, mode: "light" | "dark", noUserComponent?: ReactNode, disableSignupScreen: boolean, disableResetPassword?: boolean }) { const passwordRef = useRef(null); const [loginState, setLoginState] = useState("email"); // ["email", "password", "registration"] const [email, setEmail] = useState(); const [password, setPassword] = useState(); const [previouslyUsedMethodsForUser, setPreviouslyUsedMethodsForUser] = useState(); const [resettingPassword, setResettingPassword] = useState(false); const snackbarController = useSnackbarController(); useEffect(() => { if ((loginState === "password" || loginState === "registration") && passwordRef.current) { passwordRef.current.focus() } }, [loginState]); useEffect(() => { if (!document) return; const escFunction = (event: any) => { if (event.keyCode === 27) { onClose(); } }; document.addEventListener("keydown", escFunction, false); return () => { document.removeEventListener("keydown", escFunction, false); }; }, [onClose]); function handleEnterEmail() { if (email) { authController.fetchSignInMethodsForEmail(email).then((availableProviders) => { setPreviouslyUsedMethodsForUser(availableProviders.filter(p => p !== "password")); }); setLoginState("password"); } } function handleEnterPassword() { if (email && password) { authController.emailPasswordLogin(email, password); } } function handleRegistration() { if (email && password) { authController.createUserWithEmailAndPassword(email, password); } } const onBackPressed = () => { if (loginState === "email") { onClose(); } else if (loginState === "password" || loginState === "registration") { setLoginState("email"); } else { setPreviouslyUsedMethodsForUser(undefined); } } const handleSubmit = (event: any) => { event.preventDefault(); if (loginState === "email") { handleEnterEmail(); } else if (loginState === "password") { handleEnterPassword(); } else if (loginState === "registration") { handleRegistration(); } } const label = loginState === "registration" ? "Please enter your email and password to create an account" : (loginState === "password" ? "Please enter your password" : "Please enter your email"); return (
{loginState === "registration" && noUserComponent}
{label} {(loginState === "email" || loginState === "registration") && setEmail(event.target.value)}/>}
setPassword(event.target.value)}/>
{authController.authLoading && } {!disableResetPassword && { setResettingPassword(true); try { try { await authController.sendPasswordResetEmail(email); snackbarController.open({ message: "Password reset email sent", type: "success" }); } catch (e: any) { snackbarController.open({ message: e.message, type: "error" }); } } finally { setResettingPassword(false); } } : undefined}> Reset password } {!disableSignupScreen && loginState === "email" && }
{previouslyUsedMethodsForUser && previouslyUsedMethodsForUser.length > 0 &&
You already have an account You can use one of these methods to login with {email}
{previouslyUsedMethodsForUser && buildOauthLoginButtons(authController, previouslyUsedMethodsForUser, mode, false)}
}
); } function buildOauthLoginButtons(authController: FirebaseAuthController, providers: string[], mode: "light" | "dark", disabled: boolean) { return <> {providers.includes("google.com") && } {providers.includes("microsoft.com") && } {providers.includes("apple.com") && } {providers.includes("github.com") && } {providers.includes("facebook.com") && } {providers.includes("twitter.com") && } }