Node: This will be used to set up your project.
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.
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.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
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.
In this section, you’ll get FusionAuth up and running and use npx
to create a new application.
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
You'll find a Docker Compose file (docker-compose.yml
) and an environment variables configuration file (.env
) in the root directory of the repo.
Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.
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 your specified state.
If you ever want to reset the FusionAuth application, you need to 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:
E9FDB985-9173-4E01-9D73-AC2D60D1DC8E
.super-secret-secret-that-should-be-regenerated-for-production
.richard@example.com
and the password is password
.admin@example.com
and the password is password
.http://localhost:9011/
.You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.
If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View
icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration
section.
The .env
file contains passwords. In a real application, always add this file to your .gitignore
file and never commit secrets to version control.
Your FusionAuth instance is now running on a different machine (your computer) than the mobile app will run (either a real device or an emulator), which means that it won’t be able to access localhost
.
If the device and your computer are not connected to the same network or if you have something that blocks connections (like a firewall), learn how to expose a local FusionAuth instance to the internet. In summary, the process entails configuring ngrok on your local system, starting your FusionAuth instance on port 9011, and subsequently executing the following command.
ngrok http --request-header-add 'X-Forwarded-Port:443' 9011
This will generate a public URL that you can use to access FusionAuth when developing the app.
If the device (either real or emulator) and your computer are connected to the same network, you can use the local IP Address for your machine (for example, 192.168.15.2
). Here are a few articles to help you find your IP address, depending on the operating system you are running:
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.
We’ll use the Expo AuthSession library, which simplifies integrating with FusionAuth and creating a secure web application.
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:
scheme
that will be used when redirecting back to your application after logging in and out of FusionAuth.expo.android.package
) and iOS (expo.ios.bundleIdentifier
) builds.{
"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"
}
}
In this section, you’ll turn your application into a trivial banking application with some 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
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>
);
}
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:
exp://192.168.1.2:8081
).admin@example.com
and password password
.ExampleReactNativeApp
to edit it./--/redirect
to itexp://192.168.1.2:8081
, you’d need to add exp://192.168.1.2:8081/--/redirect
as a redirect URLGo 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.
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 gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:
Follow Expo’s “Create your first build” tutorial to learn how to create a build for your app.
invalid_redirect_uri
when running the app with Expo GoMake sure you have updated the Authorized redirect URLs in your FusionAuth instance like shown on the Run the App step.