Connect Firebase Auth with Firestore in React - reactjs

I'm trying to figure out how to connect users with data in my firestore. I didn't find anything about how to do it in React.
My idea, in the register page of my app, is:
async function register() {
try {
await auth.createUserWithEmailAndPassword(email, password).then(data => {
auth.currentUser.updateProfile({
displayName: name
})
db.collection('usersdata').doc(data.user.uid).set({nome: name})
props.history.replace('/success')
})
} catch(error) {
alert(error.message)
}
}
In this way I'm creating a new user and also a new entry in my "usersdata" collection. That entry has the same ID and the same name of the user.
After the login I can get the active user name using:
const userName = getCurrentUsername()
function getCurrentUsername() {
return auth.currentUser && auth.currentUser.displayName
}
All of this is working. But here it comes my problem:
I would like to add new data to the current user, for example "age" and "nationality".
But I don't know how to exactly get access to that user from firestore instead of auth.
And I also need to return only his data. Then, after a research, I need to return data from all users, and I guess that is the method:
const [datidb, setDatidb] = useState([])
useEffect(() => {
const fetchData = async () => {
const data = await db.collection('usersdata').get()
setDatidb(data.docs.map(doc => doc.data()))
}
fetchData()
}, [])

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.

How to Connect Firebase Auth with Google One Tap Login

I have created a web app with Firebase and React.js and implemented sign-in with Google. I then tried to implement GoogleOneTapSignin and the one-tap-sign-in UI is working successfully because I used the react-google-one-tap-login npm package.
If may react app I have a function that listens for AuthStateChange and then either registers the user if they are new or sign in them if they are already a member and also updates the state if they logged. out.
Now that I have implemented google-one-tap-login, I was expecting the onAuthSTaetChanged function to be triggered if a user signs in using the google-one-tap-login but it is not the case.
Below is the part of my App.js code that handles the user auth.
const classes = useStyles();
const dispatch = useDispatch();
const alert = useSelector(state => state.notification.alert);
// Handling Google-one-tap-signin
useGoogleOneTapLogin({
onError: error => console.log(error),
onSuccess: response => {
console.log(response);
const credential = provider.credential(response);
auth.signInWithCredential(credential).then(result => {
const {
user
} = result;
console.log(user);
});
},
googleAccountConfigs: {
client_id: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
});
//Handling firebase authentification
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async user => {
// If there is a user create the user profile and update useState
if (user) {
// createUserProfile function creates the user profile in firestore if they are new
const userRef = await createUserProfileDocument(user);
userRef.onSnapshot(snapshot => {
const doc = snapshot.data();
dispatch(
setUser({
id: snapshot.id,
...doc
})
);
});
} else {
dispatch(setUser(null));
}
});
return () => {
unsubscribe();
};
}, [dispatch]);
I tried to implement the solution suggested by the 2nd answer in this StackOverflow question but I get the error below on the console. when I use google-one-tap-sign-in. Remember I am not using the FirebaseUi library. So far my application only uses the sign in with Google
t {
code: "auth/argument-error",
message: "credential failed: must provide the ID token and/or the access token.",
a: null
}
a: null
code: "auth/argument-error"
message: "credential failed: must provide the ID token and/or the access token."
The ID token required by Firebase's signInWithCredential function exists within the credential property of the response object. Here is a sample function below using Firebase V8.
// firebase V8
function handleCredentialResponse(response) {
if (response) {
const cred = auth.GoogleAuthProvider.credential(response.credential);
// Sign in with credential from the Google user.
return auth().signInWithCredential(cred);
}
}
Firebase v9
// firebase V9
import { getAuth, GoogleAuthProvider, signInWithCredential } from "firebase/auth";
const auth = getAuth();
function handleCredentialResponse(response) {
if (response) {
const cred = GoogleAuthProvider.credential(response.credential)
// Sign in with credential from the Google user.
return signInWithCredential(auth, cred);
}
}
The response param is a credential response returned from the Google one-tap function callback.
google?.accounts.id.initialize({
client_id: your-google-app-client-id.apps.googleusercontent.com,
callback: handleCredentialResponse,
});
google?.accounts.id.prompt((notification) => {
console.log(notification);
});

Firebase onAuthStateChanged currentUser always null when first load a page React

I think I should ask this question, cause I face some very weird problem.
I have a protected route, /app, if user is not login reached this page, redirect to /auth/login, so I have this, common Context hooks:
const { user, isLoading } = useAuth()
useEffect(() => {
if (!(user || isLoading)) {
// HERE REDIRECT TO /auth/login
}
}, [user, isLoading])
//here make sense
Now in my useAuth(), I have this:
const useAuth = () => {
const [user, setUser] = useState(null)
useEffect(() => {
firebase.auth().onAuthStateChanged((currentUser) => {
setIsLoading(false)
if (currentUser) {
console.log(currentUser)
//here
getUserFromMyServer(currentUser.uid).then(res => {
let userDetails = res.detail
setUser(userDetails) // here will get User data from server
}).catch(error => setUser(null))
} else {
setUser(null)
}
})
}, [])
return {
user,
isLoading,
loginWithTwitter, // a function will mention later
getTwitterRedirectResult, //a function will mention later
logout // a function
}
}
This is what I call to server
// here is the getUserFromMyServer function in another files, it look like this:
export const getUserFromMyServer = async () => {
const path = "/user"
const firebaseToken = await firebase.auth().currentUser.getIdToken()
console.log(firebaseToken)
const myInit = {
headers: {
Authorization: `Bearer ${firebaseToken}`,
}
}
return API.get(apiName, path, myInit) // Here return a promise
}
OK, so far is working well. When done login, it will setUser(), then will redirect from /auth/login to /app according to the user, this is what I want.
Now when I implement Sign in with Twitter, it comes in a problem.
The behavior: When done Sign in with Twitter, it redirect to /app. Then when /app is loaded for 20s like that then it go back to /auth/login again. This is keep repeating, non-stop until the browser hang.
Here is my code for Sign in with Twitter.
useAuth.js (same file with code above with loginWithTwitter)
const [operation, setOperation] = useState('login')
const loginWithTwitter = () => {
setOperation(type)
let provider = getTwitterProvider()
return firebase.auth().signInWithRedirect(provider)
}
const getTwitterRedirectResult = () => {
return firebase
.auth()
.getRedirectResult()
.then((result) => {
console.log(result)
var credential = result.credential;
var token = credential.accessToken;
var secret = credential.secret;
var twitterHandle = result.additionalUserInfo.username
if (operation === 'sign-up') {
// if sign up, here save to my db
createUserInDb(twitterHandle, token, secret).then(res=> {
setUser(res.data)
}).catch(error => setUser(null))
}
if (operation === 'login') {
// here get back the user details same as above
getUser()
}
}).catch((error) => {
console.log(error.message)
setErrorMessage(error.message)
setUser(null)
});
}
Then in my /auth/login and /auth/sign-up I have this code:
const Login = () => {
const { user, getTwitterRedirectResult } = useAuth() //
useEffect(() => {
getTwitterRedirectResult() // here call to twitter redirect result
}, [])
useEffect(() => {
if (user) {
// code redirect to app
router.push('/app')
}
})
}
So now, when login/sign up with password, is no problem. Once login, go to /app. then stay there.
But when sign in with Twitter, it go to /app. Stay there for 20s, then it redirect back to /auth/login. Basically is infinite loop.
So now my question:
What is the correct way to call getTwitterRedirect in react? Is it call at useEffect() when the page load?
How to solve the infinite loop? Done login go to /app, then it go back to /auth/login again.
How to save user data to my own db when getting the result back from getTwitterRedirect. So when Sign up I need to create the record, but when login with Twitter, I just need to get back the same record. But right now, it only have 1 function getTwitterRedirect, login and sign up also use this. So how can I solve this?
Please someone tell me what I doing wrong, I have totally no clue.
Thanks in advance.

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.

What is the best way to fetch multiple documents from firebase in react?

I'm exploring the possibilities of react, ionic and firebase as a young programmer. I want to fetch multiple documents from firebase firestore and set this data in state using react hooks.
At first, I want to fetch user data like name, username and email. The collection of users is stored in a document which is stored in a collection of schools, like this:
Schools (collection)
- School X (document)
---- Users (collection)
-------- User Y (document)
So I fetch the data of User Y in School X using the following code:
App.tsx
const [gebruikerdata, setGebruikerdata ] = useGlobalState('gebruikerdata')
useEffect(() => {
LogIn(userEmail, userPassword')
getCurrentUser().then((user: any) => {
if (user) {
setGebruiker(user)
GetUserData(SelectedSchool, user.uid).then((snapdata: any) => {
setGebruikerdata(snapdata) //set state with the user data
})
}
})
})
Firebase.tsx
export function GetUserData(schoolID: string, userID: string) {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.firestore().collection('schools').doc(schoolID).collection('users').doc(userID).onSnapshot(function(snapdata) {
resolve(snapdata.data())
unsubscribe()
})
})
}
This works like a charm! It updates on datachange and works on all pages. However, I also want to fetch the school data like name and location of the particular school a user is in. So the data of 'School X' in the file structure above. I tried the following code:
App.tsx (new)
const [gebruikerdata, setGebruikerdata ] = useGlobalState('gebruikerdata')
const [schooldata, setSchooldata ] = useGlobalState('schooldata')
useEffect(() => {
LogIn(userEmail, userPassword)
getCurrentUser().then((user: any) => {
if (user) {
setGebruiker(user)
GetUserData(SelectedSchool, user.uid).then((snapdata: any) => {
setGebruikerdata(snapdata)
}
GetSchoolData(SelectedSchool).then((schoolsnapdata: any) => {
setSchooldata(schoolsnapdata) //set state with school data
})
})
}
})
})
Firebase.tsx (new)
export function GetUserData(schoolID: string, userID: string) {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.firestore().collection('schools').doc(schoolID).collection('users').doc(userID).onSnapshot(function(snapdata) {
resolve(snapdata.data())
unsubscribe()
})
})
}
export function GetSchoolData(schoolID: string) {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.firestore().collection('schools').doc(schoolID).onSnapshot(function(snapdata) {
resolve(snapdata.data())
unsubscribe()
})
})
}
This loads the data successfully but now the problem is that my application doesn't respond anymore. It freezes and I can't open other pages (switch tabs). Is there a more efficient way to do this? I am sure I need to fetch other data later on my application as well but I don't understand why my app freezes when I load data from multiple documents from firebase.
My questions:
Why does my app freeze on fetching multiple documents, how should I fetch the data?
What does 'unsubscribe()' in firebase.tsx do? Because the data is still updated on change. How does that work?
Thanks in advance!

Resources