React Native ( React Navigation ) Authentication Flow - react-native

I am trying to implement a react navigation authentication flow where there are total three screens and two screen of them are in Authentication ( Login Screen and Registration Screen ) which are used to authenticate a user and the other one is Home Screen ( Feed Screen ) which shows the application's home.
Authenication Screens are in a stack navigator which is AuhenticationNavigation and the home screen is in the HomeScreenNav stack navigator. I have these two stack navigator which are used to navigate.
As in the Code Snippet.
The logic I want to implement is that after successful login the screen should navigate to Feed Screen and in Feed Screen I have a logout function which is used to logout and change the persistent state which is isLoggedIn to know that a user is logged in or not.
const RootStack = createStackNavigator();
class AuthenticationNavigation extends Component {
render() {
return(
<RootStack.Navigator headerMode='none'>
<RootStack.Screen name='Login' component={Login}/>
<RootStack.Screen name='Register' component={Register} />
</RootStack.Navigator>
);
}
}
class HomeScreenNav extends Component {
render() {
return(
<RootStack.Navigator headerMode='none'>
<RootStack.Screen name='Feed' component={HomeScreen} />
</RootStack.Navigator>
);
}
}
Two ways I tried to resolve it:
1 :-
<NavigationContainer>
{ this.state.isLoggedIn ? (
<HomeScreenNav />
) : (
<AuthenticationNavigation />
) }
</NavigationContainer>
Above Code snippet shows me Feed Screen is the isLoggedIn is true and evaluates to Authentication Screen is the it is false.
But what if I want to navigate to the Auth Screens after a successfull logout in Feed Screen.
I have also the tried the react navigation authentication flow from navigation docs but I am not able to resolve my error.
2:-
<NavigationContainer>
<RootStack.Navigator
initialRouteName={ (this.state.isLoggedIn == true) ? 'Home' : 'Auth' }
headerMode='none'
>
<RootStack.Screen
name='Home'
component={HomeScreenNav}
/>
<RootStack.Screen
name='Auth'
component={AuthenticationNavigation}
/>
</RootStack.Navigator>
</NavigationContainer>
In this way I am unable to open the Home Screen when isLoggedIn evaluates to true and it shows me Auth screen. I think that the problem is in isLoggedIn state but again I checked whether the state is really working or not properly and it resloved into a successful evaluation. But still it is evaluating as false.
Here is the process of retrieving the state which is isLoggedIn.
constructor(props) {
super(props);
this.state = {
tokenId: '',
isLoggedIn: false,
}
}
componentDidMount = async () => {
const token = await AsyncStorage.getItem('token');
this.setState({tokenId: token});
this.setState({isLoggedIn: !Object.is(this.state.tokenId, null)});
await AsyncStorage.setItem('isLoggedIn', this.state.isLoggedIn ? 'Y' : 'N');
console.log(this.state.tokenId, this.state.isLoggedIn);
}
Request: Please provide a proper solution not any documentation links because I have searched for this problem many times on many platforms.

Related

React Native - React Navigation - Go to app root (first route)

I'm using react navigation in a react native project and i am unable to find how to go back to navigation root (first screen in app) from an arbitrary screen. Navigation is like this:
<NavigationContainer>
<Stack.Navigator>
{!loggedIn ? (
// Login
<Stack.Group>
<Stack.Screen name="LogInScreen" component={LogInScreen} />
</Stack.Group>
) : (
// App Screens
<Stack.Group>
// This Tab navigator may have more stacks inside
<Stack.Screen name="AppTabsNavigator" component={AppTabsNavigator} />
<Stack.Screen name="SomeErrorOutsideTabs" component={SomeErrorOutsideTabs} />
</Stack.Group>
)}
// Error Modals
<Stack.Group>
<Stack.Screen name="SomeErrorScreen" component={SomeErrorScreen} />
</Stack.Group>
<Stack.Navigator>
</NavigationContrainer>
Use cases are:
User in LogInScreen causes some error -> SomeErrorScreen shows -> User press button to go back to LogInScreen.
User in anywhere else causes some error -> SomeErrorScreen shows -> User press button to go back to first screen in AppTabsNavigator (sometimes does not work depending on inner stacks).
The summary is, no matter where you are, when you press button in SomeErrorScreen you must go back to "first app screen", which is LogInScreen if not logged in or first tab of AppTabsNavigator if logged in.
I have tried using popToTop() but it does not work when there are inners Stacks like SomeResourceStack inside AppTabsNavigator to contain index and show of resource, for example:
User in AppTabsNavigator > SomeResourceStack > index access to AppTabsNavigator > SomeResourceStack > show, cause SomeErrorScreen to appear and when going back, he goes back to AppTabsNavigator > SomeResourceStack > show instead of AppTabsNavigator first tab.
Answer:
The right answer is to use reset like Anthony Marino said and combine it with Ararat Ispiroglu suggestion. I controlled Android's back button behavior as well:
...
import { BackHandler } from 'react-native'
import { useFocusEffect } from '#react-navigation/native'
import { useSelector } from 'react-redux'
...
const SomeErrorScreen = ({ navigation }) => {
...
const loggedIn = useSelector(state => state.auth.loggedIn)
const onGoingBackButtonPress = React.useCallback(() => {
navigation.reset({
index: 0,
routes: [{ name: !loggedIn ? 'LogInScreen' : 'AppTabsNavigator' }]
})
}, [navigation, loggedIn])
const androidBackButtonCallback = React.useCallback(() => {
const onBackPress = () => {
onGoingBackButtonPress()
return true
}
BackHandler.addEventListener('hardwareBackPress', onBackPress)
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress)
}, [onGoingBackButtonPress])
useFocusEffect(androidBackButtonCallback)
...
return <View>
<Button onPress={onGoingBackButtonPress } />
</View>
}
export default SomeErrorScreen
You can reset your navigator
navigation.reset({
index: 0,
routes: [
{
name: 'SomeStackScreen',
params: { someParam: 'Param1' },
},
],
})
As seen in the docs here
You have two options to do that with your current navigation setup;
a) When you need to go back to root you can check if it's logged in or not
simply navigation.navigate(loggedIn ? "AppTabsNavigator" : "LogInScreen")
b) You can set the same name for the top level screen in the navigation for both logged and non-logged in screens. And navigation will pickup whichever screen is mouthed.
{!loggedIn ? (
// Login
<Stack.Group>
<Stack.Screen name="RootScreen" component={LogInScreen} />
</Stack.Group>
) : (
// App Screens
<Stack.Group>
// This Tab navigator may have more stacks inside
<Stack.Screen name="RootScreen" component={AppTabsNavigator} />
<Stack.Screen name="SomeErrorOutsideTabs" component={SomeErrorOutsideTabs} />
</Stack.Group>
)}

React Navigation 5: Switching between different stack navigators in React native

I'm finding difficulty in digesting the official documentation and I'm stuck finding out a solution to move between different stack navigators. I have provided my current implementation and code snippet to explain my problem better.
I have a bottom tab navigator and 2 stack navigators to handle different use cases.
BottomNavigation.js
StackNavigation.js
AuthStackNavigation.js
I have created multiple stack navigators within StackNavigation.js and rendering each StackNavigators within BottomNavigation.js
***StackNavigation.js***
const Stack = createStackNavigator();
const HomeStackNavigator = () => {
return (
<Stack.Navigator initialRouteName="Home" screenOptions={ScreenLogo}>
<Stack.Screen name="HomeScreen" component={Home} />
</Stack.Navigator>
);
}
const ProfileStackNavigator = () => {
return (
<Stack.Navigator initialRouteName="Home" screenOptions={ScreenLogo}>
<Stack.Screen name="MahaExpo" component={Profile} />
</Stack.Navigator>
);
}
export { HomeStackNavigator, ProfileStackNavigator };
And as I said I'm rendering each navigator inside tab navigator to switch between screens.
***BottomNavigation.js***
import { HomeStackNavigator, ProfileStackNavigator } from '../Navigations/StackNavigation'
const Tab = createMaterialBottomTabNavigator();
function BottomNavigation() {
return (
<Tab.Navigator
initialRouteName="Home" >
<Tab.Screen
name="Home"
component={HomeStackNavigator}
/>
<Tab.Screen
name="ProfileStackNavigator"
component={ProfileStackNavigator}
/>
</Tab.Navigator>
)
}
export default BottomNavigation
and I'm rendering this within app.js and inside NavigationContainer. I have created AuthStackNavigation.js which has a login and register screens.
***AuthStackNavigation.js***
const AuthStack = createStackNavigator();
const AuthStackNavigation = ({ }) => (
<AuthStack.Navigator headerMode='none'>
<AuthStack.Screen name="UserLogin" component={UserLogin}></AuthStack.Screen>
<AuthStack.Screen name="UserRegister" component={UserRegister}></AuthStack.Screen>
</AuthStack.Navigator>
);
export default AuthStackNavigation
So currently, I'm showing the home screen with some public content and user can switch to different screens using the bottom tab navigator linked to different screens. When the user clicks on profile tab, im displaying a button to login which should take the user to AuthStackNavigation.js which has a login and register screen. Thanks in advance!
const ProfileStackNavigator = () => {
return (
<Stack.Navigator initialRouteName="Home" screenOptions={ScreenLogo}>
<Stack.Screen name="MahaExpo" component={Profile} />
<Stack.Screen name="UserLogin" component={UserLogin} />
<Stack.Screen name="UserRegister" component={UserRegister} />
</Stack.Navigator>
);
}
on profile page check user is logged in or not, if not, simply navigate to UserLogin screen. Once login simply pop back to profile page are refresh.
There's no way of communicating between two separate navigators if they're not related to each other (one is a child navigator of another or etc.), unless you create something like RootStackNavigator, which would be on the higher level and which would contain all the navigators you have listed above.
Create guest navigators array, authorized navigators array and some shared routes (if needed). Guest navigator contains authstack only and authorized one contains other navigators, if you use redux, context or something like this you can check for the token on startup to determine which navigator you have to use. Logging in will give you the token and will automatically change your navigator to authorized navigators and logging out will throw you back (as you no longer have the token) to the guest navigators which will contain only authentication flow.
Pretty hard to explain ;) hope this helps..
Redux Example
`
const authNavigators = [
{
type: "screen",
key: "authNavigators",
name: "authNavigators",
component: authNavigators,
},
];
const otherNavigators = [
{
type: "screen",
key: "otherNavigators1",
name: "otherNavigator1",
component: otherNavigators1,
},
{
type: "screen",
key: "otherNavigators2",
name: "otherNavigator2",
component: otherNavigators3,
},
...
];
const RootStack = () => {
const { condition } = useSelector(
(state) => state.store);
let navigators = otherNavigators;
if(condition) {
navigators = authNavigators;
}
return (
<RootStackNavigator.Navigator>
{navigators.map((item) => {
return (
<RootStackNavigator.Screen
{...item}
options={({ route: { params } }) => ({
...item.options,
})}
/>
);
})}
</RootStackNavigator.Navigator>
);
};
export default RootStack;`
from redux, you can dispatch an action which would change that condition and this would dynamically update your navigator.

Navigating to screen in another navigator in React Navigation 5

I'm using the new React Navigation 5 in my React Native app.
The main menu is handled by the RootNavigator which is a Drawer. The login/sign up are handled by AuthNavigation which is a Stack.
I'm trying to send user to Home screen under RootNavigator after a successful login. In other words, I'm trying to send user to a screen under another navigator by issuing navigation.navigate('RootNavigator', {screen: 'Home'}); but I'm getting an error that reads:
The action 'NAVIGATE' with payload {"name":"RootNavigator"} was not
handled by any navigator.
Do you have a screen named 'RootNavigator'?
This is what my App.js looks like:
const App = () => {
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticatedUser, setIsAuthenticatedUser] = useState(false);
useEffect( () => {
// Check for valid auth_token here
// If we have valid token, isLoading is set to FALSE and isAuthenticatedUser is set to TRUE
});
return (
<Provider store={store}>
<PaperProvider>
<NavigationContainer>
{
isLoading
? <SplashScreen />
: isAuthenticatedUser ? <RootNavigator /> : <AuthNavigator />
}
</NavigationContainer>
</PaperProvider>
</Provider>
);
};
And here's my AuthNavigator which is a Stack:
import React from 'react';
import { createStackNavigator } from '#react-navigation/stack';
// Components
import Login from '../../login/Login';
const AuthStack = new createStackNavigator();
export const AuthNavigator = () => {
return(
<AuthStack.Navigator>
<AuthStack.Screen name="LoginScreen" component={Login} />
</AuthStack.Navigator>
);
};
And here's the RootNavigator which is a Drawer:
import React from 'react';
import { createDrawerNavigator } from '#react-navigation/drawer';
// Components
import Dashboard from '../../home/Dashboard';
import DrawerContent from './DrawerContent';
import ToDoList from '../../active_lists/to_do_lists/ToDoList';
const MainMenuDrawer = createDrawerNavigator();
export const RootNavigator = () => {
return (
<MainMenuDrawer.Navigator drawerContent={(navigation) => <DrawerContent navigation={navigation} />}>
<MainMenuDrawer.Screen name="Home" component={Dashboard} />
<MainMenuDrawer.Screen name="To Do List" component={ToDoList} />
</MainMenuDrawer.Navigator>
);
};
Any idea what I'm doing wrong here? I want to use the cleanest approach for handling this.
There are 2 issues:
You're doing navigation.navigate('RootNavigator', {screen: 'Home'});, which means navigate to Home screen in a navigator nested inside RootNavigator screen. but there's no screen named RootNavigator, you have a component named RootNavigator, but navigate expects a screen name.
Looking at your code, you don't have nested navigators, both navigators are still at the root, just rendered conditionally. So you'd usually do navigation.navigate('Home') for navigation.
Examples of how nesting works and how the structure looks when you nest: https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator
When you are conditionally defining screens/navigators, React Navigation will take care of rendering correct screens automatically after you change the condition. You don't do navigate in this case.
From the auth flow docs:
It's important to note that when using such a setup, you don't need to navigate to the Home screen manually by calling navigation.navigate('Home'). React Navigation will automatically navigate to the Home screen when isSignedIn becomes true.
Auth flow docs: https://reactnavigation.org/docs/auth-flow#how-it-will-work
So what you need to do, is change the isAuthenticatedUser state in your component. You can follow the guide in the docs for an example using React context, but if you're using Redux, move this state to Redux, or any other state management library you use.
And lastly, remove the navigation.navigate after successful login, since it'll happen automatically when you update your condition.

Handling deep linking with react navigation v5 authentication flow

I want to access an authenticated screen from a deep-link using react navigation v5 with expo.
So I followed the authentication flow from react navigation and I ended with a start up navigator like that :
const StartupNavigator = (props: Partial<StackNavigatorProps>): ReactElement => {
const {startUp} = useStartUp();
const renderStacks = () => {
if (startUp.isReady) {
return <Stack.Screen name={Routes.HOME} component={BottomTabNavigator}/>;
}
if (startUp.isLoading) {
return <Stack.Screen name={Routes.CHECK} component={CheckScreen}/>;
}
if (startUp.isBuilding) {
return <Stack.Screen name={Routes.BUILD} component={BuildScreen}/>;
}
if (startUp.isAuthentifying) {
return <Stack.Screen name={Routes.AUTH} component={AuthNavigator}/>;
}
};
return (
<Stack.Navigator {...props} headerMode={"none"}>
{renderStacks()}
</Stack.Navigator>
);
};
So I have 4 initial state :
isReady : The user is authenticated, and can access protected
screens.
isLoading: First state when opening the app, check the app
version..... then set the isAuthentifying state
isAuthentifying: The authentication navigator for registering, and login.
isBuilding: After authentication, fetch the user information, then set the state to isReady
Note, when opening the app, the state is set by default to isLoading
Then, I made an NavigationContainer following this link for handling deep-linking
How to redirect to an authenticated screen passing by the authentication (note that I don't have an auto login process)

how to set initialRouteName dynamically in the react-native

how to give dynamic initial route name in the react-navigation? if the unit exists we have to redirect to another route or else we have to take user another route.
Note: I'm creating a bottom tab navigator in which I have to set an initial route to that particular bottom tab navigator.
(Not the authentication flow)
import React, {Component} from 'react';
import {createAppContainer} from 'react-navigation';
import {createMaterialBottomTabNavigator} from 'react-navigation-material-bottom-tabs';
... imports
function getInitialScreen() {
AsyncStorage.getItem('unit')
.then(unit => {
return unit ? 'Home' : 'secondTab';
})
.catch(err => {
});
}
const TabNavigator = createMaterialBottomTabNavigator(
{
Home: {
screen: HomeScreen,
navigationOptions: {
.....navigation options
},
},
secondTab: {
screen: secondTab,
},
},
{
initialRouteName: getInitialScreen(),
},
);
export default createAppContainer(TabNavigator);
See according to the docs, initialRoute name should not be a async func .
So ideally what you should do is , anyways you need a splashscreen for your app right, where you display the logo and name of app. Make that page the initialRoute and in its componentDidMount, check for the async function and navigate to ddesired page.
Like what ive done :
createSwitchNavigator(
{
App: TabNavigator,
Auth: AuthStack,
SplashScreen: SplashScreen,
},
{
initialRouteName: 'SplashScreen',
},
),
And inside SplashScreen im doing :
componentDidMount(){
if (token) {
this.props.navigation.navigate('App');
} else {
this.props.navigation.navigate('Auth');
}
}
Hope its clear. Feel free for doubts
As you can see here:
If you need to set the initialRouteName as a prop it is because there
is some data that you need to fetch asynchronously before you render
the app navigation. another way to handle this is to use a
switchnavigator and have a screen that you show when you are fetching
the async data, then navigate to the appropriate initial route with
params when necessary. see docs for a full example of this.
Take a look at here.
You'll find more description!
Also quick fix for this situation is check your condition inside SplashScreen componentDidMount() function
Example of SplashScreen :
componentDidMount(){
AsyncStorage.getItem('unit')
.then(unit => {
if(unit){
this.props.navigation.navigate('Home')
}else{
this.props.navigation.navigate('secondTab')
}
})
.catch(err => {
});
}
You can check the condition on the main page or App.js
render() {
const status = get AsyncStorage.getItem('unit');
if(status != null)
{ return <Home/> }
else
{ return <AnotherScreen/> }
}
But we can switch between pages if we use stacknavigator or switchnavigator..
We cant goto the particular tabs directly according to my knowledge. (Correct me if I am wrong).
The official documentation gives you this example to achieve pretty much what you want:
isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</>
) : (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
</>
)
Ok So in your navigation page create state and change its value accordingly AsyncStorage like
render() {
const status = get AsyncStorage.getItem('unit');
if(status != null)
{ setState({ name : 'Home'}) }
else
{ setState({ name : 'anotherTab'}) }
}
then pass that state to tabNavigation
initialRouteName: this.state.name,
and this state , setState functions are classed based so you have to use useState instead to initial state on your page