I am following a tutorial on how to integrate Stripe payments in React. The problem I am having is for this page where it will check if a user is logged in and if they are logged in it will show a cookie and if they are not it will show a upgrade to premium button. All is working however there is an annoying behaviour where if I refresh the page it briefly shows the 'Upgrade to premium' button before checking the user is premium and then quickly change to the cookie. I want it to ideally do the check before the content is rendered so it's a bit smoother and doesn't look jittery.
import React, { useEffect,useState } from 'react'
import firebase from "../src/firebase";
import { useAuthState } from "react-firebase-hooks/auth";
import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut,onAuthStateChanged } from "firebase/auth";
import createCheckoutSession from "./stripe/createCheckoutSession";
import usePremiumStatus from "./stripe/usePremiumStatus";
import Login from "./Login";
import { Link, useNavigate } from 'react-router-dom'
import { Spinner } from 'react-bootstrap';
export default function Practise() {
const auth = getAuth();
const {currentUser}=getAuth()
const userIsPremium = usePremiumStatus(currentUser);
const [load,setLoading]=useState(false)
const debugging = async ()=>{
const {currentUser}= await getAuth().then(()=>{
console.log(currentUser)
})
}
const [loggedIn,setLoggedIn]= useState(false)
let navigate = useNavigate();
const handleNewInfo = ()=>{
onAuthStateChanged(auth, (user) => {
if (user) {
const uid = user.uid;
setLoggedIn(true)
const prom = async ()=>{
await ((user.getIdTokenResult()))
}
prom()
console.log(userIsPremium)
} else {
navigate('/login')
console.log('not logged in!')
}
})}
const getUser = ()=>{
console.log(currentUser)
}
useEffect(
handleNewInfo
,[]
) ;
const handleClick = (userid)=>{
setLoading(true)
createCheckoutSession(userid)
}
return (
<div>
{!loggedIn && <h1>Loading...</h1>}
{loggedIn && (
<div>
<h1>Hello, {currentUser.displayName}</h1>
{!userIsPremium && (
<button onClick={() => handleClick(currentUser.uid)}>
Upgrade to premium!
</button>
) }
{!userIsPremium && load && (
<Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
) }
{userIsPremium && <h2>Have a cookie 🍪 Premium customer!</h2>
}
</div>
)}
</div>
);
}
Related
The problem:
So basically I created a context which is used to store the logged in user data so I can easily use it accross my application. I created a component which verify if the user is logged in or not, or if the access to the content of it is restricted to administrator only. The problem occurs when I try to access a page which uses SecureCard. It basically says that user is empty so it just redirect me to the home page, but it's clearly not empty because I can see my username in the header of my website. So my question is why is that.
If you need any extra code or some more context just tell me.
Some context:
import React from 'react';
import ReactDOM from 'react-dom';
import App from "./App";
import {UserProvider} from "./contexts/UserProvider";
ReactDOM.render(
<React.StrictMode>
<UserProvider>
<App/>
</UserProvider>
</React.StrictMode>,
document.getElementById('root')
);
Where I think it originates from:
import React, {useEffect, useState} from "react";
import {ReactSession} from "react-client-session";
import {getUserByToken} from "../utils/accounts";
const UserContext = React.createContext(null);
export function UserProvider({children}) {
const [user, setUser] = useState({
_id: "",
creation_date: "",
rank: "",
username: ""
});
useEffect(() => {
if (ReactSession.get("token") && (user._id === "" || user.rank === "" || user.username === "" || user.creation_date === "")) {
getUserByToken().then(resp => {
if (!resp?.error) setUser(resp.message);
else ReactSession.remove("token");
});
}
}, []);
return (
<UserContext.Provider value={{user, setUser}}>
{children}
</UserContext.Provider>
);
}
export const UserConsumer = UserContext.Consumer;
export default UserContext;
Where the problem happens:
import {Card} from "react-bootstrap";
import {useEffect, useState} from "react";
import {ReactSession} from "react-client-session";
import {useNavigate} from "react-router-dom";
import useUser from "../hooks/useUser";
export function SecureCard(props) {
const navigate = useNavigate();
const [valid, setValid] = useState(false);
const {user} = useUser();
//Prevent the user from accessing the card if connected or not or if restricted
useEffect(() => {
console.log(user);
if (props.connected && props.restricted){
if (!ReactSession.get("token") || user.rank !== "Administrator") navigate("/");
}
else if (props.connected){
if (!ReactSession.get("token")) navigate("/");
}
else if (!props.connected){
if (ReactSession.get("token")) navigate("/");
}
setValid(true);
}, []);
if(!valid)
return null;
return (
<Card>
<Card.Header><h2>{props.title}</h2></Card.Header>
<Card.Body>
{props.children}
</Card.Body>
</Card>
);
}
How SecureCard is called
<SecureCard title="Web Admin" connected={true} restricted={true}></SecureCard>
I'm having problem with privateRoute it should be redirecting to the Home page after signin but it keeps redirecting to '/signin' page. everything is working fine if I remove PrivateRoute im not sure I'm beginner with react and it's my first time using firebase auth is there any way to fix this problem I'm sorry my code is messy
import React, {useEffect, useState} from 'react'
import firebase from 'firebase'
import StyledFirebaseAuth from 'react-firebaseui/FirebaseAuth'
import { Redirect, useHistory } from 'react-router-dom';
import axios from 'axios';
import { useAuth } from "../context/AuthContext";
import { Route, withRouter } from 'react-router-dom';
var uiconfig = {
signInFlow: 'popup',
signInSuccessUrl:'/',
signInOptions : [
firebase.auth.EmailAuthProvider.PROVIDER_ID],
callbacks: {
signInSuccessWithAuthResult: function(authResult, redirectUrl) {
return true;
}
}
};
const signOut = () => {
firebase.auth().signOut().then(function(){
return console.log('signout');
}).catch(() => {
console.error("error signout");
})
return <Redirect to='/signin'></Redirect>
}
var Cuser = null
const PrivateRoute = ({component: Component, ...rest}) => {
const [user, setUser] = useState(false)
const {currentUser} = useAuth()
console.log(Cuser)
return (
<Route {...rest} render={props => {
if(Cuser){
return <Component {...props}></Component>
} else {
return <Redirect to="/signin" />
}
}} ></Route>)
}
const Signup = () => {
const {history} = useHistory()
useEffect(() => {
const authOberver = firebase.auth().onAuthStateChanged((user) => {
setUser(user)
});
return authOberver
})
const [user, setUser] = useState(null)
if(user){
axios.post("http://localhost:4000/notes", {
email: user.email,
})
Cuser = true
}
return (
<>
<div>Signin / Register</div>
<StyledFirebaseAuth uiConfig={uiconfig} firebaseAuth={firebase.auth()}></StyledFirebaseAuth>
</>
)
}
export {signOut}
export default Signup;
export {PrivateRoute};
I tried to create a separate file for current user and using currentUser to check if the user is logged in or not but still happing the same problem
import React, { useContext, useEffect, useState } from "react";
import { auth } from "../firebase";
import firebase from "firebase";
import axios from 'axios'
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth'
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsubscribed = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
})
return unsubscribed
}, []);
const value = {
currentUser,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
I think the problem is that you are using the variable Cuser to decide what component to render. So even if Cuser is set to true after signup, when the component gets re-rendered, it's value gets back to being null which is why it goes to else {return <Redirect to="/signin" />} block.
You can modify you AuthContext to store the login status in state. In below code, lets say cUser:
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
cont [cUser, setCUser] = useState(false);
useEffect(() => {
const unsubscribed = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
})
return unsubscribed
}, []);
const value = {
currentUser,
cUser: cUser,
setCUser: setCUser,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
And then use setCUser to set and check the value
import React, {useEffect, useState} from 'react'
import firebase from 'firebase'
import StyledFirebaseAuth from 'react-firebaseui/FirebaseAuth'
import { Redirect, useHistory } from 'react-router-dom';
import axios from 'axios';
import { useAuth } from "../context/AuthContext";
import { Route, withRouter } from 'react-router-dom';
var uiconfig = {
signInFlow: 'popup',
signInSuccessUrl:'/',
signInOptions : [
firebase.auth.EmailAuthProvider.PROVIDER_ID],
callbacks: {
signInSuccessWithAuthResult: function(authResult, redirectUrl) {
return true;
}
}
};
const signOut = () => {
firebase.auth().signOut().then(function(){
setCUser(false);
return console.log('signout');
}).catch(() => {
console.error("error signout");
})
return <Redirect to='/signin'></Redirect>
}
var Cuser = null
const PrivateRoute = ({component: Component, ...rest}) => {
const [user, setUser] = useState(false)
const {currentUser, cUser, setCUser} = useAuth()
console.log(Cuser)
return (
<Route {...rest} render={props => {
if(cUser){
return <Component {...props}></Component>
} else {
return <Redirect to="/signin" />
}
}} ></Route>)
}
const Signup = () => {
const {history} = useHistory()
useEffect(() => {
const authOberver = firebase.auth().onAuthStateChanged((user) => {
setUser(user)
});
return authOberver
})
const [user, setUser] = useState(null)
if(user){
axios.post("http://localhost:4000/notes", {
email: user.email,
})
setCUser(true);
}
return (
<>
<div>Signin / Register</div>
<StyledFirebaseAuth uiConfig={uiconfig} firebaseAuth={firebase.auth()}></StyledFirebaseAuth>
</>
)
}
export {signOut}
export default Signup;
export {PrivateRoute};
I use React for frontend and Node.js in backend and Postgre for database.
I have create my own API for authenticating user and using useState and useContext hook to store the login status of the user.
I also setup a Redirect function after successful login but the useState is taking a while to update the login status of user and because of that the page is not being redirect.
I tried using async and await while fetching the data from the server but still there is delay in authenticating the user.
I also tried to follow some blogs like this
This context state handle the login functionality and update the login status within the component.
import React, { createContext, useContext, useState } from "react";
import { GlobalContext } from "./GlobalState";
export const LoginAuth = createContext();
export const ProvideAuth = ({ children }) => {
const auth = useProvideAuth();
return <LoginAuth.Provider value={auth}>{children}</LoginAuth.Provider>;
};
export const useAuth = () => {
return useContext(LoginAuth);
};
const useProvideAuth = () => {
const [user, setUser] = useState(null);
const { setIsLogin } = useContext(GlobalContext);
const login = async (userDetails) => {
const response = await fetch("http://localhost:4000/api/v1/login/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userDetails),
}).catch((err) => console.log(err.message));
const data = await response.json();
if (data?.error) {
setUser(false);
} else {
setUser(data);
setIsLogin(true);
}
};
return { user, login };
};
This state used to update the login state throughout the app
import React, { createContext, useState } from "react";
export const GlobalContext = createContext();
export const GlobalProvider = ({ children }) => {
const [isLogin, setIsLogin] = useState(false);
return (
<GlobalContext.Provider value={{ isLogin, setIsLogin }}>
{children}
</GlobalContext.Provider>
);
};
Private Route Code
import React, { useContext } from "react";
import { Redirect, Route } from "react-router";
import { GlobalContext } from "../State/GlobalState";
const PrivateRouteLogin = ({ children, ...rest }) => {
const { isLogin } = useContext(GlobalContext);
return (
<Route
{...rest}
render={({ location }) => {
return isLogin ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location },
}}
></Redirect>
);
}}
/>
);
};
export default PrivateRouteLogin;
You can have an another value for your loginStatus
false = not logged in
true = logged in
pending = for collect all data
in this situation you can have a backdrop(loading) that show over your website till detect loginStatus
I have built a simple app with Nextjs and Firebase (right now, I'm only using the authentication providing by Firebase). I made a login component and a greetingUser component whose roles are defined as follow :
greetingUser : display the name of the user currently logged in and a greeting message;
// greetingUse.js
import React, { useContext } from 'react';
import { UserContext } from './User/User';
export default function GreetUser(){
const user = useContext(UserContext);
return (
<div>
<p>hello, {user.user.name} !</p>
</div>
)
}
login : there are two buttons (login and logout). When the user log in, I want to receive the name of the user (user.displayName) and update the greetingUser component to display that name.
// login.js
import firebase from 'firebase/app';
import React, { useContext, useReducer } from 'react';
import { UserContext } from './User/User';
import { auth } from '../utils/firebase';
export default function Login() {
const provider = new firebase.auth.GoogleAuthProvider();
const state = useContext(UserContext);
console.log(state);
console.log(state.user);
const updateUserDetails = () => {
auth.onAuthStateChanged(user => {
if (user) {
return user.displayName;
} else {
return "You are logged out";
}
})
console.log(state.user);
}
return (
<>
<div className="btn-group">
<button
id="logInBtn"
className="btn btn-primary"
onClick={() => auth.signInWithPopup(provider) && state.setUser(updateUserDetails())}>
Login
</button>
<button
id="logOutBtn"
className="btn btn-danger"
onClick={() => auth.signOut()}>
Logout
</button>
</div>
<p>hello {state.user.name}</p>
</>
)
}
You'll notice that I'm using react context. After some research, that the solution I found. Here is the Provider code :
// User.js
import React, { createContext, useState } from 'react';
const UserContext = createContext({
user: {name: "User"},
setUser: () => {}
});
const UserContextProvider = (props) => {
const setUser = (user) => {
setState({...state, user: user})
}
const initState = {
user: {name: "User"},
setUser: setUser
}
const [state, setState] = useState(initState);
return (
<UserContext.Provider value={state}>
{props.children}
</UserContext.Provider>
)
};
export { UserContext, UserContextProvider };
The problem is when logging in, the value of the user object is undefined resulting in an error that prevent the page from rendering :
login.js?4f9b:42 Uncaught TypeError: Cannot read property 'name' of undefined at Login (login.js?4f9b:42)
I thing it's because while authenticating, the value of user is unknow, but after the authentification is completed, the value of user is not updated.
I did some research, but I don't find a solution adapted to my problem.
In my Main.js I create a first global state with a username and a list of users I'm following.
Then, both the Wall component and FollowingSidebar render the list of follows and their messages (plus the messages of the main user).
So far so good. But in a nested component inside FollowingSidebar called FollowingUser I have an onClick to remove a user. My understanding is that, because I change the state, useEffect would take care of the Wall component to re-render it, but nothing happens... I've checked several examples online but nothing has helped my use case so far.
Needless to say I'm not overly experienced with React and Hooks are a bit complex.
The code here:
Main.js:
import React, { useEffect, useState } from "react";
import ReactDom from "react-dom";
import db from "./firebase.js";
// Components
import Header from "./components/Header";
import FollowingSidebar from "./components/FollowingSidebar";
import SearchUsers from "./components/SearchUsers";
import NewMessageTextarea from "./components/NewMessageTextarea";
import Wall from "./components/Wall";
// Context
import StateContext from "./StateContext";
function Main() {
const [mainUser] = useState("uid_MainUser");
const [follows, setFollows] = useState([]);
const setInitialFollows = async () => {
let tempFollows = [mainUser];
const user = await db.collection("users").doc(mainUser).get();
user.data().following.forEach(follow => {
tempFollows.push(follow);
});
setFollows(tempFollows);
};
useEffect(() => {
setInitialFollows();
}, []);
const globalValues = {
mainUserId: mainUser,
followingUsers: follows
};
return (
<StateContext.Provider value={globalValues}>
<Header />
<FollowingSidebar />
<SearchUsers />
<NewMessageTextarea />
<Wall />
</StateContext.Provider>
);
}
ReactDom.render(<Main />, document.getElementById("app"));
if (module.hot) {
module.hot.accept();
}
FollowingSidebar component:
import React, { useState, useEffect, useContext } from "react";
import db from "../firebase.js";
import StateContext from "../StateContext";
import FollowingUser from "./FollowingUser";
export default function FollowingSidebar() {
const { followingUsers } = useContext(StateContext);
const [users, setUsers] = useState(followingUsers);
useEffect(() => {
const readyToRender = Object.values(followingUsers).length > 0;
if (readyToRender) {
db.collection("users")
.where("uid", "in", followingUsers)
.get()
.then(users => {
setUsers(users.docs.map(user => user.data()));
});
}
}, [followingUsers]);
return (
<section id="following">
<div className="window">
<h1 className="window__title">People you follow</h1>
<div className="window__content">
{users.map((user, index) => (
<FollowingUser avatar={user.avatar} username={user.username} uid={user.uid} key={index} />
))}
</div>
</div>
</section>
);
}
FollowingUser component:
import React, { useState, useContext } from "react";
import db from "../firebase.js";
import firebase from "firebase";
import StateContext from "../StateContext";
export default function FollowingUser({ avatar, username, uid }) {
const { mainUserId, followingUsers } = useContext(StateContext);
const [follows, setFollows] = useState(followingUsers);
const removeFollow = e => {
const userElement = e.parentElement;
const userToUnfollow = userElement.getAttribute("data-uid");
db.collection("users")
.doc(mainUserId)
.update({
following: firebase.firestore.FieldValue.arrayRemove(userToUnfollow)
})
.then(() => {
const newFollows = follows.filter(follow => follow !== userToUnfollow);
setFollows(newFollows);
});
userElement.remove();
};
return (
<article data-uid={uid} className="following-user">
<figure className="following-user__avatar">
<img src={avatar} alt="Profile picture" />
</figure>
<h2 className="following-user__username">{username}</h2>
<button>View messages</button>
{uid == mainUserId ? "" : <button onClick={e => removeFollow(e.target)}>Unfollow</button>}
</article>
);
}
Wall component:
import React, { useState, useEffect, useContext } from "react";
import db from "../firebase.js";
import Post from "./Post";
import StateContext from "../StateContext";
export default function Wall() {
const { followingUsers } = useContext(StateContext);
const [posts, setPosts] = useState([]);
useEffect(() => {
console.log(followingUsers);
const readyToRender = Object.values(followingUsers).length > 0;
if (readyToRender) {
db.collection("posts")
.where("user_id", "in", followingUsers)
.orderBy("timestamp", "desc")
.get()
.then(posts => setPosts(posts.docs.map(post => post.data())));
}
}, [followingUsers]);
return (
<section id="wall">
<div className="window">
<h1 className="window__title">Latest messages</h1>
<div className="window__content">
{posts.map((post, index) => (
<Post avatar={post.user_avatar} username={post.username} uid={post.user_id} body={post.body} timestamp={post.timestamp.toDate().toDateString()} key={index} />
))}
</div>
</div>
</section>
);
}
StateContext.js:
import { createContext } from "react";
const StateContext = createContext();
export default StateContext;
The main issue is the setting of state variables in the Main.js file (This data should actually be part of the Context to handle state globally).
Below code would not update our state globally.
const globalValues = {
mainUserId: mainUser,
followingUsers: follows
};
We have to write state in a way that it get's modified on the Global Context level.
So within your Main.js set state like below:
const [globalValues, setGlobalValues] = useState({
mainUserId: "uid_MainUser",
followingUsers: []
});
Also add all your event handlers in the Context Level in Main.js only to avoid prop-drilling and for better working.
CODESAND BOX DEMO: https://codesandbox.io/s/context-api-and-rendereing-issue-uducc
Code Snippet Demo:
import React, { useEffect, useState } from "react";
import FollowingSidebar from "./FollowingSidebar";
import StateContext from "./StateContext";
const url = "https://jsonplaceholder.typicode.com/users";
function App() {
const [globalValues, setGlobalValues] = useState({
mainUserId: "uid_MainUser",
followingUsers: []
});
const getUsers = async (url) => {
const response = await fetch(url);
const data = await response.json();
setGlobalValues({
...globalValues,
followingUsers: data
});
};
// Acts similar to componentDidMount now :) Called only initially
useEffect(() => {
getUsers();
}, []);
const handleClick = (id) => {
console.log(id);
const updatedFollowingUsers = globalValues.followingUsers.filter(
(user) => user.id !== id
);
setGlobalValues({
...globalValues,
followingUsers: updatedFollowingUsers
});
};
return (
<StateContext.Provider value={{ globalValues, handleClick }}>
<FollowingSidebar />
</StateContext.Provider>
);
}
export default App;