So I have a basic app running. There's just one button which is used to login with google and I want to store that user's info in firestore and then I want to map through every user and display every single user's which are signed in, in my app. Firebase auth is complete but I don't know how to store that user's auth info.Also I am using useContext to pass authorized users info.Here's the code:
Main Entry Level App.js
import React, { useEffect, useMemo, useState } from "react";
import Login from "./components/Login";
import { User } from "./components/User";
import db, { auth } from "./firebase";
import { UserContext } from "./Contexts/UserContext";
const App = () => {
const [user, setUser] = useState([]);
const value = useMemo(() => ({ user, setUser }), [user, setUser]);
useEffect(() => {
auth.onAuthStateChanged((user) => {
// console.log(user);
setUser(user);
});
}, []);
return (
<UserContext.Provider value={value}>
{user ? <User /> : <Login />}
</UserContext.Provider>
);
};
export default App;
User.js Component
import React, { useContext } from "react";
import { UserContext } from "../Contexts/UserContext";
import db, { auth } from "../firebase";
export const User = () => {
const { user } = useContext(UserContext);
return (
<>
<img src={user.photoURL} alt={user.displayName} />
<div>{user.displayName}</div>
<div>{user.email}</div>
<button onClick={() => auth.signOut()}>Log Out</button>
</>
);
};
Login.js
import React, { useContext, useEffect } from "react";
import { UserContext } from "../Contexts/UserContext";
import { auth, signInWithGoogle } from "../firebase";
const Login = () => {
const { setUser } = useContext(UserContext);
useEffect(() => {
auth.onAuthStateChanged((user) => {
console.log(user);
setUser(user);
});
});
return (
<>
<div style={{ textAlign: "center" }}>
<button onClick={signInWithGoogle}>
<img
src="https://img.icons8.com/ios-filled/20/000000/google-logo.png"
alt="google icon"
/>
<span> Continue with Google</span>
</button>
</div>
</>
);
};
export default Login;
signInWithGoogle
export const signInWithGoogle = () => {
auth.signInWithPopup(provider).catch((err) => alert(err.message));
};
You should use a then() block in your signInWithGoogle() function, as follows:
export const signInWithGoogle = () => {
auth.signInWithPopup(provider)
.then((result) => {
const userId = result.user.uid;
// Create a doc in a users collection
// It's up to you to build theJavaScript objec to pass to the set() methood
firestore.collection("users").doc(userId).set( {foo: bar, bar: foo} );
})
.catch((err) => alert(err.message));
};
More details in the doc.
Related
I'm building a react firebase app in which users can create their own profiles, sign in and add & delete meetups. Its currently doing something very weird in that, whenever I'm signed in as lets say user 1, I see said users email in the profile on the dashboard and their meetups. However, when I sign out user 1 and then sign in user 2, upon login I still see user 1's profile. Then I refresh the page, and waa-lah... now user 2's profile appears, but only after a page refresh and I am confused as to how to fix it as I have tried many different fixes. here is my firebase.js where I'm handling AuthContext and setting current user (this is the latest try, the commented out lines are what I had before now) using firebase v9.7.0
import { initializeApp } from "firebase/app";
import { getAuth, onAuthStateChanged } from "firebase/auth";
import {
getFirestore,
collection,
query,
where,
getDocs,
} from "firebase/firestore";
// import firebase from "firebase/compat/app";
// import "firebase/compat/firestore";
// import "firebase/compat/auth";
import { useState, useEffect, useContext, createContext } from "react";
const firebaseConfig = {
apiKey: "******************",
authDomain: "***************",
projectId: "***************",
storageBucket: "****************",
messagingSenderId: "************",
appId: "******************",
};
export const app = initializeApp(firebaseConfig);
export const firestore = getFirestore(app);
// export const app = firebase.initializeApp(firebaseConfig);
// export const firestore = firebase.firestore();
// const auth = app.auth();
const auth = getAuth();
export const AuthContext = createContext();
export const useAuthState = () => {
return useContext(AuthContext);
};
export const AuthContextProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
setCurrentUser(user);
setLoading(false);
// ...
} else {
console.log("user logged out");
setCurrentUser({});
// User is signed out
// ...
}
unsubscribe();
});
// const unsubscribe = auth.onAuthStateChanged((user) => {
// setCurrentUser(user);
// setLoading(false);
// });
// console.log(unsubscribe());
// return () => unsubscribe();
}, []);
const value = {
currentUser,
};
// useEffect(() => {
// setCurrentUser(auth.currentUser);
// setLoading(false);
// }, []);
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
};
export default firestore;
And here is my Dashboard.js which handles the logout:
import React, { useState } from "react";
import { Card, Button, Alert } from "react-bootstrap";
import { getAuth, signOut } from "firebase/auth";
import { Link, useNavigate } from "react-router-dom";
import { useAuthState } from "../firebase";
export default function Dashboard() {
const [error, setError] = useState("");
const { currentUser } = useAuthState();
const navigate = useNavigate();
async function handleLogout() {
setError("");
try {
await signOut(getAuth());
sessionStorage.removeItem("token");
navigate("/login");
} catch {
setError("Failed to log out");
}
}
return (
<>
<Card>
<Card.Body>
<h2 className="text-center mb-4">Profile</h2>
{error && <Alert variant="danger">{error}</Alert>}
<strong>Email: </strong> {currentUser.email}
<Link to="/update-profile" className="btn btn-primary w-100 mt-3">
Update Profile
</Link>
</Card.Body>
</Card>
<div className="w-100 text-center mt-2">
<Button variant="link" onClick={handleLogout}>
Log Out
</Button>
</div>
</>
);
}
I'm thinking I need to clear the current user somewhere, but I'm not exactly sure if or where. Any guidance would be greatly appreciated, thank you!
In Firebase.js created the auth context and the logout function, and exports useAuth and the values as children.
// import signout function from firebase
import {
signOut,
} from 'firebase/auth'
// create auth context
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
// singout function
async function logout() {
return signOut(auth)
}
// values for when we use "const { value } = useAuth();"
const value = {
currentUser,
login,
signup,
logout
}
In Dashboard.js we set the variable of logout after adding it to the exported values.
// import auth context
import { useAuth } from '../app/Firebase';
// set logout variable
const { logout } = useAuth();
// handle logout
async function handleLogout() {
setError("");
try {
await logout();
navigate("/login");
} catch {
setError("Failed to log out");
}
}
I have a problem with flickering my private routes while using my AuthContext. Below is the code for my Private Route:
import React from 'react';
import { Navigate } from 'react-router-dom';
import { UserAuth } from '../../Context/AuthContext';
const PrivateRoute = ({ children }) => {
const { user } = UserAuth();
if (!user) {
return <Navigate to='/login' />;
}
return children;
};
export default PrivateRoute;
No personal information shows, because the user is initialized to {} in Auth Context. but I can still see the page and navbar. Anyone have a solution?
Also, below is AuthContext.js:
import { createContext, useContext, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
} from 'firebase/auth';
import { auth } from '../../firebase';
const UserContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState({});
const createUser = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password);
};
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password)
}
const logout = () => {
return signOut(auth)
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
//console.log(currentUser);
setUser(currentUser);
});
return () => {
unsubscribe();
};
}, []);
return (
<UserContext.Provider value={{ createUser, user, logout, signIn }}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};
So I found a solution that's kind of cheeky. I'm not going to post my solution, but basically, I wrapped the return statement return children in the PrivateRoute function with an if statement for a specific item in the user object. This prevents any return and 'solves' the flicker.
I am trying to figure out how to mock the google sign in popup method using jest.
Here is the auth file
import { firebase, googleProvider } from "./firebase";
import React, { createContext, useContext, useEffect, useState } from "react";
export const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setUser] = useState(null);
const [isAuthenticating, setIsAuthenticating] = useState(true);
function startLogin() {
return firebase.auth().signInWithPopup(googleProvider);
}
function logOut() {
firebase
.auth()
.signOut()
.then(() => {
setUser(null);
});
}
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
}
setIsAuthenticating(false);
});
return unsubscribe;
}, [currentUser]);
const value = {
currentUser,
isAuthenticating,
startLogin,
logOut,
};
return (
<AuthContext.Provider value={value}>
{!isAuthenticating && children}
</AuthContext.Provider>
);
}
The login method is called through the login component
import React from "react";
import { useAuth } from "../auth/auth";
import { history } from "../routes/AppRouter";
import logo192 from "../images/logo192.png";
import Button from "#material-ui/core/Button";
import Box from "#material-ui/core/Box";
import Typography from "#material-ui/core/Typography";
import { useStyles } from "../styles/LoginStyle";
export const Login = () => {
const { startLogin } = useAuth();
const login = async () => {
try {
await startLogin();
history.push("/welcome");
} catch (e) {
throw new Error(e);
}
};
const classes = useStyles();
return (
<Box className={classes.loginDiv}>
<img src={logo192} alt="react-logo" className={classes.reactLogo} />
<Box className={classes.welcomeText}>
<Typography variant="h3">React Quiz App</Typography>
<Typography variant="h5">
Take 10 random questions and find out how smart you are
</Typography>
</Box>
<Button
data-testid="login-button"
className={classes.login}
onClick={login}
>
LOGIN
</Button>
</Box>
);
};
Now I want to test (mock the method I think) using jest so something like the below
Not sure if i should test by importing the login component or the auth.js file?
describe("firebase auth methods", () => {
const container = document.createElement("div");
document.body.appendChild(container);
ReactDOM.render(
<AuthContext.Provider value={{}}>
<Login />
</AuthContext.Provider>,
container
);
it("should call google sign in with popup", () => {
///call the sign in with pop method here
});
});
Any help/advice would be appreciated :D
I'm currently attempting to build an 'AuthContext' so I can use it in various screens and pass the data down.
I thought I'd built it right.. But when I try to call one of the functions in my Provider, it's throwing a component exception, stating 'element type is invalid: expected a string or a class/function but got undefined'.
Here is the context file:
import React, { useState, useContext } from 'react';
import { navigate } from '../navigationRef';
import { Magic } from '#magic-sdk/react-native';
const m = new Magic('api key');
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState([]);
const userSignedIn = async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
navigate('loginFlow')
}
};
const signIn = () => {
};
const signUp = () => {
};
const logOut = () => {
};
return (
<AuthContext.Provider value={{ user, userSignedIn, signIn, signUp, logOut }}>
{ children }
</AuthContext.Provider>
)
}
And here is the component which is attempting to use the context:
import React, { useContext, useEffect } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import AuthContext from '../context/AuthContext';
const LoadingScreen = ({ navigation }) => {
const { userSignedIn } = useContext(AuthContext)
useEffect(() => {
userSignedIn()
}, [])
return (
<View style={styles.mainView}>
<ActivityIndicator style={styles.indicator} />
</View>
)
}
And finally, here is my app.js file (cut most of it out due to length, but wanted to show Provider):
import { Provider as AuthProvider } from './src/context/AuthContext';
const App = createAppContainer(switchNavigator)
export default () => {
return (
<AuthProvider>
<App />
</AuthProvider>
)
};
Can anyone see what's going wrong here?
You exported your AuthContext as a named-export ... but you're importing a default-export
import AuthContext from '../context/AuthContext'; // <--- Here
const LoadingScreen = ({ navigation }) => {};
Instead...
import { AuthContext} from '../context/AuthContext';
Same goes for this one as well...
import { Provider as AuthProvider } from './src/context/AuthContext';
Which should be
import { AuthContext: { Provider as AuthProvider } } from './src/context/AuthContext';
OR
import { AuthContext } from './src/context/AuthContext';
return (
<AuthContext.Provider>
<App />
</AuthContext.Provider>
)
I have a simple Dashboard component that relies on React context to manage auth. It contains a custom hook useAuth to extract the current user as well as the auth related functions: login, logout, etc.
This is the Context file: AuthContext.js:
import React, { createContext, useContext, useState, useEffect } from "react";
import { auth } from "../config/firebase";
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password);
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
function logout() {
return auth.signOut();
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
signup,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
This is the Dashboard.js component:
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
export default function Dashboard() {
const { currentUser, logout } = useAuth();
const [error, setError] = useState("");
const history = useHistory();
const handleLogout = async () => {
setError("");
try {
await logout();
history.push("/login");
} catch (e) {
setError(e.message);
}
};
return (
<div>
{error && <p>{error}</p>}
<h1>This is the Dashboard</h1>
<h5>Email: {currentUser.email}</h5>
<button onClick={handleLogout} type="button">
Logout
</button>
</div>
);
}
As recommened by React Testing Library I have created a test-utils.js file:
import React, { createContext } from "react";
import { render } from "#testing-library/react";
import { BrowserRouter as Router } from "react-router-dom";
const AuthContext = createContext();
const currentUser = {
email: "abc#abc.com",
};
const signup = jest.fn();
const login = jest.fn();
const logout = jest.fn();
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};
const customRender = (ui, options) => {
render(ui, { wrapper: AllTheProviders, ...options });
};
export * from "#testing-library/react";
export { customRender as render };
However, when running Dashboard.test.js I get error
TypeError: Cannot destructure property 'currentUser' of '((cov_5mwatn2cf(...).s[0]++) , (0 , _AuthContext.useAuth)(...))' as it is undefined.
4 |
5 | export default function Dashboard() {
> 6 | const { currentUser, logout } = useAuth();
| ^
7 | const [error, setError] = useState("");
8 | const history = useHistory();
import React from "react";
import Dashboard from "./Dashboard";
import { act, render, screen } from "../config/test-utils-dva";
beforeEach(async () => {
await act(async () => {
render(<Dashboard />);
});
});
test("displays dashboard", () => {
expect(screen.getByText(/dashboard/i)).toBeInTheDocument();
});
I think it is because Dashboard component is trying to use useAuth from AuthContext.js, how can I force the rendered Dashboard component to use the mocked data that I am sending in the test-utils.jsfile?
Instead of creating a new context, use the AuthContext from context/AuthContext for <AuthContext.Provider>, as that's the context that the hook uses.
So, in AuthContext.js, export the context instance:
export const AuthContext = createContext();
Then, in your test-util.js file, instead of again calling createContext (which will create a completely separate context instance - the contexts are not the same even if they are stored in a variable with the same name!), just import the previously exported instance:
import { AuthContext } from "../context/AuthContext";
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};