Build a protected iOS app with React Native
This tutorial covers the basics of developing a ForgeRock-protected, mobile app with React Native. This part focuses on developing the iOS bridge code along with a minimal React UI to authenticate a user.
First, what is a protected mobile app?
A protected app (client or server) is simply an app that uses some type of access artifact to verify a user’s identity (authentication) and permissions (authorization) prior to giving access to a resource. This "access artifact" can be a session cookie, an access token, or an assertion, and is shared between the entities of a system.
Additionally, a protected mobile app (client) is responsible for providing a user with methods for acquiring, using, and removing this access artifact upon request. The focus of this guide is implementing these capabilities using the Ping SDK for iOS within a React Native app.
What you will learn
"Bridge code" development is a concept common to mobile apps built using hybrid technologies. "Hybrid" is a term used when a portion of the mobile app is written in a language that is not native to the platform (Android and Java or iOS and Swift). React Native, a cross-platform, view-library from Facebook, requires this bridging code to provide the React (JavaScript) layer access to native (Swift in this case) APIs or dependencies.
We also touch on some of the concepts and patterns popularized by the React library. Since we do not (as of this writing) provide a React Native version of our SDK, we present this how-to as a guide to basic development of "bridge code" for connecting the Ping SDK for iOS to the React Native layer.
This guide covers how to implement the following application features using version 3 of the Ping SDK for iOS and Ping SDK for JavaScript:
-
Authentication through a simple journey/tree.
-
Requesting OAuth/OIDC tokens.
-
Requesting user information.
-
Logging a user out.
This is not a guide on how to build a React Native app
How to architect or construct React Native apps is outside the scope of this guide. It’s also worth noting that there are many React Native libraries and frameworks for building mobile applications. What is "best" is highly contextual to the product and user requirements for any given project.
To simply demonstrate our SDK integration, we use a few libraries to simplify the development of the application. We chose React Navigation as the navigation library, and NativeBase as the UI library, but neither is featured heavily in this part of the multi-part guide.
Use this guide
This is a "hands on" guide. We are providing the mobile app and resource server for you. You can find the repo on GitHub to follow along. All you’ll need is your own PingOne Advanced Identity Cloud tenant or PingAM service. If you don’t have access to either, [reach out to a representative today^], and we’ll be happy to get you started.
Two ways of using this guide
-
Follow along by building portions of the app yourself: continue by ensuring you can meet the requirements below
-
If you’re just curious about the bridge code and React Native implementation details: skip to Implement the iOS bridge code for authentication.
Requirements
Knowledge requirements
-
Xcode, the iOS Simulator and related tools.
-
Swift and CocoaPods.
-
JavaScript and the npm ecosystem of modules.
-
The command line interface (such as Terminal, Shell or Bash).
-
Core Git commands (such as
clone
, andcheckout
). -
Functional components: we use the functional-style, React components.
-
Hooks: local state and side effects are managed with this concept.
-
Context API: global state is managed with this concept.
Technical requirements
-
Admin access to an instance of PingOne Advanced Identity Cloud or PingAM.
-
Xcode 12 or higher with the developer tools installed.
-
Node.js 14 or higher & npm 7 or higher (please check your version with
node -v
andnpm -v
). -
A tool or service to generate a security certificate and key (self-signed is fine).
Authorization server setup
Step 1. Create a simple login journey/tree
You need a simple username and password authentication journey/tree for this guide.
-
Create a Page Node and connect it to the start of the journey.
-
Add the Username Collector and Password Collector nodes within the Page Node.
-
Add the Data Store Decision node and connect it to the Page Node.
-
Connect the
True
outcome of the decision node to the Success node. -
Connect the
False
outcome of the decision node to the Failure node.
For more information about building journeys/trees, refer to our documentation on tree configuration on PingAM or journey configuration in PingOne Advanced Identity Cloud. |
Step 2. Create an OAuth client
Within the server, create a public, "Native", OAuth client for the mobile client app with the following values:
-
Client name/ID:
ReactNativeOAuthClient
-
Client type:
Public
-
Secret:
<leave empty>
-
Scopes:
openid profile email address
-
Grant types:
Authorization Code
Refresh Token
-
Implicit consent: enabled
-
Redirection URLs/Sign-in URLs:
https://com.example.reactnative.todo/callback
-
Token authentication endpoint method:
none
Each of the above is required, so double check you set each of the items correctly. If you don’t, you’ll likely get an "invalid OAuth client" error in Xcode logs.
For more information about configuring OAuth clients, refer to our documentation on OAuth 2.0 client configuration in PingAM or OAuth client/application configuration in PingOne Advanced Identity Cloud. |
Step 3. Create a test user
Create a test user (identity) in your server within the realm you will use.
If using PingOne Advanced Identity Cloud, follow our documentation for creating a user in PingOne Advanced Identity Cloud.
Or, if you are using PingAM, click Identities in the left navigation. Use the following instructions to create a user:
-
Click Add Identity to view the create user form.
-
In a separate browser tab, visit this UUID v4 generator and copy the UUID.
-
Switch back to PingAM and paste the UUID into the User ID input.
-
Provide a password and email address.
You will use this UUID as the username for logging into the app. |
Local project setup
Step 1. Clone the project
First, clone the React Native Sample project
to your local computer, cd
(change directory) into the project folder, and checkout
the branch for this guide:
git clone https://github.com/ForgeRock/sdk-sample-apps.git
cd forgerock-react-native-sample
git checkout blog/build-protected-app/part-one/start
In addition, there’s a branch that represents the completion of this guide.
If you get stuck, you can visit /blog/build-protected-app/part-one/complete
branch in GitHub and use it as a reference.
The branch used in this guide is an overly-simplified version of the sample app
found in the main Git branch within the forgerock-react-native-sample directory.
This "full version" is a to-do application that communicates with a protected REST API.
|
Step 2. Install the project dependencies
This React Native app requires two types of dependencies:
-
JavaScript and its Node Package Modules
-
Swift with CocoaPods.
First, let’s install the JavaScript dependencies.
Within the project directory: forgerock-react-native-sample/reactnative-todo/
(file and directory references are from this location),
use the following command:
npm install
Once the above command finishes, cd
into the ios
directory and install the needed CocoaPods.
When done, you can return to the project directory.
cd ios
pod install
cd ..
Step 3. Create the .env.js
file
Using the authorization server settings from above,
create the .env.js
file from the .env.js.template
file within the project.
Add your relevant values to configure all the important server settings to your project.
Not all variables will need values at this time.
A hypothetical example (required variables commented):
/**
* Avoid trailing slashes in the URL string values below
*/
const AM_URL = 'https://auth.forgerock.com/am'; // Required; enter _your_ {am_name} URL
const API_PORT = 8080; // Required; default port is 8080
const API_BASE_URL = '+http://localhost+'; // Required; default domain is http://localhost
const DEBUGGER_OFF = true;
const REALM_PATH = 'alpha'; // Required
const REST_OAUTH_CLIENT = 'RestOAuthClient';
const REST_OAUTH_SECRET = '0Rtg8s3s23w4e35L7zHr0dfkZZ';
Descriptions of relevant values:
-
AM_URL
: The URL that references PingAM itself (for PingOne Advanced Identity Cloud, the URL is likelyhttps://<tenant-name>.forgeblocks.com/am
). -
API_PORT
andAPI_BASE_URL
just need to be "truthy" (not 0 or empty string) right now to avoid errors and will be used in a future part of this series. -
DEBUGGER_OFF
: Whentrue
, this disables thedebugger
statements in the JavaScript layer. These debugger statements are for learning the integration points at runtime in your browser. When the browser’s developer tools are open, the app pauses at each integration point. Code comments above each integration point explain its use. -
REALM_PATH
: The realm of your server (likelyroot
,alpha
orbravo
). -
REST_OAUTH_CLIENT
andREST_OAUTH_SECRET
: These will be used in a future part of this series, so any string value will do.
Build and run the project
Now that everything is set up, build and run the to-do app project.
-
Open your Finder application and find the following file
ios/reactnativetodo.xcworkspace
. -
Double click this file to open and load the project within Xcode.
-
Once Xcode is ready, select iPhone 11 or higher as the target for the device simulator on which to run the app.
-
Now, click the build/play button to build and run this application in the target simulator.
With everything up and running, you will need to rebuild the project with Xcode when you modify the bridge code (Swift files). But, when modifying the React Native code, it will use "hot module reloading" to automatically reflect the changes in the app without having to manually rebuild the project.
If the app doesn’t build
-
Make sure
libFRAuth.a
is added to your Target’s Frameworks, Libraries, and Embedded Content under the General tab. -
Make sure the Metro server is running;
npx react-native start
if you want to run it manually. -
Bridge code has been altered, so be aware of API name changes.
-
If you get the error,
[!] CocoaPods could not find compatible versions for pod "FRAuth"
, runpod repo update
thenpod install
.
Xcode, iOS Simulator and Safari dev tools
We recommend the use of iPhone 11 or higher as the target for the iOS Simulator. When you first run the build command in Xcode (clicking the "play" button), it takes a while for the app to build, the OS to load, and app to launch within the Simulator. Once the app is launched, rebuilding it is much faster if the changes are not automatically "hot reloaded" when made in the React layer.
Only the home screen will render successfully at this moment. If you click on the Sign In button, it won’t be fully functional. This is intended as you will develop this functionality throughout this tutorial. |
Once the app is built and running, you will have access to all the logs within Xcode’s output console. Both the native and JavaScript logs display here. Because of this, there’s quite a lot of output, so you may want to use it only when the Safari console does not provide enough information for debugging purposes.
For additional tooling, click "Device" within the top menu, and then select "Shake". This triggers the React Native dev tools, allowing you to reload the app, inspect the UI, as well as other actions.
Due to a particular confusing bug in React Native, we do not recommend using the Chrome debugger, but recommend using Safari for debugging. To use Safari for debugging the React code, follow the instructions found in the React Native docs. |
Tips if the home screen doesn’t render
-
Restart the app (in Xcode) and Metro (in terminal).
-
Didn’t work? Using Xcode, clean the build folder and rebuild/rerun the app.
-
If that doesn’t work, remove the following from the
reactnative-todo
directory:node_modules
,package-lock.json
,ios/.Pods
,ios/Podfile.lock
, and then reinstall dependencies withnpm i
and within theios/
directorypod install
. -
If you’re still having issues, within the simulator, click the Home button and long press the React Todo application to .remove it. Then, restart from the project Xcode.
-
You can also use Device > Erase All Content and Settings if the problem persists.
Implement the iOS bridge code for authentication
Step 1. Review the iOS related files
We first need to review the files that allow for the "bridging" between our React Native project and our native SDKs.
Within Xcode, navigate to the ios/reactnativetodo
directory, and you will see a few important files:
-
reactnativetodo-Bridging-Header.h
: Header file that exposes the React Native bridging module and the FRAuth module into the Swift context. -
FRAuthSampleBridge.m
: The module file that defines the exported interfaces of our bridging code. -
FRAuthSampleBridge.swift
: The main Swift bridging code that provides the callable methods for the React Native layer. -
FRAuthSampleStructs.swift
: Provides the structs for the Swift bridging code. -
FRAuthSampleHelpers.swift
: Provides the extensions to often used objects within the bridge code. -
FRAuthConfig
: A.plist
file that configures the Ping SDK for iOS to the appropriate authorization server.
We provide the header file as is. The file’s creation, naming and use requires very specific conventions that are outside the scope of this tutorial. You will not need to modify it.
The remainder of the files within the workspace are automatically generated when you create a React Native project with the CLI command, so you can ignore them. |
Step 2. Configure your .plist
file
Within Xcode’s directory/file list section (aka Project Navigator), complete the following:
-
Find
FRAuthConfig.plist
file within theios/reactnativetodos
directory. -
Add the name of your PingOne Advanced Identity Cloud or PingAM cookie.
-
Add the OAuth client you created from above.
-
Add your authorization server URLs.
-
Add the login tree you created above.
A hypothetical example (your values may vary):
<dict>
<key>forgerock_cookie_name</key>
- <string></string>
+ <string>iPlanetDirectoryPro</string>
<key>forgerock_enable_cookie</key>
<true/>
<key>forgerock_oauth_client_id</key>
<string>ReactNativeOAuthClient</string>
<key>forgerock_oauth_redirect_uri</key>
<string>https://com.example.reactnative.todo/callback</string>
<key>forgerock_oauth_scope</key>
<string>openid profile email</string>
<key>forgerock_oauth_url</key>
- <string></string>
+ <string>https://auth.forgerock.com/am</string>
<key>forgerock_oauth_threshold</key>
<string>60</string>
<key>forgerock_url</key>
- <string></string>
+ <string>https://auth.forgerock.com/am</string>
<key>forgerock_realm</key>
- <string></string>
+ <string>alpha</string>
<key>forgerock_timeout</key>
<string>60</string>
<key>forgerock_keychain_access_group</key>
<string>org.reactjs.native.example.reactnativetodo</string>
<key>forgerock_auth_service_name</key>
- <string></string>
+ <string>UsernamePassword</string>
<key>forgerock_registration_service_name</key>
- <string></string>
+ <string>Registration</string>
</dict>
Descriptions of relevant values:
-
forgerock_cookie_name
: If you have PingOne Advanced Identity Cloud, you can find this random string value under the Tenant Settings found in the top-right dropdown in the admin UI. If you have your own installation of PingAM, this is ofteniPlanetDirectoryPro
. -
forgerock_url
&forgerock_oauth_url
: The URL of PingAM within your server installation. -
forgerock_realm
: The realm of your server (likelyroot
,alpha
orbeta
). -
forgerock_auth_service_name
: This is the journey/tree that you use for login. -
forgerock_registration_service_name
: This is the journey/tree that you use for registration, but it will not be used until a future part of this tutorial series.
Step 3. Write the start()
method
Staying within the reactnativetodo
directory, find the FRAuthSampleBridge
file and open it.
We have some of the files already stubbed out and the dependencies are already installed.
All you need to do is write the functionality.
For the SDK to initialize with the FRAuth.plist
configuration from Step 2, write the start()
function as follows:
import Foundation
import FRAuth
import FRCore
import UIKit
@objc(FRAuthSampleBridge)
public class FRAuthSampleBridge: NSObject {
var currentNode: Node?
@objc static func requiresMainQueueSetup() -> Bool {
return false
}
+ @objc func start(
+ _ resolve: @escaping RCTPromiseResolveBlock,
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
+ /**
+ * Set log level to all
+ */
+ FRLog.setLogLevel([.all])
+
+ do {
+ try FRAuth.start()
+ let initMessage = "SDK is initialized"
+ FRLog.i(initMessage)
+ resolve(initMessage)
+ } catch {
+ FRLog.e(error.localizedDescription)
+ reject("Error", "SDK Failed to initialize", error)
+ }
+ }
/**
* Method for calling the `getUserInfo` to retrieve the user information from
* the OIDC endpoint
*/
@objc func getUserInfo(
_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
@@ collapsed @@
The start()
function above calls the Ping SDK for iOS’s start()
method on the FRAuth
class.
There’s a bit more that may be required within this function for a production app.
We’ll get more into this in a separate part of this series, but for now, let’s keep this simple.
Step 4. Write the login()
method
Once the start()
method is called and it has initialized, the SDK is now ready to handle user requests.
Let’s start with login()
.
Just underneath the start()
method we wrote above, add the login()
method.
@@ collapsed @@
@objc func start(
_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
/**
* Set log level according to all
*/
FRLog.setLogLevel([.all])
do {
try FRAuth.start()
let initMessage = "SDK is initialized"
FRLog.i(initMessage)
resolve(initMessage)
} catch {
FRLog.e(error.localizedDescription)
reject("Error", "SDK Failed to initialize", error)
}
}
+ @objc func login(
+ _ resolve: @escaping RCTPromiseResolveBlock,
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
+
+ FRUser.login { (user, node, error) in
+ self.handleNode(user, node, error, resolve: resolve, rejecter: reject)
+ }
+ }
@@ collapsed @@
This login()
function initializes the journey/tree specified for authentication.
You call this method without arguments as it does not login the user.
This initial call to the server will return the first set of callbacks
that represents the first node in your journeyt/tree to collect user data.
Also, notice that we have a special "handler" function within the callback of FRUser.login()
.
This handleNode()
method serializes the node
object that the Ping SDK for iOS returns in a JSON string.
Data passed between the "native" layer and the React layer is limited to strings.
This method can be written in many ways and should be written in whatever way is best for your application.
However, a unique use of the Ping SDK for JavaScript to convert this basic JSON of data
into a decorated object for better ergonomics is used in this tutorial.
Step 5. Write the next()
method
To finalize the functionality needed to complete user authentication,
we need a way to iteratively call next
until the tree completes successfully or fails.
To do this, continue in the bridge file, and add a private method called handleNode()
.
First, we will write the decoding of the JSON string and prepare the node for submission.
@@ collapsed @@
@objc func login(
_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
FRUser.login { (user, node, error) in
self.handleNode(user, node, error, resolve: resolve, rejecter: reject)
}
}
+ @objc func next(
+ _ response: String,
+ resolve: @escaping RCTPromiseResolveBlock,
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
+
+ let decoder = JSONDecoder()
+ let jsonData = Data(response.utf8)
+ if let node = self.currentNode {
+ var responseObject: Response?
+ do {
+ responseObject = try decoder.decode(Response.self, from: jsonData)
+ } catch {
+ FRLog.e(String(describing: error))
+ reject("Error", "UnknownError", error)
+ }
+
+ let callbacksArray = responseObject!.callbacks ?? []
+
+ for (outerIndex, nodeCallback) in node.callbacks.enumerated() {
+ if let thisCallback = nodeCallback as? SingleValueCallback {
+ for (innerIndex, rawCallback) in callbacksArray.enumerated() {
+ if let inputsArray = rawCallback.input, outerIndex == innerIndex,
+ let value = inputsArray.first?.value {
+
+ thisCallback.setValue(value.value as! String)
+ }
+ }
+ }
+ }
+ } else {
+ reject("Error", "UnknownError", nil)
+ }
+ }
@@ collapsed @@
Now that you’ve prepared the data for submission, introduce the node.next
call from the Ping SDK for iOS.
Then, handle the subsequent node
returned from the next
call,
or process the success or failure representing the completion of the journey/tree.
@@ collapsed @@
for (outerIndex, nodeCallback) in node.callbacks.enumerated() {
if let thisCallback = nodeCallback as? SingleValueCallback {
for (innerIndex, rawCallback) in callbacksArray.enumerated() {
if let inputsArray = rawCallback.input, outerIndex == innerIndex,
let value = inputsArray.first?.value {
thisCallback.setValue(value)
}
}
}
}
+ node.next(completion: { (user: FRUser?, node, error) in
+ if let node = node {
+ self.handleNode(user, node, error, resolve: resolve, rejecter: reject)
+ } else {
+ if let error = error {
+ reject("Error", "LoginFailure", error)
+ return
+ }
+
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ if let user = user,
+ let token = user.token,
+ let data = try? encoder.encode(token),
+ let accessInfo = String(data: data, encoding: .utf8) {
+
+ resolve(["type": "LoginSuccess", "accessInfo": accessInfo])
+ } else {
+ resolve(["type": "LoginSuccess", "accessInfo": ""])
+ }
+ }
+ })
} else {
reject("Error", "UnknownError", nil)
}
}
@@ collapsed @@
The above code handles a limited number of callback types.
Handling full authentication and registration journeys/trees requires additional callback handling.
To keep this tutorial simple, we’ll focus just on SingleValueCallback
type.
Build a React Native form for simple login
Step 1. Initialize the SDK
First, let’s review how the application renders the home view:
index.js
> src/index.js
> src/router.js
> src/screens/home.js
Open up the second file in the above sequence, the src/index.js
file, and write the following:
-
Import
useEffect
from the React library. -
Import
NativeModules
from thereact-native
package. -
Pull
FRAuthSampleBridge
from theNativeModules
object. -
Write an
async
function within theuseEffect
callback to call the SDKstart()
method.
- import React from 'react';
+ import React, { useEffect } from 'react';
+ import { NativeModules } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import Theme from './theme/index';
import { AppContext, useGlobalStateMgmt } from './global-state';
import Router from './router';
+ const { FRAuthSampleBridge } = NativeModules;
export default function App() {
const stateMgmt = useGlobalStateMgmt({});
+ useEffect(() => {
+ async function start() {
+ await FRAuthSampleBridge.start();
+ }
+ start();
+ }, []);
return (
<Theme>
<AppContext.Provider value={stateMgmt}>
<SafeAreaProvider>
<Router />
</SafeAreaProvider>
</AppContext.Provider>
</Theme>
);
}
FRAuthSampleBridge
is the JavaScript representation of the Swift bridge code we developed earlier.
Any public methods added to the Swift class within the bridge code are available in the FRAuthSampleBridge
object.
It’s important to initialize the SDK at a root level. Call this initialization step, so it resolves before any other native SDK methods can be used. |
Step 2. Build the login view
Navigate to the app’s login view within the Simulator. You should see a "loading" spinner and a message that’s persistent, since the app doesn’t have the data needed to render the form. To ensure the correct form is rendered, the initial data needs to be retrieved from the server. That will be the first task.
Since most of the action is taking place in src/components/journey/form.js
, open it and add the following:
-
Import
FRStep
from the@forgerock/javascript-sdk
for improved ergonomics for handling callbacks. -
Import
NativeModules
from thereact-native
package. -
Pull
FRAuthSampleBridge
from theNativeModules
object.
+ import { FRStep } from '@forgerock/javascript-sdk';
import React from 'react';
+ import { NativeModules } from 'react-native';
import Loading from '../utilities/loading';
+ const { FRAuthSampleBridge } = NativeModules;
@@ collapsed @@
To develop the login functionality, we first need to use the login()
method from the bridge code
to get the first set of callbacks, and then render the form appropriately.
This login()
method is an asynchronous method, so import a few additional packages from React
to encapsulate this "side effect".
Let’s get started!
Import two new modules from React: useState
and useEffect
.
The useState
() method is for managing the data received from the server,
and the useEffect
is for the FRAuthSampleBridge.login()
method’s asynchronous, network request.
Compose the data gathering process using the following:
-
Import
useEffect
from the React library. -
Write the
useEffect
function inside the component function. -
Write an
async
function within theuseEffect
for callinglogin
. -
Write an
async
logout
function to ensure user if fully logged out before attempting tologin
. -
Call
FRAuthSampleBridge.login()
to initiate the call to the login journey/tree. -
When the
login()
call returns with the data, parse the JSON string. -
Assign that data to our component state via the
setState()
method. -
Lastly, call this new method to execute this process.
import { FRStep } from '@forgerock/javascript-sdk';
- import React from 'react';
+ import React, { useEffect, useState } from 'react';
import { NativeModules } from 'react-native';
import Loading from '../utilities/loading';
const { FRAuthSampleBridge } = NativeModules;
export default function Form() {
+ const [step, setStep] = useState(null);
+ console.log(step);
+
+ useEffect(() => {
+ async function getStep() {
+ try {
+ await FRAuthSampleBridge.logout();
+ const dataString = await FRAuthSampleBridge.login();
+ const data = JSON.parse(dataString);
+ const initialStep = new FRStep(data);
+ setStep(initialStep);
+ } catch (err) {
+ setStep({
+ type: 'LoginFailure',
+ message: 'Application state has an error.',
+ });
+ }
+ }
+ getStep();
+ }, []);
return <Loading message="Checking your session ..." />;
}
We are passing an empty array as the second argument into useEffect .
This instructs the useEffect to only run once after the component mounts.
This is functionally is equivalent to a
class component using componentDidMount to run an asynchronous method after the component mounts.
|
The above code will result in two logs to your console:
-
null
-
An object with a few properties.
The property to focus on is the callbacks
property.
This property contains the instructions for what needs to be rendered to the user for input collection.
Import the components from NativeBase as well as the custom, local components within this journey/
directory:
import { FRStep } from '@forgerock/javascript-sdk';
+ import { Box, Button, FormControl, ScrollView } from 'native-base';
import React, { useEffect, useState } from 'react';
import { NativeModules } from 'react-native';
import Loading from '../utilities/loading';
+ import Alert from '../utilities/alert';
+ import Password from './password';
+ import Text from './text';
+ import Unknown from './unknown';
@@ collapsed @@
Now, within the Form
function body, create the function that maps these imported components
to their appropriate callbacks.
@@ collapsed @@
export default function Form() {
const [step, setStep] = useState(null);
console.log(step);
@@ collapsed @@
+ function mapCallbacksToComponents(cb, idx) {
+ const name = cb?.payload?.input?.[0].name;
+ switch (cb.getType()) {
+ case 'NameCallback':
+ return <Text callback={cb} inputName={name} key="username" />;
+ case 'PasswordCallback':
+ return <Password callback={cb} inputName={name} key="password" />;
+ default:
+ // If current callback is not supported, render a warning message
+ return <Unknown callback={cb} key={`unknown-${idx}`} />;
+ }
+ }
return <Loading message="Checking your session ..." />;
}
Finally, return the appropriate component for the following states:
-
If there is no
step
data, render theLoading
component to indicate the request is still processing. -
If there is
step
data, and it is of type'Step'
, then map overstep.callbacks
with the function from above. -
If there is
step
data, but the type is'LoginSuccess'
or'LoginFailure'
, render an alert.
@@ collapsed @@
+ if (!step) {
return <Loading message='Checking your session ...' />;
+ } else if (step.type === 'Step') {
+ return (
+ <ScrollView>
+ <Box safeArea flex={1} p={2} w="90%" mx="auto">
+ <FormControl>
+ {step.callbacks?.map(mapCallbacksToComponents)}
+ <Button>Sign In</Button>
+ </FormControl>
+ </Box>
+ </ScrollView>
+ );
+ } else {
+ // Handle success or failure of the journey/tree
+ return (
+ <Box safeArea flex={1} p={2} w="90%" mx="auto">
+ <Alert message={step.message} type={step.type} />
+ </Box>
+ );
+ }
Refresh the page, and you should now have a dynamic form that reacts to the callbacks returned from our initial call to ForgeRock.
Step 3. Handle the login form submission
Since a form that can’t submit anything isn’t very useful,
we’ll now handle the submission of the user input values to ForgeRock.
First, add a second useState
to track whether the user is authenticated or not,
and then edit the current Button
element, adding an onPress
handler with a simple, inline function.
This function should do the following:
-
Submit the modified
step
data to the server with theFRAuthSampleBridge.next()
method. -
Test if the response property
type
has the value of'LoginSuccess'
. -
If successful, parse the
response
JSON. -
Call
setStep
() with the new object parsed from the JSON (this is mostly just for logging the step to the console). -
Call
setAuthentication()
to true, which is a global state method that triggers the app to react (pun intended!) to the new user state. -
Handle errors with a generic failure message.
@@ collapsed @@
export default function Form() {
const [step, setStep] = useState(null);
+ const [isAuthenticated, setAuthentication] = useState(false);
console.log(step);
@@ collapsed @@
return (
<ScrollView>
<Box safeArea flex={1} p={2} w="90%" mx="auto">
<FormControl>
{step.callbacks?.map(mapCallbacksToComponents)}
- <Button>Sign In</Button>
+ <Button
+ onPress={() => {
+ async function getNextStep() {
+ try {
+ const response = await FRAuthSampleBridge.next(
+ JSON.stringify(step.payload),
+ );
+ if (response.type === 'LoginSuccess') {
+ const accessInfo = JSON.parse(response.accessInfo);
+ setStep({
+ accessInfo,
+ message: 'Successfully logged in.',
+ type: 'LoginSuccess',
+ });
+ setAuthentication(true);
+ } else {
+ setStep({
+ message: 'There has been a login failure.',
+ type: 'LoginFailure',
+ });
+ }
+ } catch (err) {
+ console.error(`Error: form submission; ${err}`);
+ }
+ }
+ getNextStep();
+ }}
+ >
+ Sign In
+ </Button>
</FormControl>
</Box>
</ScrollView>
);
@@ collapsed @@
After the app refreshes, use the test user to login. If successful, you should see a success message. Congratulations, you are now able to authenticate users!
What’s more, you can verify the authentication details by going to the Xcode or Safari log
and observing the result of the last call to the server.
It should have a type of "LoginSuccess"
along with token information.
If you got a login failure, you can re-attempt the login by going to the Device menu on the Simulator and selecting "Shake". This will allow you to reload the app, providing a fresh login form. |
Handle the user provided values
You may ask, "How did the user’s input values get added to the step
object?"
Let’s take a look at the component for rendering the username input.
Open up the Text
component: components/journey/text.js
.
Notice how special methods are being used on the callback object.
These are provided as convenience methods by the Ping SDK for JavaScript for getting and setting data.
@@ collapsed @@
export default function Text({ callback }) {
@@ collapsed @@
const error = handleFailedPolicies(
callback.getFailedPolicies ? callback.getFailedPolicies() : [],
);
const isRequired = callback.isRequired ? callback.isRequired() : false;
const label = callback.getPrompt();
const setText = (text) => callback.setInputValue(text);
return (
<FormControl isRequired={isRequired} isInvalid={error}>
<FormControl.Label mb={0}>{label}</FormControl.Label>
<Input
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
onChangeText={setText}
size="lg"
type="text"
/>
<FormControl.ErrorMessage>
{error.length ? error : ''}
</FormControl.ErrorMessage>
</FormControl>
);
}
There are two important items to focus on
-
callback.getPrompt()
: Retrieves the input’s label to be used in the UI. -
callback.setInputValue()
: Sets the user’s input on the callback while they are typing (i.e.onChangeText
).
Since the callback
is passed from the Form
to the components by "reference" (not by "value"),
any mutation of the callback
object within the Text
(or Password
) component is also contained
in the step
object in the Form
component.
You may think, "That’s not very idiomatic React! Shared, mutable state is bad." And, yes, you are correct, but we are taking advantage of this to keep everything simple (and this guide from being too long), so I hope you can excuse the pattern. |
Each callback type has its own collection of methods for getting and setting data in addition to a base set of generic callback methods. The SDK automatically adds these methods to the callback’s prototype. For more information about these callback methods, see our API documentation, or the source code in GitHub, for more details.
Step 4. Request user info and redirecting to home screen
Now that the user can login, let’s go one step further and request information about the authenticated user
to display their name and other information.
We will now utilize the existing FRAuthSampleBridge.getUserInfo()
method already included in the bridge code.
Let’s do a little setup before we make the request to the server:
-
Add
useContext
to the import from React so that we have access to the global state. -
Import
AppContext
from theglobal-state
module. -
Call
useContext
with ourAppContext
to provide access to the setter methods. -
Add one more
useEffect
function to detect the change of the user’s authentication.
import { FRStep } from '@forgerock/javascript-sdk';
import { Box, Button, FormControl, ScrollView } from 'native-base';
- import React, { useEffect, useState } from 'react';
+ import React, { useContext, useEffect, useState } from 'react';
import { NativeModules } from 'react-native';
+ import { AppContext } from '../../global-state';
@@ collapsed @@
export default function Form() {
+ const [_, methods] = useContext(AppContext);
const [step, setStep] = useState(null);
const [isAuthenticated, setAuthentication] = useState(false);
console.log(step);
useEffect(() => {
async function getStep() {
try {
await FRAuthSampleBridge.logout();
const dataString = await FRAuthSampleBridge.login();
const data = JSON.parse(dataString);
const initialStep = new FRStep(data);
setStep(initialStep);
} catch (err) {
console.error(`Error: request for initial step; ${err}`);
}
}
getStep();
}, []);
+ useEffect(() => {
+
+ }, [isAuthenticated]);
@@ collapsed @@
It’s worth noting that the isAuthenticated
declared in the array communicates to React
that this useEffect
should only execute if the state of that variable changes.
This prevents unnecessary code execution since the value is initially false
,
and continues to be false
until the user completes authentication.
With the setup complete, implement the request to the server for the user’s information.
Within this empty useEffect
, add an async
function to make that call to FRAuthSampleBridge.getUserInfo()
and call it only when isAuthenticated
is true
.
@@ collapsed @@
useEffect(() => {
+ async function getUserInfo() {
+ const userInfo = await FRAuthSampleBridge.getUserInfo();
+ console.log(userInfo);
+
+ methods.setName(userInfo.name);
+ methods.setEmail(userInfo.email);
+ methods.setAuthentication(true);
+ }
+
+ if (isAuthenticated) {
+ getUserInfo();
+ }
}, [isAuthenticated]);
@@ collapsed @@
In the code above, we collected the user information and set a few values to the global state to allow the app to react to this information. In addition to updating the global state, the React Navigation also reacts to the global state change and renders the new screens and tab navigation.
When you test this in the Simulator, completing a successful authentication results in the home screen being rendered with a success message. The user’s name and email are included for visual validation. You can also view the console in Safari and see the user’s information logged.
Add logout functionality to our bridge and React Native code
Clicking the Sign Out button within the navigation results in the logout page rendering with a persistent "loading" spinner and message. This is due to the missing logic that we’ll add now.
Handle logout request in React Native
Now, add the logic into the view to call this new Swift method.
Open up the screens/logout.js
file and import the following:
-
useEffect
anduseContext
from React -
useHistory
from React Router -
AppContext
from the global state module
- import React from 'react';
+ import React, { useContext, useEffect } from 'react';
+ import { NativeModules } from 'react-native';
+ import { AppContext } from '../global-state';
import { Loading } from '../components/utilities/loading';
+ const { FRAuthSampleBridge } = NativeModules;
@@ collapsed @@
Since logging out requires an async
, network request, we need to wrap it in a useEffect
and pass in a callback function with the following functionality:
@@ collapsed @@
export default function Logout() {
+ const [_, { setAuthentication }] = useContext(AppContext);
+
+ useEffect(() => {
+ async function logoutUser() {
+ try {
+ await FRAuthSampleBridge.logout();
+ } catch (err) {
+ console.error(`Error: logout; ${err}`);
+ }
+ setAuthentication(false);
+ }
+ logoutUser();
+ }, []);
+
return <Loading message="You're being logged out ..." />;
}
Since we only want to call this method once, after the component mounts,
we will pass in an empty array as a second argument for useEffect()
.
The use of the setAuthentication()
method empties or falsifies the global state to clean up
and re-renders the home screen.
Revisit the app within the Simulator, and tap the Sign Out button. You should see a quick flash of the loading screen, and then the home screen should be displayed with the logged out UI state.
You should now be able to successfully authenticate a user, display the user’s information, and log a user out. Congratulations, you just built a protected iOS app with React Native.
In part two of this series, we explore how to write the bridge code for Android, while utilizing the existing React Native UIs written here.