React-navigation: Deep linking with authentication
Asked Answered
A

5

29

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 !

Aft answered 24/1, 2019 at 16:15 Comment(1)
How were you able to manage deep linking and authentication flow? Everytime I try to open a deep link (myApp://profile), it auto navigates to Home screen (because I'm authenticated)Okechuku
L
10

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!

Lexie answered 8/3, 2019 at 18:8 Comment(2)
This was very helpful. Helped me with a very similar situation. Also wanted to know, how are you getting the nested action out once you navigate to Auth flow. Couple of lines of code will be really helpful.Azucenaazure
I'm having the same problem as @VikalpJain. How can I execute the sub action after successful login?Vermiculation
C
4

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)
Calhoun answered 26/11, 2019 at 15:59 Comment(0)
F
3

I ended up using a custom URI to intercept the deeplink launch, and then passing those params to the intended route. My loading screen handles the auth check.

const previousGetActionForPathAndParams = AppContainer.router.getActionForPathAndParams

Object.assign(AppContainer.router, {
  getActionForPathAndParams (path, params) {
    if (path === 'auth' && params.routeName && params.userId ) {
      // returns a profile navigate action for myApp://auth?routeName=chat&userId=1234
      return NavigationActions.navigate({
        routeName: 'Loading',
          params: { ...params, path },
      })
    }
    return previousGetActionForPathAndParams(path, params)
  },
})

https://reactnavigation.org/docs/en/routers.html#handling-custom-uris

Then, in my Loading Route, I parse the params like normal but then route to the intended location, passing them once again.

const userId = this.props.navigation.getParam('userId')

https://reactnavigation.org/docs/en/params.html

Felisha answered 16/4, 2019 at 2:42 Comment(0)
H
3

I found an easier way, i'm maintaining the linking in a separate file and importing it in the main App.js

linking.js

 const config = {
   screens: {
      Home:'home',
      Profile:'profile,
     },
  };
    
 const linking = {
  prefixes: ['demo://app'],
  config,
};
     
 export default linking;

App.js

& during login I keep the token inside async storage and when user logs out the token is deleted. Based on the availability of token i'm attaching the linking to navigation and detaching it using state & when its detached it falls-back to SplashScreen.

Make sure to set initialRouteName="SplashScreen"

import React, {useState, useEffect} from 'react';
import {Linking} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {createStackNavigator} from '@react-navigation/stack';
import {NavigationContainer} from '@react-navigation/native';
import linking from './utils/linking';
import {Home, Profile, SplashScreen} from './components';

const Stack = createStackNavigator();

// This will be used to retrieve the AsyncStorage String value
const getData = async (key) => {
  try {
    const value = await AsyncStorage.getItem(key);
    return value != null ? value : '';
  } catch (error) {
    console.error(`Error Caught while getting async storage data: ${error}`);
  }
};

function _handleOpenUrl(event) {
  console.log('handleOpenUrl', event.url);
}

const App = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => {
    // Checks if the user is logged in or not, if not logged in then
    // the app prevents the access to deep link & falls back to splash screen.
    getData('studentToken').then((token) => {
      if (token === '' || token === undefined) setIsLoggedIn(false);
      else setIsLoggedIn(true);
    });

    Linking.addEventListener('url', _handleOpenUrl);
    return () => {
      Linking.removeEventListener('url', _handleOpenUrl);
    };
  }, []);

  return (
    //linking is enabled only if the user is logged in
    <NavigationContainer linking={isLoggedIn && linking}>
      <Stack.Navigator
        initialRouteName="SplashScreen"
        screenOptions={{...TransitionPresets.SlideFromRightIOS}}>
        <Stack.Screen
          name="SplashScreen"
          component={SplashScreen}
          options={{headerShown: false}}
        />
        <Stack.Screen
          name="Home"
          component={Home}
          options={{headerShown: false, gestureEnabled: false}}
        />
        <Stack.Screen
          name="Profile"
          component={Profile}
          options={{headerShown: false, gestureEnabled: false}}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default App;

When a logged in user opens the deep link from notification then it will take him to the respective deep linked screen, if he's not logged in then it will open from splash screen.

Hereabout answered 23/9, 2021 at 8:47 Comment(1)
This will not take users to their desired screen after they log in through arriving from a deep link.Agrology
S
2

I created a package for Auto Deep linking with Authentication Flow.

You can try it. auth-linking https://github.com/SohelIslamImran/auth-linking

auth-linking

Auto Deep linking with Authentication Flow

Deep linking is very easy to use with authentication. But some people take it hard way. So this package will help you to achieve the easiest way to handle Deep linking with Authentication Flow.

Installation

npm install auth-linking
yarn add auth-linking

Usage

AuthLinkingProvider

You need to wrap your app with AuthLinkingProvider.

import AuthLinkingProvider from "auth-linking";

...

const App = () => {
  return (
    <AuthLinkingProvider onAuthChange={onAuthChange}>
      {/* Your app components */}
    </AuthLinkingProvider>
  );
};

export default App;

onAuthChange prop

You need to provide an onAuthChange prop to AuthLinkingProvider. This a function that should return a promise with the user or truthy value (if logged in) and null or falsy (if the user is not logged in).

    const onAuthChange = () => {
      return new Promise((resolve, reject) => {
        onAuthStateChanged(auth, resolve, reject);
      });
    };
    ...

    <AuthLinkingProvider onAuthChange={onAuthChange}>

useAutoRedirectToDeepLink

Call this hook inside a screen that will render after all auth flow is completed. So this hook will automatically redirect to the deep link through which the app is opened.

import { useAutoRedirectToDeepLink } from "auth-linking";

...

const Home = () => {
  useAutoRedirectToDeepLink()

  return (
    <View>{...}</View>
  );
};

All done.

Szczecin answered 10/10, 2022 at 19:53 Comment(1)
The linked package hasn't been updated since 2022 :(Jadotville

© 2022 - 2024 — McMap. All rights reserved.