How to upload image to Firebase during sign up? - reactjs

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 });
});
}
});

Related

What is the best way about send multiple query in useMutation (React-Query)

I developed login function use by react-query in my react app
The logic is as follows
First restAPI for checking duplication Email
If response data is true, send Second restAPI for sign up.
In this case, I try this way
// to declare useMutation object
let loginQuery = useMutation(checkDuple,{
// after check duplication i'm going to sign up
onSuccess : (res) => {
if(res === false && warning.current !== null){
warning.current.innerText = "Sorry, This mail is duplicatied"
}else{
let res = await signUp()
}
}
})
//---------------------------------------------------------------------------------
const checkDuple = async() => {
let duple = await axios.post("http://localhost:8080/join/duple",{
id : id,
})
}
const signUp = async() => {
let res = await axios.post("http://localhost:8080/join/signUp",{
id : id,
pass : pass
})
console.log(res.data)
localStorage.setItem("token", res.data)
navigate("/todo")
}
I think, this isn't the best way, If you know of a better way than this, please advise.
Better to have another async function that does both things.
something like
const checkDupleAndSignUp = async () => {
await checkDuple();
await signUp();
};
And then use that in your useMutation instead.
Other things to consider:
Maybe move the logic to set local storage and navigate to another page in the onSuccess instead.
You can also throw your own error if one or the other request fails and then check which error happened using onError lifecycle of useMutation, and maybe display a message for the user depending on which request failed.
You can handle both of them in a single function and in mutation just add token in localStorage and navigate
like this:
const checkDupleAndSignUp = async () => {
return checkDuple().then(async res => {
if (res === false) {
return {
isSuccess: false,
message: 'Sorry, This mail is duplicatied',
};
}
const { data } = await signUp();
return {
isSuccess: true,
data,
};
});
};
and in mutation :
let loginQuery = useMutation(checkDupleAndSignUp, {
onSuccess: res => {
if (res.isSuccess) {
console.log(res.data);
localStorage.setItem('token', res.data);
navigate('/todo');
} else if (warning.current !== null) {
warning.current.innerText = res.message;
}
},
});

reactjs firebase authentication update profile does not refresh

In my reactJS app, I am using firebase authentication. I can login successfully. I tried to update user profile, like photoUrl and displayName as follows. Data is updated but I can't get the latest update data unless refresh the page.
Please ignore updateCurrentUser function. it is an API call to update display name and photo URL to database.
after updateCurrentUser function callback, I call again
const auth = getAuth();
const currentUser = getAuth().currentUser;
But updated display name and photo is still old data in currentUser. I want to know how can I update profile. I am showing the user info in the header section as well.
updateUser function
async function updateUser(updateUserId, displayName, photoUrl) {
const auth = getAuth();
const currentUser = getAuth().currentUser;
return await updateProfile(auth.currentUser, {
displayName: displayName,
photoUrl: photoUrl,
})
.then(async function () {
const auth = getAuth();
if (auth.currentUser != null) {
updateCurrentUser(updateUserId, displayName, photoUrl)
.then((updatedUser) => {
if (updatedUser) {
const auth = getAuth();
const currentUser = getAuth().currentUser;
setUser(currentUser);
setAccessToken(currentUser.getIdToken(true));
store.dispatch(saveUser(currentUser));
} else {
setUser(null);
setAccessToken(null);
store.dispatch(saveUser(null));
}
})
.catch((error) => {
throw error;
});
}
})
.catch((error) => {
throw error;
});
}
Firebase show updates of user profile only after refresh and automatically after login/logout operation. The only way you can do this will be by using the same values you submitted
async function updateUser(updateUserId, displayName, photoUrl) {
const auth = getAuth();
const currentUser = getAuth().currentUser;
return await updateProfile(auth.currentUser, {
displayName: displayName,
photoUrl: photoUrl,
})
.then(async function () {
const auth = getAuth();
if (auth.currentUser != null) {
updateCurrentUser(updateUserId, displayName, photoUrl)
.then((updatedUser) => {
if (updatedUser) {
const auth = getAuth();
let currentUser = getAuth().currentUser;
currentUser.displayName = displayName;
currentUser.photoUrl = photoUrl
setUser(currentUser);
setAccessToken(currentUser.getIdToken(true));
store.dispatch(saveUser(currentUser));
} else {
setUser(null);
setAccessToken(null);
store.dispatch(saveUser(null));
}
})
.catch((error) => {
throw error;
});
}
})
.catch((error) => {
throw error;
});
}
I make a silly mistake. It is wrong spelling. It should be photoURL instead of photoUrl. It is working when refresh because, at api server side, need to update user info again. At server side, variable name is correct.

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

What's causing this async function to work only after a page refresh?

I've been attempting to find discussions about this for over a week now, but most issues seem related to trouble persisting through a refresh, while I'm having state troubles without refreshing, so I'm not getting much of anywhere with it.
I'm attempting to load a gallery of images after a user logs in. The login is functioning properly--updates the state with a reducer and pushes from /login to /gallery and I can see in the inspector that the user ID updates from null to a value.
At /gallery I attempt to retrieve some data through axios asynchronously. It's a POST request so that I can send the user's ID in the body rather than the url/using params.
On initial login, state.images doesn't update and throws this error:
"data: "Cast to string failed for value "{ user: '60fc726d827a4e3daff47619' }" (type Object) at path "user" for model "Upload"" " Relative to my database/models: I've tried adjusting type on the model, both the $type approach and adjusting for a String that is an array, the former caused errors large enough for the page not to load, the latter affected no discernible changes.
If I reload the page, everything works and the images load. If I click from gallery to home and then back to gallery again, nothing changes in the state. I have no idea if this is an issue with my amateur async function structure, my mongodb setup, my reducer, the axios post itself, or something else entirely.
I've read that pretty much everything needs to be lined up inside of useEffect() but I've had absolutely no luck getting that to function either.
The whole of the code is at https://github.com/polysnacktyl/react-foraging, but here's the (seemingly) most relevant:
Login
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { getLoggedIn } = useContext(AuthContext);
const { dispatch } = useContext(Context);
const history = useHistory();
async function login(e) {
e.preventDefault();
try {
const { data } = await axios.post('http://localhost:3000/auth/login', {
email,
password
});
await getLoggedIn();
dispatch({
type: 'login',
payload: { user: data._id }
})
window.localStorage.setItem('user', JSON.stringify(data._id))
history.push('/gallery');
} catch (err) {
console.error(err);
}
}
return (... and so forth
Image Gallery
const { state, dispatch } = useContext(Context);
const [isLoading, setLoading] = useState(true);
const [images, setImages] = useState({ images: [] });
const user = state.user;
const success = async () => {
try {
const res = await axios.post('http://localhost:3000/auth/mine', { user });
dispatch({
type: 'fetchSuccess',
payload: { images: res.data }
})
setImages(res.data);
} catch (err) { console.log(err.response) }
}
const fail = (error) =>
dispatch({
type: 'fetchFail',
payload: { error: error.message }
});
function loadImages() {
dispatch({ type: 'fetchImages' });
setTimeout(async () => {
try {
await success();
setLoading(false)
} catch (error) {
await fail(error);
}
}, 1000);
}
useEffect(() => {
loadImages()
//eslint-disable-next-line
}, [])
if (isLoading) {
return (<div className='loading'>...loading</div>)
} else {
return (...and so on
Let's have a look at your loadImages. This function is called during componentDidMount. This seems OK on first glance but it is a bug actually.
loadImages internally calls success that actually depends on user variable.
Having this in mind what we can do is following:
useEffect(() => {
if(!user) { // if no user present somehow, let's return
return;
}
loadImages() // load the images
//eslint-disable-next-line
}, [user]) // add a dependency to the user, since we load user images actually
I think with this approach the issue will be fixed and also you can remove the setTimeout in loadImages in my opinion.
As it turns out, my issue was in my axios-post-with-req-body strategy. I switched it to a GET request and sent the user ID in the params and now everything is loading on initial login and persisting through page relaods. I'm still not sure why it was causing the behavior it was, but at least I know how to avoid it.
the setup that ultimately worked out for me:
function Gallery() {
const { state, dispatch } = useContext(Context);
const [isLoading, setLoading] = useState(true);
const [images, setImages] = useState([]);
const user = state.user;
const success = async () => {
const res = await axios.get('http://localhost:3000/auth/mine', {
params: { user }
})
dispatch({
type: 'fetchSuccess',
payload: res.data
})
setImages(res.data);
}
const fail = (error) =>
dispatch({
type: 'fetchFail',
payload: { error: error.message }
});
function loadImages() {
dispatch({ type: 'fetchImages' });
setTimeout(async () => {
try {
await success();
setLoading(false)
} catch (error) {
await fail(error);
}
}, 0);
}
useEffect(() => {
loadImages()
//eslint-disable-next-line
}, [])
if (isLoading) {
return (<div className='loading'>...loading</div>)
} else {
return (...images that actually load. wonderful.)
Additionally, I had to adjust the router to accept req.params.user instead of req.body.user.

Multiple nested axios calls don't resolve as expected

As described in comments between my code snippet, the asynchronicity is not working as expected. For each id, an object/item should return but it only returns one item since my async await isn't implemented properly. What could be a possible workaround?
Thanks in advance
useEffect(() => {
axios.get('url-here').then((res) => {
res.data.favProperties?.map((el) => {
console.log(el) // this returns multitple id's of saved/liked items
axios.get('url-here').then(async (r) => {
if (r.data) {
console.log(r.data) // Problem starts here
// This returns the full object of the liked items
// But only one object is returned, not every object for which an id was stored
await storageRef
.child(r.data.firebaseRef + '/' + r.data.images[0])
.getDownloadURL()
.then((url) => {
// Here i need to fetch the image for each object
console.log(url)
})
.catch((err) => console.log(err))
}
})
})
})
}, [])
I think breaking down your operations into functions will prevent this Promise Hell. I would recommend using async await for these kinda operations. Also I was confused about the last part of console logging the download URL, by my guess you're trying to save all the download URLs for these liked items in an array.
useEffect(() => {
firstFunction();
}, []);
const firstFunction = async () => {
const { data } = await axios.get("url-here");
const favProperties = data.favProperties;
const fetchedUrls = await Promise.all(
favProperties?.map(async (el) => (
await secondFunction(el.id) /** use el to pass some ID */
))
);
};
const secondFunction = async (someId) => {
/** your second URL must point to some ID (or some parameters) specific API otherwise
running same op in a loop without variations doesn't make any sense */
const { data } = await axios.get(`some-other-url/${someId}`);
if (data) {
console.log(data);
const fetchedUrl = await storageThing(data);
return fetchedUrl;
}
};
const storageThing = async ({ firebaseRef, images }) => {
try {
const downloadURL = await storageRef
.child(firebaseRef + "/" + images[0])
.getDownloadURL();
console.log(downloadURL);
return downloadURL;
} catch (error) {
console.log(error);
return '';
}
};

Resources