Learn about React Native Charts with Cubejs and Victory

Learn about React Native Charts with Cubejs and Victory

React-Native makes building and maintaining native applications much easier. By coupling it with Cube.js and Victory-Native

Giving mobile users access to analytical data is always a hard problem to solve. Browsing a heavy website on a small screen usually is not the best user experience. Building native mobile apps is a great solution, but usually requires a lot of effort.

React-Native makes building and maintaining native applications much easier. By coupling it with Cube.js and Victory-Native, I'll show you how to build an analytics dashboard embedded into a native mobile app. The resulting app can run both on iOS and Android; you can try it out by using the Expo app on your own device.

Below is a screenshot of the final app. Here's the app snack on Expo—you can run it online or launch it on your device. Also, there you can find all the source code from this tutorial.

The data

We are going to use the Cube.js backend with sample data from Nginx logs. I've covered how to collect Nginx logs and analyze them with AWS Athena and Cube.js in this tutorial. Let's recap the data schema we're going to query:

cube(`Logs`, {
 measures: {
  count: {
   type: `count`,
  },

  errorCount: {
   type: count,
   filters: [
    { sql: ${CUBE.isError} = 'Yes' }
   ]
  },

  errorRate: {
   type: number,
   sql: 100.0 * ${errorCount} / ${count},
   format: percent
  }
 },

 dimensions: {
  status: {
   sql: status,
   type: number
  },

  isError: {
   type: string,
   case: {
    when: [{
     sql: ${CUBE}.status >= 400, label: Yes
    }],
    else: { label: No }
   }
  },

  createdAt: {
   sql: from_unixtime(created_at),
   type: time
  }
 }
});

Cube.js uses data schemas like the one above to generate SQL and execute it against your database. If you are new to Cube.js, I recommend checking this Cube.js 101 tutorial.

React-Native

To build a react-native app, we'll be using the react-native-cli package. Go ahead and install it:

npm install -g react-native-cli

or

yarn global add react-native-cli

Now you can create a new app:

react-native init cubejs-rn-demo
cd cubejs-rn-demo

This will create a barebones react-native app.

Cube.js

Cube.js provides a client package for loading data from the backend:

npm install -s @cubejs-client/core

or

yarn add @cubejs-client/core

It works for both web and native apps. Also, Cube.js has a React component, which is easier to work with:

npm install -s @cubejs-client/react

or

yarn add @cubejs-client/react

The Cube.js React client also works great with React-Native. The client itself doesn't provide any visualisations and is designed to work with existing chart libraries. It provides a set of methods to access Cube.js API and to work with query result.

Victory

For charts, we will be using the victory-native library.

npm install -s victory-native

or

yarn add victory-native

Now we can create a simple pie chart just like on the demo dashboard. Here's the code:

<VictoryChart width={this.state.width}>
 <VictoryPie
  data={data.chartPivot()}
  y="Logs.count"
  labels={item => numberFormatter(item[data.seriesNames()[0].key])}
 />
</VictoryChart>
Building a Dashboard

We are going to build a simple dashboard with just a couple of tiles, so we'll use a ScrollView. If you're going to have many tiles on a dashboard, it would be better to switch to a FlatList because of potential performance issues. So, let's create a simple dashboard. First, we are going to create a Chart component, where we'll define all required data.

const Empty = () => <Text>No component for that yet</Text>;

const chartElement = (type, data) => {
 switch (type) {
  case 'line':
   return <LineChart data={data} />;
  case 'pie':
   return <PieChart data={data} />;
  case 'bar':
   return <BarChart data={data} />;
  default:
   return <Empty />;
 }
};

const Chart = ({ type }) => (
 <QueryRenderer
  query={queries[type]}
  cubejsApi={cubejsApi}
  render={({ resultSet }) => {
   if (!resultSet) {
    return <ActivityIndicator size="large" color="#0000ff" />;
   }

   return chartElement(type, resultSet);
  }}
 />
);

Also, we will be using Victory's zoomContainer to allow users to zoom into the data. We'll also save current device orientation to add more data in landscape mode and change paddings:

const padding = {
  portrait: { left: 55, top: 40, right: 45, bottom: 50 },
  landscape: { left: 100, top: 40, right: 70, bottom: 50 }
};

const tickCount = {
 portrait: 4,
 landscape: 9
};

export const colors = [
 "#7DB3FF",
 "#49457B",
 "#FF7C78",
 "#FED3D0",
 "#6F76D9",
 "#9ADFB4",
 "#2E7987"
];

class ChartWrapper extends React.Component {
 constructor() {
  super();
  this.state = { orientation: 'portrait', ...Dimensions.get('window')};
  this.updateDimensions = this.updateDimensions.bind(this);
 }

 componentDidMount() {
  this.updateDimensions();
 }

 handleZoom(domain) {
  this.setState({ selectedDomain: domain });
 }

 updateDimensions() {
  const windowSize = Dimensions.get('window');
  const orientation = windowSize.width < windowSize.height ? 'portrait' : 'landscape';
  this.setState({ orientation, ...windowSize });
 }

 render() {
  return (
   <View style={vStyles.container} onLayout={this.updateDimensions}>
    <VictoryChart
     width={this.state.width}
     padding={padding[this.state.orientation]}
     domainPadding={{x: 10, y: 25}}
     colorScale={colors}
     tickCount={4}
     containerComponent={
      <VictoryZoomContainer responsive={true}
       zoomDimension="x"
       zoomDomain={this.state.zoomDomain}
       onZoomDomainChange={this.handleZoom.bind(this)}
      />
     }
    >
     {this.props.children}
     {!this.props.hideAxis &&
      <VictoryAxis tickCount={tickCount[this.state.orientation]} />
     }
     {!this.props.hideAxis &&
      <VictoryAxis dependentAxis />
     }
    </VictoryChart>
   </View>
  );
 }
}

Please note that the default app.json config has locked portrait screen orientation. To allow device rotation, set "orientation" to "default"—that will allow all orientations except upside down. This code scales the charts when the device is rotated:

Let's start with a line chart. First, we'll need to define a basic Cube.js query in Chart.js to get the data:

{
 measures: ["Logs.errorRate"],
 timeDimensions: [
  {
   dimension: "Logs.createdAt",
   dateRange: ["2019-04-01", "2019-04-09"],
   granularity: "day"
  }
 ]
}

Now we can create a LineChart component. It's a basic Victory chart with a bit of styling:

const LineChart = ({ data }) => (
 <ChartWrapper>
  <VictoryLine
   data={data.chartPivot()}
   x={dateFormatter}
   labels={null}
   y={data.seriesNames()[0].key}
   style={{
    data: { stroke: "#6a6ee5" },
    parent: { border: "1px solid #ccc"}
   }}
  />
 </ChartWrapper>
);

We can include this component in the Chart.js file and render in the Dashboard.js screen:

const Dashboard = () => {
 return (
  <ScrollView>
   <View style={styles.item}>
    <Text style={styles.text}>Error Rate by Day</Text>
    <Chart type="line" />
   </View>
  </ScrollView>
 );
};

The same applies to Stacked Bar Chart. The only complication is that it consists of multiple series, so we add a bar for each series and make a legend:

const BarChart = ({ data }) => (
 <ChartWrapper>
  <VictoryStack colorScale={colors}>
   {data.seriesNames().map((series, i) => (
    <VictoryBar
     key={i}
     x={dateFormatter}
     y={series.key.replace(":", ", ")}
     data={data.chartPivot()}
     labels={null}
     style={{
      parent: { border: "1px solid #ccc"}
     }}
    />
   ))}
  </VictoryStack>
  <VictoryLegend x={40} y={280}
   orientation="horizontal"
   colorScale={colors}
   data={data.seriesNames().map(({ title }) => ({ name: title.substring(0, 3) }))}
  />
 </ChartWrapper>
);

Now we come to the pie chart. There's a trick to hide the axis—we add an empty VictoryAxis here:

const PieChart = ({ data }) => (
 <ChartWrapper hideAxis>
  <VictoryPie
   data={data.chartPivot()}
   y="Logs.count"
   labels={item => numberFormatter(item[data.seriesNames()[0].key])}
   padAngle={3}
   innerRadius={40}
   labelRadius={70}
   style={{ labels: { fill: "white", fontSize: 14 } }}
   colorScale={colors}
  />
  <VictoryAxis style={{ axis: { stroke: "none" }, tickLabels: { fill: "none" } }} />
  <VictoryLegend x={40} y={260}
   orientation="horizontal"
   colorScale={colors}
   data={data.chartPivot().map(({ x }) => ({ name: x }))}
  />
 </ChartWrapper>
);

Here's a screenshot of the pie chart on the dashboard:

And our dashboard is done! You can find all the code and the app on Expo. You can run this app online or launch it on your device via the Expo app. It will work relatively slow as it's not compiled with platform-specific native code, but you can always download the source code and build a native app for your platform via Xcode or Android Studio.

I hope this tutorial helps you build great apps!


6 Tools for Debugging React Native

 6 Tools for Debugging React Native

In the React Native world, debugging may be done in different ways and with different tools, since React Native is composed of different environments (iOS and Android), which means there’s an assortment of problems and a variety of tools needed for debugging. In this brief guide, we’ll explore the 6 tools for debugging React Native.

Debugging is an essential part of software development. It’s through debugging that we know what’s wrong and what’s right, what works and what doesn’t. Debugging provides the opportunity to assess our code and fix problems before they’re pushed to production.

In the React Native world, debugging may be done in different ways and with different tools, since React Native is composed of different environments (iOS and Android), which means there’s an assortment of problems and a variety of tools needed for debugging.

Thanks to the large number of contributors to the React Native ecosystem, many debugging tools are available. In this brief guide, we’ll explore the most commonly used of them, starting with the Developer Menu.

Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. — Brian W. Kernighan

1. The Developer Menu

The in-app developer menu is your first gate for debugging React Native, it has many options which we can use to do different things. Let’s break down each option.

  • Reload: reloads the app
  • Debug JS Remotely: opens a channel to a JavaScript debugger
  • Enable Live Reload: makes the app reload automatically on clicking Save
  • Enable Hot Reloading: watches for changes accrued in a changed file
  • Toggle Inspector: toggles an inspector interface, which allows us to inspect any UI element on the screen and its properties, and presents and interface that has other tabs like networking, which shows us the HTTP calls, and a tab for performance.
2. YellowBoxes and RedBoxes

RedBoxes are used to display errors. Every time an app throws an error, it will display a RedBox and the description of the error. You can display it any time by writing console.error. But it doesn’t work in the production, meaning that if an error happens in that environment, the app will crash and stop running.

The RedBox is your friend. One of the helpful things about it is that it displays the error and gives you the suggestions on how to fix it, which you won’t find in the console. For example, I’ll frequently write a style property that’s not supported by React Native, or a property that’s used for a specific element—such as setting backroundImage for the View element. The Redbox will throw an error, but it will also show the list of supported style properties that you can apply to the View.

YellowBoxes are used to display warnings. You can disable them by adding line of code shown below inside index.js in the root directory of your app. But that’s not recommended, as YellowBoxes are very useful. They warn you about things like performance issues and deprecated code. You can use the YellowBox element from react-native to display a specific warning.

import {YellowBox} from 'react-native';
YellowBox.ignoreWarnings(['Warning: ...']);

Most YellowBox warnings are related to some bad practice in your code. For example, you might get a warning that you have an eventListener that you aren’t removing when the component unmounts, or that you have deprecated features and dependencies like this:

warning: ViewPagerAndroid has been extracted from react-native core and will be removed in a future release. It can now be installed and imported from '@react-native-community/viewpager' instead of 'react-native'. See https://github.com/react-native-community/react-native-viewpager

Fixing these issues will prevent many bugs and will improve the quality of your code.

You can always learn more about debugging React Native in the official docs.

3. Chrome’s DevTools

Chrome is possibly the first tool you’d think of for debugging React Native. It’s common to use Chrome’s DevTools to debug web apps, but we can also use them to debug React Native since it’s powered by JavaScript.

To use Chrome’s DevTools with React Native, first make sure you’re connected to the same Wi-Fi, then press command + R if you’re using macOS, or Ctrl + M on Windows/Linux. When the developer menu appears, choose Debug Js Remotely. This will open the default JS debugger.

Then check this address http://localhost:8081/debugger-ui/ in Chrome. You should see this page:

You may have to do some troubleshooting on Android.

To solve this problem, make sure your your machine and the device are connected on the same Wi-Fi, and then add android:usesCleartextTraffic="true" to android/app/src/main/AndroidManifest.xml. If this doesn’t work for you, check out these links for other solutions:

After you have successfully connected to Chrome, you can simply toggle the Chrome inspector.

Then take a look at the logs of your React Native app. Also have a look through the other features offered by Chrome’s DevTools, and use them with React Native as you do with any Web app.

One of the limitations of using Chrome’s DevTools with React Native is that you can’t debug the styles and edit the style properties as you usually do when you debug web apps. It’s also limited in comparison with React’s devtools when inspecting React’s component hierarchy.

4. React Developer Tools

To debug React Native using React’s Developer Tools, you need to use the desktop app. You can install it globally or locally in your project by just running this following command:

yarn add react-devtools

Or npm:

npm install react-devtools --save

Then start the app by running yarn react-devtools, which will launch the app.

React’s Developer Tools may be the best tool for debugging React Native for these two reasons:

  • It allows for debugging React components.
  • It makes it possible to debug styles in React Native (my favorite feature of these developer tools!). The new version comes with this feature that also works with inspector in the developer menu. Previously, it was a problem to write styles and have to wait for the app to reload to see the changes. Now we can debug and implement style properties and see the effect of the change instantly without reloading the app.

You can learn more about using React’s Developer Tools app here.

5. React Native Debugger

If you’re using Redux in your React Native app, React Native Debugger is probably the right debugger for you. It’s a standalone desktop app that works on macOS, Windows, and Linux. It integrates both Redux’s DevTools and React’s Developer Tools in one app so you don’t have to work with two separate apps for debugging.

React Native Debugger is my favorite debugger and the one I usually use in my work with React Native. It has an interface for Redux’s DevTools where you can see the Redux logs and the actions, and an interface for React’s Developer Tools where you can inspect and debug React elements. You can find the installation instructions here. You can connect with this debugger in the same way you open Chrome’s inspector.

Using React Native Debugger over Chrome’s DevTools has some advantages. For example, you can toggle the inspector from the Dev Menu to inspect React Native elements and edit styles, which isn’t available in Chrome’s DevTools.

6. React Native CLI

You can use the React Native CLI to do some debugging as well. You can use it for showing the logs of the app. Hitting react-native log-android will show you the logs of db logcat on Android, and to view the logs in iOS you can run react-native log-ios, and with console.log you can dispatch logs to the terminal:

console.log("some error🛑")

You should then see the logs in the terminal.

You can use the React Native CLI to get some relevant info about the libraries and the dependencies you’re using in your app, by running react-native info on the terminal. It shows us some useful info about the tools you’re using. For instance, here’s an example of an output when you run react-native info:

You can use this info to fix some bugs caused by version mismatches of a tool you’re using in your project, so it’s always good to check the environment info using this command.

Summary

There are lots of tools for debugging React Native in addition to the ones we’ve looked at here. It really depends on the specific needs of your project. I mostly use React Native Debugger Desktop because I often work with Redux, and it combines an array of tools in one app which is great and a time saving.

Fun, Fast, and Free Login with React Native

Fun, Fast, and Free Login with React Native

React Native is a mobile app development framework that allows you to use React to build native iOS and Android mobile apps. Instead of using a web view and rendering HTML and JavaScript, it converts React components to native platform components....

React Native is a mobile app development framework that allows you to use React to build native iOS and Android mobile apps. Instead of using a web view and rendering HTML and JavaScript, it converts React components to native platform components. This means you can use React Native in your existing Android and iOS projects, or you can create a whole new app from scratch.

In this post, I'll show you how to add a login feature to a React Native application using OAuth 2.0 and OpenID Connect. Authentication and authorization are important parts of most applications as you often need to know who a user is (authentication) and what they're allowed to do (authorization). OAuth 2.0 is an industry-standard protocol for authorization and OpenID Connect (OIDC) is an authentication layer on top of OAuth 2.0. Integrating with an always-on service like Okta for OAuth makes set up and configuration easy, particularly with React Native and Okta's React Native SDK.

React Native 0.61 was just released a couple of weeks ago. One of its biggest features is Fast Refresh - a unification of live reloading (reload on save) and hot reloading. Fast Refreshfully supports modern React's functional components and hooks and recovers after typos and other mistakes. In previous versions of React Native, a common complaint was that "hot reloading" was broken.

Prerequisites:

  • Node 10
  • Yarn
  • Watchman
  • Java 8 (for Android)
  • An Okta Developer Account

To install these prerequisites on a Mac, Linux, or Windows Subsystem for Linux (WSL), I recommend using Homebrew.

brew install node
brew install yarn
brew install watchman
brew tap AdoptOpenJDK/openjdk
brew cask install adoptopenjdk8

If you're not using WSL for Windows, you can use Chocolatey to install everything from the command line:

choco install -y nodejs.install python2 jdk8

You'll also need to download and install IDEs for Android and iOS:

You can sign up for an Okta developer account at developer.okta.com/signup.

Essential Reading: Learn React from Scratch! (2019 Edition)

Create a React Native Application

React Native CLI is a popular way to get started with React Native development.

npm install -g [email protected]

Once you have React Native CLI installed, you can create a new application using the init command.

react-native init ReactNativeLogin
Add Login with OIDC

Okta provides a React Native SDK which conveniently wraps the Okta native Android OIDC and iOS OIDC libraries.

I’m going to show you two ways to add OIDC-based login with Okta: The fast way with a tool I created and the step-by-step instructions.

This tool is based on Schematics and manipulates your project to install and configure everything.

Install Schematics globally.

npm install -g @angular-devkit/[email protected]
Create a Native App in Okta

Log in to your Okta Developer account (or sign up if you don't have an account).

  • From the Applications page, choose Add Application.
  • On the Create New Application page, select Native as the platform and click Next.
  • Give your app a memorable name, select Refresh Token as a grant type, and click Done.
  • Click the Edit button and add a Logout redirect URI that matches the default Login redirect URI (e.g., com.okta.dev-123456:/callback).
  • Click Save.
Install React Native OIDC Login

In a terminal, navigate into your ReactNativeLogin directory and install OktaDev Schematics:

npm i @oktadev/[email protected]

NOTE: If you have a React Native 0.60.x app, use @oktadev/[email protected]. The only difference between the two is the tests.

Run the add-auth schematic in your ReactNativeLogin project.

schematics @oktadev/schematics:add-auth

You will be prompted for an issuer and a clientId. You can find your issuer under API > Authorization Servers on Okta.

The client ID will be on your application screen.

This process will take a minute to complete.

Configure Your iOS Project to use Swift

React Native uses Objective-C, but the Okta React Native library uses Swift. Because of this, you have to add a Swift file in your iOS project for it to compile. Run the following command to open your native iOS project in Xcode.

open ios/ReactNativeLogin.xcworkspace

To add a Swift file, complete the following steps:

  1. Right-click on your project and select New File....
  2. Select Swift File, and click Next.
  3. Enter a name (e.g., Polyfill) and click Create.
  4. If prompted for a header file, it is not required to create one.

Then cd into ReactNativeLogin/ios and run pod install.

TIP: If you don't have CocoaPoads installed, you can install it with gem install cocoapods.

Run Your React Native App on iOS

Navigate back to the root directory of your app. Start your app and you should be able to authenticate with Okta. 🎉

react-native run-ios

Once you're signed in, you'll see options to log out, get the user's information from an ID token, and get the user's information from the React Native SDK's getUser() method (a.k.a. the request).

NOTE: The prompt when you click Login cannot be avoided. This is an iOS safety mechanism. It also pops up when you log out. See this issue for more information.

Run Your React Native App on Android

The schematic you ran modifies all the necessary files for Android; there are no code modifications required!

You will need to run an AVD (Android Virtual Device) before starting your app, or you can plug in your Android phone and use that. If you have neither, launch Android Studio and go to Tools > AVD Manager. Click Create Virtual Device at the bottom and select a phone of your choice. I chose a Pixel 3 XL with Android 10.

Start your AVD, then your app, and authenticate with Okta. 🎊

react-native run-android

Click the Get User from ID Token button to confirm you can retrieve the user's information.

Run Installed React Native Authentication Tests

In addition to integrating login, OktaDev Schematics also installed some tests that verify login and authentication work. Run npm test to see these tests run in your terminal.

Snapshot Summary
› 1 snapshot written from 1 test suite.

Test Suites: 2 passed, 2 total
Tests:       12 passed, 12 total
Snapshots:   1 written, 1 total
Time:        8.952s
Ran all test suites.

NOTE: OktaDev Schematics puts tests in a tests directory rather than the default __tests__ directory because Angular Schematics uses double underscore as a placeholder.

Using a Custom Login Screen with Okta

This example showed you how to add an OIDC flow that opens a browser when a user logs in and logs out. If you require a smoother login experience that doesn't pop open a browser, see Okta's Custom Sign-In Example for example code that shows you how to implement that type of flow.

Add OIDC Login the Hard Way

The previous section showed you how to use OktaDev Schematics to quickly add a login feature (and tests!) to a React Native application. However, you might have an existing React Native application that doesn't have the same structure as a brand new React Native application.

This section shows you everything that OktaDev Schematics does for you, in detail.

Create a project with React Native CLI and install Okta's SDK.

react-native init ReactNativeLogin
cd ReactNativeLogin
npm install @okta/[email protected]

For iOS, modify ios/Podfile to change it from iOS 9 to iOS 11.

platform :ios, '11.0'

Open your project in Xcode.

open ios/ReactNativeLogin.xcworkspace

Add a Swift file.

  1. Right-click on your project and select New File....
  2. Select Swift File, and click Next.
  3. Enter a name (e.g., Polyfill) and click Create.
  4. If prompted for a header file, it is not required to create one.

Install iOS native dependencies with CocoaPods.

cd ios
pod install

Add Jest and Enzyme to Test Your React Native Login

Jest is a library for testing JavaScript apps and Enzyme is a library that makes it easier to select and query elements in tests. They're often used alongside one another.

Install testing dependencies with npm.

npm i [email protected] [email protected] [email protected] [email protected]

Then change your jest key in package.json to match the following:

"jest": {
 "preset": "react-native",
 "automock": false,
 "transformIgnorePatterns": [
   "node_modules/([email protected]|react-native)"
 ],
 "setupFiles": [
   "./setupJest.js"
 ]
}

Create setupJest.js to polyfill React Native for Okta.

// Required to correctly polyfill React-Native

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { NativeModules } from 'react-native';

configure({ adapter: new Adapter() });

global.XMLHttpRequest = jest.fn();
global.fetch = jest.fn();

if (typeof window !== 'object') {
 global.window = global;
 global.window.navigator = {};
}

NativeModules.OktaSdkBridge = {
 createConfig: jest.fn(),
 signIn: jest.fn(),
 signOut: jest.fn(),
 getAccessToken: jest.fn(),
 getIdToken: jest.fn(),
 getUser: jest.fn(),
 isAuthenticated: jest.fn(),
 revokeAccessToken: jest.fn(),
 revokeIdToken: jest.fn(),
 revokeRefreshToken: jest.fn(),
 introspectAccessToken: jest.fn(),
 introspectIdToken: jest.fn(),
 introspectRefreshToken: jest.fn(),
 refreshTokens: jest.fn(),
};

Create Auth.js to handle your authentication code.

import React, { Component, Fragment } from 'react';

import { SafeAreaView, ScrollView, Button, StyleSheet, Text, View } from 'react-native';
import { createConfig, signIn, signOut, isAuthenticated, getUser, getUserFromIdToken, EventEmitter } from '@okta/okta-react-native';
import configFile from './auth.config';

export default class Auth extends Component {
 constructor() {
   super();
   this.state = {
     authenticated: false,
     context: null,
   };
   this.checkAuthentication = this.checkAuthentication.bind(this);
 }

 async componentDidMount() {
   let that = this;
   EventEmitter.addListener('signInSuccess', function (e: Event) {
     that.setState({authenticated: true});
     that.setContext('Logged in!');
   });
   EventEmitter.addListener('signOutSuccess', function (e: Event) {
     that.setState({authenticated: false});
     that.setContext('Logged out!');
   });
   EventEmitter.addListener('onError', function (e: Event) {
     console.warn(e);
     that.setContext(e.error_message);
   });
   EventEmitter.addListener('onCancelled', function (e: Event) {
     console.warn(e);
   });
   await createConfig({
     clientId: configFile.oidc.clientId,
     redirectUri: configFile.oidc.redirectUri,
     endSessionRedirectUri: configFile.oidc.endSessionRedirectUri,
     discoveryUri: configFile.oidc.discoveryUri,
     scopes: configFile.oidc.scopes,
     requireHardwareBackedKeyStore: configFile.oidc.requireHardwareBackedKeyStore,
   });
   this.checkAuthentication();
 }

 componentWillUnmount() {
   EventEmitter.removeAllListeners('signInSuccess');
   EventEmitter.removeAllListeners('signOutSuccess');
   EventEmitter.removeAllListeners('onError');
   EventEmitter.removeAllListeners('onCancelled');
 }

 async componentDidUpdate() {
   this.checkAuthentication();
 }

 async checkAuthentication() {
   const result = await isAuthenticated();
   if (result.authenticated !== this.state.authenticated) {
     this.setState({authenticated: result.authenticated});
   }
 }

 async login() {
   signIn();
 }

 async logout() {
   signOut();
 }

 async getUserIdToken() {
   let user = await getUserFromIdToken();
   this.setContext(JSON.stringify(user, null, 2));
 }

 async getMyUser() {
   let user = await getUser();
   this.setContext(JSON.stringify(user, null, 2));
 }

 setContext = message => {
   this.setState({
     context: message,
   });
 };

 renderButtons() {
   if (this.state.authenticated) {
     return (
       <View style={styles.buttonContainer}>
         <View style={styles.button}>
           <Button
             onPress={async () => {
               this.getUserIdToken();
             }}
             title="Get User From Id Token"
           />
         </View>
       </View>
     );
   }
 }

 render() {
   return (
     <Fragment>
       <SafeAreaView style={styles.container}>
         <View style={styles.buttonContainer}>
           <View style={styles.button}>
             {this.state.authenticated ? (
               <Button
                 style={styles.button}
                 testID="logoutButton"
                 onPress={async () => { this.logout() }}
                 title="Logout"
               />
             ) : (
               <Button
                 style={styles.button}
                 testID="loginButton"
                 onPress={async () => { this.login() }}
                 title="Login"
               />
             )}
           </View>
         </View>
         {this.renderButtons()}
         <ScrollView
           contentInsetAdjustmentBehavior="automatic"
           style={styles.context}>
           <Text>{this.state.context}</Text>
         </ScrollView>
       </SafeAreaView>
     </Fragment>
   );
 }
}

const styles = StyleSheet.create({
 buttonContainer: {
   flexDirection: 'column',
   justifyContent: 'space-between',
 },
 button: {
   width: 300,
   height: 40,
   marginTop: 10,
 },
 container: {
   flex: 1,
   flexDirection: 'column',
   alignItems: 'center',
 }
});

You might notice it imports a config file at the top.

import configFile from './auth.config';

Create auth.config with your OIDC settings from Okta.

export default {
 oidc: {
   clientId: '$yourClientId',
   redirectUri: 'com.okta.dev-#######:/callback',
   endSessionRedirectUri: 'com.okta.dev-#######:/callback',
   discoveryUri: 'https://dev-#######.okta.com/oauth2/default',
   scopes: ['openid', 'profile', 'offline_access'],
   requireHardwareBackedKeyStore: false,
 },
};

Create an app on Okta to get the values for $yourClientId and ######.

  • From the Applications page, choose Add Application.
  • On the Create New Application page, select Native as the platform and click Next.
  • Give your app a memorable name, select Refresh Token as a grant type, and click Done.
  • Click the Edit button and add a Logout redirect URI that matches the default Login redirect URI (e.g., com.okta.dev-123456:/callback).
  • Click Save.

In App.js, import Auth.

import Auth from './Auth';

And use it in a new <View /> after the Hermes logic.

<ScrollView
 contentInsetAdjustmentBehavior="automatic"
 style={styles.scrollView}>
 <Header />
 {global.HermesInternal == null ? null : (
   <View style={styles.engine}>
     <Text style={styles.footer}>Engine: Hermes</Text>
   </View>
 )}
 <View style={styles.body}>
   <View style={styles.sectionContainer}>
     <Text style={styles.sectionTitle}>Step Zero</Text>
     <Text style={styles.sectionDescription}>
       Use <Text style={styles.highlight}>Okta</Text> for
       authentication.
     </Text>
     <Auth />
   </View>

At this point, your tests will not pass because Okta uses an EventEmitter to communicate between components.

Add React Native Authentication Tests

To mock the native event emitter that Okta uses, add a mock for it in __tests__/App-test.js.

/_*
_ @format
*/

import 'react-native';
import React from 'react';
import renderer from 'react-test-renderer';
import App from '../App';

jest.mock(
 '../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter',
);

it('renders correctly', () => {
 renderer.create(<App />);
});

To make sure all the login and authentication logic works, create __tests__/Auth-test.js.

import React from 'react';
import Auth from '../Auth';
import { shallow } from 'enzyme';
import renderer from 'react-test-renderer';
import { waitForState } from 'enzyme-async-helpers';
import { NativeEventEmitter } from 'react-native';

const nativeEmitter = new NativeEventEmitter();

jest
 .mock(
   '../node_modules/react-native/Libraries/Components/StatusBar/StatusBar',
   () => 'StatusBar',
 )
 .mock(
   '../node_modules/react-native/Libraries/Components/ScrollView/ScrollView',
   () => 'ScrollView',
 )
 .mock(
   '../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter',
 );

global.fetch = jest
 .fn()
 .mockImplementation(() => {
   return new Promise((resolve, reject) => {
     resolve({
       json: () => {
         return {
           user: [{ foo: 'foo', bar: 'bar' }],
         }
       },
       ok: true,
     });
   });
 })
 .mockImplementationOnce(() => {
   return new Promise((resolve, reject) => {
     resolve({
       json: () => {
         return {
           userinfo_endpoint: 'dummy_endpoint',
         }
       },
       ok: true,
     });
   });
 });

describe('auth setup', () => {
 it('should render without crashing', () => {
   const rendered = renderer.create(<Auth />).toJSON();
   expect(rendered).toBeTruthy();
 });

 it('should render correctly', () => {
   const rendered = renderer.create(<Auth />).toJSON();
   expect(rendered).toMatchSnapshot();
 });

 it('should initialize with default state', () => {
   const wrapper = shallow(<Auth />);
   expect(wrapper.state().authenticated).toBe(false);
   expect(wrapper.state().context).toBe(null);
 });

 it('should render login button if not authenticated', () => {
   const wrapper = shallow(<Auth />);
   const loginButton = wrapper.find('Button').get(0);
   expect(loginButton.props.title).toBe('Login');
 });

 it('should render logout and get user info buttons if authenticated', () => {
   const wrapper = shallow(<Auth />);
   wrapper.setState({authenticated: true});
   const logoutButton = wrapper.find('Button').get(0);
   const getUserFromIdButton = wrapper.find('Button').get(1);
   const getUserButton = wrapper.find('Button').get(2);
   expect(logoutButton.props.title).toBe('Logout');
   expect(getUserFromIdButton.props.title).toBe('Get User From Id Token');
   expect(getUserButton.props.title).toBe('Get User From Request');
 });

 it('should not render login button if authenticated', () => {
   const wrapper = shallow(<Auth />);
   wrapper.setState({authenticated: true});
   const loginButton = wrapper.find('Button').get(0);
   expect(loginButton.props.title).not.toBe('Login');
 });

 it('should not render logout and get user info buttons if not authenticated', () => {
   const wrapper = shallow(<Auth />);
   const logoutButton = wrapper.find('Button').get(0);
   const getUserFromIdButton = wrapper.find('Button').get(1);
   const getUserButton = wrapper.find('Button').get(2);
   expect(logoutButton.props.title).not.toBe('Logout');
   expect(getUserFromIdButton).toBe(undefined);
   expect(getUserButton).toBe(undefined);
 });
});

describe('authentication flow', () => {
 it('should detect when the user has logged in', async () => {
   const wrapper = shallow(<Auth />);
   const loginButton = wrapper.find('Button').get(0);
   await loginButton.props.onPress();
   expect(loginButton.props.title).toBe('Login');
   nativeEmitter.emit('signInSuccess');
   expect(wrapper.state().authenticated).toBe(true);
   expect(wrapper.state().context).toBe('Logged in!');
 });

 it('should detect when the user has signed out', async () => {
   const wrapper = shallow(<Auth />);
   wrapper.setState({authenticated: true});
   const logoutButton = wrapper.find('Button').get(0);
   await logoutButton.props.onPress();
   expect(logoutButton.props.title).toBe('Logout');
   nativeEmitter.emit('signOutSuccess');
   expect(wrapper.state().authenticated).toBe(false);
   expect(wrapper.state().context).toBe('Logged out!');
 });

 it('should return user profile information from id token', async () => {
   const mockGetIdToken = require('react-native').NativeModules.OktaSdkBridge.getIdToken;
   mockGetIdToken.mockImplementationOnce(() => {
     // id_token returns { a: 'b' }
     return {'id_token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoiYiJ9.jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8'};
   });
   const wrapper = shallow(<Auth />);
   wrapper.setState({authenticated: true});
   const profileButton = wrapper.find('Button').get(1);
   await profileButton.props.onPress();
   await waitForState(wrapper, state => state.context !== null);
   expect(profileButton.props.title).toBe('Get User From Id Token');
   expect(wrapper.state().context).toContain('"a": "b"');
 });
});

Run npm test to bask in the fruits of your labor!

To run your app on iOS, use react-native run-ios.

To run it on Android, you'll need to modify your Gradle build files.

Okta's React Native SDK depends on the Okta OIDC Android library. You have to add this library through Gradle.

Add Okta's BinTray repo to android/build.gradle, under allprojects -> repositories.

maven { url  "https://dl.bintray.com/okta/com.okta.android" }

Make sure your minSdkVersion is 19 in android/build.gradle.

Define a redirect scheme to capture the authorization redirect. In android/app/build.gradle, under android -> defaultConfig, add:

manifestPlaceholders = [ appAuthRedirectScheme: 'com.okta.dev-###### ]

Finally, start a virtual device (or plug in your phone), and run react-native run-android.

Learn More About React Native and OIDC Login

This tutorial showed you how to add a login feature to a React Native application. You learned that OAuth 2.0 is an authorization protocol and OIDC is an authentication layer on top of it. You also used PKCE (Public Key Code Exchange) in your implementation, which is the more secure way to implement OAuth 2.0 in mobile applications.

I hope you enjoy your React Native development journey and its fast refresh feature!