Integrating Magic Links with Firebase and React Native

Introduction

In a mobile project I am a part of, we recently transitioned from traditional email and password authentication to a magic link system using Firebase.

This article explores how I implemented email magic links in the project using Firebase with React Native.

Getting started

To begin, we want to set up the Firebase console to enable email link sign-in:

  • Open the Firebase console
  • Navigate to the Authentication section.

    • Under the Sign-in method tab, enable the Email/Password provider. Note that email/password sign-in must be enabled to use email link sign-in.
    • In the same section, enable the Email link (passwordless sign-in) method.

screenshots a little bit of trivia, this is the firebase project I used in the app that got me into Toptal. that's what I now use for all my firebase testing

  • Under the Settings tab, and under the Authorized domains section:
  • Add the domains for the url in the magic link setup, or use the default domains, project_name.web.app.

screenshots

Configuring Firebase Dynamic Links

I assume you'd already have your React Native project up and running already.

We want to use the values from our app, and add to the Firebase console. screenshots

  • For iOS, add your app with the following details on the Firebase console:

    • Bundle ID
    • Apple Developer Team ID screenshots
  • For Android - you just need to have an Android app configured with a package name
  • Next, we want to enable Firebase dynamic links, enter the domain you want to use. In this case, I'm using: edwardsmosesapp.page.link screenshots

Setting up our React Native project

We want to setup our Xcode project configuration for the firebase universal links.

  • Open the Xcode project, and in the Capabilities tab, enable Associated Domains screenshots

    • Add the following to the associated domains list: your dynamic links domain, mine is edwardsmosesapp.page.link

    screenshots

Installing Packages

You can find more information on this in the library's documentation (https://rnfirebase.io).

First, we want to install:

Copy
yarn add @react-native-firebase/app

Then install the authentication package (https://rnfirebase.io/auth/usage):

Copy
yarn add @react-native-firebase/auth

Then finally install the dynamic links package (https://rnfirebase.io/dynamic-links/usage)

Copy
yarn add @react-native-firebase/dynamic-links

Also install the below for async storage:

Copy
yarn add @react-native-async-storage/async-storage

After which, we'd run to install :

Copy
npx pod install

When running the above command, if you run into the below error: you can follow the steps in this section: https://rnfirebase.io/#altering-cocoapods-to-use-frameworks screenshots guess who ran into the above error

If you haven't yet configured your Firebase project, follow the below steps in the documentation: https://rnfirebase.io/#generating-ios-credentials

Sending the Magic link

To send a magic link to the user’s email, we want to use the sendSignInLinkToEmail method.

Copy
import React, { useState, useEffect } from "react";
import {
  StyleSheet,
  Text,
  View,
  Alert,
  TextInput,
  Button,
  ActivityIndicator,
} from "react-native";

import AsyncStorage from "@react-native-async-storage/async-storage";
import auth from "@react-native-firebase/auth";
import dynamicLinks from "@react-native-firebase/dynamic-links";

const MagicLinkLogin = () => {
  const [email, setEmail] = useState("");

  const BUNDLE_ID =
    "org.reactjs.native.example.reactnative-firebase-magiclink-app";

  const sendSignInLink = async (email: string) => {
    try {
      await AsyncStorage.setItem("emailForSignIn", email);
      await auth().sendSignInLinkToEmail(email, {
        handleCodeInApp: true,
        url: "https://bike-rentals-5f360.web.app",
        iOS: {
          bundleId: BUNDLE_ID,
        },
        android: {
          packageName: BUNDLE_ID,
        },
      });

      Alert.alert(`Login link sent to ${email}`);
    } catch (error) {
      console.error(error);
      Alert.alert("Error sending login link");
    }
  };

  return (
    <View style={styles.authContainer}>
      <Text style={styles.sectionTitle}>Magic Link Sign In</Text>
      <TextInput
        style={styles.textInput}
        value={email}
        onChangeText={(text) => setEmail(text)}
      />
      <Button
        color={"007AFF"}
        title="Send login link"
        onPress={() => sendSignInLink(email)}
      />
    </View>
  );
};
  • We're setting handleCodeInApp to true since we want the link from the email to open our app and be handled there.
  • More details on supported options can be found here.

Then, use it in our App.js:

Copy
function App(): React.JSX.Element {
  const isDarkMode = useColorScheme() === "dark";

  const backgroundStyle = {
    backgroundColor: "lightgray",
  };

  return (
    <SafeAreaView style={backgroundStyle}>
      <StatusBar
        barStyle={isDarkMode ? "light-content" : "dark-content"}
        backgroundColor={backgroundStyle.backgroundColor}
      />
      <View style={styles.sectionContainer}>
        <MagicLinkLogin />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  sectionContainer: {
    paddingHorizontal: 24,
    height: "100%",
  },
  authContainer: {
    flex: 1,
    justifyContent: "center",
  },
  loadingContainer: {
    backgroundColor: "rgba(250,250,250,0.33)",
    justifyContent: "center",
    alignItems: "center",
  },
  textInput: {
    borderColor: "blue",
    borderWidth: 1,
    marginTop: 16,
    backgroundColor: "white",
    padding: 10,
    borderRadius: 8,
    fontSize: 16,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: "600",
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: "400",
  },
  highlight: {
    fontWeight: "700",
  },
});

Here's our how our app looks: screenshots definitely not winning any awards for the aesthetics on this app

If you have everything configured sucessfully, you should get a link in your email. screenshots guess how I'm spending my weekend.

Handling the Link Inside the App

We want to use the @react-native-firebase/dynamic-links package to handle the link inside our app.

Copy
const useEmailLinkEffect = () => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const handleDynamicLink = async (link: any) => {
      console.log('what is link', link);
      if (auth().isSignInWithEmailLink(link.url)) {
        setLoading(true);

        try {
          const email = await AsyncStorage.getItem('emailForSignIn');
          await auth().signInWithEmailLink(email as string, link.url);
        } catch (e: any) {
          setError(e);
        } finally {
          setLoading(false);
        }
      }
    };

    const unsubscribe = dynamicLinks().onLink(handleDynamicLink);
    dynamicLinks()
      .getInitialLink()
      .then(link => link && handleDynamicLink(link));

    return () => unsubscribe();
  }, []);

  return {error, loading};
};


const EmailLinkHandler = () => {
  const {loading, error} = useEmailLinkEffect();

  if (loading || error) {
    return (
      <View style={[StyleSheet.absoluteFillObject, styles.loadingContainer]}>
        {loading && <ActivityIndicator />}
      </View>
    );
  }

  return null;
};

Handling the successful sign-in

When the user successfully signs in, we want to use the onAuthStateChanged listener to monitor the authentication state of the user.

Copy
const MagicLinkSignIn = () => {
  // Set an initializing state whilst Firebase connects
  const [initializing, setInitializing] = useState(true);
  const [user, setUser] = (useState < FirebaseAuthTypes.User) | (null > null);

  // Handle user state changes
  useEffect(() => {
    const subscriber = auth().onAuthStateChanged((authUser) => {
      setUser(authUser);
      if (initializing) {
        setInitializing(false);
      }
    });
    return () => subscriber(); // unsubscribe on unmount
  }, [initializing]);

  if (initializing) {
    return null;
  }

  if (!user) {
    return <MagicLinkLogin />;
  }

  return (
    <View style={styles.authContainer}>
      <Text style={styles.sectionTitle}>Welcome {user.email}</Text>
    </View>
  );
};

Then, let's update our App.js:

Copy
function App(): React.JSX.Element {
  const isDarkMode = useColorScheme() === "dark";

  const backgroundStyle = {
    backgroundColor: "lightgray",
  };

  return (
    <SafeAreaView style={backgroundStyle}>
      <StatusBar
        barStyle={isDarkMode ? "light-content" : "dark-content"}
        backgroundColor={backgroundStyle.backgroundColor}
      />
      <View style={styles.sectionContainer}>
        <EmailLinkHandler />
        <MagicLinkSignIn />
      </View>
    </SafeAreaView>
  );
}

Testing

We can test the integration by simulating link handling in our simulator:

  • Copy the link address from your email, and paste the below into the terminal to open the app:
Copy
xcrun simctl openurl booted {paste_the_link_here}

And here we have it, the user is automatically signed into the app: screenshots interesting coincidence: what's the similarity between both screenshots. hint: peep the minute

Conclusion

This setup allows users to sign into your React Native app using Firebase magic links.

Here's the accompanying GitHub repo for this article: https://github.com/edwardsmoses/integrating-magic-links-with-firebase-react-native

Notes:

Firebase has announced the shutdown of their dynamic links service, but they will continue to support email link authentication; see Firebase FAQ on the topic.

Comments

Edwards Moses - Web & Mobile — React & React Native Consultant

Edwards Moses
Web & Mobile — React & React Native Consultant

I'm Edwards, based in Lagos, Nigeria.
Freelancer Software Developer — collaborating with teams to craft extraordinary products.

From conception through to completion, I find immense joy in witnessing the evolution of an idea into a fully realized product in the hands of users. Check out my projects and articles to see what I've been up to lately.

Ready to bring your ideas to life?