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
- Node: This will be used to set up your project.
- Docker: The quickest way to stand up FusionAuth. (There are other ways).
- To test on Android devices, you can either connect a physical device or Android Studio to set up an emulator.
- To test on iOS devices, you’ll need a Mac and install Xcode to set up an emulator.
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.
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.
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:
- Your client Id is
E9FDB985-9173-4E01-9D73-AC2D60D1DC8E
- Your client secret is
super-secret-secret-that-should-be-regenerated-for-production
. - Your example username is
richard@example.com
and the password ispassword
. - Your admin username is
admin@example.com
and the password ispassword
. - The base URL of FusionAuth
http://localhost:9011/
.
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:
- A
scheme
that will be used when redirecting back to your application after logging in and out of FusionAuth. - The package name for both Android (
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"
}
}
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.
- For Android devices: you can either connect a physical device or install Android Studio to set up an emulator.
- For iOS devices: if you are running a Mac, you can install Xcode to set up an emulator or run on a physical device.
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:
- Copy that listening address (
exp://192.168.1.2:8081
). - Navigate to your FusionAuth instance on localhost:9011.
- Log in with the email
admin@example.com
and passwordpassword
. - Browse to Applications.
- Click on in your
ExampleReactNativeApp
to edit it. - Go to the OAuth tab.
- Add that address to Authorized redirect URLs and append
/--/redirect
to it- Example: for
exp://192.168.1.2:8081
, you’d need to addexp://192.168.1.2:8081/--/redirect
as a redirect URL
- Example: for
- Click on to save your changes.
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
- Hosted pages such as login, registration, email verification, and many more
- Email templates
- User data and custom claims in access token JWTs
Security
- Expo AuthSession handles token validation and refresh
- You may want to customize the token expiration times and policies in FusionAuth
- Choose password rules and a hashing algorithm that meet your security needs
Tenant and Application Management
- Model your application topology using Applications, Roles, Groups, Entities, and more
- Set up MFA, Social login, and/or SAML integrations
- Integrate with external systems using Webhooks, SCIM, and Lambdas
Build your App for Distribution
Follow Expo’s “Create your first build” tutorial to learn how to create a build for your app.
Troubleshooting
- I keep receiving an
invalid_redirect_uri
when running the app with Expo Go
Make sure you have updated the Authorized redirect URLs in your FusionAuth instance like shown on the Run the App step.