react-navigation.github.io icon indicating copy to clipboard operation
react-navigation.github.io copied to clipboard

Deep linking with Authentication Flow

Open dhirendrarathod2000 opened this issue 6 years ago • 46 comments

Hello,

I am really new to this library and working on my app to replace old deprecated navigator with this one.

I got stuck when i need to verify authentication with deep linking. May be we can put it in the docs on how to handle authentication flow with deep linking.

Thanks D

dhirendrarathod2000 avatar Mar 20 '18 19:03 dhirendrarathod2000

hi there! i do not quite understand this question. i'd suggest formulating it more clearly and reaching out to one of the sources for help listed on https://reactnavigation.org/help

brentvatne avatar Mar 23 '18 19:03 brentvatne

@brentvatne This question is about Scenarios like below

I have a URL that points to the app and i open it, now that URL is suppose to take me to the area of the application that is not accessible without authentication. (this can also be from notifications) How to handle those scenarios with deep linking with react-navigation?

dhirendrarathod2000 avatar Mar 26 '18 13:03 dhirendrarathod2000

for example.

Let's say you got an email from netflix. In the email, You click on the movie you wanted to see, but you are not signed in. So you go to login screen instead of the movie screen and after login you go to the movie screen not home screen.

dhirendrarathod2000 avatar Mar 26 '18 17:03 dhirendrarathod2000

Any update about this issue ?!!!!

codiri avatar Mar 07 '19 16:03 codiri

nope, i'd suggest just opting out of react-navigation's automatic url handling and handle the deep link on your own.

const AppContainer = createAppContainer(/* your stuff here */);

export default class App extends React.Component {
   /* Add your own deep linking logic here */

  render() {
    return <AppContainer enableURLHandling={false} />
  }
}

brentvatne avatar Mar 07 '19 18:03 brentvatne

Hi! I just found a nice solution on StackOverflow, it goes with something like:

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);

Where, if link requires auth, it redirects to SplashLoading, forwarding initial path and params so it can recover later, when authenticated, the initially wanted user flow.

AnthonyDugarte avatar Jan 12 '20 00:01 AnthonyDugarte

@dhirendrarathod2000 Did you find a solution?

mohamed-ikram avatar Aug 08 '20 05:08 mohamed-ikram

Hi! Is there a solution using react navigation v5?

sclavijo93 avatar Nov 02 '20 12:11 sclavijo93

Anything for react navigation 5?

Lokendra-rawat avatar Nov 06 '20 21:11 Lokendra-rawat

Hey all - think I may have at least a partial solution here. With @react-navigation/[email protected] I was able to get deep link redirection to the authentication screen working if the user isn't logged in, and I have an idea about how you could implement continuing to the original deep link after login is completed too. However, to do this you have to use >=v5.8.x

First, follow their guide on setting up authentication flows here. Specifically, set up your navigation screens so that if the user is logged out, the non-authentication screens won't even be rendered at all within your navigators

const App = () => {
  return (
    <NavigationContainer>
      <RootNavigator/>
    </NavigationContainer>
  )
}

const RootNavigator = () => {
  // ...
  // Some useState and useEffect code for setting and retrieving
  // the accessToken from AsyncStorage and checking it isn't expired
  // ...
  
  const isAuthenticated = !!someState.accessToken;
  
  return (
    <Stack.Navigator>
      {!isAuthenticated ? (
        <Stack.Screen name="Login" component={LoginScreen}/>
      ) : (
        <>
          <Stack.Screen name="Home" component={HomeScreen}/>
          <Stack.Screen name="Settings" component={SettingsScreen}/>
        </>
      )}
    </Stack.Navigator>
  );
}

Next, we can use the NavigationContainer's linking prop to specify how we want our deep links to map to our screens/routes, as outlined here. We will also specify the prefixes that we want react-navigation to allow for deep links. Note that the deep link config object has to match the navigation structure of our app

This configuration object now accepts * to define a NotFound screen, that will be rendered if a user tries to navigate to a deep link path that doesn't exist. Because we are conditionally rendering our screens based on the authentication state of the user, we can use this option to set the LoginScreen as the default screen when a deep link doesn't match a screen's route

const App = () => {
  return (
    <NavigationContainer 
      linking={{
        prefixes: ['someapp://', 'https://dynamiclinks.someapp.com'],
        config: {
          screens: {
            Login: '*',
            Home: {
              path: 'home'
            },
            Settings: {
              path: 'settings'
            }
          }
        }
      }}
    >
      // ... 
    </NavigationContainer>
  )
}

Now if a user opens the app with the deep link someapp://home and they aren't logged in anymore, they will be taken to the Login screen because the Home screen won't be rendered at all in our navigation structure. If the user was logged in, the screen will exist and they will be taken there

Note that this approach won't work fully if you want to be able to deep link a user to the Login screen while they are still logged in, as that screen won't be rendered. Also note there won't be problems with navigating logged in users to stale deep links (like user deletable content) because the login screen isn't rendered and the wildcard fallback will fail gracefully

I didn't have time to do the second part of this, which is "forwarding" the initial deep link to our Login component, so that it can know whether the user opened the app via deep link and wishes to continue to a specific screen other than the Home screen (e.g. the Settings screen). However, I think this could be easily done by:

  1. Add a new state variable to the App component named initialDeepLink
  2. Register the NavigationContainer's getInitialUrl() prop added in v5.8.x (documented here)
  3. Inside that method, grab the initial deep link by the default method await Linking.getInitialUrl() (or w/e works for your app)
  4. Update the new initialDeepLink state variable (causing single re-render)
  5. Setup a React.createContext() object specifically for passing the initial deep link, something like InitialDeepLinkContext
  6. Register the <InitialDeepLinkContext.Provider value={initialDeepLink}> in the App component as a parent of the RootNavigator (in case of example above)
  7. Access the initial deep link from the Login screen using React context: const initialDeepLink = useContext(InitialDeepLinkContext)
  8. Check if the initialDeepLink is null or empty, if so, define a default deep link to navigate to (e.g. someapp://home), something like const initialOrDefaultDeepLink = initialDeepLink || 'someapp://home';
  9. Use the newer useLinkingProps hook from react-navigation to create an onPress handler for navigating via deep link from a component: const { onPress: navigateInsideApp } = useLinkingProps({ to: initialOrDefaultDeepLink }}
  10. When the user presses your login button, call navigateInsideApp() to navigate to the default screen, or continue following an initial deep link
  11. (?) Somehow clean-up the initialDeepLink state to null so that logging out and back in doesn't take the user to settings instead of home. Also would prevent a horrible UX-loop if the initial link is to deleted content...

Long-winded post but hopefully this helps someone out!

EDIT: Added some more findings with this approach. I think there also might be a better solution by hoisting auth state up into the App component to conditionally update the deep link config, etc.

chas-ps avatar Dec 19 '20 17:12 chas-ps

@chas-ps generally, after login, the RootNavigator will undergo a state change since isAuthenticated is likely changed.

We are experiencing an issue where we are trying to navigate to an authenticated screen but the authenticated state is updated after we try to navigate causing the NavigationContainer to be re-rendered and thus losing our new navigation state.

We have been struggling with this for a few days, is there any combination of hooks that we can use to listen to be sure the RootNavigator is in the authenticated state, then navigate to our desired screen?

huttotw avatar Dec 23 '20 15:12 huttotw

is there any combination of hooks that we can use to listen to be sure the RootNavigator is in the authenticated state, then navigate to our desired screen

The useEffect hook in the component containing the container should fire after that component finishes re-rendering (commit phase), which would include all children re-rendering as well. However, instead of manually navigating to a screen, you could just sort your screens so that the screen you want to be focused is the first one in the conditionl, and it would automatically happen after re-rendering.

satya164 avatar Dec 23 '20 16:12 satya164

Quick summary of what I did, maybe it could help others.

I followed a different path from what @chas-ps tried (that probably works) :

  1. While I am doing the authentication call to my server (is_authentication is null), I keep the bootsplash shown
  2. As long as is_authentication is null, I DO NOT RENDER the NavigationContainer (I simply return null, it's not an issue as the splashscreen is displayed)
  3. When my authentication call is over, is_authentication is true or false, and then I can render the NavigationContainer
  4. This is kind of a workaround, but it allows "waiting for the authentication status to be known" before rendering the whole NavigationContainer, so when the linking configuration is loaded by react-navigation, the status is already known

christophemenager avatar Sep 01 '21 10:09 christophemenager

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.

Md-Mudassir47 avatar Sep 23 '21 08:09 Md-Mudassir47

amazing @Md-Mudassir47 , thats working for me :)

Jonovono avatar Sep 29 '21 23:09 Jonovono

Is there an implementation suitable for Auth flow? I can't render all stack.screens at once due to conditions.

<RootSiblingParent>
  <NavigationContainer linking={linkingOptions}>
    <RootStack.Navigator
      screenOptions={{
        headerShown: false,
        animationEnabled: false,
      }}>
      {state.loading && (
        <RootStack.Screen
          name={'Splash'}
          component={Splash}
          screenOptions={{
            headerShown: false,
          }}
        />
      )}
      {state.user?.auth ? (
        <RootStack.Screen name={'MainStack'}>
          {() => (
            <MainStackNavigator category={state.user.category} />
          )}
        </RootStack.Screen>
      ) : (
        <RootStack.Screen
          name={'AuthStack'}
          component={AuthStackNavigator}
        />
      )}
    </RootStack.Navigator>
  </NavigationContainer>
</RootSiblingParent>

mehmetcansahin avatar Oct 27 '21 15:10 mehmetcansahin

I was able to work around this issue by using the same route name for AuthStack initial screen and Authenticated Stack screen.

Something like this

isSignedIn ? (
  <>
    <Stack.Screen name="Initial" component={HomeScreen} />
    <Stack.Screen name="Profile" component={ProfileScreen} />
    <Stack.Screen name="Settings" component={SettingsScreen} />
  </>
) : (
  <>
    <Stack.Screen name="Initial" component={SignInScreen} />
    <Stack.Screen name="SignUp" component={SignUpScreen} />
  </>
)

Then in my linking definition

const config = {
  screens: {
    Initial: 'app/:some-special-screen-with-params',
  },
};

if the app is running and authenticated, it would pass the parameters to the HomeScreen. if the app is not running or not authenticated, it would pass the parameters to the SignInScreen and after signing handle the parameters accordingly.

muiruri avatar Nov 25 '21 09:11 muiruri

Authstack does not work if it is conditional. The solution is to remove all conditions. However, this contradicts existing documentation.

mehmetcansahin avatar Nov 26 '21 12:11 mehmetcansahin

@muiruri smart workaround ! :)

christophemenager avatar Nov 29 '21 08:11 christophemenager

Authstack does not work if it is conditional

Why ?

christophemenager avatar Nov 29 '21 08:11 christophemenager

I don't know exactly why. Presumably the condition is checked as soon as the application is opened.

mehmetcansahin avatar Nov 29 '21 10:11 mehmetcansahin

I'm not sure if anyone has ran into this related issue (maybe this is what @mehmetcansahin is referring to when they say conditional auth stack doesn't work) but when opening a deep link for a closed app, we actually get an undefined exception if the user is not logged in useDescriptors.tsx at line 153

The exception is undefined is not an object (evaluating 'config.props')

  >((acc, route, i) => {
    const config = screens[route.name];
    const screen = config.props;
    const navigation = navigations[route.key];

    const optionsList = [

I would imagine this should fail more gracefully in general but our work around is somewhat obvious: We just don't include the routes we need authentication for in our linking config:

  if (!isLoggedIn) {
    return ({
      prefixes: [],
      config: {
        screens: {
          Login: "login"
        }
      }
    });
  }

  return ({
    config: {
      screens: {
        Home: "home",
        // All other routes here
      }
    },
})

Now back to the original post, where you might want to navigate to that initial url after login (if it is valid).

For react navigation v6, I imagine you could store the initial url from getInitialURL in state and then using a useEffect on isLoggedIn update the navigation state for that url? using getStateFromPathDefault ?

Sorry for rambling, may try this later. Am sort of surprised there isn't really an established pattern for this though.

ludu12 avatar Dec 02 '21 16:12 ludu12

Hi together,

I'm a supabase user and there is a problem (but i don't know if it's really a problem) where you get the session at the second request (the first one is null). I also using the official auth flow from react-navigation and no suggested solution is working. If the app is in foreground my pushes will be redirected to the right screen.

Are there any official solutions from the react-navigation development team?

Best regards, Andy

megacherry avatar Dec 07 '21 22:12 megacherry

Hi together,

I'm a supabase user and there is a problem (but i don't know if it's really a problem) where you get the session at the second request (the first one is null). I also using the official auth flow from react-navigation and no suggested solution is working. If the app is in foreground my pushes will be redirected to the right screen.

Are there any official solutions from the react-navigation development team?

Best regards, Andy

I found a workaround to handle it, here's the solution

Md-Mudassir47 avatar Dec 08 '21 05:12 Md-Mudassir47

From what ive read, i cant seem to find any "official/correct" way to handle this scenario.

The way i see it, there are 2 ways to handle deep linking to a specific screen with auth flow.

  1. Navigation happens automatically
  2. Navigation happens manually

And i think the automatic is obviously the prefered one.

For example on the auth flow docs: https://reactnavigation.org/docs/auth-flow/

There is this section: https://reactnavigation.org/docs/auth-flow/#dont-manually-navigate-when-conditionally-rendering-screens Which explains why you shouldt navigate manually, but let react-navigation navigate when the auth status changes.

On the configuring links docs which explains how to handle deep linking: https://reactnavigation.org/docs/configuring-links it says: When you specify the linking prop, React Navigation will handle incoming links automatically.

Both documentations emphasize that you should not navigate manually, instead set it up correctly so that react-navigation can handle it. However, i cannot find any documentation for handling deep linking with auth flow. in other words, how to handle deep linking when certain screens are not initially available.(which brought me and others to this thread).

Since no "official" way of doing this is documented, there generally isent a sufficient answer for these questions on stackoverflow for example. and any example i can find seems to implement their own custom solution, perhaps where they navigate manually. (I personally worked on a project previously where the deep linking was handled manually. Which was a mess. They manually ran a regex on the deep link to grab the query params) Which totally goes against what the docs recommend doing.

From what i see, developers have to pick 1 of them to be handled automatically, while the other has to be handled manually.

So im curious, what is the ideal thing to do here? Custom implementations seem to have been the solution for the past few years.

Adnan-Bacic avatar Dec 14 '21 13:12 Adnan-Bacic

unable to pass params from deep link. getting undefined when running:

npx uri-scheme open [prefix]://news/3 --android

  1. NewsScreen.js

    import React from 'react';
    
    const NewsScreen = ({ route, navigation }) => {
       console.log(route.params); // undefined
    };
    
  2. Linking.js

    import LINKING_PREFIXES from 'src/shared/constants';
    
    export const linking = {
       prefixes: LINKING_PREFIXES,
       config: {
          screens: {
             Home: {
                screens: {
                   News: {
                      path: 'news/:id?',
                      parse: {
                         id: id => `${id}`,
                      },
                   },
                },
             },
             NotFound: '*',
          },
       },
    };
    
  3. Router.js

    import React from 'react';
    import { NavigationContainer } from '@react-navigation/native';
    import {useAuth} from 'src/contexts/AuthContext';
    import {Loading} from 'src/components/Loading';
    import {AppStack} from './AppStack';
    import {AuthStack} from './AuthStack';
    import {GuestStack} from './GuestStack';
    import SplashScreen from 'src/screens/guest/SplashScreen';
    import linking from './Linking.js';
    
    export const Router = () => {
       const {authData, loading, isFirstTime, appBooting} = useAuth();
    
       if (loading) {
          return <Loading />;
       }
    
       const loadRoutes = () => {
          if (appBooting) {
             return <SplashScreen />;
          }
    
          if (isFirstTime) {
             return <GuestStack />;
          }
    
          if (!authData || !authData.name || !authData.confirmed) {
             return <AuthStack />;
          }
    
          return <AppStack />;
       };
    
       return <NavigationContainer>{loadRoutes()}</NavigationContainer>;
    };
    
  4. AppStack.js

    import React from 'react';
    import {createDrawerNavigator} from '@react-navigation/drawer';
    import {SpeedNewsStack} from 'src/routes/NewsStack';
    
    const Drawer = createDrawerNavigator();
    
    export const AppStack = () => {
       return (
          <Drawer.Navigator>
             <Drawer.Screen name="Home" component={NewsStack} />
          </Drawer.Navigator>
       );
    };
    
  5. NewsStack.js

    import React from 'react';
    import {createStackNavigator} from '@react-navigation/stack';
    import SpeedNewsScreen from 'src/screens/NewsScreen';
    
    const Stack = createStackNavigator();
    
    export const NewsStack = () => {
       return (
          <Stack.Navigator>
             <Stack.Screen name="News" component={NewsScreen} />
          </Stack.Navigator>
       );
    };
    

pokhiii avatar Jan 11 '22 22:01 pokhiii

so i think i found something that works where you can have both automatic auth flow and automatic deep linking.

im using react-navigaton v6, but v5 should be the same. dont know about v4 and earlier, but i would hope at this point that most projects would be updated to at least v5.

The problem

the problem here is basically that we render the <NavigationContainer> BEFORE we know if the user is logged-in or not. so even if we correctly conditionally render the individual screens, we still render the whole <NavigationContainer>.

this is a problem because the initial authentication when the user opens the app isent done when we render the <NavigationContainer>. this means the linking prop takes effect right away, but since the authentication hasent happened yet, we can never be taken to a screen for logged-in users.

so we have to make sure we know if the user is logged-in or not, before rendering the <NavigationContainer> with the linking prop. this way it can correctly automatically take us to the screen for logged-in users.

i will show a stripped-down version of how the setup was for me:

  1. reduce amount of repetitive boilerplate when there are multiple screens
  2. most imports which are specific to my project are removed
  3. typescript.

then i will show the changes i made to get it working.

my App.tsx just returns a <AppNavigationContainer> which is where my <NavigationContainer> is.

Initial setup

AppNavigationContainer.tsx:

import React, { useEffect } from 'react';
import { Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import NativeSplashScreen from 'react-native-splash-screen';
import RootStackNavigator from './rootStack';

export const AppNavigationContainer = () => {

  useEffect(() => {
    setTimeout(() => {
      NativeSplashScreen.hide();
    }, 500);
  }, []);

  const linking = {
    // todo: NotFound screen
    prefixes: ['myprefix://'],
    config: {
      initialRouteName: 'Test',
      screens: {
        Test: 'test',
        BenefitsGuide: 'benefitsGuide',
        Main: {
          initialRouteName: 'Home',
          screens: {
            Membership: 'membership',
            Usages: 'usages',
          },
        },
      },
    },
  };

  return (
    <NavigationContainer
      linking={linking}
      fallback={(
        <Text>
          fallback component here
        </Text>
      )}
    >
      <RootStackNavigator />
    </NavigationContainer>
  );
};

my <RootStackNavigator> is just a simple createStackNavigator() with some screens that are available to users at all times(such as "Terms" and "Error"). its not really relevant but i will post it just to clear any possible confustion:

RootStackNavigator.tsx:

import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import MainStackNavigator from '../mainStack';

const RootStack = createStackNavigator();

const RootStackNavigator = () => {
  return (
    <RootStack.Navigator
      initialRouteName="Main"
    >
      <RootStack.Screen
        name="Main"
        component={MainStackNavigator}
      />
      {/* Benefits */}
      <RootStack.Screen
        name="BenefitsGuide"
        component={BenefitsGuideScreen}
      />
      {/* other screens here */}
    </RootStack.Navigator>
  );
};

export default RootStackNavigator;

now as you can see in my linking configuration, it follows the setup. one of the screens is called "Main", which is where i have another createStackNavigator(). this is where the actual conditional rendering happens.

MainStackNavigator.tsx:

import React, { useEffect } from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { useSelector } from 'react-redux';

const MainStack = createStackNavigator();

const MainStackNavigator = () => {
  const auth = useSelector(authSelector);
  const splash = useSelector(splashSelector);

  return (
    <MainStack.Navigator>

      {/* Splash */}
      {splash.isLoading && (
        <MainStack.Screen
          name="Splash"
          component={SplashScreen}
        />
      )}

      {/* Authentication */}
      {auth.isLoading || !auth.isLoggedIn ? (
        <MainStack.Screen
          name="SignIn"
          component={SignInScreen}
        />
      ) : (
        <>
          {/* Home */}
          <MainStack.Screen
            name="Home"
            component={HomeScreen}
          />

          {/* Usages */}
          <MainStack.Screen
            name="Usages"
            component={UsagesScreen}
          />

          {/* Membership */}
          <MainStack.Screen
            name="Membership"
            component={MembershipScreen}
          />

          {/* other screens here */}
        </>
      )}
    </MainStack.Navigator>
  );
};

export default MainStackNavigator;

so pretty standard here. i show a custom <SplashScreen> component. this screen shows the user an animation while they are being authenticated, to determine if they are logged-in or not. not to be confused with a native splash screen. this app uses react-native-splash-screen to show a native splash screen.

then we conditionally render the screens depending on the users auth status. i use redux so the variables are pulled in from there. not logged-in shows the <Signin> screen and logged-in are shown the <Home> screen.

your setup may be sligtly different, but i suspect it will be at least somewhat similar.

at this point, the only thing that works is just opening the app with a deep link or opening one of the screens that are always available. in my example here im showing the <UsagesScreen> and <MembershipScreen>, and i cant deep link to those.

so to test it out on ios my deep link is:

npx uri-scheme open myprefix:// --ios

this opens the app.

i can also open a screen that is available at all times:

npx uri-scheme open myprefix://benefitsGuide --ios

and there is another problem for my app specifically. i previously mentioned that the authentication happens in my <SplashScreen> component. by doing this i never see this component. due to the deep link i open the specified screen.

when i normally open the app it goes like this:

  1. shows a native splash screen with react-native-splash-screen
  2. show my custom <SplashScreen> component
  3. show screen depending on auth status

when i deep like now it goes like this:

  1. shows a native splash screen with react-native-splash-screen
  2. shows screen i deep linked to.

so i dont get the animation on my custom <SplashScreen> component. but most importantly i dont get the authentication code i have in that file. and again, that is just me setup. you may not have a custom <SplashScreen> component, and you may not run any authentication logic in there.

but lets say the user is logged-in and i want to open the "usages" screen. i would do:

npx uri-scheme open myprefix://usages --ios

this wont work. i still just be shown the <HomeScreen>. the reason for this is, as i mentioned earlier, that the user is not yet authenticated when we render the <NavigationContainer> with the linking prop.

Solution

so the solution wasent really that complicated(in my case). as i have mentioned, the recurring problem was rendering my <NavigationContainer> before we authenticate the user.

so since in my app, the authentication happens in my custom <SplashScreen> component, i have to render that before my <NavigationContainer>. while it is currently a screen in my <NavigationContainer>.

i basically just had to move my custom <SplashScreen> component. it could no longer be a part of my <NavigationContainer>.

instead i had to early return the custom <SplashScreen> component before the <NavigationContainer>.

Updated files

MainStackNavigator.tsx:

remove custom <SplashScreen> component

- {/* Splash */}
- {splash.isLoading && (
-  <MainStack.Screen
-    name="Splash"
-    component={SplashScreen}
-  />
- )}

so now its:

import React, { useEffect } from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { useSelector } from 'react-redux';

const MainStack = createStackNavigator();

const MainStackNavigator = () => {
  const auth = useSelector(authSelector);
- const splash = useSelector(splashSelector);

  return (
    <MainStack.Navigator>

-     {/* Splash */}
-     {splash.isLoading && (
-       <MainStack.Screen
-         name="Splash"
-         component={SplashScreen}
-       />
-     )}

      {/* Authentication */}
      {auth.isLoading || !auth.isLoggedIn ? (
        <MainStack.Screen
          name="SignIn"
          component={SignInScreen}
        />
      ) : (
        <>
          {/* Home */}
          <MainStack.Screen
            name="Home"
            component={HomeScreen}
          />

          {/* Usages */}
          <MainStack.Screen
            name="Usages"
            component={UsagesScreen}
          />

          {/* Membership */}
          <MainStack.Screen
            name="Membership"
            component={MembershipScreen}
          />

          {/* other screens here */}
        </>
      )}
    </MainStack.Navigator>
  );
};

export default MainStackNavigator;

AppNavigationContainer.tsx:

add custom <SplashScreen> component

+ if (splash.isLoading) {
+    return (
+      <SplashScreen />
+    );
+ }

so now its:

import React, { useEffect } from 'react';
import { Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import NativeSplashScreen from 'react-native-splash-screen';
import RootStackNavigator from './rootStack';
+ import { useSelector } from 'react-redux';

export const AppNavigationContainer = () => {
+ const splash = useSelector(splashSelector);

  useEffect(() => {
    setTimeout(() => {
      NativeSplashScreen.hide();
    }, 500);
  }, []);

  const linking = {
    // todo: NotFound screen
    prefixes: ['myprefix://'],
    config: {
      initialRouteName: 'Test',
      screens: {
        Test: 'test',
        BenefitsGuide: 'benefitsGuide',
        Main: {
          initialRouteName: 'Home',
          screens: {
            Membership: 'membership',
            Usages: 'usages',
          },
        },
      },
    },
  };

+ if (splash.isLoading) {
+    return (
+      <SplashScreen />
+    );
+ }

  return (
    <NavigationContainer
      linking={linking}
      fallback={(
        <Text>
          fallback component here
        </Text>
      )}
    >
      <RootStackNavigator />
    </NavigationContainer>
  );
};

so in short:

  1. authentication happens in <SplashScreen>
  2. early return <SplashScreen> before <NavigationContainer>
  3. render <NavigationContainer> when we know if the user is authenticated or not

now i can deep link to specific screens

Extra notes

sorry if this was too long. i just spent a lot of time trying to combine auth flow with deep linking. and i wanted to share my findings. and i wanted to explain it fully so there would be as little confusion as possible.

just a few things to keep in mind that i found.

'fallback' prop

in my <NavigationContainer> i use the fallback prop: https://reactnavigation.org/docs/navigation-container#fallback

which says:

If you have a native splash screen, please use onReady instead of fallback prop.

but the onReady prop: https://reactnavigation.org/docs/navigation-container#onready

says:

Function which is called after the navigation container and all its children finish mounting for the first time. You can use it for:

Making sure that the ref is usable. See docs regarding initialization of the ref for more details.

Hiding your native splash screen

as i have mentioned before i also have a native splash screen. however i cant use the onReady prop to hide it. as i have to hide it earlier to render an animation in my custom <SplashScreen>, where my authentication happens. and i only render the <NavigationContainer> after all of that. so here i couldt follow the documentions recommendation and instead use the fallback prop.

react-native "Linking" module

react-native also has the Linking module you can import: https://reactnative.dev/docs/linking

here there are useful functions such as getInitialURL() and addEventListener(). i havent used these at all here. they seem useful, but react-navigation handles all the routing for me. i dont know if there is something that i am missing.

Adnan-Bacic avatar Apr 07 '22 14:04 Adnan-Bacic

I've extracted the whole linking configuration into function

function createLinking({ isAuthenticated,}: { isAuthenticated: boolean}): LinkingOptions<RootStackParamList> { ... }

This way you can do additional checks in your configuration and link to appropriate screen. I'm using React.Context for state management, so whenever authentication state is changed, whole navigation is rerendered (this is done anyway) with new configuration and correct isAuthenticated flag.

Let me know if someone has any issues with it, but for now it seems to work for all the test cases.

huberzeljko avatar Jul 08 '22 06:07 huberzeljko

@chas-ps In general it is a good solution, but there are some caviats to note about this solution:

  1. The wildcard '*' will only be used as a fallback if the pattern is not found in the linking configuration. That means that the user tries to access myapp://home, he will not be directed to SignIn page. There will be an error message in the console saying Home component is not rendered
  2. If we call navigateInsideApp() using the onPress handler of the sign in button, React Navigation can't navigate to the desired component, as it was not rendered yet. We need no handle the deeplink after React Navigation reacts to the authentication.
  3. The method getInitialUrl is not called everytime the user opens a deeplink, I couldn't understand why. When I close the app and open a deeplink, getInitialUrl is called successfully. But when I do it with the app running in background, getInitialUrl is not called.

guto-marzagao avatar Jul 24 '22 16:07 guto-marzagao

Any perfect solution?

SohelIslamImran avatar Aug 07 '22 18:08 SohelIslamImran