I have this code using AppState to show a user a modal where finger print scan is required to unlock the app once the app goes to background and comes back to active
In useEffect of the HomeScreen
AppState.addEventListener('change', this.handleAppStateChange);
return () => {
AppState.removeEventListener('change', this.handleAppStateChange);
};
method
handleAppStateChange (nextAppState){
if (nextAppState === 'active') {
this.props.navigation.navigate('AuthModal');
}
This all works fine and good. On a separate screen where I have my logout, when I logout and get navigated to the login screen, the appState event is still triggered and I don't want that. The event should only be restricted to the AuthStack. I have tried a couple of things but no luck, how do I handle this?
you may use Async Storage to handle that without remove listener.
if you remove listener after logout you must listen again after login plus in app start.
solution with Async Storage without remove listener
you code may be look like this :
//in logout function
const logout = async() => {
await AsyncStorage.setItem('authState', "logout");
...
}
//in login function
const login = async() => {
await AsyncStorage.setItem('authState', "login");
...
}
and then wrap this.props.navigation.navigate('AuthModal') with if statment to check authentication
handleAppStateChange (nextAppState){
if (nextAppState === 'active') {
(async() => {
const authState = await AsyncStorage.getItem('authState');
if(authState === "login"){
this.props.navigation.navigate('AuthModal');
}
})();
}
}
Related
I've been struggling with this problem for a while. I have an Auth component inside which I try to access to local storage to see if there is a token in there and send it to server to validate that token.
if token is valid the user gets logged-in automatically.
./components/Auth.tsx
const Auth: React.FC<Props> = ({ children }) => {
const dispatch = useDispatch(); // I'm using redux-toolkit to mange the app-wide state
useEffect(() => {
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
const userId = localStorage.getItem("userId");
if (userId) {
axios
.post("/api/get-user-data", { userId, token })
.then((res) => {
dispatch(userActions.login(res.data.user)); // the user gets logged-in
})
.catch((error) => {
localStorage.clear();
console.log(error);
});
}
}
}, [dispatch]);
return <Fragment>{children}</Fragment>;
};
export default Auth;
then I wrap every page components with Auth.tsx in _app.tsx file in order to manage the authentication state globally.
./pages/_app.tsx
<Provider store={store}>
<Auth>
<Component {...pageProps} />
</Auth>
</Provider>
I have a user-profile page in which user can see all his/her information.
in this page first of all I check if the user is authenticated to access this page or not.
if not I redirect him to login page
./pages/user-profile.tsx
useEffect(() => {
if (isAuthenticated) {
// some code
} else {
router.push("/sign-in");
}
}, [isAuthenticated]);
The problem is when the user is in user-profile page and reloads . then the user always gets redirected to login-page even if the user is authenticated.
It's because the code in user-profile useEffect gets executed before the code in Auth component.
(user-profile page is a child to Auth component)
How should i run the code in Auth component before the code in user-profile page ?
I wanna get the user redirected only when he's not authenticated and run all the authentication-related codes before any other code.
Are you sure that the problem is that user-profile's useEffect is executed before Auth's useEffect? I would assume that the outermost useEffect is fired first.
What most probably happens in your case is that the code that you run in the Auth useEffect is asynchronous. You send a request to your API with Axios, then the useEffect method continues to run without waiting for the result. Normally, this is a good situation, but in your profile, you assume that you already have the result of this call.
You would probably have to implement an async function and await the result of both the axios.post method and dispatch method. You would need something like this:
useEffect(() => {
async () => {
if (typeof window !== 'undefined') {
const token = localStorage.getItem("token")
const userId = localStorage.getItem("userId")
if (userId) {
try {
const resp = await axios.post("/api/get-user-data", {userId, token})
await dispatch(userActions.login(res.data.user)) // the user gets logged-in
} catch(error) {
localStorage.clear()
console.log(error)
}
}
}
}()
}, [dispatch])
I think this should work, but it would cause your components to wait for the response before anything is rendered.
I'm creating a React Native app in which some actions like adding to favorites require the user to be logged in.
The problem
If a certain action needs authentication, the following flow is executed:
User tab over the favorite button(Protected action)
A modal(screen with presentation: "modal") is rendered to allow the user to enter their credentials
If the user is successfully logged in, the modal is closed and the user is directed to the screen on which it was located(goBack() navigation action).
THE PROBLEM: user needs to press again over the favorite button, the idea is, if the user is successfully logged in, the action (add to favorites) is executed immediately without the user having to perform the action again.
NOTE: I can have different protected actions on the same screen
Question
how can I request the user to log in to perform the action and have the action executed automatically after successful login?
execute the protected action only once, only when the user logs in successfully and the modal is closed, if the user is already authenticated the protected action should not be executed again.
Example flow
function FavoriteScreen({ navigation }) {
const { isAuthenticated } = useAuth();
if (isAuthenticated) {
addFavorite(productId);
} else {
navigation.navigate("LoginScreen");
}
}
Things I've tried
Send a callback through the parameters(called nexAction) from the protected action screen to the login screen, run the callback after successfully log in, and close the modal, but I'm getting non-serializable warning, and this screen implements deep linking, so I cannot ignore the warning as the documentation suggests.
if (isAuthenticated) {
addFavorite();
} else {
navigation.navigate(NavigationScreens.LoginScreen, {
nextAction: addFavorite,
});
}
Use the focus event to run the protected action after the user is successfully logged in and the modal is closed. This approach has some problems, each time user focuses on the screen and is authenticated, the protected action is going to be run, and there may be more than one protected action on the screen, meaning that they will all be executed when the screen is focused.
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
if (isAuthenticated) {
addFavorite();
}
});
return unsubscribe;
}, [isAuthenticated, navigation, addFavorite]);
I think you're on the right path with useEffect. What about storing the user action when clicking a protected action, and then performing the action and clearing the stored value after the user has logged in?
Something like:
const [pendingAction, setPendingAction] = useState();
user clicks addFavorite action, but is not logged in:
setPendingAction({type: 'addFavorite', value: 12});
Then handling the login in the modal, the previous screen will stay mounted and keep its state.
And then calling the pendingAction in the useEffect:
useEffect(() => {
if (isAuth) {
if (pendingAction) {
switch(pendingAction.type) {
// do something
}
setPendingAction(undefined);
}
} else {
// code runs when auth changes to false
}
}, [isAuth, pendingAction, setPendingAction]);
Have you tried an effect?
// Will run once on mount and again when auth state changes
useEffect(() => {
if (isAuth) {
// code runs when auth changes from false to true
} else {
// code runs when auth changes to false
}
}, [isAuth]);
You can't send a function on nav params, but you can send data. You can try parroting the data back from the login screen and performing your one time action like this:
const Fav = ({ route }) => {
const { isAuthenticated } = useAuth();
const {nextAction} = route.params;
useEffect(() => {
if (isAuthenticated) {
// If login navigated back to us with a nextAction, complete it now
if (nextAction === CallbackActions.ADD_FAVORITE) addFavorite();
} else {
navigation.navigate(NavigationScreens.LoginScreen, {
nextAction: CallbackActions.ADD_FAVORITE, // Send some data to the login screen
});
}
}, []);
};
const Login = ({ route }) => {
const {nextAction} = route.params;
// Do login stuff
login().then(() => {
navigation.navigate(NavigationScreens.FavoriteScreen, {
nextAction, // Send back the caller's nextAction
});
});
};
I have the following code in my React project using Supabase:
// supabaseClient.ts
export const onAuthStateChangedListener = (callback) => {
supabase.auth.onAuthStateChange(callback);
};
// inside user context
useEffect(() => {
const unsubscribe = onAuthStateChangedListener((event, session) => {
console.log(event);
});
return unsubscribe;
}, []);
However, every time I switch tabs away from the tab rendering the website to something else, and back, I see a new log from this listener, even if literally no change happened on the website.
Does anyone know the reason for this? The useEffect inside my user context component is the only place in my app where the listener is being called. To test, I wrote this dummy function inside my supabaseClient.ts file:
const testFunction = async () => {
supabase.auth.onAuthStateChange(() => {
console.log("auth state has changed");
});
};
testFunction()
This function also renders every time I switch tabs. This makes it a little annoying because my components that are related to userContext re render every time a tab is switched, so if a user is trying to update their profile data or something, they cannot switch tabs away in the middle of editing their data.
Supabase onAuthStateChange by default triggers every time a tab is switched. To prevent this, when initializing the client, add {multiTab: false} as a parameter.
Example:
const supabase = createClient(supabaseUrl, supabaseAnonKey, {multiTab: false,});
Here is my solution to the same problem. The way I've found is saving the access token value in a cookie every time the session changes, and retrieve it when onAuthStateChange get triggered, so I can decide to not update anything if the session access token is the same.
// >> Subscribe to auth state changes
useEffect(() => {
let subscription: Subscription
async function run() {
subscription = Supabase.auth.onAuthStateChange(async (event, newSession) => {
// get current token from manually saved cookie every time session changes
const currentAccessToken = await getCurrentAccessToken()
if (currentAccessToken != newSession?.access_token) {
console.log('<<< SUPABASE SESSION CHANGED >>>')
authStateChanged(event, newSession)
} else {
console.log('<<< SUPABASE SESSION NOT CHANGED >>>')
}
}).data.subscription
// ** Get the user's session on load
await me()
}
run()
return function cleanup() {
// will be called when the component unmounts
if (subscription) subscription.unsubscribe()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
I have this example from https://github.com/vercel/next.js/blob/canary/examples/with-firebase-authentication/utils/auth/useUser.js
The effect works fine (fires once) but for some reason, the functions inside are called twice.
useEffect(() => {
const cancelAuthListener = firebase
.auth()
.onIdTokenChanged(async (user) => {
console.log('once or twice?')
if (user) {
// This fires twice
const userData = await mapUserData(user)
setUserCookie(userData)
setUser(userData)
} else {
removeUserCookie()
setUser()
}
})
const userFromCookie = getUserFromCookie()
if (!userFromCookie) {
router.push('/')
return
}
setUser(userFromCookie)
console.log(' i fire once')
return () => {
console.log('clean up')
cancelAuthListener()
}
}, [])
How can I make it to fire once?
I added some console logs:
On the first render I get: 'i fire once', 'once or twice', 'once or twice'
If I leave the page the cleanup console log fires (as it's supposed to do)
Many thanks
Later edit:
this is the code
export const mapUserData = async (user) => {
const { uid, email } = user
const token = await user.getIdToken()
return {
id: uid,
email,
token
}
}
If getIdToken() gets 'true' as an argument it will force a refresh regardless of token expiration.
https://firebase.google.com/docs/reference/js/firebase.User#getidtoken
Solved!!
the user was calling getIdToken(true) which forces a refresh.
https://firebase.google.com/docs/reference/js/firebase.User#getidtoken
Sorry guys, my bad!!!
You have a setState() inside useEffect thats the culprit, where useEffect having empty params [], one request on initial mount and another when do
setUser(userData) the component re-renders and useEffect() is invoked again.
Instead of using user as state, try using as ref and check. That might resolve this.
i have tried to do routing according to user check and i did but my method worked two times. i understand it from my log
i'm using redux and here is my "useeffect" in mainrouter.js
// currentUser : null // in initialState for users
useEffect(() => {
if(states.currentUser== null)
{
console.log("Checking user for routing");
actions.getCurrentUserACT();
}
else{
console.log("Logged in")
}
},[]);
my mainrouter.js return
return (
<NavigationContainer>
{
states.currentUser == null ?
(this.SignStackScreen()) :
(this.MainTabNavigator())
}
</NavigationContainer>
);
here is my "getCurrentUserAct" method in redux actions
export function getCurrentUserACT() {
return function (dispatch) {
firebase.auth().onAuthStateChanged(user => {
if (user) {
console.log("User authed..");
dispatch({type:actionTypes.CURRENT_USER,payload:user})
} else {
console.log("User notauthed..");
dispatch({type:actionTypes.CURRENT_USER,payload:null})
}
});
}
when my app start, logs are .. (if i logged in before)
Checking user for routing
User authed
User authed
i think the reason for this, useeffect worked first time and state changed which use in return and rerender"
but then i removed to condition, logs were same.
so how i can avoid this?
is there a way to run method before from useeffect and avoid second render? or wait data in useeffect?
Your code looks right and there is no reason to log two times because of useEffect because you used it as componentDidMount so it is called one time.
The reason of logging two times is this code snippet:
firebase.auth().onAuthStateChanged(user => {
here onAuthStateChanged is callback and it is called when state of authentication changed on Firebase.
If you are going to check if the user logged in then you need to avoid to do like below
if (user) { }
Please try console.log(user) and check if user keeps same on double logging.
I believe user inner properties will be changed.
Hope this helps you to understand
export function getCurrentUserACT() {
return function (dispatch) {
firebase.auth().onAuthStateChanged(user => {
dispatch(setCurrentUserACT(user))
console.log("control point" + user);
// if (user) {
// console.log("User authed..");
// dispatch({type:actionTypes.CURRENT_USER,payload:user})
// } else {
// console.log("User notauthed..");
// dispatch({type:actionTypes.CURRENT_USER,payload:null})
// }
});
}
Logs were double
Checking user for routing
control point[object Object]
control point[object Object]
______________________Edited 31.05______________________
I change my code a little bit like below
const dispatch = useDispatch();
const currentUser = useSelector(state => state.currentUserReducer);
useEffect(() => {
console.log(JSON.stringify(currentUser));
if (currentUser == null) {
console.log("Checking user for routing");
//dispatch(getCurrentUserACT());
}
else {
console.log("Logged in")
}
}, []);
I found something like hint but I can't fix because i dont know
When app start logs are
null
Checking user for routing
I didnt dispatch getCurrentUserAct(), so logs are right. If i dispatch, user will be checking (user already logged in), component rerender and here is double logs from double render.
I need dispatch before useeffect, not in useeffect
Is there a way?