Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React-navigation: Deep linking with authentication

I am building a mobile app with react-native and the react-navigation library for managing the navigation in my app. Right now, my app looks something like that:

App [SwitchNavigator]
    Splash [Screen]
    Auth [Screen]
    MainApp [StackNavigator]
        Home [Screen]            (/home)
        Profile [Screen]         (/profile)
        Notifications [Screen]   (/notifications)

I have integrated Deep Linking with the patterns above for the screens Home, Profile and Notifications, and it works as expected. The issue I am facing is how to manage my user's authentication when using a deep link. Right now whenever I open a deep link (myapp://profile for instance) the app takes me on the screen whether or not I am authenticated. What I would want it to do is to check before in AsyncStorage if there is a userToken and if there isn't or it is not valid anymore then just redirect on the Auth screen.

I set up the authentication flow in almost exactly the same way as described here. So when my application starts the Splash screen checks in the user's phone if there is a valid token and sends him either on the Auth screen or Home screen.

The only solution I have come up with for now is to direct every deep link to Splash, authentify my user, and then parse the link to navigate to the good screen. So for example when a user opens myapp://profile, I open the app on Splash, validate the token, then parse the url (/profile), and finally redirect either to Auth or Profile.

Is that the good way to do so, or does react-navigation provide a better way to do this ? The Deep linking page on their website is a little light.

Thanks for the help !

like image 660
Zuma Avatar asked Jan 24 '19 16:01

Zuma


Video Answer


2 Answers

My setup is similar to yours. I followed Authentication flows · React Navigation and SplashScreen - Expo Documentation to set up my Auth flow, so I was a little disappointed that it was a challenge to get deep links to flow through it as well. I was able to get this working by customizing my main switch navigator, the approach is similar to what you stated was the solution you have for now. I just wanted to share my solution for this so there’s a concrete example of how it’s possible to get working. I have my main switch navigator set up like this (also I’m using TypeScript so ignore the type definitions if they are unfamiliar):

const MainNavigation = createSwitchNavigator(
  {
    SplashLoading,
    Onboarding: OnboardingStackNavigator,
    App: AppNavigator,
  },
  {
    initialRouteName: 'SplashLoading',
  }
);

const previousGetActionForPathAndParams =
  MainNavigation.router.getActionForPathAndParams;

Object.assign(MainNavigation.router, {
  getActionForPathAndParams(path: string, params: any) {
    const isAuthLink = path.startsWith('auth-link');

    if (isAuthLink) {
      return NavigationActions.navigate({
        routeName: 'SplashLoading',
        params: { ...params, path },
      });
    }

    return previousGetActionForPathAndParams(path, params);
  },
});

export const AppNavigation = createAppContainer(MainNavigation);

Any deep link you want to route through your auth flow will need to start with auth-link, or whatever you choose to prepend it with. Here is what SplashLoading looks like:

export const SplashLoading = (props: NavigationScreenProps) => {
  const [isSplashReady, setIsSplashReady] = useState(false);

  const _cacheFonts: CacheFontsFn = fonts =>
    fonts.map(font => Font.loadAsync(font as any));

  const _cacheSplashAssets = () => {
    const splashIcon = require(splashIconPath);
    return Asset.fromModule(splashIcon).downloadAsync();
  };

  const _cacheAppAssets = async () => {
    SplashScreen.hide();
    const fontAssetPromises = _cacheFonts(fontMap);
    return Promise.all([...fontAssetPromises]);
  };

  const _initializeApp = async () => {
    // Cache assets
    await _cacheAppAssets();

    // Check if user is logged in
    const sessionId = await SecureStore.getItemAsync(CCSID_KEY);

      // Get deep linking params
    const params = props.navigation.state.params;
    let action: any;

    if (params && params.routeName) {
      const { routeName, ...routeParams } = params;
      action = NavigationActions.navigate({ routeName, params: routeParams });
    }

    // If not logged in, navigate to Auth flow
    if (!sessionId) {
      return props.navigation.dispatch(
        NavigationActions.navigate({
          routeName: 'Onboarding',
          action,
        })
      );
    }

    // Otherwise, navigate to App flow
    return props.navigation.navigate(
      NavigationActions.navigate({
        routeName: 'App',
        action,
      })
    );
  };

  if (!isSplashReady) {
    return (
      <AppLoading
        startAsync={_cacheSplashAssets}
        onFinish={() => setIsSplashReady(true)}
        onError={console.warn}
        autoHideSplash={false}
      />
    );
  }

  return (
    <View style={{ flex: 1 }}>
      <Image source={require(splashIconPath)} onLoad={_initializeApp} />
    </View>
  );
};

I create the deep link with a routeName query param, which is the name of the screen to navigate to after the auth check has been performed (you can obviously add whatever other query params you need). Since my SplashLoading screen handles loading all fonts/assets as well as auth checking, I need every deep link to route through it. I was facing the issue where I would manually quit the app from multitasking, tap a deep link url, and have the app crash because the deep link bypassed SplashLoading so fonts weren’t loaded.

The approach above declares an action variable, which if not set will do nothing. If the routeName query param is not undefined, I set the action variable. This makes it so once the Switch router decides which path to take based on auth (Onboarding or App), that route gets the child action and navigates to the routeName after exiting the auth/splash loading flow.

Here’s an example link I created that is working fine with this system: exp://192.168.1.7:19000/--/auth-link?routeName=ForgotPasswordChange&cacheKey=a9b3ra50-5fc2-4er7-b4e7-0d6c0925c536

Hopefully the library authors will make this a natively supported feature in the future so the hacks aren’t necessary. I'd love to see what you came up with as well!

like image 183
nhuesmann Avatar answered Oct 23 '22 09:10

nhuesmann


On my side I achieved this without having to manually parse the route to extract path & params. Here are the steps:

  • getting the navigation action returned by: getActionForPathAndParams
  • passing the navigation action to the Authentication view as param
  • then when the authentication succeed or if the authentication is already ok I dispatch the navigation action to go on the intended route

const previousGetActionForPathAndParams = AppContainer.router.getActionForPathAndParams

Object.assign(AppContainer.router, {
  getActionForPathAndParams(path: string, params: NavigationParams) {
    const navigationAction = previousGetActionForPathAndParams(path, params)
    return NavigationActions.navigate({
      routeName: 'Authentication',
      params: { navigationAction }
    })
  }
})

Then In the Authentication view:

const navigationAction = this.navigation.getParam('navigationAction')
if (navigationAction)
  this.props.navigation.dispatch(navigationAction)
like image 4
Charles Avatar answered Oct 23 '22 11:10

Charles