Expo Client + Firebase javascript SDK Authentication - firebase-authentication

I have been trying to develop an App with Expo Client + Firebase Javascript SDK. I am using redux(redux-thunk) to manage state. However I am having some problems to authenticate the user because every Firebase call I make, it just executes if I force the live reload of the app or if I tap anywhere.
import { Alert } from "react-native";
import { Actions } from "react-native-router-flux";
import {
LOGIN,
LOGIN_FAIL,
LOGIN_SUCCESS,
} from "./types";
import { FirebaseService } from "../services";
import i18n from "../i18n";
import * as Facebook from "expo-facebook";
import * as Google from "expo-google-app-auth";
export const login = (email, password) => {
return (dispatch) => {
console.log(email);
console.log(password);
dispatch({ type: LOGIN });
FirebaseService.signIn(email, password)
.then((user) => {
dispatch({ type: LOGIN_SUCCESS, payload: user });
Actions.tabbar();
})
.catch((error) => {
dispatch({ type: LOGIN_FAIL });
if (error) {
Alert.alert(
i18n.t("app.attention"),
i18n.t("login.enter.message"),
[{ text: i18n.t("app.ok") }],
{ cancelable: true }
);
}
});
};
I called this action creator when a button is pressed and the FirebaseService.signIn(email, password) is
static async signIn(email, password) {
return await firebase.auth().signInWithEmailAndPassword(email, password);
}
firebase was initialized with the right credentials. To be clearer, dispatch({ type: LOGIN_SUCCESS, payload: user }); will only be dispatched if I tap the screen anywhere or if I cause the hot reload. Otherwise, nothing happens and the app stay on hold. Do you have any clue of what could it be?
I am running it in an Iphone via Expo Client.

Related

useAuthStateChange isn't updating state in useEffect Supabase

Using expo and supabase (no typescript), I am trying to call useAuthStateChange inside a useEffect hook to update useState with the session. From what I understand, this is the most common approach
Logging in and signing up both work, according to Supabase Postgres logs, but I believe useAuthStateChange may be firing upon page load and then not refiring.
Here is my code. Please let me know if I am missing something:
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});
supabase.auth.onAuthStateChange(({ data: { session } }) => {
setSession(session);
});
}, []);
Have you followed this initialisation recommendation for Expo/React native: https://supabase.com/docs/guides/getting-started/tutorials/with-expo#initialize-a-react-native-app ?
import AsyncStorage from '#react-native-async-storage/async-storage'
import { createClient } from '#supabase/supabase-js'
const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: AsyncStorage as any,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
})
Then this should work:
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
})
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
})
}, [])
Looking at your code above, I think you're accessing the session incorrectly in the onAuthStateChange callback.

Authentication flow react native

My app functions in a way that part of the app is visible without logging in, and to view the rest of it, users have to be signed in. My app consists of 2 stacks, the auth stack and the app stack. The auth stack contains the Login and Signup screens. Currently this is the logic of my app. For example, lets say the user goes the to Messages Tab which is only visible is the user is signed in. On MessagesScreen.js, I have the following code.
const [user, setUser] = useState();
useEffect(() => {
console.log('Use effect called');
if (!user) {
fetchUser();
if (!user) {
console.log('THis is called');
navigation.navigate('Auth', {
screen: 'Login',
params: {comingFromScreen: 'Messages'},
});
} else {
console.log(user);
}
}
}, []);
const fetchUser = async () => {
try {
const userData = await getUser();
setUser(userData);
} catch (e) {
console.log('No user found');
}
};
getUser, is the following function:
export const getUser = async () => {
try {
let userData = await AsyncStorage.getItem('userData');
let data = JSON.parse(userData);
} catch (error) {
console.log('Something went wrong', error);
}
};
And the LoginScreen consists of the following code:
const handleLogin = () => {
if (email === '' || password === '') {
alert('Email or password not provided');
} else {
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((res) => {
storeUser(JSON.stringify(res.user));
})
.catch((e) => alert(e.message));
navigation.navigate('Home', {screen: comingFromScreen});
}
};
storeUser is the following:
export const storeUser = async (user) => {
try {
await AsyncStorage.setItem('userData', JSON.stringify(user));
} catch (error) {
console.log('Something went wrong', error);
}
};
When I first navigate to the Messages Screen, the logic works and I get presented with the login screen. But if I click on the 'X' button on the login screen which takes me back to the home screen and then go back to the Messages Screen, I get presented with the screen and moreover, useEffect is not even called.
I'm a little new to react native so can someone tell me what I need to change to achieve my desired effect?
You could make the useEffect depend on the user state by doing the following and it will call every-time the user state changes.
It will always call useEffect as long as user changes like below:
useEffect(() => {
console.log('Use effect called');
if (!user) {
fetchUser();
if (!user) {
navigation.navigate('Auth', {
screen: 'Login',
params: {comingFromScreen: 'Messages'},
});
} else {
console.log(user);
}
}
}, [user]);
Found the solution to this problem, I used the useFocusEffect hook instead of useEffect and it seemed to solve the problem.

AWS amplify + cognito user pool : not able to get authenticated user after login without second call

I am using vue js and connecting to aws user pool via amplify, using
Auth.federatedSignIn();
After user login from aws amplify hosted ui successfully, they will be redirected back to my home page (App.vue below).
I try to get the authenticated user when the page create, but it is throwing "not authenticated".
I try to call the function again (or refresh the page), and the user is able to be retrieved.
I also try to use setTimeout to add a delay, but seem vue doesn't update the page, which i am not able to see my currentUser on the screen.
What will be the best practice to getting the authenticated user right after the callback?
App.vue
<template>
<div id="app">
<v-btn #click="getAgain()">Get again</v-btn> #just a button to call again, and it able to get the user
<h1 v-if="currentUser">user id : {{currentUser}}</h1>
</div>
</template>
<script>
import { Auth } from "aws-amplify";
export default {
name: 'app',
components: {
},
data() {
return {
currentUser: null
}
},
created() {
Auth.currentAuthenticatedUser()
.then(user => {
this.currentUser = user;
console.log(user);
})
.catch(e => {});
},
methods: {
getAgain() {
Auth.currentAuthenticatedUser()
.then(user => {
this.currentUser = user;
})
.catch(e => {});
}
}
}
</script>
You can listen to the sign in event and get retrieve the user once the event is triggered
Hub.listen('auth', ({ payload: { event, data } }) => {
if (event === 'signIn') {
Auth.currentAuthenticatedUser()
.then(user => {
this.currentUser = user;
console.log(user);
})
.catch(e => {console.log(e)});
}
});
Ref: https://github.com/aws-amplify/amplify-js/issues/4621

Screen redirection takes time to execute after asynchronous call in React-native

I am developing a small app with Expo, React-native-router-flux, firebase and react-redux. I am trying to implement a launch screen that appears after the splash screen and checks if the user is loaded or not. The launch screen calls the following action inside componentDIdMount function:
export const tryToSignInSilently = user => {
return () => {
console.log(user);
console.log(Actions);
setTimeout(() => {
if (user != null) Actions.tabbar();
else Actions.LoginScreen();
}, 1000);
};
};
I had to add that setTimeout to be able to redirect the screen otherwise, it would not change screen. 1) Is that the recommended solution to the problem?
After It redirects to the login screen and the submit button is pressed, another action is created:
export const login = (email, password) => {
return dispatch => {
dispatch({ type: LOGIN });
console.log("This executes");
FirebaseService.signIn(email, password)
.then(user => {
console.log("This takes almost a minute to execute");
dispatch({ type: LOGIN_SUCCESS, payload: user });
Actions.tabbar();
})
.catch(error => {
dispatch({ type: LOGIN_FAIL });
if (error) {
Alert.alert(
i18n.t("app.attention"),
i18n.t("login.enter.message"),
[{ text: i18n.t("app.ok") }],
{ cancelable: true }
);
}
}); };};
FirebaseService.signIn function =>
static async signIn(email, password) {
return await firebase.auth().signInWithEmailAndPassword(email, password); }
The interesting note is: If I press the submit button in the login screen, and save the code (causing the live reload), the firebase function is executed immediately and the page is correctly redirected to the home screen.
2) What could be causing that behavior?
Thank you very much!
Try to encapsulate your component with a using useContext hook approach.
Do all the login inside the context component by using useEffect hook with the Firebase function onAuthStateChanged. See sample code below:
const AuthProvider = ({ children }) => {
const [userObject, setUserObject] = useState(null);
const [loggedIn, setLoggedIn] = useState(null);
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if(user){
setLoggedIn(true);
setUserObject(user);
}
else {
setLoggedIn(false);
setUserObject(null);
}
});
// Cleanup subscription on unmount
return () => unsubscribe();
}, []);
const [state, dispatch] = useReducer(reducer, []);
return(
<AuthContext.Provider value={{ loggedIn, userObject }}>{ children }</AuthContext.Provider>
);
}
export { AuthProvider, AuthContext };
Then on the launch screen use the context variable 'loggedIn' to detect if the user is already loggedin or not.

Why isn´t mock from __mock__ folder invoked when running test for redux action?

I call the react-navigation NavigationService within a redux action.
Testing the action I need to mock the navigate function.
/app/utils/NavigationService.js
import { NavigationActions } from 'react-navigation';
let navigator;
function setTopLevelNavigator(navigatorRef) {
navigator = navigatorRef;
}
function navigate(routeName, params) {
navigator.dispatch(NavigationActions.navigate({
type: NavigationActions.NAVIGATE,
routeName,
params,
}));
}
// add other navigation functions that you need and export them
export default {
navigate,
setTopLevelNavigator,
};
I created a __mock__ folder immediately adjacent to the NavigationService.js file.
app/utils/__mocks__/NavigationService.js UPDATED
const navigate = jest.fn();
const setTopLevelNavigator = jest.fn();
export default {
navigate,
setTopLevelNavigator,
};
Why doesn´t jest auto-mock the navigate function when the test is run?
https://jestjs.io/docs/en/manual-mocks
__tests__/actions/AuthActions.test.js UPDATED
jest.mock('../../app/utils/NavigationService'); //at the top directly behind other imports
it('should call firebase on signIn', () => {
const user = {
email: 'test#test.com',
password: 'sign',
};
const expected = [
{ type: types.LOGIN_USER },
{ payload: 1, type: types.DB_VERSION },
{ payload: 'prod', type: types.USER_TYPE },
{ payload: { name: 'data' }, type: types.WEEKPLAN_FETCH_SUCCESS },
{ payload: { name: 'data' }, type: types.RECIPELIBRARY_FETCH_SUCCESS },
{
payload: { user: { name: 'user' }, userVersionAndType: { dbVersion: 1, userType: 'prod' } },
type: types.LOGIN_USER_SUCCESS,
},
];
return store.dispatch(actions.loginUser(user)).then(() => {
expect(store.getActions()).toEqual(expected);
});
});
app/actions/AuthActions.js
export const loginUser = ({ email, password }) => (dispatch) => {
dispatch({ type: LOGIN_USER });
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.catch((signInError) => {
dispatch({ type: CREATE_USER, payload: signInError.message });
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(async (user) => {
const userVersionAndType = await dispatch(initUser());
await dispatch(initWeekplan(userVersionAndType));
await dispatch(initRecipeLibrary(userVersionAndType));
return user;
});
})
.then(async (user) => {
saveCredentials(email, password);
const userVersionAndType = await dispatch(getUserVersionAndType());
await dispatch(weekplanFetch(userVersionAndType));
await dispatch(recipeLibraryFetch(userVersionAndType));
dispatch(loginUserSuccess({ user, userVersionAndType }));
NavigationService.navigate('Home');
})
.catch(error => dispatch(loginUserFail(error.message)));
};
You've create a manual mock for a user module.
Activating a manual mock of a user module for a particular test file requires a call to jest.mock.
For this particular case add this line to the top of __tests__/actions/AuthActions.test.js and the mock will be used for all tests within that test file:
jest.mock('../../app/utils/NavigationService'); // use the manual mock in this test file
Note that manual mocks for user modules and Node core modules (like fs, path, util, etc.) both have to be activated for a particular test file by a call to jest.mock, and that this behavior is different than manual mocks for Node modules which are automatically applied to all tests.