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? Let's connect!