React Native Firebase Authentication with Hooks loses userState on app resume - javascript

I'm currently creating a React Native mobile application with Typescript.
The application uses the Firebase authentication with the Google OAuth Provider.
In order to use the username and some other details (retrieved from Firestore) I'm using a React Provider like shown in the following example:
import React, {useState, useEffect} from 'react';
import auth from '#react-native-firebase/auth';
import { GoogleSignin } from '#react-native-community/google-signin';
import firestore from '#react-native-firebase/firestore';
GoogleSignin.configure({
webClientId: 'x.googleusercontent.com',
});
const getUserById = async (id: string) => {
const admin = await firestore().collection("users").doc(id).collection("priv").doc("admin").get();
const prot = await firestore().collection("users").doc(id).collection("priv").doc("protected").get();
const jsonData = {
admin: admin.data(),
protected: prot.data(),
};
return jsonData;
}
const AuthContext = React.createContext({});
function AuthProvider(props: any) {
const [user, setUser] = useState(auth().currentUser);
const [details, setDetails] = useState({});
const [initializing, setInitializing] = useState(true);
const onAuthStateChanged = async (authUser: any) => {
setUser(authUser);
if (authUser !== null)
refreshDetails();
}
const refreshDetails = async () => {
const details = (await getUserById(user.uid));
setDetails(details);
}
useEffect(() => {
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber; // unsubscribe on unmount
}, []);
const loginWithGoogle = async () => {
const { idToken } = await GoogleSignin.signIn();
// Create a Google credential with the token
const googleCredential = auth.GoogleAuthProvider.credential(idToken);
// Sign-in the user with the credential
return auth().signInWithCredential(googleCredential);
}
const logout = () => {
auth()
.signOut()
}
return (
<AuthContext.Provider value={{user, loginWithGoogle, logout, refreshDetails, details, initializing}} {...props}></AuthContext.Provider>
)
}
const useAuth = () => {
const state = React.useContext(AuthContext);
return {
...state,
};
}
export {AuthProvider, useAuth};
As you can see in the example I'm using this useEffect method from React to subscribe to authentication changes.
Unfortunately if I close the app and reopen it again, this authentication change isn't triggered so the user state isn't set and I get a bunch of errors.
What would be the best practice in a scenario like this? I think I only need to trigger the onAuthStateChangeEvent when the app was started again.
Thanks for all help
IJustDev

onAuthStateChanged function must be triggered when the app re-opens. However, it's supposed to run asynchronously you have to implement the case user's value is invalid.

Related

React & Firebase Global State

I'm using react-firebase-hook , and I'm trying to check if the user is admin or not, and I want to it to be a global state where I don't have to add this code in every and each component to check if the user is admin or not, here is the code..
import { useState, useEffect } from 'react';
import { query, collection, getDocs, where } from "firebase/firestore";
import { auth, db } from "../../config/fbConfig";
import { useAuthState } from "react-firebase-hooks/auth";
const CreateAnn = () => {
const [ann, setAnn] = useState(''); // ignore this
const [admin, setAdmin] = useState(false);
const [user] = useAuthState(auth);
const fetchAdmin = async () => {
try {
const q = query(collection(db, "users"), where("uid", "==", user?.uid));
const doc = await getDocs(q);
const data = doc.docs[0].data();
if(data.admin === true) {
setAdmin(true);
}
else { setAdmin(false); }
} catch (err) {
// do nothing
}
};
useEffect(() => {
fetchAdmin();
});
I want to have this as a global state, tried to useContext but i think I'm using it the wrong way, so anyone can help?
You are correct to use a context, however, you might use it wrong as you said.
You should set up a context that handles the currently logged in user.
In this context you can also fetch the extra details of the user from the user collection.
Also, you can grab the user directly with ID instead of where:
const docRef = doc(db, "users", user.uid);
const docSnap = await getDoc(docRef);
const data = docSnap.exists ? docSnap.data() : undefined
Follow this link to set up the context of auth correct.
https://dev.to/dchowitz/react-firebase-a-simple-context-based-authentication-provider-1ool

How to prevent user for access inner page if not loggged in Using Reactjs

I am working with Reactjs and using nextjs framework,I am working on Login and logout module,I want if any user trying to access inner page without "loggged in" then he should redirect to "index/login" page,How can i do this ? Here is my current code if someone login with correct credentials
const handleSubmit = (e: any) => {
sessionStorage.setItem("email", response.data.email);
const email = sessionStorage.getItem("email");
router.push('/dashboard');
}
I think for checking user authentication it's better to store email or token in cookie and in getServerSideProps check cookie (because you have no access to localStorage in it)
and in getServerSideProps you can access cookie via req.cookies or use cookies package
sth like this :
import Cookies from 'cookies';
export const getServerSideProps = async ({ req, res, locale, ...ctx }) => {
if (req) {
// const posts = axios.get...
const cookies = new Cookies(req, res);
const token = cookies.get('email');
if (!token) {
return {
redirect: {
destination: '/login',
statusCode: 401
}
}
}
return {
props: {
posts,
// your other props
},
};
}
}
and in client code to set cookie you can use js-cookie package
forexample :
import Cookies from 'js-cookie'
const handleSubmit = (e: any) => {
const email = response.data.email
Cookies.set('email', email )
router.push('/dashboard');
}
Well you can do this by holding a context in your whole project or a state or props in specific pages that need permission to view.
So let's assume you have a Dashboard page. You can handle your login in getServerSideProps for better experience and also redirect your user with redirect property before even letting the user to see your page:
export const DashboardPage = (props) => {
console.log(props.data);
return /* ... */;
};
export async function getServerSideProps(context) {
const res = await fetch(`Your Login URL`)
const data = await res.json()
if (!data) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
/* The rest logic of your getServerSideProps */
return {
props: {
data: data,
/* other props */
}
}
}
In this way you should use the logic in every page you have.
Instead you can send the login request from client and set the response to a context value and use that context in every other pages and components.
Context File:
import React, { useState, useEffect } from 'react';
export const UserContext = React.createContext();
export function UserProvider({ children }) {
const [userData, setUserData] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState();
const value = { userData, setUserData, isLoggedIn, setMode };
const handleLogin = async () => {
const res = await fetch(`Your Login URL`);
if(res.status === 200) {
const data = await res.json();
setUserData(data);
setIsLoggedIn(true);
} else {
setIsLoggedIn(false);
}
}
useEffect(() => {
handleLogin();
}, []);
return (
<UserContext.Provider value={value}>
{ children }
</UserContext.Provider>
);
}
And then use the values in your pages, for example Dashboard:
export const DashboardPage = (props) => {
const { isLoggedIn } = useContext(UserContext);
useEffect(() => {
if(isLoggedIn === false) {
router.push('/login');
}
}, [isLoggedIn]);
return /* ... */;
};

Waiting for Firebase RTDB to update state

I am creating a data context for my react native app. There is an array of property objects in a firebase node and the aim is to pull this data into the app on load and provide that data throughout the app. I have a loader that shows if the loading state is true and should only be false if the data has been successfully pulled from firebase.
The issue I am having is the Loader ends before all the data is called and then the properties are not shown on the home page of the app until i refresh the app again.
Below is the code for the app:
import { onValue, ref } from 'firebase/database'
import React, { useEffect, useState } from 'react'
import { projectDatabase } from '../../config'
import useAuth from '../hooks/useAuth'
export const DataContext = React.createContext()
const DataLayer = ({ children }) => {
const { userCredentials: user } = useAuth() //get user data
const [company, setCompany] = useState('');
const [loading, setLoading] = useState(true);
const [properties, setProperties] = useState([]);
useEffect(() => {
const userRef = ref(projectDatabase, 'users/' + user.uid + '/company');
onValue(userRef, (snapshot) => {
setCompany(snapshot.val());
const propertiesRef = ref(projectDatabase, 'providers/' + snapshot.val() + '/properties');
onValue(propertiesRef, (propertiesSnapshot) => {
// console.log(propertiesSnapshot.val())
setProperties(propertiesSnapshot.val());
setLoading(false);
});
});
}, [])
return (
<DataContext.Provider
value={{
properties:properties,
company: company,
loading: loading,
userEnquiries:userEnquiries,
chatId:chatId,
acceptedBookings:acceptedBookings
}}
>
{children}
</DataContext.Provider>
)
}
export default DataLayer
Is there a way to make the call to the firebase RTDB complete BEFORE the loading is resolved as false or do i just do a useEffect call to every page the data is needed? For context the array has 200 properties.
I would really appreciate any help I can get. I love firebase and would really want this to work.

Window is not defined while using firebase auth in next.js

Following code is from firebaseConfig.js:
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
//credentials//
};
export const app = initializeApp(firebaseConfig);
export const analytics=getAnalytics(app)
export const authentication=getAuth(app);
Following code is from index.js:
export default function Home() {
const auth = getAuth();
const generateRecaptcha=()=>{
window.recaptchaVerifier = new RecaptchaVerifier('recaptcha-container', {}, authentication);
}
window.recaptchaVerifier = new RecaptchaVerifier('recaptcha-container', {}, auth);
const getOTP=()=>{
generateRecaptcha()
}
I am getting error:
ReferenceError: window is not defined
After removing export getAnyalytics, I am still getting the same error but at window.recaptchaVerifier function in index.js.
Also please tell me the use of getAnalytics.
getAnalytics() will instantiate an instance of Firebase Analytics that you can use to log events throughout your app.
The solution for me when using analytics was to create a provider as follows:
FirebaseTrackingProvider.tsx
export const FirebaseTrackingProvider = (props: {children: ReactNode}) => {
const router = useRouter();
const [analytics, setAnalytics] = useState(null);
useEffect(() => {
setAnalytics(getAnalytics(firebaseApp));
if (analytics) {
setAnalyticsCollectionEnabled(analytics, true);
}
const handleRouteChange = (url: string) => {
if (!analytics) {
return;
}
logEvent(analytics, 'page_view', {
page_location: url,
page_title: document?.title,
});
setCurrentScreen(analytics, document.title ?? 'Undefined');
};
router.events.on('routeChangeStart', handleRouteChange);
return () => {
router.events.off('routeChangeStart', handleRouteChange);
};
}, [analytics, router.events]);
return <FirebaseContext.Provider value={analytics}>{props.children}</FirebaseContext.Provider>;
};
I can then consume it different pages or components:
const analytics = useContext(FirebaseContext);
// in sign up flow
logEvent(analytics, 'sign_up', {
uid: data.uid,
email: data.email,
});
Regarding the recapture erorr: NextJS will first attempt to render serverside content if there is any, before bootstrapping the react application. This means that the window has not been defined yet when you are trying to instantiate a new RecaptchaVerifier instance. You can use an if(window) to make sure you are only doing so when the window is instantiated, or alternatively, you can run a useEffect as follows:
useEfect(() => {
// This wont change on re renders
let completed = false;
if (!completed && window){
// recaptca instantiation
completed = true;
}
}, [window])

Calling a function on socket event to update data that has been fetched using useEffect returns undefined

I am currently working on developing a Whatsapp Web clone exactly similar to 'Whatsapp Web' using Whatsapp Web JS and Next.JS and Express, the data is coming from real whatsapp with the help of Wwebjs module (link above^)
This is the folder structure
|
|
|-context
|--> (StateContext.js) Contains the data that has been fetched using useEffect() and then further sent down the tree using useContext()
|-pages
|--> Components (Contains all the components)
|--> (_app.js) (Contains the <State.Provider>)
|--> (index.js)
StateContext.js
import React, { createContext, useContext, useState, useEffect } from "react";
import axios from "axios";
import { io } from "socket.io-client";
import useSound from 'use-sound';
const Context = createContext();
const socket = io('http://localhost:8000')
export const StateContext = ({ children }) => {
//Contains msgs from all the chats in ("phone_no.": "messages_from_this_no.") <- this pattern as objects
const [allMsgs, setAllMsgs] = useState();
//Contains info of the current logged in user
const [currentUser, setCurrentUser] = useState();
//Contains the chats for all phone no`s.
const [chats, setChats] = useState([]);
//Contains the latest msgs received from chats
const [lastMsgs, setLastMsgs] = useState([]);
//Chat that has been clicked on to see messages from that chat
const [selectedChat, setSelectedChat] = useState();
//Loading state while messages and other data is being fetched
const [loading, setLoading] = useState(false);
//Notification sound
// const [play] = useSound('/notification.mp3');
useEffect(() => {
socket.on('connect', () => {
console.log(socket.id);
})
socket.on('disconnect', () => {
console.log(socket.id + 'disconnected.');
})
//Triggers when a message has been received
socket.on('newMessage', (msg) => {
updateChats(msg, lastMsgs, chats)
})
return async () => {
socket.off('connect');
socket.off('disconnect');
socket.off('newMessage');
setLoading(true)
const chats_res = await axios.get('http://localhost:8000/chats');
const user_res = await axios.get('http://localhost:8000/users/me');
const allMessages_res = await axios.get('http://localhost:8000/messages');
const lastMsgs_res = await axios.get('http://localhost:8000/messages/last');
setChats(chats_res.data.response);
setCurrentUser(user_res.data);
setLastMsgs(lastMsgs_res.data.response);
setAllMsgs(allMessages_res.data.response);
setLoading(false)
}
}, []);
// function used to update data when a new message has been received to update the last message and the message unread count
function updateChats(newMessage, lastMessages, chats) {
console.log('Updating Chats');
const updatedLastMsgs = lastMessages.map((lastMsg) => {
if (newMessage.from === lastMsg?.from) {
return newMessage
}
return lastMsg
});
const newChats = chats.map((chat) => {
if (chat.id._serialized === newMessage.from) {
chat.unreadCount += 1;
console.log(chat.unreadCount);
}
return chat
});
setLastMsgs(updatedLastMsgs);
setChats(newChats)
console.log(newChats);
}
return <Context.Provider value={{ allMsgs, lastMsgs, currentUser, chats, loading, selectedChat, msgs, setAllMsgs, setChats, setCurrentUser, setLastMsgs, setSelectedChat, setMsgs }}>
{children}
</Context.Provider>
}
export const useStateContext = () => useContext(Context)
Now after the data has been fetched and later on a message is received, the updateChats() function is executed but after execution the chats vanishes. On console logging the "const newChats" it logs undefined, but for some reason when the next app reloads due to server side changes on refresh it seems to work magically and absolutely fine.
Well I think it is due to the socket.on('newMessage') receiving the state data when it is yet to be fetched and after that refresh it gets the data so it start working.
So what is it that I could do to get this working right?

Categories