How to set a custom username in Firebase? - reactjs

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

Related

how can i wait for firebase to check the user is valid before sending a POST request with reactjs?

I am using the following code to obtain the users idToken before sending it to the backend as an authorisation header:
const user = firebase.auth().currentUser
const idToken = await user.getIdToken()
sent like this:
var res = await axios.post(backUrl + "account/load_balance", {
uid: uid,
id: id
},
{
headers: {
Authorization: 'Bearer ' + idToken
}});
It works well but on one of my pages the request is sent to the server before idtoken variable has filled and the user is still null.
I have read that i need to implement onAuthStateChanged as it waits for the token before triggering: https://firebase.google.com/docs/auth/web/manage-users#web-version-8
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
var uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
However i am unsure how to implement this in to my code.
Can anyone advise?
Full code:
const RoutingForPortfolio = (props) => {
let uid = localStorage.getItem("account-info");
let { id } = useParams();
const loadBlockchainData = async (dispatch) => {
if (id === null || id === undefined) {
id = "test";
}
const user = firebase.auth().currentUser
const idToken = await user.getIdToken()
console.log(idToken)
var res = await axios.post(backUrl + "account/load_balance", {
uid: uid,
id: id
},
{
headers: {
Authorization: 'Bearer ' + idToken
}});
if (res.data === null) {
await wait(2);
document.location.href = "/logout"
return;
}
else {
// const web3 = new Web3(new Web3.providers.HttpProvider('https://data.stocksfc.com:3200'));
// dispatch(web3Loaded(web3));
const account = res.data.address;
dispatch(web3AccountLoaded(account));
localStorage.setItem("account-address", account);
if (res.data.token_flag && res.data.exchange_flag) {
await dispatch(setLoginUserName(res.data.name));
await dispatch(setLoginUserEmail(res.data.email));
if (res.data.balance !== null) {
await dispatch(etherBalanceLoaded(res.data.balance[0]));
await dispatch(tokenBalanceLoaded(res.data.balance[1]));
await dispatch(exchangeEtherBalanceLoaded(res.data.balance[2]));
await dispatch(exchangeTokenBalanceLoaded(res.data.balance[3]));
}
}
else {
Swal.fire({
icon: "error",
title: "Error...",
text: "Error 485 - Please report to admin",
});
return;
}
}
};
useEffect(() => {
if (uid) {
loadBlockchainData(props.dispatch);
}
}, [props.dispatch, uid]);
return (
<>
{uid ? (
<div>
<Portfolio id={id} />
</div>
) : (
<Login />
)}
</>
);
};
As you correctly identified, firebase.auth().currentUser is a synchronous action that only gets the user object when it is called. You've also correctly surmised that you instead need to use firebase.auth().onAuthStateChanged() to wait to check if the user is logged in.
This can be achieved by wrapping an onAuthStateChanged listener into a Promise where it is immediately detached after being called once.
function getValidatedUser() {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.auth()
.onAuthStateChanged(
(user) => {
unsubscribe();
resolve(user);
},
reject // pass up any errors attaching the listener
);
});
}
This now allows you to use:
const user = await getValidatedUser();
if (!user) {
// todo: handle no user signed in, such as:
throw new Error("User not signed in!");
}
// if here, user is User object
const idToken = await user.getIdToken()

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

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

How to add phone number to a logged in user(created with email and password) in firebase Auth in react?

I am using firebasev9 authentication for my react project. I have used email authentication for logging in/signing up a user. I want to add phone number too in the user but I am doing all the right steps but when I call updatePhoneNumber with user and phone crediential it throws an error and doesnt add phone number. I am updating the displayName too within the same function which works fine.
I have enabled phone signin in firebase dashboard
The error I am getting is this:
(https://i.postimg.cc/yY55Qzg2/Screenshot-2022-06-25-200735.jpg)
This is my signup function:
const signup = async (email, password, displayName, phoneNumber,phoneCrediential, userType) => {
setError(null);
setIsPending(true);
try {
const res = await createUserWithEmailAndPassword(
auth,
email,
password
);
console.log(res.user);
if (!res) {
throw new Error("Could not complete signUp");
}
debugger;
await updateProfile(res.user, { displayName });
**This updatePhonenumber function throws this error flow moves to catch block**
const resPhone = await updatePhoneNumber(auth.currentUser, phoneCrediential );
console.log(resPhone)
dispatch({ type: "LOGIN", payload: res.user });
console.log(res.user)
addDocument({
name: res.user.displayName,
email: res.user.email,
uid: res.user.uid,
type: userType,
});
if (!isCancelled) {
setError(null);
setIsPending(false);
}
} catch (err) {
if (!isCancelled) {
console.log(err.message);
setError(err.message);
setIsPending(false);
}
}
};
In my component, I take phone number, generate Otp, and take otp and pass phoneCredential to the signup function:
const [verificationIdState, setVerificationIdState] = useState(null);
const [phoneCredientialState, setPhoneCredientialState] = useState(null);
const handleRecaptcha = () => {
window.recaptchaVerifier = new RecaptchaVerifier(
"sign-in-button",
{
size: "invisible",
callback: (response) => {
// reCAPTCHA solved
},
},
auth
);
};
const handleGetOTP = () => {
handleRecaptcha();
const phoneNumber = "+91" + userPhoneNumber;
const applicationVerifier = window.recaptchaVerifier;
const provider = new PhoneAuthProvider(auth);
const verificationId = provider.verifyPhoneNumber(
phoneNumber,
applicationVerifier
);
if (verificationId) {
setVerificationIdState(verificationId);
}
};
const handleOTPSubmit = () => {
const phoneCredential = PhoneAuthProvider.credential(
verificationIdState,
userOTP
);
if (phoneCredential) {
setPhoneCredientialState(phoneCredential);
console.log(phoneCredential);
}
};
//Base Register
const handleRegisterSubmit = (e) => {
e.preventDefault();
signup(
userEmail,
userPassword,
userName,
userPhoneNumber,
phoneCredientialState,
userType
);
};

How can I make my function wait until the user data is updated in my useContext provider?

new to react here, I want a new user to enter their details on their first sign in. This includes enterting a username, name, profile picture etc.
When they have submitted their details, I wait for confirmation from firebase and then I want to forward them to their profile (the link structure is domain/p/:username).
However, every time I try it, it ends up trying to head to domain/p/undefined?
When I use react dev tools to inspect, I can see that the username was successfully sent up to my state provider, so I think it's just a matter of timing thats the problem.
Heres the welcome page functions:
//The first method begins the update and checks if the username already exists.
const update = async (e) => {
if (
firstName.trim() === "" ||
lastName.trim() === "" ||
username.trim() === "" ||
bio.trim() === "" ||
addressOne.trim() === "" ||
city.trim() === "" ||
county.trim() === "" ||
postCode.trim() === "" ||
photos.length === 0
) {
window.alert("Invalid data!\nOnly Address line 2 can be empty");
} else {
var usernameRef = db
.collection("users")
.where("username", "==", username);
usernameRef.get().then((docs) => {
if (docs.size === 1) {
docs.forEach((doc) => {
if (doc.id === currentUser.uid) {
sendUpdate();
} else {
window.alert("Username taken");
}
});
} else {
sendUpdate();
}
});
}
};
//This method puts the initial data into firebase except the profile picture
function sendUpdate() {
setLoading("loading");
db.collection("users")
.doc(currentUser.uid)
.set(
{
username: username,
name: firstName,
surname: lastName,
bio: bio,
address1: addressOne,
address2: addressTwo,
notifications: [],
city: city,
county: county,
postcode: postCode,
newUser: false,
},
{ merge: true }
)
.then(() => {
updatePhoto();
})
.catch((err) => console.log(err));
}
//This method uploads the profile picture, then gets the downloadURL of the photo just uploaded and puts it into the user document created in method 2.
//It also trys to send the user to their profile afterwards, but it always ends up as undefined.
const updatePhoto = async () => {
const promises = [];
var userREF = db.collection("users").doc(currentUser.uid);
photos.forEach((photo) => {
const uploadTask = firebase
.storage()
.ref()
.child(
`users/` + currentUser.uid + `/profilePicture/profilePicture.jpg`
)
.put(photo);
promises.push(uploadTask);
uploadTask.on(
firebase.storage.TaskEvent.STATE_CHANGED,
(snapshot) => {
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
if (snapshot.state === firebase.storage.TaskState.RUNNING) {
console.log(`Progress: ${progress}%`);
}
},
(error) => console.log(error.code),
async () => {
const downloadURL = await uploadTask.snapshot.ref.getDownloadURL();
userREF
.update({
profilePicture: downloadURL,
})
.then(async () => {
updateUserData().then(() => {
setLoading("complete");
setTimeout(() => {
history.push("/p/" + userData.username);
}, 3000);
});
});
}
);
return "completed";
});
};
Here is my AuthContext provider: (the function UpdateUserData() is what updates the data after its been put into firebase)
import React, { useContext, useState, useEffect } from "react";
import { auth, db } from "../firebase";
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [userData, setUserData] = useState();
const [loading, setLoading] = useState(true);
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password);
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
async function updateUserData() {
if (currentUser) {
var userData = db.collection("users").doc(currentUser.uid);
await userData
.get()
.then((doc) => {
if (doc.exists) {
setUserData(doc.data());
return "success";
}
})
.catch((error) => {
console.log("Error getting document:", error);
return "error";
});
}
}
function logout() {
setUserData();
return auth.signOut();
}
function resetPassword(email) {
return auth.sendPasswordResetEmail(email);
}
function updateEmail(email) {
return currentUser.updateEmail(email);
}
function updatePassword(password) {
return currentUser.updatePassword(password);
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
if (user) {
var userData = db.collection("users").doc(auth.currentUser.uid);
userData
.get()
.then((doc) => {
if (doc.exists) {
setUserData(doc.data());
}
})
.catch((error) => {
console.log("Error getting document:", error);
});
}
});
return unsubscribe;
}, []);
const value = {
currentUser,
userData,
updateUserData,
login,
signup,
logout,
resetPassword,
updateEmail,
updatePassword,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
And as you can see, once the undefined page has been attempted to load, we can see the username did in fact end up in userData from my context provider:
TIA!
You can resolve this issue by move the redirect link out side of you updatePhoto and put it in useEffect (or any other option base on code flow) then just set an state or check the needed data like userdata.userName is already exists, if its undefined prevent redirect and you can display loader component for example, else execute redirect...
Basic Example:
useEffect(() => {
if(userData.username){
history.push("/p/" + userData.username);
}
}, [userData.username])
const myUpdateFunction = useCallBack(() => {
fetch().then(v => {
setUserData(v);
})
}, [])

handling useEffect and server side changes

I want to confirm that a new user has a unique nickname or not. And if it is not unique i want to show a message to user. For that reason i wrote that code:
const [vNicknameServer, setvNicknameServer] = useState(true);
...
const validateNicknameServer = async (nickname) => {
let flag = 0;
await firebase
.firestore()
.collection("Users")
.where("nickname", "==", nickname)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((documentSnapshot) => {
if (documentSnapshot.data().nickname == nickname) {
setvNicknameServer(false); //Changing state
flag = 1;
}
});
if (flag == 0) {
setvNicknameServer(true); // changing state
}
})
.catch((err) => console.log(err));
};
....
{vNickname ? (
<Text style={{ color: "#DC8989" }}>
Nickname min 4 max 14 caracters
</Text>
) : vNicknameServer == false ? ( // according to state it appears or not
<Text style={{ color: "#DC8989" }}>Nickname must be unique</Text>
) : null}
But the render process always done after some other process. I should use useEffect but i am a little bit confused how to use it in there? Anyone can help me to use it?
Note:
This validation made it in a submit handler :
const Submit = () => {
validateEmail(email) ? setvEmail(false) : setvEmail(true);
validateNickname(nickname) ? setvNickname(false) : setvNickname(true);
validatePassword(password) ? setvPassword(false) : setvPassword(true);
validateNicknameServer(nickname);
console.log(vEmailServer);
console.log(email);
if (
validateEmail(email) &&
validateNickname(nickname) &&
validatePassword(password) &&
vNicknameServer &&
vEmailServer
) {
navigation.navigate("UserCredential", {
email: email,
password: password,
nickname: nickname,
});
}
};
I want to control it just when i clicked the button not in every case.
NOTE: Please check the below answer and comments.
If I understand your question correctly, you are trying to use an async in a useEffect hook. To do that, you can use the following pattern
// probably need to setup a useState hook to contain nickname data
const [nickname, setNickname] = useState(true);
useEffect(() => {
// get nickname from the component state instead of passing the data as a param
const validateNicknameServer = async () => {
let flag = 0;
await firebase
.firestore()
.collection("Users")
.where("nickname", "==", nickname)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((documentSnapshot) => {
if (documentSnapshot.data().nickname == nickname) {
setvNicknameServer(false); //Changing state
flag = 1;
}
});
if (flag == 0) {
setvNicknameServer(true); // changing state
}
})
.catch((err) => console.log(err));
};
validateNicknameServer();
}, [nickname])
Update based on the note
I can't verify if it runs without any error. But hopefully, it provides some ideas for you to solve the issue.
const validateNicknameServer = async (nickname) => {
let flag = 0;
const querySnapshot = await firebase
.firestore()
.collection("Users")
.where("nickname", "==", nickname)
.get()
.catch((err) => console.log(err));
querySnapshot.forEach((documentSnapshot) => {
if (documentSnapshot.data().nickname == nickname) {
setvNicknameServer(false); //Changing state
flag = 1;
return false
}
});
if (flag == 0) {
setvNicknameServer(true); // changing state
return true
}
};
const Submit = async () => {
// .... other code
// validate the name before navigation happens
// and use vNicknameServer to displaying messages
const isValidName = await validateNicknameServer(nickname);
// .... other code
if (
validateEmail(email) &&
validateNickname(nickname) &&
validatePassword(password) &&
isValidName &&
vEmailServer
) {
navigation.navigate("UserCredential", {
email: email,
password: password,
nickname: nickname,
});
}
};
I think you don't need to use useEffect, and use for in instead forEach for easy control the flow.
const validateNicknameServer = async (nickname) => {
await firebase
.firestore()
.collection("Users")
.where("nickname", "==", nickname)
.get()
.then((querySnapshot) => {
for (var documentSnapshot in querySnapshot) {
if (documentSnapshot.data().nickname == nickname) {
return false;
}
}
return true;
})
.catch((err) => console.log(err));
};
const Submit = async () => {
// Other validation ...
// Waiting validate nickname on server. Should display loading state for better UX.
const validateNicknameServer = await validateNicknameServer(nickname);
// Change state after validate
setvNicknameServer(false);
// Form valid -> Navigate to UserCredential screen
if (
validateEmail(email) &&
validateNickname(nickname) &&
validatePassword(password) &&
vEmailServer
validateNicknameServer &&
) {
navigation.navigate("UserCredential", {
email: email,
password: password,
nickname: nickname,
});
}
};
On click of submit you're doing async operation but not waiting for it that's why
render process always done after some other process
So submit method's if's code will execute before validateNicknameServer's async operation is completed.
I think You don't need to use useEffect here.
also after setvNicknameServer() whole component is gonna re-render so Submit method still gonna have vNicknameServer's previous value even if you've updated it.
Here's what you can do
For method validateNicknameServer when using await you should use try/catch instead of then/catch
const validateNicknameServer = async (nickname) => {
let flag = 0;
try {
const querySnapshot = await firebase
.firestore()
.collection('Users')
.where('nickname', '==', nickname)
.get();
querySnapshot.forEach((documentSnapshot) => {
if (documentSnapshot.data().nickname == nickname) {
flag = 1;
}
});
return flag == 0 ? true : false;
} catch (err) {
console.log(err);
}
return false;
};
now in Submit method wait for validateNicknameServer to complete and same for other methods (validateEmail, validateNickname, validatePassword) too if they're also asynchronous.
const Submit = async () => {
validateEmail(email) ? setvEmail(false) : setvEmail(true);
validateNickname(nickname) ? setvNickname(false) : setvNickname(true);
validatePassword(password) ? setvPassword(false) : setvPassword(true);
// wait for this method to complete, same for above methods too if they are async
const isNickNameUnique = await validateNicknameServer(nickname);
setvNicknameServer(isNickNameUnique);
console.log(vEmailServer);
console.log(email);
if (
validateEmail(email) &&
validateNickname(nickname) &&
validatePassword(password) &&
isNickNameUnique &&
vEmailServer
) {
navigation.navigate('UserCredential', {
email: email,
password: password,
nickname: nickname,
});
}
};
here these type of operations are expected to be async so it's best practice to show LoadingIndicator to the user while waiting.

Resources