Why cant react native find my navigation route when logging user in? - javascript

Im using react native and trying to handle authentication in my app. I'm having a problem where when the user successfully logs in, my app is unable to navigate them to the home screen. I am conditionally rendering two different types of navigators based on a value being present in AsyncStorage.
The problem seems like it's coming from the useEffect hook in app.js. When the app loads and the user is not logged in, the useEffect hook runs once. Then when I successfully login and try to navigate the user to the home screen, the app doesnt know about the <Drawer.Navigator> that I render in my else condition and because of that is not able to navigate there.
What am I doing wrong?
App.js
export default function App() {
const [token, setToken] = useState("");
useEffect(() => {
console.log("use effect");
readData();
});
const readData = async () => {
try {
const value = await AsyncStorage.getItem("token");
if (value !== null) {
setToken(value);
}
} catch (e) {
console.log(e);
}
};
return (
<AuthProvider>
<PaperProvider theme={theme}>
<NavigationContainer ref={navigationRef}>
{token == null ? (
<Stack.Navigator
initialRouteName="Login"
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="Verify" component={VerifyScreen} />
</Stack.Navigator>
) : (
<Drawer.Navigator initialRouteName="Home">
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Episodes" component={EpisodeListScreen} />
<Drawer.Screen name="Account" component={AccountScreen} />
</Drawer.Navigator>
)}
</NavigationContainer>
</PaperProvider>
</AuthProvider>
);
}
AuthContext.js
import createDataContext from "./createDataContext";
import authApi from "../api/auth";
import AsyncStorage from "#react-native-async-storage/async-storage";
import { v4 as uuid } from "uuid";
import * as RootNavigation from "../common/RootNavigation";
const authReducer = (state, action) => {
switch (action.type) {
case "add_error":
return { ...state, errorMessage: action.payload };
case "login":
return { errorMessage: "", token: action.payload };
case "logout":
return { token: null, errorMessage: "" };
case "clear_error_message":
return { ...state, errorMessage: "" };
default:
state;
}
};
const register = (dispatch) => ({ fullName, email, password }) => {
const id = uuid();
const user = { id, fullName, email, password };
authApi
.put("register", user)
.then((response) => {
if (response.status === 200) {
RootNavigation.navigate("Verify");
}
})
.catch(() => {
dispatch({
type: "add_error",
payload: "Something went wrong during registation.",
});
});
};
const login = (dispatch) => async ({ email, password }) => {
// make api request to api
try {
const user = { email, password };
const response = await authApi.post("/login", user);
await AsyncStorage.setItem("token", response.data.access_token);
dispatch({ type: "login", payload: response.data.access_token });
RootNavigation.navigate("Episodes");
} catch (e) {
dispatch({ type: "add_error", payload: e.response.data.message });
}
};
const tryLocalLogin = (dispatch) => async () => {
const token = await AsyncStorage.getItem("token");
if (token) {
dispatch({ type: "login", payload: token });
RootNavigation.navigate("Episodes");
} else {
RootNavigation.navigate("Login");
}
};
const logout = (dispatch) => async () => {
await AsyncStorage.removeItem("token");
dispatch({ type: "logout" });
RootNavigation.navigate("Login");
try {
} catch (e) {
console.log(e)
}
};
const clearErrorMessage = (dispatch) => () => {
dispatch({ type: "clear_error_message" });
};
export const { Provider, Context } = createDataContext(
authReducer,
{ register, login, logout, tryLocalLogin, clearErrorMessage },
{ token: null, errorMessage: "" }
);

Just taking a quick look at this, are you saying your issue is the Drawer.Navigator never seems to load in / work?
If so I think your issue is that every time the component loads it will assess the token as null, as it will be waiting on the Async function and you are assessing token as null on each load as its rendering before the Async returns anything.
One fix may be to use useState and set the token (or at least a boolean to indicate its present) in state. This may help, because setting state will cause a re-render and in turn assess the latest state value (which will for example be tokenState !== null) and in turn may render your drawer navigator.
If this doesn't help, could you also not generally look to use state across your app to handle if the user is signed in and get their token etc? Sure, it may occasionally mean firing an API call on app load sometimes, but I remember a while ago generally looking into Redux (there may be better cross-application app state management tools now) to replace a lot of what I was using Async Storage for.

Related

How to redirect the user to the home page when he logs in successfully in react?

I'm fairly new to redux toolkit so I'm still having a few issues with it!
I'm stuck on this. I couldn't find what to do, how to follow a method. so i need your help.
My only request is to redirect the user to the home page when he logs in successfully, but I couldn't find what to check, where and how. I will share the code pages that I think will help you.
import { createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
import { TokenService } from "./token.service";
export const AuthService = {};
AuthService.userLogin = createAsyncThunk(
"userSlice/userLogin",
async ({ username, password }) => {
console.log(username, password);
try {
const response = await axios.post(
"https://localhost:7163/api/Login",
{ username, password },
{
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: typeof username === "string" ? username.toString() : "",
password: typeof password === "string" ? password.toString() : "",
}),
}
);
if (response.data.status) {
const tokenResponse = { accessToken: response?.data?.data?.token };
TokenService.setToken(tokenResponse);
} else {
console.log("false false false");
return response.data.status;
}
return response.data;
} catch (error) {
console.error(error);
}
}
);
import { createSlice } from "#reduxjs/toolkit";
import { AuthService } from "../../services/auth.service";
const userToken = localStorage.getItem("userToken")
? localStorage.getItem("userToken")
: null;
const initialState = {
userToken,
isLoading: false,
hasError: false,
userinfo: null,
};
const userSlice = createSlice({
name: "userSlice",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(AuthService.userLogin.pending, (state, action) => {
state.isLoading = true;
state.hasError = false;
})
.addCase(AuthService.userLogin.fulfilled, (state, action) => {
state.userToken = action.payload;
state.isLoading = false;
state.hasError = false;
})
.addCase(AuthService.userLogin.rejected, (state, action) => {
state.isLoading = false;
state.hasError = true;
});
},
});
export const selectUserToken = (state) => state.userSlice.userToken;
export const selectLoadingState = (state) => state.userSlice.isLoading;
export const selectErrorState = (state) => state.userSlice.hasError;
export default userSlice.reducer;
export default function SignIn() {
const navigate = useNavigate();
const error = useSelector(selectErrorState);
console.log(error);
const [username, setName] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const data = {
username: username,
password: password,
};
const handleNameChange = (event) => {
setName(event.target.value);
};
const handlePasswordChange = (event) => {
setPassword(event.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(username, password);
const response = dispatch(AuthService.userLogin(data))
.unwrap()
.then(() => {
if (response.success) {
navigate("/");
}
});
console.log("RESPONSE", response);
console.log(error);
};
//index.js
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<Routes>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="" element={<Home />} />
<Route path="events" element={<Events />} />
</Routes>
</BrowserRouter>
</Provider>
</React.StrictMode>
);
log
my result from API:
Does it make more sense to use the status property here?
{data: {…}, status: true, exception: null, code: 200, message: null}
code: 200
data: {token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfb…yMzl9.1P25An_PHA9n4dyQ9JRKOjwPWWtQShWgW9In-gqS7Ek'}
exception: null
message: null
status: true
[[Prototype]]: Object
Even if I enter wrong information, the promise always seems to be fulfilled.
I think I need to use token in this process. The user will see his own information. I can get the token somehow, but I couldn't use it after I put it in localstorage.
While on the login page, it struggled with the data returned from the login meta, but I couldn't. I want to know what is the most logical method in such a situation. Thank you in advance for your answers.

React Native - Why doesn't my app change screens after state is updated?

In my App.js I call a function that checks the local storage to see if the user is logged in:
function App() {
const {state, tryLocalStorage} = useContext(AuthContext);
useEffect(() => {
tryLocalStorage();
}, []);
return (
<NavigationContainer ref={navigationRef}>
<View style={styles.root}>
<StatusBar barStyle="light-content" />
{state.user === 'true' ? <MainFlow /> : <AuthFlow />}
</View>
</NavigationContainer>
);
}
When user logs in, if they are new they go to on board screen if not, it switches to the MainFlow and go directly to home screen.
In my Login Screen when user logs in I have this function:
const login = dispatch =>() => {
try {
axios
.get(url)
.then(res => {
const user = res.data;
if (user) {
RNSInfo.setItem('user', 'true', {});
dispatch({type: 'user', payload: 'true'});
} else dispatch({type: 'user', payload: 'false'});
});
} catch (err) {
dispatch({
type: 'error_1',
payload: 'error',
});
}
};
After I successfully log in, my state updates and my console logs the updated state from my home screen in the MainFlow which means that it should successfully switch to MainFlow, but the problem is that the screen doesn't switch to the home screen until I refresh the screen.
In my useReducer I have default state value like this:
const defaultValue = {
user: 'false',
error_1: '',
};
When I console log state.user in my App.js before I log in I get, undefined
After I log in I get:
undefined
undefined
undefined
true
I feel like that is where my problem is coming from but not sure and don't know how to fix.
Appreciate any help!

Supabase onAuthStateChanged - How do I properly wait for the request to finish prevent flickering with useEffect?

Everything auth-wise is working fine. I even have a loading state setup so that the loader shows until the state is changed, but I still get this flickering on reload. This flickering only happens with Supabase. I was using the Firebase version before and it worked perfectly with my code.
Here is a video for reference: https://imgur.com/a/5hywXj5
Edit: Updated code to current version
export default function Navigation() {
const { user, setUser } = useContext(AuthenticatedUserContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const session = supabase.auth.session();
setUser(session?.user ?? null);
const { data: listener } = supabase.auth.onAuthStateChange((_: any, session: any) => {
setUser(session?.user ?? null);
});
setIsLoading(false);
return () => {
listener?.unsubscribe();
};
}, []);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator color={Theme.colors.purple} size="large" />
</View>
);
}
return (
<NavigationContainer linking={LinkingConfiguration}>{user ? <AppStack /> : <AuthStack />}</NavigationContainer>
);
}
To recap for others, onAuthStateChange will not execute on first page load so you are triggering it using the getUserAuthStatus async function. However session() function is not async and will immediately return a result of null if there is no user session, or return a session that has been stored in localStorage.
In this case the result of the getUserAuthStatus will always return null. Then onAuthStateChange will trigger with the SIGNED_IN event and a session which will then set the user.
Furthermore the onAuthStateChange function should be registered before you perform the session step so as to capture any events triggered. In the current form an event may be triggered directly after the session() call but before the handler is registered.
So to recap the rendering steps will be:
Step 1
isLoading: true
user: null
Step 2
isLoading: false
user: null
Step 3
isLoading false
user: {...}
So far as I can tell, using session directly without thinking it's async will do the trick.
Ok, Supabase has released some updates since I first asked this question. Here is how I am now able to stop flickering when loading the application.
First, we need to set up our AuthContext for our application. Be sure to wrap your App.tsx with the <AuthContextProvider>.
AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { Session, User } from '#supabase/supabase-js';
import { supabase } from '../config/supabase';
export const AuthContext = createContext<{ user: User | null; session: Session | null }>({
user: null,
session: null,
});
export const AuthContextProvider = (props: any) => {
const [userSession, setUserSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUserSession(session);
setUser(session?.user ?? null);
});
const { data: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
console.log(`Supabase auth event: ${event}`);
setUserSession(session);
setUser(session?.user ?? null);
});
return () => {
authListener.subscription;
};
}, []);
const value = {
userSession,
user,
};
return <AuthContext.Provider value={value} {...props} />;
};
export const useUser = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useUser must be used within a AuthContextProvider.');
}
return context;
};
Now, if you're using React Navigation like me we need to check if we have a valid user to send them to the logged-in home screen. Here's how I do it.
Navigation.tsx
export default function Navigation() {
const { user } = useUser();
return (
<NavigationContainer linking={LinkingConfiguration}>
{user ? <AppStackNavigator /> : <AuthStackNavigator />}
</NavigationContainer>
);
}

React Native - How can I listen to AsyncStorage Changes in Real Time (Conditional Screens Rendering) without refreshing the app?

I was trying to build an Authentication system in React Native using :
react-navigation
AsyncStorage
redux
this is my system flow: I want only to show the Sign screen when (isAuthenticated == false), when it's true I want to show UserUI Screen
<Provider store={store}>
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerShown:false
}}
>
{isAuthenticated == false ? (
<>
<Stack.Screen name="Sign" component={Sign} />
</>
) : (
<>
<Stack.Screen name="UserUI" component={Tabs} />
</>
)}
</Stack.Navigator>
</NavigationContainer>
</Provider>
also the (isAuthenticated) depends on this function :
React.useEffect(()=>{
const showStorage = async () => {
try {
const userObj = await AsyncStorage.getItem('currentStoredUserState')
if(userObj != null){
let jsonObject = JSON.parse(userObj)
setIsAuthenticated(jsonObject.state)
}else{
setIsAuthenticated(false)
}
keys()
}catch(err){
console.log(err)
}
}
showStorage()
},[itemsLength])
but the problem is that (isAuthenticated) does not change to true when the user authenticates and I have to refresh the app manually -after AsyncStorage is not empty- to access'UserUI' Screen.
as you can see, I set (itemsLength) as dependecy but it does not work as well :
const [itemsLength,setItemLength] = useState(0)
const keys = async () => {
let key = await AsyncStorage.getAllKeys()
setItemLength(key.length)
}
everything above is inside App.js
sign in action :
const setAuthStorage = async (data) => {
try {
let authState = { state: true,currentStoredUser_ : data }
await AsyncStorage.setItem('currentStoredUserState', JSON.stringify(authState))
} catch (err) {
console.log(err)
}
}
export const sign_in = (userSignData) => (dispatch) => {
auth.signInWithEmailAndPassword(userSignData.email, userSignData.password)
.then(userCredential => {
const user = userCredential.user
db.collection('users').doc(auth.currentUser.uid.toString()).get()
.then(doc => {
if (doc.exists) {
setAuthStorage(doc.data())
dispatch({
type: SIGN_IN,
payload: {
authenticated: true,
currentUser: user,
}
})
}
})
}).catch(err => {
console.log(err)
})
}
Sign Hanlder in Sign Screen :
const signHandler = () => {
if (isSignedUp) {
dispatch(sign_in(userSignData))
} else {
dispatch(sign_up(userSignData))
}
}
Solved, but not quite perfect, –
thank you guys for your help I really appreciate it, you asked me to change the rendering condition using redux state but I could not access the state outside the , for this mission I had to subscribe to store changes in the App.js Component and when we say subscribe == listening to every state change and this is not good for the app performance :
store.subscribe(()=>{
/*
I combined two states: rootReducer (for user details) and postReducer(for posts changes)
*/
// making sure it's rootReducer state
if(store.getState().hasOwnProperty('rootReducer')){
//when user sign in or sign up rootReducer.authenticated will change from false to true
if(store.getState().rootReducer.authenticated == true){
// change the rendering condition
setIsAuthenticated(true)
}else{
setIsAuthenticated(false)
}
}
})
I also made some changes inside actions to change the state only after storing data
const setAuthStorage = async (data) => {
try {
let authState = { state: true,currentStoredUser_ : data }
// to provide promise chaining this function will store & return a promise
return await AsyncStorage.setItem('currentStoredUserState', JSON.stringify(authState))
} catch (err) {
console.log(err)
}
}
export const sign_in = (userSignData) => (dispatch) => {
auth.signInWithEmailAndPassword(userSignData.email, userSignData.password)
.then(userCredential => {
const user = userCredential.user
db.collection('users').doc(auth.currentUser.uid.toString()).get()
.then(doc => {
if (doc.exists) {
setAuthStorage(doc.data())
.then(()=>{
dispatch({
type: SIGN_IN,
payload: {
authenticated: true,
currentUser: user,
}
})
})
}
})
}).catch(err => {
console.log(err)
})
}
If a found a better solution I will share it with you guys 🤞
As per my understanding what you are trying to achieve is,
use login screen when not logged in, use user screen when logged in.
once user do login save data and keep user logged in.
How do it most similar to what you have:
Store user data in redux.(i think you have already done.).
use that redux state to select which set of screens to be available (you have used async storage which on update do not re-render).
persist the user data in redux. Can use redux-persist for it.(to save user info for next time user opens app).
Edit
Adding code to make the solution in question better
App.js file
import AppNavigation from './AppNavigation'
...
<Provider store={store}>
<AppNavigation/>
</Provider>
AppNavigation.js
import {useSelector} from 'react-redux'
...
const AppNavigation =()=>{
const isAuthenticated = useSelector(state=>state.authenticated===true);
return(
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerShown:false
}}
>
{isAuthenticated == false ? (
<>
<Stack.Screen name="Sign" component={Sign} />
</>
) : (
<>
<Stack.Screen name="UserUI" component={Tabs} />
</>
)}
</Stack.Navigator>
</NavigationContainer>
)
}
You should display the routes with condition on the redux state and then it will automatically change the routes on login.

Accessing centralized store through useSelector

I have Login functionality as below -
function SignIn() {
const loginInfo = useSelector(state => state.loginDetails);
const iLoginCreds ={
userName:'',
password:'',
isLoggedIn:false
}
const dispatch = useDispatch();
const [loginCreds, setLoginCredentials] = useState(iLoginCreds)
useEffect(() => {
alert("state changed : "+loginCreds.isLoggedIn);
}, [loginCreds])
function checkIfSignedIn()
{
axios.get(`https://localhost:44301/api/login/ValidateLogin`)
.then(res=>{
console.log(JSON.stringify(res.data));
setLoginCredentials({
...loginCreds,
isLoggedIn:res.data
});
dispatch(StoreUserAuthenticationStatusAction(res.data));
});
}
if(loginInfo.isLoggedIn==true)
{
return (
<MainPage></MainPage>
)
}
else
{
return (
...
...
<FormGroup>
<Button style={{width:'100%',backgroundColor:"#FCB724",color:"black",fontWeight:"bold"}} onClick={checkIfSignedIn} >Sign in using our secure server</Button>
</FormGroup>
)
}
Reducer Index :-
import { combineReducers } from "redux";
import {SaveLoginStatusReducer} from "./LoginReducers"
export const reducers=combineReducers({
loginDetails:SaveLoginStatusReducer
})
Issue -
When if(loginInfo.isLoggedIn==true) , which I am fetching from useSelector at the beginning , I want to render to MainPage. But somehow cannot see , data is been fetched from central store. Page is not getting rendered to MainPage eventhough the state is been updated.
I am able to get alert for useEffect I have used when state changes. It shows "true".
EDIT 1 :-
When I am using if(loginCreds.isLoggedIn==true) , I am able to see MainPage , but when I try to retrieve it from store through if(loginInfo.isLoggedIn==true) , I dont get true.
Edit 2 :-
Action.js -
export const StoreUserAuthenticationStatusAction=(loginPayload)=>{
return {
type:'SaveLoginStatus',
payload:loginPayload
}
}
export const SetProductList=(productListPayload)=>{
return {
type:'SetProductList',
payload:productListPayload
}
}
Reducer.js -
const iLoginCreds ={
userName:'',
password:'',
isLoggedIn:false
}
export const SaveLoginStatusReducer =(state=iLoginCreds,action)=>{
switch (action.type) {
case 'SaveLoginStatus':
return {
...state,
user:action.paylod
}
break;
default:
return state;
}
}
Code pattern looks bad, you have to make authGuard to protect private routes.
That should redirects to auth page if the user is not signed in.
Redirects to main page after sign in.
const handleLogin = (e) => {
e.preventDefault();
if (user.email === email && user.password === password) {
login();
history.push("/main-page");
}
};
<Route
{...rest}
render={(props) =>
isLogin() ? <Component {...props} /> : <Redirect to="/login" />
}
/>

Categories