Handling deep linking with react navigation v5 authentication flow - react-native

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)

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.

React Native ( React Navigation ) Authentication Flow

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.

Navigation Error when navigating to Home page from Firebase registration

I'm following this guide: https://www.freecodecamp.org/news/react-native-firebase-tutorial/ in attempt to learn how to use firebase, and even though I've followed the code very closely, I'm receiving a NAVIGATION error:
The action 'NAVIGATE' with payload {"name":"Home","params":{"user":{"id":"AWSKEmmUsua5koR1V3x5bapc3Eq2","email":"tk#gmail.com","fullName":"t"}}} was not handled by any navigator.
Do you have a screen named 'Home'?
I do however, have a screen named Home. App.js:
import Home from './src/Home';
import Login from './src/Login/Login';
import Registration from './src/Registration/Registration';
const Stack = createStackNavigator();
export default function App() {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null);
return (
<NavigationContainer>
<Stack.Navigator>
{ user ? (
<Stack.Screen name="Home">
{props => <Home {...props} extraData={user} />}
</Stack.Screen>
) : (
<>
<Stack.Screen name="Login" component={Login} />
<Stack.Screen name="Registration" component={Registration} />
</>
)}
</Stack.Navigator>
</NavigationContainer>
);
}
When I use the Registration form to register a new user and Navigate to the Home page is when I get the error. Registration.js:
import { firebase } from '../firebase/config';
export default function Registration({ navigation }) {
const [fullName, setFullName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const onFooterLinkPress = () => {
navigation.navigate('Login');
}
const onRegisterPress = () => {
if (password !== confirmPassword) {
alert("Passwords do not match!");
return
}
// This works. However, navigation does not for some reason
firebase.auth()
.createUserWithEmailAndPassword(email, password)
.then((response) => {
const uid = response.user.uid
const data = {
id: uid,
email,
fullName
}
const usersRef = firebase.firestore().collection("users");
usersRef.doc(uid).set(data).then(() => {
// This is where the navigation error lies. It has nothing to do with the component
// This error happened even when I created a new plain Home component
navigation.navigate("Home", { user: data})
})
.catch((error) => alert(error))
})
.catch((error) => alert(error))
}
return (
....Input Forms
<TouchableOpacity
style={styles.loginButton}
onPress={() => onRegisterPress()}
>
<Text style={styles.buttonTitle}>Create Account</Text>
</TouchableOpacity>
I have used React Navigation before and haven't run into this issue. I am not using nested navigators and cannot see where the issue lies. Thank you for reading.
Adding to Göksel Pırnal answers:
At first, suppose there is no user. So We are in Registration Screen. At that stage, our navigator doesn’t even know whether there is any “Home” Screen. At this stage, our navigator only knows 2 screens: “Login” and “Registration” screens.
You need to notify our app.js whether anyone registered in the Registration screen or not. After that our app.js should change the value of 'user' in [user,setUser].
In your, App.js put this lines of code:
const [initializing,setInitializing]=useState(true)
useEffect(()=>{
const subscriber=firebase.auth().onAuthStateChanged((user)=>{
setUser(user)
setInitializing(false)
})
return subscriber
},[])
if (initializing) return null //Here you may use an Activity indicator
Then after rerendering our navigator will see the value of “user” has changed and it should navigate to the Home screen.
And guess what! You do not need to navigate manually from Registration Screen as you already put a condition in App.js ( in return () ).
You have a problem where you check the user value in App.js. After the registration is done, you did not assign the state in the App.js page and it will always be null. The Home page will not be added to the stack because the user value is null. That's why you got the error.
Solution: You need to notify App.js after registration.

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.