Open dhirendrarathod2000 opened 6 years ago
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 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?
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.
Any update about this issue ?!!!!
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} />
}
}
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.
@dhirendrarathod2000 Did you find a solution?
Hi! Is there a solution using react navigation v5?
Anything for react navigation 5?
Hey all - think I may have at least a partial solution here. With @react-navigation/native@5.8.10
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:
App
component named initialDeepLink
NavigationContainer
's getInitialUrl()
prop added in v5.8.x
(documented here)await Linking.getInitialUrl()
(or w/e works for your app)initialDeepLink
state variable (causing single re-render)React.createContext()
object specifically for passing the initial deep link, something like InitialDeepLinkContext
<InitialDeepLinkContext.Provider value={initialDeepLink}>
in the App
component as a parent of the RootNavigator
(in case of example above)Login
screen using React context: const initialDeepLink = useContext(InitialDeepLinkContext)
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';
useLinkingProps
hook from react-navigation
to create an onPress
handler for navigating via deep link from a component: const { onPress: navigateInsideApp } = useLinkingProps({ to: initialOrDefaultDeepLink }}
navigateInsideApp()
to navigate to the default screen, or continue following an initial deep linkinitialDeepLink
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 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?
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.
Quick summary of what I did, maybe it could help others.
I followed a different path from what @chas-ps tried (that probably works) :
is_authentication
is null), I keep the bootsplash shownis_authentication
is null, I DO NOT RENDER the NavigationContainer (I simply return null, it's not an issue as the splashscreen is displayed)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.
amazing @Md-Mudassir47 , thats working for me :)
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>
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.
Authstack does not work if it is conditional. The solution is to remove all conditions. However, this contradicts existing documentation.
@muiruri smart workaround ! :)
Authstack does not work if it is conditional
Why ?
I don't know exactly why. Presumably the condition is checked as soon as the application is opened.
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.
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
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
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.
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.
unable to pass params from deep link. getting undefined
when running:
npx uri-scheme open [prefix]://news/3 --android
NewsScreen.js
import React from 'react';
const NewsScreen = ({ route, navigation }) => {
console.log(route.params); // undefined
};
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: '*',
},
},
};
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>;
};
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>
);
};
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>
);
};
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 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:
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.
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:
react-native-splash-screen
<SplashScreen>
componentwhen i deep like now it goes like this:
react-native-splash-screen
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.
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>
.
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:
<SplashScreen>
<SplashScreen>
before <NavigationContainer>
<NavigationContainer>
when we know if the user is authenticated or notnow i can deep link to specific screens
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.
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 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.
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.
@chas-ps In general it is a good solution, but there are some caviats to note about this solution:
Any perfect solution?
@Adnan-Bacic worked for me (RN + RN for web) and was not complex to implement. I still need to refactor and searching for other builtin solutions
@huberzeljko i don't get if your suggestion addresses the same issue ? how ?
@Adnan-Bacic @davidlevy I don't really understand the proposal. How are you going to manage the auth in the SplashScreen
without being in the sign in screen?
@Estevete you manage the auth in the SplashScreen
component. here you find out if they are logged in our not. only when you know this, then you can render the NavigationContainer
which conditionally renders screens depending if users are logged in or not.
But the problem @dhirendrarathod2000 mentioned persists... right?
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.
In your proposal, your SplashScreen
will check if you are logged in or not. You know you are not logged in, so you display the SignIn
screen because that's the flow you have in your NavigationContainer
, but then, there's no redirection. Or am I missing anything?
hmm i guess you are right. i didnt think about that. since in my case i was just trying to get it to work with auth flow.
i havent had to do this yet, but i assume to do this you would have to save the users deep link string. then after they are authenticated, you would run Linking.openUrl()
on the saved deep link.
so if the deep link is: myapp://movies/123
, maybe you would save that string in AsyncStorage/Redux. then when they are authenticated and you render the correct screen/stack, you call Linking.openUrl(myapp://movies/123)
and then most likely clear the string wherever you saved it, after running the deep link.
dont know if there are any issues with saving the deep link like this, but i assume you have to persist the string somehow for a few seconds at least, while the authentication happens.
a problem i can see with this approach is that you would (maybe) need to find a way to only run this function if you know the deep link should run only if the user needs to log in. since you have no reason to run it if they are logged in already.
Tried all the solutions above, this works for me finally:
Create linking config to NavigationContainer, pass linking as a prop.
Linking.openUrl(linkYouStored)
, then react-navigation will handle this link using your linking configuration.Return different NavigationContainer before and after authentication, but linking prop should only be added to NavigationContainer after authentication.
If pre-login flow has multiple screens, add NavigationContainer, otherwise just return a single component.
if(loading) {
return <SplashComponent />
}
if(!authenticated) {
return (
<NavitationContainer> //Do not add linking prop.
<Stack.Navigator>
<Stack.Screen
name="Landing"
component={Landing}
/>
<Stack.Screen
name="SignIn"
component={SignIn}
/>
</Stack.Navigator>
</NavigationContainer>
)
}
const linking = {
prefixes: [...],
config: {...},
getInitialURL: async () => {
const { isAvailable } = utils().playServicesAvailability;
if (isAvailable) {
const initLink = await dynamicLinks().getInitialLink();
if (initLink) {
return initLink.url;
}
}
// As a fallback, you may want to do the default deep link handling
const url = await Linking.getInitialURL();
return url;
},
subscribe: (listener) => {
// Listen to incoming links from Firebase Dynamic Links
const unsubscribeFirebase = dynamicLinks().onLink((link) => {
listener(link.url);
});
// Listen to incoming links from deep linking
Linking.addEventListener('url', (link) => {
listener(link.url);
});
return () => {
// Clean up the event listeners
unsubscribeFirebase();
Linking.removeAllListeners('url');
};
},
};
return (
<NavigationContainer linking={linking}> //Add linking prop
<Stack.Navigator>
<Stack.Screen
name="Home"
component={Home}
/>
<Stack.Screen
name="Profile"
component={Profile}
/>
...
...
</Stack.Navigator>
</NavigationContainer>
Return different NavigationContainer before and after authentication, but linking prop should only be added to NavigationContainer after authentication.
this is not recommended, as seen here: https://reactnavigation.org/docs/getting-started/#wrapping-your-app-in-navigationcontainer
Note: In a typical React Native app, the NavigationContainer should be only used once in your app at the root. You shouldn't nest multiple NavigationContainers unless you have a specific use case for them.
i think it would be better to delay rendering the NavigationContainer until the authentication logic is done running. i made a comment higher up how i did this.
@Adnan-Bacic What I did was conditionally return different NavigationContainers. The note says we should not nest NavigationContainers i.e., one container inside another. I think those are two different things, correct me if I am wrong.
Will implement and check your solution as well when I get a chance. But this solution is working for my use case, where I need deep linking only after login. If we need deep linking both before and after login, my solution does not work.
If I give linking prop to NavigationConatiner pre-login, getInitialUrl is called on that container, therefore the state is lost and after login the new container does not receive the initialUrl. We can, of course maintain the initialUrl in state and somehow use it later, but my use case is so simple and straight-forward.
@Tharun122 fair enough. i think i have read before somewhere in the docs that there shouldt be more than 1 NavigationContainer, but i cant find anything.
the only thing i can find is from version 4: https://reactnavigation.org/docs/4.x/common-mistakes#explicitly-rendering-more-than-one-navigator
but maybe something changed from version 4 to 5, as NavigationContainer didnt exist before version 5.
My solution is a bit of a hack. I have a Screen
component that wraps all of my screens. I have added a needsAuth
prop which causes the component to check if the user is logged in. If they're not, render nothing and push a login screen. Pop login screen once auth is complete and the user can keep doing whatever they wanna do!
Has anyone found a solid solution to this? I'm putting together a solution, but it feels brittle having to take the path that was receiving on the initial deepLink and walk back through the StackParamList to find the correct screen name to navigate to.
I used to get the initial url by using expo-linking useURL()
hook. Then assign it to a state constant initialUrl
, and when tapping login, if initialUrl !== null
, then openURL(initialUrl )
but for some reason that stopped working. Now it opens the url in the browser.
hey there i have a solution it working for both when app is killed also when app is in foreground or background state for the app is killed in background state i am checking if the app is opened by url if then it will redirect to certasin screen following is my solution // root navigator const RootNavigator = () => { const { state: {userData}, fetchDataFromLocalStorage, } = useContext(UserContext);
// check if user sign in already useEffect(() => { fetchDataFromLocalStorage({ navigation: async e => { try { // when app open by deep link it will redirect to our screen //if link is attach to some screen // a hot fix bacause we are using authentication flow //in which we are conditional changing stack so link try to find screen but // we are still cheking if user logged in or not let initURL = await Linking.getInitialURL(); console.log('respose of get inital url is', initURL); if (initURL) { // if url link then navigate to that linked screen await Linking.openURL(initURL); } SplashScreen.hide(); } catch (error) { console.log('unable to get url', error); } }, }); }, []);
return (
Implement getInitialURL
properly.
...,
config: deepLinkConfigs,
async getInitialURL() {
const url = decodeURI((await Linking.getInitialURL()) ?? '');
if (url) return url;
// Check if there is an initial firebase notification
const message = await messaging().getInitialNotification();
// Get deep link from data
// if this is undefined, the app will open the default/home page
return message?.data?.link ?? '';
},
...
I solve this problem by using MMKV(similar AsyncStorage).
const linking = {
prefixes: [...],
config: deepLinksConfig,
async getInitialURL() {
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
if (url != null) return url;
const initialNotification = await notifee.getInitialNotification();
if (!initialNotification) return null;
const {data} = initialNotification.notification;
if (!data || !data.link) return null;
storage.set('openedDeepLinkUrl', data.link as string);
},
};
// ...set app loading and login status true
const openedDeepLinkUrl = storage.getString('openedDeepLinkUrl');
if (openedDeepLinkUrl) {
await Linking.openURL(openedDeepLinkUrl);
storage.delete('openedDeepLinkUrl');
}
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