How to add a field in Firestore collection using Next.js? - reactjs

I want to add a field in the DB collection after the user has created that is a boolean, like isAdmin: false. Creation of user and adding doc to the DB is working but I'm not sure if I should add it while adding the doc or use updateDoc. I tried this but not working:
await updateDoc(collection(firestore, "Users", User.id), {
isAdmin: false
});
I'm using Next.js and react-firebase-hooks. Thanks in advance for any help.
My current code that is working with user creation and adding the user to Firebase db:
const [signUpForm, setSignUpForm] = useState({
email: "",
password: "",
confirmPassword: "",
});
const [error, setError] = useState(false);
const [createUserWithEmailAndPassword, user, loading, userError] =
useCreateUserWithEmailAndPassword(auth);
const onSubmit = async (event) => {
try {
event.preventDefault();
if (error) setError("");
if (signUpForm.password !== signUpForm.confirmPassword) {
return setError("Password do not match");
}
createUserWithEmailAndPassword(signUpForm.email, signUpForm.password);
} catch (error) {
console.log("Error", error.msg);
}
};
//Sets inputs value in signUpForm useState
const onChange = (event) => {
setSignUpForm((prev) => ({
...prev,
[event.target.name]: event.target.value,
}));
};
//Adds the user to Firebase db
const createUserDocument = async (User) => {
await addDoc(
collection(firestore, "Users"),
JSON.parse(JSON.stringify(User))
);
};
useEffect(() => {
if (user) {
createUserDocument(user.user);
console.log(user);
}
}, [user]);

Collections have no fields. Collections have documents. You have mistake in updateDoc() function.
Here is how you should update documents:
await updateDoc(doc(firestore, "Users", user.uid), {
isAdmin: false
});
In your arrow function, you have another mistake. You're creating a document with random ID. You should use setDoc() instead of addDoc() function.
const createUserDocument = async (user) => {
await setDoc(
doc(firestore, "Users", user.uid),
JSON.parse(JSON.stringify(User))
);
};
Edit:
Assuming your user object is a User not UserCredential it has .uid field not .id. If it is a UserCredential you need first take a user so: user.user.uid

Related

Fetch returns undefined the first time a user signs in

I sort of know why it happens, but not sure how to go on about solving it.
I have a React project that uses Cloud Firestore as database, and I have a simple login-page where you can sign in via your Google account. The first time you sign in a new document gets added to the "users" collection in Firebase.
After the document has been created it fetches that user data from Firebase and stores it in Redux.
const signInWithGoogle = async () => {
try {
const res = await signInWithPopup(auth, googleProvider);
const user = res.user;
const q = query(collection(db, "users"), where("uid", "==", user.uid));
const docs = await getDocs(q);
if(docs.docs.length === 0){
const firstName = user.displayName.split(' ')[0];
await addDoc(collection(db, "users"), {
uid: user.uid,
name: user.displayName,
firstName: firstName,
photoURL: user.photoURL,
authProvider: "google",
email: user.email,
})
dispatch(getUser(user))
}
} catch(err) {
console.error(err);
alert(err.message);
}
}
I also check whenever the user's auth state changes (here I also do another fetch and store it in Redux).
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
if(user){
dispatch(getUser(user))
} else {
console.log("user logout")
}
});
return unsubscribe;
}, []);
But when a new user signs in the first time, I get an error from the fetch:
export const getUser = createAsyncThunk("profile/getUser", async (user) => {
try {
const userQuery = query(
collection(db, "users"),
where("uid", "==", user?.uid)
);
const doc = await getDocs(userQuery);
const data = doc.docs[0].data();
return data;
} catch (err) {
console.error(err);
alert("An error occured while fetching user data");
}
});
"data" in above block is undefined for a small moment when the user signs in, so the alert in the try/catch block always goes off (it does manage to fetch the data after though).
This error only happens when it's a new user.
I understand that the fetch occurs before a document has been created in the "users" collection, but I'm not sure how to solve this. I've tried to add if/else to certain parts of the code (but just felt like I was grasping for straws).
I'm very much new to Firebase and still learning React, so every bit of help is really appreciated!
Problem is that your signInWithGoogle & useEffect both are running on user's auth status change. And, when its the new user, signInWithGoogle function makes aysnc call to create default doc, whereas useEffect runs to dispatch action, but at that moment user doesn't have any linked document. That is why you are getting undefined.
Ideally, you should remove one. You can merge the useEffect into signInWithGoogle to set the user details and dispatch as well.
const signInWithGoogle = async () => {
try {
const res = await signInWithPopup(auth, googleProvider);
const user = res.user;
const q = query(collection(db, "users"), where("uid", "==", user.uid));
const docs = await getDocs(q);
// create `doc` if its the new user
if(docs.docs.length === 0){
const firstName = user.displayName.split(' ')[0];
await addDoc(collection(db, "users"), {
uid: user.uid,
name: user.displayName,
firstName: firstName,
photoURL: user.photoURL,
authProvider: "google",
email: user.email,
})
}
// set user info and dispatch
setCurrentUser(user);
setLoading(false);
dispatch(getUser(user))
} catch(err) {
console.error(err);
alert(err.message);
}
}
Hope that answers your query.
Thanks

Firebase v9 signup a new user and store users in a users collection

I am able to sign up a new user, but how can I store that user in a collection in firestore? I am not sure with firebase v9 if the addDoc function is in the right place, but I don't know where else or how to code it.
export const useSignup = () => {
const [error, setError] = useState("");
const { dispatch } = useAuthContext();
const signup = (email: string, password: string, username: string) => {
setError("");
createUserWithEmailAndPassword(auth, email, password)
.then((res) => {
dispatch({ type: "LOGIN", payload: res.user });
const uid = res.user.uid;
const data = {
id: uid,
email,
username,
};
const ref = collection(db, "users");
addDoc(ref, {
data,
});
})
.catch((err) => {
setError(err.message);
});
};
return { error, signup };
};
To create a user-document for a newly signed-up user, you can do the following:
Access their bid from the user object (like you already did).
Create a document reference whose path ends in the above mid. Be aware that this document does not exist yet.
Use the setDoc method with above document reference, and user data to be stored as inputs.
It looks like the following in the form of code:
const uid = res.user.uid;
const data = {
id: uid,
email,
username,
};
const ref = collection(db, `users/${uid}`);
setDoc(ref, data)
.then(() => console.log("Created New User Document Successfully"))
.catch(() => console.log("Error"))
You must write the above code right after your dispatch call. Hope this helps!

How to set a custom username in Firebase?

The first time a user logins with Google Auth provider a "username" field with an empty value is set in Users collection user.uid document. Now I want to first check if the username length is greater than 3 (which will be the minimum for a username). If greater than 3 usernames are already set, else a modal should open for the user to set a username.
The code below does not work and not sure if it's the correct approach I was trying. The code runs once the user logs in.
const [user] = useAuthState(auth);
const CheckUsername = async () => {
const docRef = doc(db, "UsersData", user.uid);
const docSnap = await getDoc(docRef);
if (!docSnap.exists() && docSnap.data().username.length > 3) {
//<Show SetUserName Modal - Recoil>
} else if (docSnap.exists() && docSnap.data().username.length > 3) {
//<Don't show SetUserName Modal>
}
};
useEffect(() => {
if (user) {
CheckUsername();
}
}, [user]);
SetUsername Modal:
const [user] = useAuthState(auth);
const [usernameValue, setUsernameValue] = useState("");
const SetUsername = async () => {
try {
const UserRef = collection(db, "UsersData")
const UsernameQuery = query(UserRef, where("username", "==", usernameValue))
const Username = await getDoc(UsernameQuery)
if(!Username.exists()) {
await updateDoc(doc(db, "UsersData", user.uid), {
username: usernameValue,
});
} else {
console.log("Username already exists, please try another one");
}
} catch (error) {
console.log("error in try catch")
}
}
return (
<div>
<input type="text" onChange={(e) => setUsernameValue(e.target.value)} />
<button onClick={SetUsername}>Set username</button>
</div>
);
Solution I came up with:
This is in layout:
const [user] = useAuthState(auth);
const [open, setOpen] = useRecoilState(setUsernameModal);
const [update, setUpdate] = useState(true);
const CheckUser = async () => {
try {
//Where Users are stored
const userDocRef = doc(db, "UsersData1", user.uid);
//Using Transaction for if something goes wrong mid process no action taken at all
await runTransaction(db, async (transaction) => {
const userDoc = await transaction.get(userDocRef);
//Read ELSE first
//If userDoc exists means they logged in before AND/OR never finished the registration process
if (userDoc.exists()) {
const User = await getDoc(userDocRef);
//if usernameSet = false means they never set the username before
if (User.data().usernameSet === false) {
//Opens a modal to set username - (for my case it's the last process for registration)
setOpen(true);
}
} else {
//If User doesn't exist in "UsersData" means it's the first time they are logging in
await setDoc(doc(db, "UsersData1", user.uid), {
//usernameSet to check if username is set or not.
usernameSet: false,
username: "",
//usernameValue for search if username is taken and should be in uppercase OR lowercase since Fx: John & john are not the same
usernameValue: "",
//Add's default Firebase info
user: JSON.parse(JSON.stringify(user)),
});
//Updates useEffect so the user falls in userDoc.exists
setUpdate(!update);
}
});
} catch (error) {}
};
useEffect(() => {
if (user) {
CheckUser();
}
}, [user, update]);
Then a modal to update: username: "" and usernameSet: to true and then use usernameValue to check if user already exists

React creating a mock login service with async await

I am creating a mock login button which will return a user object after 5 seconds of clicking the login button.
I have the following event handler for the login button:
import UserService from "../services/user";
export default class Login extends Component {
constructor(props) {
super(props);
this.state = {
field: {
username: "",
password: "",
},
};
}
login = async (event) => {
event.preventDefault();
const res = await UserService.login({
username: this.state.field.username,
password: this.state.field.password,
});
console.log("RESPONSE DATA", res);
// Set user variable to the response
};
And this is the user service:
let user = null;
const login = async (credentials) => {
await setTimeout(() => {
user = {
name: "test user",
username: credentials.username,
password: credentials.password,
token: "test token",
};
console.log("login service finished");
}, 5000);
return user;
};
Basically, I want the user object to appear on the console.log("RESPONSE DATA", res) part. However, what I'm getting instead is null. How do I go around on doing this properly?
==================================
EDIT:
Decided to rewrite into function components. I'm still getting the same results
const Login = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const userContext = useContext(UserContext);
const login = async (event) => {
event.preventDefault();
console.log("logging in:", username, password);
try {
const user = await UserService.login({
username: username,
password: password,
});
console.log(user);
// userContext[1](user);
// console.log(userContext);
} catch (exception) {
console.log(exception);
}
};
The main issue here is that setTimeout return value is basically
a positive integer value which identifies the timer created by the call to setTimeout(). (which can be passed to clearTimeout() to cancel the timeout)
So in order to achive this you need to transform your function so it returns a promise. You can find some options here. In your case you could edit it slightly the answers so do something like:
let user = null;
const login = async (credentials) => {
return new Promise(resolve => setTimeout(() => {
user = {
name: "test user",
username: credentials?.username,
password: credentials?.password,
token: "test token",
};
console.log("login service finished");
return resolve(user);
}, 5000))
};

How to upload image to Firebase during sign up?

I am trying to upload an image to Firebase storage. The problem is that since the user has not signed up yet, I don't have their uid.
I depend on onAuthStateChanged to get the user id and upload an image to their bucket, but so far it hasn't turned out well.
const { userObject } = useContext(Context) //trying to get the uid from here
onSubmit={(values, { setSubmitting }) => {
async function writeToFirebase() {
firebaseService.auth.createUserWithEmailAndPassword(values.email, values.password)
await firebaseService.firestore.collection('businessesPendingAdminApproval').add(values)
}
writeToFirebase()
async function sendToFirebaseImageBucket(photo, uid) {
const businessRef = await firebaseService.firestore.doc(
`businessesPendingAdminApproval/${uid}`,
)
firebaseService.storage
.ref()
.child('businesses')
.child(uid)
.child('avatar-image')
.put(photo)
.then(response => response.ref.getDownloadURL())
.then(photoURL => businessRef.update({ avatarImage: photoURL })) //try to update avatarImage
}
const uid = userObject.uid //undefined, can't get uid
sendToFirebaseImageBucket(values.avatarImage, uid) //uid gets passed as undefined
}}>
The way I am setting the userObject which is where I'm trying to get the uid from.
Setting the userObject eventually works but maybe not fast enought for me to be able to pass it to a function (as in the code above).
useEffect(() => {
firebaseService.auth.onAuthStateChanged(async function (userAuth) {
if (userAuth) {
const user = await firebaseService.createUserProfileDocument(userAuth)
setUserObject(user) //set userObject which has an uid field.
} else {
console.log('no one signed in')
}
})
}, [])
Just add your image to cloud storage right after you have logged in and was able to get uid. the following code can help you, it works for me as well. put the following code inside useEffect.
const unsubscribe = auth().onAuthStateChanged(user => {
if (user.uid){
const ref = storage.ref(`images/${user.uid}`);
const task = ref.putFile(_image, { contentType: 'image/jpeg' });
task.on(firebase.storage.TaskEvent.STATE_CHANGED, snap => {
setState({ type: 'cents', value: snap.bytesTransferred / snap.totalBytes * 100 });
}, err => { console.log('Error in help:persisAppState: ', err) }, async () => {
const image = await ref.getDownloadURL();
if (image) await db.collection("imagelinks").doc(user.id).set({ image });
});
}
});

Resources