React Native, React Navigation auth flow and Auth0

I am trying to get Auth0 to play nicely with React Navigation in a native project, I have followed this article Authentication flows | React Navigation as I am trying to separate out some routes in my native app:

  • Loading (contains a stack to a loading page, used when retrieving token from storage and checking validity)
  • Unauthenticated (contains the guest stack for welcome and login pages)
  • User (contains bottom tabs and stacks to serve up main app functionality)

My App.js file looks like this which on the surface works fine, when I login it moves to the user navigation, logout reverts back to the guest navigation:

import * as React from 'react';
import { View } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import EncryptedStorage from 'react-native-encrypted-storage';
import { ActivityIndicator } from 'react-native-paper';
import Auth0 from "react-native-auth0";
import jwt_decode from "jwt-decode";

import WelcomePage from './src/surfaces/loggedout/welcome';
import { SocialFeed } from './src/surfaces/social';

const AuthContext = React.createContext();
const auth0 = new Auth0({ 
  domain: 'xxxxxxxx.auth0.com',
  clientId: 'xxxxxxxxxxxxxxxxxxxxxxxx'
});

function LoadingScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <ActivityIndicator animating={true} color='#02BFAB' />
    </View>
  );
}

const Stack = createNativeStackNavigator();
const Tab = createMaterialBottomTabNavigator();

export default function App() {
  const [state, dispatch] = React.useReducer(
    (prevState, action) => {
      switch (action.type) {
        case 'RESTORE_TOKEN':
          return {
            ...prevState,
            userToken: action.token,
            isLoading: false,
          };
        case 'SIGN_IN':
          return {
            ...prevState,
            isSignout: false,
            userToken: action.token,
          };
        case 'SIGN_OUT':
          return {
            ...prevState,
            isSignout: true,
            userToken: null,
          };
      }
    },
    {
      isLoading: true,
      isSignout: false,
      userToken: null,
    }
  );
  console.log("State: " + JSON.stringify(state));

  React.useEffect(() => {
    async function bootstrapAsync() {
      let session = await EncryptedStorage.getItem("user_session");
      return await session;
    }
    
    bootstrapAsync()
      .then(response => {
        if (response) {
          let obj = JSON.parse(response);
          console.log('Session data: ' + JSON.stringify(obj));
          return obj;
        } else {
          console.log('Session empty.');
          return null;
        };
      })
      .then(token => {
        dispatch({ type: 'RESTORE_TOKEN', token: token.accessToken });
      })
      .catch(e => console.log(e));
    
  }, []);

  const authContext = React.useMemo(
    () => ({
      signIn: async () => {
        auth0.webAuth
          .authorize({
            scope: "openid email"
          })
          .then(res => {
            var idToken = res.idToken;
            var decodedIdToken = jwt_decode(idToken);
            console.log("ID token: " + idToken + ". Decoded: " + JSON.stringify(decodedIdToken));

            async function storeUserSession() {
              try {
                  let response = await EncryptedStorage.setItem(
                      "user_session",
                      JSON.stringify({
                          accessToken : res.accessToken,
                          idToken : res.idToken,
                          decodedToken : decodedIdToken
                      })
                  );
                  console.log("EncryptedStorage setItem response: " + response)
              } catch (error) {
                console.log("Error storing session in EncryptedStorage: " + JSON.stringify(error))
              }
            };
            storeUserSession();
            console.log("Logged in.");

            return res.accessToken;

          })
          .then( token => {
              dispatch({ type: 'SIGN_IN', token: token });
          })
          .catch(error => console.log("Auth0 webAuth error: " + JSON.stringify(error)))
      },
      signOut: async () => {
        auth0.webAuth
          .clearSession()
          .then(success => {
              async function clearStorage() {
                  try {
                      let response = await EncryptedStorage.clear();
                      console.log("EncryptedStorage clear response: " + response)
                  } catch (error) {
                    console.log("Error clearing EncryptedStorage: " + JSON.stringify(error))
                  }
              };
              clearStorage();
              dispatch({ type: 'SIGN_OUT' });
              console.log("Logged out.");
          })
          .catch(error => console.log("Auth0 clearSession error: " + JSON.stringify(error)))
      },
    }),
    []
  );

  if (state.isLoading) {
    // Loading Spinner
    return (
      <AuthContext.Provider value={authContext}>
        <Stack.Navigator>
          <Stack.Screen name="Splash" options={{ headerShown: false }} component={LoadingScreen} />
        </Stack.Navigator>
      </AuthContext.Provider>
    )
  } else if (state.userToken == null) {
    // Logged Out Stack
    return (
      <AuthContext.Provider value={authContext}>
        <Stack.Navigator>
          <Stack.Screen
            name="Welcome"
            component={WelcomePage}
            initialParams={{ login: authContext.signIn }}
            options={{
              title: 'Sign in',
              headerShown: false,
              animationTypeForReplace: state.isSignout ? 'pop' : 'push',
            }}
          />
        </Stack.Navigator>
      </AuthContext.Provider>
    )
  } else {
    // User Routes
    return (
      <AuthContext.Provider value={authContext} >
        <Tab.Navigator>
          <Tab.Screen name="Feed" component={SocialFeed} initialParams={{ logout: authContext.signOut }} />
        </Tab.Navigator>
      </AuthContext.Provider>
    )
  };
}

This is a little flawed however as passing the authContext signIn and signOut down into children is giving me non-serializable values warnings and as I build out my app functionality I am not sure if that will even be sustainable. I also need to be able to access the tokens via the state in other pages (ie. access the decoded id token in a child page so I can call a 3rd party API with a custom claim key value).

So my question is… is there a better way to do this? I have spent the best part of a day getting to this point but its not great :slight_smile:

What I was trying to achieve was something similar to the useAuth0 hook from React SDK (auth0/auth0-react) to be able to access the authentication state for switching the Navigation Stacks and user (inc. itToken claims) for injecting Auth0 user data anywhere in my native project where I might need it.