2

I am trying to setup the Passwordless Firebase Authentication in my React (hook-based) project.

The desired workflow looks like this:

  1. The user goes to the LoginPage (going to any PrivateRoute should not be allowed and should redirect the user to the LoginPage).
  2. The user enters their email and presses Send. There email is saved in local storage.
  3. The user receives an email with a link. They press the link and are sent back to LoginPage.
  4. LoginPage reads the saved email from LocalStorage and logs the user in.
  5. The user can now go to the any PrivateRoute.

By following this guid on Firebase Email-Link Authentication and this guide on PrivateRoute In React I have managed to get steps 1-4 complete. The problem I have now is that the even after being authenticated by Firebase and setting isAuth = true, when I visit localhost:8080/dashboard (a PrivateRoute) the value of isAuth appears to still be false and I am redirected back to the LoginPage.

What am I missing? Thanks!

LoginPage.js

const LoginPage = () => {
    const [email, setEmail] = useState(window.localStorage.getItem("emailForSignIn") || "");
    const [message, setMessage] = useState("");
    const { onLogin } = useAuthDataContext();

    useEffect(() => {
        const savedEmail = window.localStorage.getItem("emailForSignIn");
        if (firebase.auth().isSignInWithEmailLink(window.location.href) && !!savedEmail) {
            firebase
                .auth()
                .signInWithEmailLink(savedEmail, window.location.href)
                .then(() => {
                    window.localStorage.removeItem("emailForSignIn");
                })
                .then(onLogin)
                .catch((error) => {
                    console.log(error);
                    setMessage(error);
                });
        }
    }, []);

    const onSubmit = () => {
        if (firebase.auth().isSignInWithEmailLink(window.location.href) && !!email) {
            firebase
                .auth()
                .signInWithEmailLink(email, window.location.href)
                .then(() => {
                    window.localStorage.removeItem("emailForSignIn");
                })
                .then(onLogin)
                .catch((error) => {
                    console.log(error);
                });
        } else {
            const isValidEmail = validateEmail(email);
            if (isValidEmail) {
                firebase
                    .auth()
                    .sendSignInLinkToEmail(email, {
                        url: "http://localhost:8080",
                        handleCodeInApp: true
                    })
                    .then(() => {
                        window.localStorage.setItem("emailForSignIn", email);
                        setMessage("Email sent");
                    })
                    .catch((error) => {
                        setMessage(error);
                        console.log(error);
                    });
            } else {
                setMessage("Email is not valid");
            }
        }
    };

    const onEmailChange = (e) => {
        setEmail(e.target.value);
    };

    return (
        <div>
            <p>Please enter your email and press send</p>
            <input type="text" value={email} onChange={onEmailChange} />
            <button type="button" onClick={() => onSubmit()}>
                Send
            </button>
            <p>{message}</p>
        </div>
    );
};

useAuthContext.js

export const AuthDataContext = createContext(null);

const AuthProvider = (props) => {
    const [isAuth, setIsAuth] = useState(false);

    useEffect(() => {
        firebase.auth().onAuthStateChanged((user) => {
            if (user) {
                setIsAuth(true);
            }
        });
    }, []);

    const onLogin = () => setIsAuth(true);

    const onLogout = () => setIsAuth(false);

    return <AuthDataContext.Provider value={{ isAuth, onLogin, onLogout }} {...props} />;
};

export default AuthProvider;
export const useAuthDataContext = () => useContext(AuthDataContext);

PrivateRoute.js

const PrivateRoute = ({ component: Component, ...rest }) => {
    const { isAuth } = useAuthDataContext();

    // isAuth is False even after onLogin is called
    console.log("isAuth", isAuth);
    return <Route {...rest} component={(props) => (isAuth ? <Component {...props} /> : <Redirect to="/" />)} />;
};

AppRouter.js

const AppRouter = () => (
    <BrowserRouter>
        <AuthProvider>
            <Switch>
                <Route path="/" component={LoginPage} exact />
                <PrivateRoute path="/dashboard" component={DashboardPage} />
            </Switch>
        </AuthProvider>
    </BrowserRouter>
);
skyboyer
  • 22,209
  • 7
  • 57
  • 64
Espresso
  • 740
  • 13
  • 32
  • The problem here as I see based on your description. Your auth is alway `false` at the beginning so it redirects to login page before it switch to `true` in `useEffect`. – Yunhai May 09 '21 at 18:58
  • @Yunhai, you can use `useLayoutEffect` instead of `useEffect` to avoid flickering. – Henrikh Kantuni Jan 03 '22 at 20:36
  • 1
    @HenrikhKantuni Don't expect someone would respond my old comment since I'm not the OP. `useLayoutEffect` certainly works, there is actually 3 states: true/false and loading the auth info. A more standard approach is to have a loading state before the result, which I believe Firebase package has it. OP missed that part hence he might need to take a look at the official example (from Github or Doc). – Yunhai Jan 05 '22 at 00:08

0 Answers0