Stop Firebase processes on previous page in React Native - reactjs

I'm developing a React Native project, I have a problem. On the example home page, I am listening to the firebase table in useEffect. When I log out and go to the login page, the firebase process on the home page still works and gives an error because there is no user id. How can I stop Firebase processes on the home page when I'm logged out?
useEffect(() => {
const userRef = database().ref('messages')
.child(session.userId)
.child(session.fbid);
const onChildChanged = userRef.on(
'child_changed',
snapshot => {
console.log(snapshot)
},
function (error) {
console.log(error);
},
);
return () => {
userRef.off('child_changed', onChildChanged);
};
}, [session.fbid, session.userId, navigation]);
[Error: [database/permission-denied] Client doesn't have permission to access the desired data.]

It's required to check user authentication status before executing Firebase authentication-based features.
useEffect(() => {
// Get current authenticated user
const user = firebase.auth().currentUser;
// Listen to data changes when user is authenticated only.
if (user) {
const userRef = database()
.ref("messages")
.child(session.userId)
.child(session.fbid);
const onChildChanged = userRef.on(
"child_changed",
(snapshot) => {
console.log(snapshot);
},
function (error) {
console.log(error);
}
);
}
return () => {
user && userRef.off("child_changed", onChildChanged);
};
}, [session.fbid, session.userId, navigation, user]);

Related

onAuthStateChanged and getRedirectResult returning null user after successful google login redirect

Somewhat randomly my login flow stops working for some users when using google as a login provider (possibly facebook too, unsure) on iOS. They are able to log in on desktop using google login with the same app bundle. I think this is happening for ~1/10 users.
After a user selects which google account to use, the google redirects back to my app. The onAuthStateChange triggers, but the user is null. This is after successfully "logging in" with google. I think this may have to do with users trying to use multiple auth providers and getting stuck in a weird state. I'm not receiving any error or console logs when this happens, it's as if it's a fresh page load instead of a redirect.
I haven't been able to reproduce the issue using my own account until recently. I tried reverting back to earlier builds when the issue wasn't present for my account, with no luck. I'm now unable to log into my own app 😂
in UserProvider.tsx:
firebaseConfig.apiKey = process.env.REACT_APP_AUTH_API_KEY;
firebaseConfig.authDomain = process.env.REACT_APP_AUTH_DOMAIN;
firebaseConfig.projectId = "redacted";
firebaseConfig.storageBucket = "redacted.appspot.com";
firebaseConfig.messagingSenderId = "redacted";
firebaseConfig.appId = "redacted";
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Initialize Firebase Authentication and get a reference to the service
const auth = initializeAuth(app, {
popupRedirectResolver: browserPopupRedirectResolver,
persistence: [indexedDBLocalPersistence, browserLocalPersistence, inMemoryPersistence],
errorMap: debugErrorMap
});
const UserContextProvider = (props) => {
const signInWithGoogle = () => {
signInWithRedirect(auth, googleAuthProvider);
}
const handleUserCredential = async (result: UserCredential) => {
if (result.user) {
const additionalInfo = getAdditionalUserInfo(result)
dispatch({
type: 'SET_ADDITIONAL_USER_INFO', data: {
additionalInfo: additionalInfo,
}
});
}
}
useEffect(() => {
dispatch({ type: 'LOGIN_INITIATED' });
const handleRedirectResult = async () => {
alert("handling redirect result");
const result = await getRedirectResult(auth);
alert(`redirect result: ${result}`);
if (result) {
handleUserCredential(result)
}
}
onAuthStateChanged(auth, async (user: User | null) => {
alert(`on auth change fired: ${user}`);
if (user) {
// set the token now
const token = await getIdToken(user);
const refreshToken = async () => {
return await getIdToken(user, true);
}
onLoginSuccess(user, token, refreshToken);
} else {
onLogoutSuccess();
}
}, (error) => {
onError(error)
});
handleRedirectResult();
}, [])
onLogoutSuccess() is triggering because user is null.
PS: I spammed a bunch of alerts in there because I'm struggling to debug on my device, those aren't in my production build.

Firebase Passwordless Email Authentication Error in Expo App

I am setting up passwordless Auth in my Expo app using the Firebase SDK. I've gotten to the point where emails are being sent to the user's desired address with a redirect link back to the app. When the user clicks the link, they are indeed redirected but they are not being authenticated. I am receiving a generic error in the console :
ERROR: [Error: An internal error has occurred.]
But I know that my credentials are passing through properly as I have logged them out when the function runs:
isSignInWithEmailLink:true, url: exp://10.0.0.27:19000?apiKey=AIzaSyAmpd5DdsjOb-MNfVH3MgF1Gn2nT3TBcnY&oobCode=7FJTfBjM28gkn6GfBSAdgAk7wOegg9k4D5poVcylhSYAAAF8BO5gHQ&mode=signIn&lang=en
I am calling useEffect on this function:
useEffect(() => {
signInWithEmailLink();
}, []);
Send Link To Email (WORKING)
const sendSignInLinkToEmail = (email) => {
return auth
.sendSignInLinkToEmail(email, {
handleCodeInApp: true,
url: proxyUrl,
})
.then(() => {
return true;
});
};
User clicks on a link from the email to redirect to the app to Authenticate (NOT WORKING)
const signInWithEmailLink = async () => {
const url = await Linking.getInitialURL();
if (url) {
handleUrl(url);
}
Linking.addEventListener('url', ({ url }) => {
handleUrl(url);
});
};
(RETURNING ERROR)
const handleUrl = async (url) => {
const isSignInWithEmailLink = auth.isSignInWithEmailLink(url);
console.log('isSignInWithEmailLink: ', isSignInWithEmailLink, 'url', url);
if (isSignInWithEmailLink) {
try {
await auth.signInWithEmailLink(email, url);
} catch (error) {
console.log('ERROR:', error);
}
}
};
Have you enabled email sign in in your firebase console?
Are you storing the email in localStorage? It looks undefined in your logic.
Your listener should be in the useEffect hook.
I've code my code working looking like this:
const handleGetInitialURL = async () => {
const url = await Linking.getInitialURL()
if (url) {
handleSignInUrl(url)
}
}
const handleDeepLink = (event: Linking.EventType) => {
handleSignInUrl(event.url)
}
useEffect(() => {
handleGetInitialURL()
Linking.addEventListener('url', handleDeepLink)
return () => {
Linking.removeEventListener('url', handleDeepLink)
}
}, [])
You should use the onAuthStateChanged within useEffect rather than try and log the user in at that point in time. useEffect is used when you need your page to re-render based on changes.
For example:
useEffect(() => {
// onAuthStateChanged returns an unsubscriber
const unsubscribeAuth = auth.onAuthStateChanged(async authenticatedUser => {
try {
await (authenticatedUser ? setUser(authenticatedUser) : setUser(null));
setIsLoading(false);
} catch (error) {
console.log(error);
}
});
// unsubscribe auth listener on unmount
return unsubscribeAuth;
}, []);
You should invoke the user sign in method through other means such as a button to sign in, or validate user credentials at some other point within your app.
custom function:
const onLogin = async () => {
try {
if (email !== '' && password !== '') {
await auth.signInWithEmailAndPassword(email, password);
}
} catch (error) {
setLoginError(error.message);
}
};
Source: https://blog.jscrambler.com/how-to-integrate-firebase-authentication-with-an-expo-app

Persist auth state in react/react native for Firebase

I am using react native for an ios app and firebase for authentication. Every time I leave the app and come back, it asks for a login. I want to persist the firebase login but don't really know where to put it.
I know I need to put this in:
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
I have the following signIn function that runs when the login button is pressed on the signInScreen:
const signIn = async () => {
setLoading(true);
try {
await firebase.signIn(email, password);
const uid = firebase.getCurrentUser().uid;
const userInfo = await firebase.getUserInfo(uid);
const emailArr = userInfo.email.split("#");
setUser({
username: emailArr[0],
email: userInfo.email,
uid,
isLoggedIn: true,
});
} catch (error) {
alert(error.message);
} finally {
isMounted.current && setLoading(false);
}
};
I have the following signIn stuff in my firebaseContext:
const Firebase = {
getCurrentUser: () => {
return firebase.auth().currentUser;
},
signIn: async (email, password) => {
return firebase.auth().signInWithEmailAndPassword(email, password);
},
getUserInfo: async (uid) => {
try {
const user = await db.collection("users").doc(uid).get();
if (user.exists) {
return user.data();
}
} catch (error) {
console.log("Error #getUserInfo", error);
}
},
logOut: async () => {
return firebase
.auth()
.signOut()
.then(() => {
return true;
})
.catch((error) => {
console.log("Error #logout", error);
});
},
};
Where do I put the persist code I listed above from the docs?
Thanks!
When do you check if someon is signed in or not?
From the code shown it looks like you check it manuelly by calling currentUser. You have to consider that the persistance of auth state is asynchronous. That means if you call currentUser on auth before the localy saved auth state is loaded you would get there null and thing that the user is not signed in.
To get the auth state Firebase recommend to use the onAuthStateChanges event listener. With that you can listen to auth state changes no matter if you logged in or the persistet auth state is loaded.
The usage is very simple:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
That is the reson I asked where you check if someon is signed in or not. If I could see that code I could help you adopt it to use that event listener.

React Linking.addEventListener runs a Firebase sign in link twice in a useEffect

I have the following function to authenticate a user with React Native (Expo) and Firebase:
export default function AuthenticateUser() {
const user = useStore((state) => state.user); // Gets the user from state
const setUser = useStore((state) => state.setUser);
const setLoadingUser = useStore((state) => state.setLoadingUser);
const [GQL_getOrCreateUser] = useMutation(getOrCreateUser); // GraphQL function
useEffect(() => {
let unsubscribe: any;
let urlHandler: any;
function handleUrl(event: any) {
const { url }: { url: string } = event;
if (url.includes('/account')) {
const isSignInWithEmailLink = firebase.auth().isSignInWithEmailLink(url);
if (isSignInWithEmailLink) {
AsyncStorage.getItem('unverifiedEmail').then((email) => {
if (email) {
firebase
.auth()
.signInWithEmailLink(email, url)
.then(() => {
// We are signed in
})
.catch((error) => {
// Failed to sign in
});
} else {
// Missing pending email from AsyncStorage
}
});
}
};
}
function handleLinking(userDetails: User) {
urlHandler = ({ url }: { url: string }) => {
handleUrl({ url, userDetails });
};
// Listen to incoming deep link when app first opens
Linking.getInitialURL().then((url) => {
if (url) {
handleUrl({ url, userDetails });
}
});
// Listen to incoming deep link while app is open
Linking.addEventListener('url', urlHandler);
}
if (!user) {
unsubscribe = firebase.auth().onAuthStateChanged((authenticatedUser) => {
setLoadingUser(false);
if (authenticatedUser) {
const uid = authenticatedUser.uid;
const phoneNumber = authenticatedUser.phoneNumber;
let email: string;
if (authenticatedUser.email) {
email = authenticatedUser.email;
} else {
// retreiving email from AsyncStorage. We add it there when requesting a passwordless sign in link email, as recommended by Firebase.
AsyncStorage.getItem('unverifiedEmail').then((unverifiedEmail) => {
email = unverifiedEmail ? unverifiedEmail : '';
});
}
const emailVerified = authenticatedUser.emailVerified;
// Updating user record with GraphQL
GQL_getOrCreateUser({ variables: { uid, phoneNumber } })
.then(async (document) => {
const data = document.data.getOrCreateUser;
const userDetails = { ...data, phoneNumber, email, emailVerified }
setUser(userDetails); // Setting the stateful user record
handleLinking(userDetails); // Handle deeplink
})
.catch(() => {
// GraphQL failed
});
}
});
}
return () => {
unsubscribe?.();
// Removing event listener;
Linking.removeEventListener('url', urlHandler);
};
}, [GQL_getOrCreateUser, setLoadingUser, setUser, user]);
}
My problem is that the sign in method runs too often resulting in unexpected behavior.
I suspect it is caused by re-rendering triggered by the user auth state and the GraphQL running (GraphQL call to get or create a user causes three renders, which seems to be how it should behave).
I use deeplinking to handle passwordless email sign-in (firebase.auth().isSignInWithEmailLink(url))
The URL is detected with either Linking.getInitialURL (when the deeplink opens the app) or Linking.addEventListener('url', handler) when the app is already running.
As example, let's take scenario 1: Linking.getInitialUrl
I click the link. It asks to open the app.
The app opens
The user is not logged in (user is not in the app state) so the code inside the if (!user) is triggered.
The user email is in AsyncStorage because we just requested the login link email and I save it when the user asks for it.
GraphQL fetches the user and causes two more renders
I set the user in state with setUser and run handleLinking.
Because the app was closed, getInitialURL for the URL is triggered and it goes correctly through the steps and signs me in.
HOWEVER, handleLinking runs a second time (possibly the extra two renders caused by GraphQL trigger a Linking.addEventListener event to fire?) and returns an error because the sign in link cannot be used a second time.
I think there is a fundamental flow in my logic. What is it? How can this be improved and done correctly?
Thanks for helping!
If any of the objects in your useEffect dependency changes, then the useEffect function will re-run. So if your useEffect callback runs too often, the dependency array is where you should look at.
It is important to know that a "change" can be a simple as an object being reinstantiated or a function being re-created, even though the underlying value remains the same.

How to log in user with Google (through Firebase) in a React app with functional components?

I am trying to log in a user with Google in my React/Firebase app. I've followed a tutorial on youtube (https://www.youtube.com/watch?v=umr9eNbx3ag) but the results are different. When I click the Log In button, I get redirected to Google, choose an account and then get redirected to my site.
It seems as my 'if' statement never runs, auth.currentUser never evaluates to true.
This is my Firebase file
firebase.initializeApp(firebaseConfig)
export const firestore = firebase.firestore()
export const auth = firebase.auth()
export const provider = new firebase.auth.GoogleAuthProvider()
export const signInWithGoogle = () => auth.signInWithRedirect(provider)
export const signOut = () => auth.signOut()
export default firebase
This is my log in component
import { auth, signInWithGoogle, signOut } from '../../Firebase/Firebase'
const LoginOrRegister = () => {
const { username, setUsername, idToken, setIdToken } = useContext(Context)
useEffect(() => {
auth.onAuthStateChanged(async nextUser => {
if (auth.currentUser) {
setIdToken(await auth.currentUser.getIdToken())
setUsername(auth.currentUser.displayName)
} else {
setIdToken(null)
}
})
}, [])
return (
<div>
<LogInForm>
<button onClick={signInWithGoogle}> Log in with Google </button>
</div>
)
Since you are using signInWithRedirect you need to make use of auth.getRedirectResult() instead of auth.onAuthStateChanged as you are actually navigating away from the app and coming back in
Below code will work or you.
useEffect(() => {
auth
.getRedirectResult()
.then(function(result) {
console.log(result);
if (result.credential) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
setToken(token);
// ...
}
// The signed-in user info.
var user = result.user;
console.log(user);
setData(user);
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
console.log(errorCode, errorMessage);
// ...
});
}, []);
You can find the reference documentation here

Resources