How do I listen to email verification event with firebase authentication and react native?

Viewed 5172

EDIT - this question is still unanswered. There was an idea to listen to onIdTokenChanged, however the token is refreshed once every hour, which is not practical solution for me. I posted follow up question here if people can give me a hand that would be grant, because I am sitting on this problem since one week.

I am writing a simple react native app, and I want to show my main page only after user has verified their email. As far as I understand, there is no listener which I can use to listen to event where the user has been verified their email. I am using firebase.auth().onAuthStateChanged((user) => {....}) but the listener onAuthStateChanged has been called after user is logged in or registered in, not after a user has verified their email. Few places suggested to use firebase.auth().user.reload() so that it will reload the current user and it will pick up the verification status from the database. However, I dont think it is a solution because I dont know when should I reload the current user, i.e. I dont know when the verification link has been clicked. So possible solution to this problem would be:

  1. Send a confirmation 6 digit code to the user, and wait for the user to type it in the app; after the user types it, if the code is the same, I refresh the user. However I dont know how to send custom verification emails with firebase. I checked the documentation, but it is not helpful for me. If someone can point me to example written in react native, or write a small working example with custom email which I can send to the user (again in react native) that would be grant! EDIT - this doesn't seem like possible solution, since Firebase doesn't let you customize the emails
  2. Is it possible solution for me to override onAuthStateChanged listener? S.t. it will listen for changes if the user's email has been verified or not? If that's a good idea can someone point me to the current onAuthStateChanged implementation in react-native, so I can see it as an "inspiration" when overriding? Or if someone has done something similar before, can you show me an example?
  3. I've read several suggestions to use a deep link and to intersept the event when the link has been clicked, but I am not sure how to do this, or even if this is a proper solution to the problem.
  4. Some people suggested to use firebase.auth().user.reload() when the app has been closed and reopened again. This comes from the assumption that when a user has been sent the link, in order for them to click on the link, they need to close the app, and reopen it again. I think this is pretty strong assumption, considering the fact, that they might verify their email via laptop and never close the app, so I dont think this is a good solution either.

Apparently this seems like a well known problem, yet there are not many good solutions. I think best possible solution would be to send 6 digit verification code to the user and after that code has been confirmed, I would reload the current user, pick up the emailVerified field, it will be set to true and then I will show the main screen. However, can someone help me with how do I send custom email in react native and firebase?

If anyone has any other suggestions, please let me know!

9 Answers

You can simply do this by passing a continue url in the actionCodeSettings as below:

const res = await firebase.auth().createUserWithEmailAndPassword(
  email,
  password
);  
await res.user.sendEmailVerification({
  url: "https://yoursite.com/continue-url"
});

Is it possible solution for me to override onAuthStateChanged listener? S.t. it will listen for changes if the user's email has been verified or not?

The onAuthStateChanged is called when the user's authentication state changes, so when they go from not being signed in to being signed in or vice versa. The email verification flag being set is not a change in authentication state, so the callback is not called in that case.

You can listen for onIdTokenChanged instead, which fires every time the ID token changes. Since the ID token includes the flag whether the user's email is verified, a callback on onIdTokenChanged will also be called when that changes.

I used @1man solution, just i make sure to delete the interval and unsubscribe from the onAuthStateChanged event:

const onAuthStateChangedUnsubscribe = 
          firebase.auth().onAuthStateChanged(async (user) => {
            if (user) {
              // -> Alert Email Verification
              await user.sendEmailVerification()
              const onIdTokenChangedUnsubscribe = firebase.auth().onIdTokenChanged((user) => {
                const unsubscribeSetInterval = setTimeout(() => {
                  firebase.auth().currentUser.reload();
                  firebase.auth().currentUser.getIdToken(/* forceRefresh */ true)
                }, 10000);
  
                if (user && user.emailVerified) {
                  clearInterval(unsubscribeSetInterval) //delete interval
                  onAuthStateChangedUnsubscribe() //unsubscribe onAuthStateChanged
                  // -> Go to your screnn
                  return onIdTokenChangedUnsubscribe() //unsubscribe onIdTokenChanged
                }
              })
            }
          })

#3 is a common workflow - Firebase sends the link which, when clicked, opens your app. Your app reads the deep link and handles the payload (email verified). I don't know what language you're using, but you mentioned that you don't know how to do this and it's probably something you'll want to explore.

Your concern in #4 (someone opening the link on a laptop) is only an issue if you allow it to be one. I don't know what language you're using, but when you call the verify email function, you have to pass a url to Firebase which it will use in the email it sends. So your users will be taken wherever you send them. If you send them to a web app or something because you want them to open it on a laptop, then I think your best bet in app would be to have your website (or wherever you're sending them) also write something to a Firestore or RTDB document and have your app listening to that doc for updates.

If the link you pass to Firebase is a deep link to your app, it won't work on their laptop. And in this case, you go back to #3 - read the deep link in your app and handle it early. Also, it's incumbent on you to explain to users how this works, so I'd have my send link confirmation screen explain that they should click the link on the current device.

An alternative would be to have your send link function in-app start a background timer that polls the auth record every few seconds/minutes (whatever your use case), and cancel it when the record is updated or the link expires. I don't love this because email links are valid for 3 days - that's an awful long time to be polling every few seconds in app.

So, on my project I made a combination of sendEmailVerification() and reload().

Try it:

await firebase
        .auth()
        .currentUser.sendEmailVerification(actionCodeSettings)
        .then(() => {
          //useState used on my loading (user can cancel this loading and exit               this task
          setTextContent('Waiting for verification. Check your email!\nYou can close this verification and came back later');
          const unsubscribeOnUserChanged = firebase
            .auth()
            .onUserChanged(response => {
               const unsubscribeSetInterval = setInterval(() => {//this works as a next in for-like
                  firebase.auth().currentUser.reload();
               }, 30000);
              if (response.emailVerified) {
                 clearInterval(unsubscribeSetInterval); //stop setInterval
                setLoading(false); //close loading describes above
                navigation.goBack(); //return to parent (in my case to profile)
                return unsubscribeOnUserChanged(); //unsubscribe onUserChanged
              } 
            });
        })
        .catch(error => {
          setLoading(false); 
          setError(true);
          errorHandle(error);
        });

I wanted to do the same thing on the web. I tried the previous three answers and searched a lot but was not able to find the answer. I ended up combining @Frank van Puffelen and @Hermanyo H's solutions into one and it worked for me:

const onAuthStateChangedUnsubscribe = firebase.auth().onAuthStateChanged(async (user) => {
  if (user) {
    setEmailVerified("Sent");
    await user.sendEmailVerification();
    const onIdTokenChangedUnsubscribe = firebase.auth().onIdTokenChanged((user) => {
      if (user && user.emailVerified) {
        setEmailVerified("Verified");
        return onIdTokenChangedUnsubscribe(); //unsubscribe
      }
      setTimeout(() => {
        firebase.auth().currentUser.reload();
        firebase.auth().currentUser.getIdToken(/* forceRefresh */ true);
      }, 10000);
    });
  }
});

I wrote my own events instead of using onAuthStateChange.

//Write this where you wrote onAuthStateChange event
        import auth from '@react-native-firebase/auth';
        import {DeviceEventEmitter} from 'react-native';
        useEffect(()=>{
        let loginListener = DeviceEventEmitter.addListener('@verified_login', params=>{
            setUserDetails(auth()._user);
            });
        return loginListener;
        }, []);

Then you can emit this event when you want to allow the user to log in. There's a lot of room for customization here.

        await auth().signInWithEmailAndPassword(email, password);
        if(auth()._user.emailVerified)
            DeviceEventEmitter.emit('@verified_login');
        else{
                auth()._user.sendEmailVerification()
                .then(()=>{
                    console.log('A verification link has been sent to your email. Please verify to proceed.');
                    let emailVerificationEventListener = setInterval(async ()=>{
                        auth().currentUser.reload();
                        if (auth().currentUser.emailVerified) {
                            clearInterval(emailVerificationEventListener);
                            DeviceEventEmitter.emit('@verified_login');
                        }
                    }, 1000); 
                })
                .catch(error=>{
                    console.log(error);
            });
        }

This issue can be fixed smoothly using firebase dynamic links

when a user requests to authenticate their emails we send a dynamic link with the request:

auth().currentUser.sendEmailVerification({
   url: "https://oursite.com/verified-email",
});

when the user clicks on the link in the email he will be redirected to the dynamic link we included above

then we listen to the link and handle it on the client:

dynamicLinks().onLink((link) => {

if (link.url.includes("verified-email")) {
auth().currentUser.reload();
}};

Did you consider the documentation on the Firebase documentation pages? https://firebase.google.com/docs/auth/web/email-link-auth

Sample code on that page:

import { getAuth, isSignInWithEmailLink, signInWithEmailLink } from "firebase/auth";

// Confirm the link is a sign-in with email link.
const auth = getAuth();
if (isSignInWithEmailLink(auth, window.location.href)) {
  // Additional state parameters can also be passed via URL.
  // This can be used to continue the user's intended action before triggering
  // the sign-in operation.
  // Get the email if available. This should be available if the user completes
  // the flow on the same device where they started it.
  let email = window.localStorage.getItem('emailForSignIn');
  if (!email) {
    // User opened the link on a different device. To prevent session fixation
    // attacks, ask the user to provide the associated email again. For example:
    email = window.prompt('Please provide your email for confirmation');
  }
  // The client SDK will parse the code from the link for you.
  signInWithEmailLink(auth, email, window.location.href)
    .then((result) => {
      // Clear email from storage.
      window.localStorage.removeItem('emailForSignIn');
      // You can access the new user via result.user
      // Additional user info profile not available via:
      // result.additionalUserInfo.profile == null
      // You can check if the user is new or existing:
      // result.additionalUserInfo.isNewUser
    })
    .catch((error) => {
      // Some error occurred, you can inspect the code: error.code
      // Common errors could be invalid email and invalid or expired OTPs.
    });
}
Related