native

React Native

React Native

In this quickstart you are going to build an application with React Native and integrate it with FusionAuth. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.

The docker compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-react-native

Prerequisites

This app has been tested with React Native 0.72.4 and Node 20. This example should work with other compatible versions of React Native.

General Architecture

While this sample application doesn’t have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.

In that case, the system might look like this before FusionAuth is introduced.

UserApplicationView HomepageClick Login LinkShow Login FormFill Out and Submit Login FormAuthenticates UserDisplay User's Account or OtherInfoUserApplication

Request flow during login before FusionAuth

The login flow will look like this after FusionAuth is introduced.

UserApplicationFusionAuthView HomepageClick Login Link (to FusionAuth)View Login FormShow Login FormFill Out and Submit Login FormAuthenticates UserGo to Redirect URIRequest the Redirect URIIs User Authenticated?User is AuthenticatedDisplay User's Account or OtherInfoUserApplicationFusionAuth

Request flow during login after FusionAuth

In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.

Getting Started

In this section, you’ll get FusionAuth up and running and use npx to create a new application.

Clone the Code

First off, grab the code from the repository and change into that directory.

git clone https://github.com/FusionAuth/fusionauth-quickstart-react-native.git
cd fusionauth-quickstart-react-native

Run FusionAuth via Docker

In the root directory of the repo you’ll find a Docker compose file (docker-compose.yml) and an environment variables configuration file (.env). Assuming you have Docker installed on your machine, you can stand up FusionAuth up on your machine with:

docker compose up -d

Here you are using a bootstrapping feature of FusionAuth, called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json file and configure FusionAuth to a certain initial state.

If you ever want to reset the FusionAuth system, delete the volumes created by Docker Compose by executing docker compose down -v, then re-run docker compose up -d.

FusionAuth will be initially configured with these settings:

You can log into the FusionAuth admin UI and look around if you want, but with Docker/Kickstart you don’t need to.

Expose your Instance

To make the FusionAuth instance running on your machine accessible to the app, you need to expose it to the Internet following these instructions. Then, copy the address ngrok gave you as you’ll need it shortly. It looks something like https://SOME-RANDOM-STRINGS.ngrok-free.app.

Create your React Native Application

Now you’re going to create a React Native application. While this section builds a simple React Native application, you can use the same configuration to integrate your existing React Native application with FusionAuth. To make things easier, you’re going to use create-expo-app, a library that sets up the environment using Expo, a platform that runs natively on all your users’ devices

npx create-expo-app my-react-native-app && cd my-react-native-app

You’ll have to create all files in the root directory for your application.

If this is your first time setting up a React Native application, you’ll receive a message asking if you want to install create-expo-app, so press y to confirm.

Authentication

We’ll use the Expo AuthSession library, which simplifies integrating with FusionAuth and creating a secure web application.

Configure Expo AuthSession

Install expo-auth-session, its dependency expo-crypto to handle cryptographic operations and expo-web-browser to interact with the device Browser in order to log the user in or out.

npx expo install expo-auth-session expo-crypto expo-web-browser

Create a .env file to hold information about your FusionAuth instance and application, replacing the value in EXPO_PUBLIC_FUSIONAUTH_URL with the address you copied from ngrok when exposing your instance.

EXPO_PUBLIC_FUSIONAUTH_CLIENT_ID=e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
EXPO_PUBLIC_FUSIONAUTH_ISSUER=acme.com
EXPO_PUBLIC_FUSIONAUTH_URL=Change this value to the address ngrok gave you

Replace app.json with the contents below to add some details about your app:

{
  "expo": {
    "name": "my-react-native-app",
    "slug": "my-react-native-app",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "light",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "io.fusionauth.app"
    },
    "android": {
      "package": "io.fusionauth.app",
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      }
    },
    "web": {
      "favicon": "./assets/favicon.png"
    },
    "scheme": "io.fusionauth.app"
  }
}

App Customization

In this section, you’ll turn your application into a trivial banking application with some styling.

Add Styling

First, run the command below to install some libraries needed for theming.

npm install expo-image expo-constants react-native-currency-input

Instead of using CSS, React Native has its own concept of stylesheets. Create a file named changebank.style.js with the contents below to style your ChangeBank app.

import {StyleSheet} from 'react-native';

export default StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#fff',
    },
    h1: {
        color: '#096324',
        fontSize: 30,
        fontWeight: '600',
    },
    h3: {
        color: '#096324',
        marginTop: 20,
        marginBottom: 40,
        fontSize: 24,
        fontWeight: '600',
    },
    a: {
        color: '#096324',
    },
    p: {
        fontSize: 18,
    },
    headerEmail: {
        color: '#096324',
    },
    finePrint: {
        fontSize: 16,
    },
    body: {
        fontFamily: 'sans-serif',
        padding: 0,
        margin: 0,
    },
    hRow: {
        display: 'flex',
        alignItems: 'flex-end',
        flex: 1,
        rowGap: 10,
    },
    pageHeader: {
        display: 'flex',
        flexDirection: 'column',
        width: '100%',
    },
    logoHeader: {
        display: 'flex',
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        height: 150,
        paddingVertical: 10,
        marginHorizontal: 20,
    },
    menuBar: {
        display: 'flex',
        flexDirection: 'row',
        columnGap: 10,
        alignItems: 'center',
        paddingVertical: 15,
        paddingHorizontal: 20,
        backgroundColor: '#096324',
    },
    menuLink: {
        fontWeight: '600',
        color: '#ffffff',
    },
    buttonLg: {
        backgroundColor: '#096324',
        color: '#ffffff',
        fontSize: 16,
        fontWeight: '700',
        borderRadius: 10,
        textAlign: 'center',
        paddingHorizontal: 15,
        paddingVertical: 5,
        textDecorationLine: 'none',
        overflow: 'hidden',
    },
    contentContainer: {
        flex: 1,
        display: 'flex',
        flexDirection: 'column',
        paddingTop: 60,
        paddingRight: 20,
        paddingBottom: 20,
        paddingLeft: 40,
    },
    balance: {
        fontSize: 50,
        fontWeight: '800',
    },
    changeLabel: {
        flex: 1,
        fontSize: 20,
        marginRight: 5,
    },
    changeInput: {
        flex: 1,
        flexGrow: 1,
        flexShrink: 0,
        fontSize: 20,
        borderColor: '#999',
        borderWidth: 1,
        padding: 5,
        textAlign: 'right',
    },
    changeMessage: {
        fontSize: 20,
        marginBottom: 15,
    },
    appContainer: {
        width: '100%',
        marginTop: 40,
        paddingHorizontal: 20,
    },
    changeContainer: {
        flex: 1,
    },
    image: {
        flex: 1,
        height: '100%',
    },
    inputContainer: {
        display: 'flex',
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: "space-between",
        marginVertical: 10,
    },
});

Run the command below to download the ChangeBank logo into the assets folder.

wget -O assets/changebank.svg https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-react-native/main/complete-application/assets/changebank.svg

Finish Setting up the App

Replace the existing App.js to integrate expo-auth-session, add the ChangeBank template, and stitch everything up.

import {useEffect, useState} from 'react';
import Constants from 'expo-constants';
import CurrencyInput from 'react-native-currency-input';
import styles from './changebank.style';
import {openAuthSessionAsync} from 'expo-web-browser';
import {Text, TouchableOpacity, View} from 'react-native';
import {
  exchangeCodeAsync,
  fetchUserInfoAsync,
  makeRedirectUri,
  useAuthRequest,
  useAutoDiscovery,
  AuthRequest,
  AuthSessionResult
} from 'expo-auth-session';
import {Image} from 'expo-image';
import {StatusBar} from 'expo-status-bar';

export default function App() {
  /**
   * This will hold the access token and the user details after successful authorization
   */
  const [authResponse, setAuthResponse] = useState(null);

  /**
   * This is what the ChangeBank app will use to make change
   */
  const [amount, setAmount] = useState(0);

  /**
   * This is a helper function from expo-auth-session to retrieve the URLs used for authorization
   */
  const discovery = useAutoDiscovery(process.env.EXPO_PUBLIC_FUSIONAUTH_URL);

  /**
   * Creating a new Redirect URI using the scheme configured in app.json.
   * Expo Go will override this with a local URL when developing.
   */
  const redirectUri = makeRedirectUri({
    scheme: Constants.expoConfig.scheme,
    path: 'redirect',
  });

  /**
   * useAuthRequest() is another helper function from expo-auth-session that handles the authorization request.
   * It returns a promptLogin() function that should be called to initiate the process.
   */
  const [requestLogin, responseLogin, promptLogin] = useAuthRequest({
    clientId: process.env.EXPO_PUBLIC_FUSIONAUTH_CLIENT_ID,
    scopes: ['openid', 'offline_access'],
    usePKCE: true,
    redirectUri,
  }, discovery);

  /**
   * We do the same thing as above but for the user registration endpoint.
   */
  const [requestRegister, responseRegister, promptRegister] = useAuthRequest({
    clientId: process.env.EXPO_PUBLIC_FUSIONAUTH_CLIENT_ID,
    scopes: ['openid', 'offline_access'],
    usePKCE: true,
    redirectUri,
  }, (discovery) ? {
    ...discovery,
    authorizationEndpoint: discovery.authorizationEndpoint.replace('/authorize', '/register')
  } : null);

  /**
   * To log the user out, we redirect to the end session endpoint
   *
   * @return {void}
   */
  const logout = () => {
    const params = new URLSearchParams({
      client_id: process.env.EXPO_PUBLIC_FUSIONAUTH_CLIENT_ID,
      post_logout_redirect_uri: redirectUri,
    });
    openAuthSessionAsync(discovery.endSessionEndpoint + '?' + params.toString(), redirectUri)
      .then((result) => {
        if (result.type !== 'success') {
          handleError(new Error('Please, confirm the logout request and wait for it to finish.'));
          console.error(result);
          return;
        }
        setAuthResponse(null);
      });
  };

  /**
   * Auxiliary function to handle displaying errors
   *
   * @param {Error} error
   */
  const handleError = (error) => {
    console.error(error);
    alert(error.message);
  };

  /**
   * This will handle login and register operations
   *
   * @param {AuthRequest} request
   * @param {AuthSessionResult} response
   */
  const handleOperation = (request, response) => {
    if (!response) {
      return;
    }

    /**
     * If something wrong happened, we call our error helper function
     */
    if (response.type !== 'success') {
      handleError(response.error || new Error(`Operation failed: ${response.type}`));
      return;
    }

    /**
     * If the authorization process worked, we need to exchange the authorization code for an access token.
     */
    exchangeCodeAsync({
      clientId: process.env.EXPO_PUBLIC_FUSIONAUTH_CLIENT_ID,
      code: response.params.code,
      extraParams: {
        code_verifier: request.codeVerifier,
      },
      redirectUri,
    }, discovery).then((response) => {
      // Now that we have an access token, we can call the /oauth2/userinfo endpoint
      fetchUserInfoAsync(response, discovery).then((userRecord) => setAuthResponse({
        accessToken: response.accessToken,
        user: userRecord,
      })).catch(handleError);
    }).catch(handleError);
  };

  /*
   * This is a React Hook that will call the handleOperation() method
   * whenever the login process redirects from the browser to our app.
   */
  useEffect(() => {
    handleOperation(requestLogin, responseLogin);
  }, [responseLogin]);

  /*
   * This is a React Hook that will call the handleOperation() method
   * whenever the signup process redirects from the browser to our app.
   */
  useEffect(() => {
    handleOperation(requestRegister, responseRegister);
  }, [responseRegister]);

  /**
   * Making change for our ChangeBank app
   */
  const amountCents = amount * 100;
  const nickels = Math.floor(amountCents / 5);

  return (
      <View style={styles.container}>
        <StatusBar style="auto"/>
        <View style={[styles.pageHeader, {marginTop: Constants.statusBarHeight}]}>
          <View style={styles.logoHeader}>
            <Image
                source={require('./assets/changebank.svg')}
                style={styles.image}
                contentFit="contain"
                transition={1000}
            />
            <View style={styles.hRow}>
              {(authResponse) ? (
                  <>
                    <Text style={styles.headerEmail}>{authResponse.user.email}</Text>
                    <TouchableOpacity disabled={!requestLogin} onPress={() => logout()}>
                      <Text style={styles.buttonLg}>Log out</Text>
                    </TouchableOpacity>
                  </>
              ) : (
                  <>
                    <TouchableOpacity disabled={!requestLogin} onPress={() => promptLogin()}>
                      <Text style={styles.buttonLg}>Log in</Text>
                    </TouchableOpacity>
                    <TouchableOpacity disabled={!requestLogin} onPress={() => promptRegister()}>
                      <Text style={styles.buttonLg}>Register</Text>
                    </TouchableOpacity>
                  </>
              )}
            </View>
          </View>

          <View style={styles.menuBar}>
            <Text style={styles.menuLink}>{(authResponse) ? 'Make Change' : 'Home'}</Text>
          </View>
        </View>

        <View style={styles.appContainer}>
          {(authResponse) ? (
              <>
                <Text style={styles.h1}>We Make Change</Text>
                <View style={styles.inputContainer}>
                  <Text style={styles.changeLabel}>Amount in USD:</Text>
                  <CurrencyInput
                      prefix="$ "
                      delimiter=","
                      separator="."
                      value={amount}
                      onChangeValue={setAmount}
                      style={styles.changeInput}
                  />
                </View>
                <Text style={styles.changeMessage}>
                  We can make change for ${(amount || 0).toFixed(2)} with {nickels} nickels and{' '}
                  {Math.round(amountCents % 5)} pennies!
                </Text>
              </>
          ) : (
              <Text style={styles.h1}>Log in to manage your account</Text>
          )}
        </View>
      </View>
  );
}

Run the App

Depending on where you want to test your app, follow these instructions.

Now that you have a device connected or an emulator running, start up the React Native application from the root my-react-native-app directory using the command below.

npx expo start

After waiting a few moments, you should see a QR Code and a menu with some actions. Right below the QR Code, you’ll see a message like this one (the real address may vary).

› Metro waiting on exp://192.168.1.2:8081

To use Expo Go, a client for testing your apps on Android and iOS devices without building anything locally, you need to:

Go back to the terminal with the Expo menu. To test on the connected or emulated Android device, press a. Otherwise, press i to run on the iOS device.

Wait a few seconds and Expo will build and install the app in your device. You should then see the ChangeBank welcome page. Click the Log in button in the top right corner of that screen.

If it is your first time running the app, ngrok will ask if you really want to continue to that page, so click Visit Site.

You’ll finally arrive at the FusionAuth login screen. Fill in richard@example.com and password and click on Submit to be redirected back to the logged-in ChangeBank page.

Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you’re going to want to do.

FusionAuth Customization

FusionAuth gives you the ability to customize just about everything with the user’s experience and your application’s integration. This includes

Security

Tenant and Application Management

Build your App for Distribution

Follow Expo’s “Create your first build” tutorial to learn how to create a build for your app.

Troubleshooting

Make sure you have updated the Authorized redirect URLs in your FusionAuth instance like shown on the Run the App step.