Updating data in firebase doesnt work inside async handleclick func - reactjs

I am working on an app that has a payment tab, when i click on the payment tab it takes me to a page which only has just one button, when i click on that button, it takes me to the stripe checkout system (https://stripe.com/docs/payments/checkout/client) and once it handles the payment, it takes me back to the app. I have a database in firebase that tracks if the current user that is logged in is either paid or not. if the current user is unpaid, i want the membership field of my db for that user to be updated from 'false' to 'true' AFTER the payment has been completed successfully. The problem is that if i do update of the db right after the payment is done inside the try block, the db is not updated, but if i do the update inside componentDidMount() func, i do see changes made to the db. What am i doing wrong? is the fact that the func is async casuing the problem here? if i want to update the database after payment is successful, than how would i go about doing that?
import React from 'react';
import firebase from "firebase/app";
import 'firebase/firestore';
import "firebase/database";
import { auth, database } from '../../firebase';
import { UserAndDbObjConsumer, UserAndDbObjContext } from '../../dbAndUserObjContext';
import { loadStripe } from '#stripe/stripe-js';
// Make sure to call `loadStripe` outside of a component’s render to avoid
// recreating the `Stripe` object on every render.
const stripePromise = loadStripe('test key here');
export default class Payment extends React.Component {
static contextType = UserAndDbObjContext;
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
componentDidMount() {
const {user, database} = this.context;
// When i update my data below, it updates the firebase db and i can
// see the changes taking effect
// database.collection("userCollection").doc(user.uid).update({
// membership: true
// })
}
handleClick = async (event) => {
const {user, database} = this.context;
console.log("i am inside handle but outside try")
try {
// When the customer clicks on the button, redirect them to Checkout.
const stripe = await stripePromise;
const { error } = await stripe.redirectToCheckout({
lineItems: [{
price: 'price_1IF4cPAt4f9zG3h2hTG64agQ', // Replace with the ID of your price
quantity: 1,
}],
mode: 'payment',
successUrl: 'https://app.guideanalytics.ca/',
cancelUrl: 'https://guideanalytics.ca/termsofuse.html',
});
// If `redirectToCheckout` fails due to a browser or network
// error, display the localized error message to your customer
// using `error.message`.
// When i update the firebase db here, than the changes on firebase dont take place
database.collection("userCollection").doc(user.uid).update({
membership: true
})
}
catch(e) {
console.log(e);
}
}
render() {
return (
<div>
<button role="link" onClick={(e) => this.handleClickTest()}>
Checkout
</button>
</div>
)
}
}
//Payment.contextType = Context;

stripe.redirectToCheckout will only error if there's a browser or network error, so in practically all cases your database.collection function call will never execute.
Instead you would need to call that function when the user hits the successUrl you defined in your Checkout Session. You might want to customize the successUrl to ensure that you know which customer to update. For instance:
successUrl: `https://app.guideanalytics.a/?userId=${user.uid}&sessionId={CHECKOUT_SESSION_ID}`,
The last part is a wildcard which Stripe will then automatically replace with the Session's ID.
Then on your success page you'd update the database based on the user ID and Checkout Session ID in the URL.

Related

Firebase auth with custom user fields: React custom hook to manage user state

I'm trying to implement authentication in my app using Firebase and I need to store some custom user fields (e.g. schoolName, programType, etc.) on the user documents that I'm storing in Firestore. I want to have these custom fields in my React state (I'm using Recoil for state management), and I'm very unsure of the best way to do this.
I currently have a Cloud Function responsible for creating a new user document when new auth users are created, which is great, however, I'm having trouble figuring out a good way to get that new user (with the custom fields) into my state, so I came up with a solution but I'm not sure if it's ideal and would love some feedback:
I define the firebase/auth functions (e.g. signInWithPopup, logout, etc.) in an external static file and simply import them in my login/signup forms.
To manage the user state, I created a custom hook useAuth:
const useAuth = () => {
const [user] = useAuthState(auth); // firebase auth state
const [currentUser, setCurrentUser] = useRecoilState(userState); // global recoil state
useEffect(() => {
// User has logged out; firebase auth state has been cleared; so clear app state
if (!user?.uid && currentUser) {
return setCurrentUser(null);
}
const userDoc = doc(firestore, "users", user?.uid as string);
const unsubscribe = onSnapshot(userDoc, (doc) => {
console.log("CURRENT DATA", doc.data());
if (!doc.data()) return;
setCurrentUser(doc.data() as any);
});
if (currentUser) {
console.log("WE ARE UNSUBBING FROM LISTENER");
unsubscribe();
}
return () => unsubscribe();
}, [user, currentUser]);
};
This hook uses react-firebase-hooks and attempts to handle all cases of the authentication process:
New users
Existing users
Persisting user login on refresh (the part that makes this most complicated - I think)
To summarize the above hook, it essentially listens to changes in firebase auth state via useAuthState, then I add a useEffect which creates a listener of the user document in firestore, and when that user has successfully been inputted into the db by the Cloud Function, the listener will fire, and it will populate recoil state with doc.data() (which contains the custom fields) via setCurrentUser. As for existing users, the document will already exist, so a single snapshot will do the trick. The rationale behind the listener is the case of new users, where a second snapshot will be required as the first doc.data() will be undefined even though useAuthState will have a user in it, so it's essentially just waiting for the Cloud Function to finish.
I call this hook immediately as the app renders to check for a Firebase Auth user in order to persist login on refresh/revisit.
I've been messing around on this for quite some time, and this outlined solution does work, but I have come up with multiple solutions so I would love some guidance.
Thank you very much for reading.
Step 1: Define CurrentUser, and UserProfile states
import { atom, selector } from "recoil";
import { type User } from "firebase/auth";
export const CurrentUser = atom<User | null | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
defaultValue: undefined,
});
export const UserProfile = atomFamily<Profile | null, string | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
get(uid) {
return undefined;
}
});
Step 2: Listen to the authenticated user state changes
export const CurrentUser = atom<User | null | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
defaultValue: undefined,
effects: [
(ctx) => {
if (ctx.trigger === "get") {
// Import Firebase App instanced defined in a separate chunk
const promise = import("../core/firebase")
.then((fb) =>
fb.auth.onAuthStateChanged((user) => {
ctx.setSelf(user);
})
)
.catch((err) => ctx.setSelf(Promise.reject(err)));
return () => promise.then((unsubscribe) => unsubscribe?.());
}
},
],
});
Step 3: Load user profile by Firebase user UID
export const UserProfile = atomFamily<User | null | undefined, string | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
get(uid) {
return async function() {
if (!uid) return null;
import("../core/firebase").then(({ fs }) => {
// TODO: Retrieve Firestore document with the user profile
return getDoc(doc(dollection(fs, "users"), uid));
});
};
}
});
Step 4: Add React hooks
import { useRecoilValue } from "recoil";
export function useCurrentUser() {
return useRecoilValue(CurrentUser);
}
export function useCurrentUserProfile() {
const me = useRecoilValue(CurrentUser);
return useRecoilValue(UserProfile(me?.uid));
}
Usage Example
import { useCurrentUser, useCurrentUserProfile } from "../state/firebase";
export function Example(): JSX.Element {
const me = useCurrentUser(); // Firebase user object
const profile = useCurrentUserProfile(); // Custom profile from Firestore
}
See https://github.com/kriasoft/cloudflare-starter-kit for a working example

How can I stay the user in the same page?

Every time I reload the my account page, it will go to the log in page for a while and will directed to the Logged in Homepage. How can I stay on the same even after refreshing the page?
I'm just practicing reactjs and I think this is the code that's causing this redirecting to log-in then to home
//if the currentUser is signed in in the application
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged(userAuth => {
unsubscribe();
resolve(userAuth); //this tell us if the user is signed in with the application or not
}, reject);
})
};
.....
import {useEffect} from 'react';
import { useSelector } from 'react-redux';
const mapState = ({ user }) => ({
currentUser: user.currentUser
});
//custom hook
const useAuth = props => {
//get that value, if the current user is null, meaning the user is not logged in
// if they want to access the page, they need to be redirected in a way to log in
const { currentUser } = useSelector(mapState);
useEffect(() => {
//checks if the current user is null
if(!currentUser){
//redirect the user to the log in page
//we have access to history because of withRoute in withAuth.js
props.history.push('/login');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[currentUser]); //whenever currentUser changes, it will run this code
return currentUser;
};
export default useAuth;
You can make use of local storage as previously mentioned in the comments:
When user logs in
localStorage.setItem('currentUserLogged', true);
And before if(!currentUser)
var currentUser = localStorage.getItem('currentUserLogged');
Please have a look into the following example
Otherwise I recommend you to take a look into Redux Subscribers where you can persist states like so:
store.subscribe(() => {
// store state
})
There are two ways through which you can authenticate your application by using local storage.
The first one is :
set a token value in local storage at the time of logging into your application
localStorage.setItem("auth_token", "any random generated token or any value");
you can use the componentDidMount() method. This method runs on the mounting of any component. you can check here if the value stored in local storage is present or not if it is present it means the user is logged in and can access your page and if not you can redirect the user to the login page.
componentDidMount = () => { if(!localStorage.getItem("auth_token")){ // redirect to login page } }
The second option to authenticate your application by making guards. You can create auth-guards and integrate those guards on your routes. Those guards will check the requirement before rendering each route. It will make your code clean and you do not need to put auth check on every component like the first option.
There are many other ways too for eg. if you are using redux you can use Persist storage or redux store for storing the value but more secure and easy to use.

ReactJS update data in component when user is logged in

I have dashboard which should show data if a user is logged in and other data if no user is logged in. I already managed to figure out if a user is logged in it is not reflected on the page. It only changes after reloading the page.
This is what I have: An Account object with a userstatus component to hold details of the user. The Account object is placed in a context that is wrapped in the App.js. It also has a getSession function which gets the user details from the authentication mechanism. getSession also updates the userstatus according to the result (logged_in or not_logged_in). Second I have a dashboard component which runs the getSession method and puts the result in the console. Everythings fine. But the render function did not get the changed userstatus.
This is my code (Accounts.js):
export const AccountContext = createContext();
export const Account = {
userstatus: {
loggedinStatus: "not_logged_in",
values: {},
touched: {},
errors: {}
},
getSession: async () =>
await new Promise((resolve, reject) => {
...
}
}),
}
This is the Dashboard.js:
const Dashboard = () => {
const [status, setStatus] = useState();
const { getSession, userstatus } = useContext(AccountContext);
getSession()
.then(session => {
console.log('Dashboard Session:', session);
userstatus.loggedinStatus = "logged_in"
setStatus(1)
})
.catch(() => {
console.log('No Session found.');
userstatus.loggedinStatus = "not_logged_in"
setStatus(0);
});
const classes = useStyles();
return (
<div className={classes.root}>
{userstatus.loggedinStatus}
{status}
{userstatus.loggedinStatus === "logged_in" ? 'User logged in': 'not logged in'}
<Grid
container
spacing={4}
...
I already tried with useState and useEffect, both without luck. The userstatus seems to be the most logical, however, it does not update automatically. How can I reflect the current state in the Dashboard (and other components)?
React only re-renders component when any state change occur.
userstatus is simply a variable whose changes does not reflect for react. Either you should use userstatusas your app state or you can pass it in CreateContext and then use reducers for update. Once any of two ways you use, you would see react's render function reflect the changes in userstatus.
For how to use Context API, refer docs

How can I get role before render()

I want to do the following:
send a request to the server, verify JSON web-token and get user details (name, email and role);
then the page appears with the specific menu items (if the user is admin it shows him Main, Admin and Logout items; if not - Main and Logout).
I thought about just getting the token from localStorage, then decoding it and taking a role from it. But what should I do if I change the role in database (for example from admin to user)? Decoded token on the client-side will contain the "admin" role. So this user will be able to see the admin page.
You can use a variable in store called isFetchingUserDetails and set it to true when you make that async call to the server to get the details.
Till isFetchingUserDetails is set true, you can have an if statement in the render() to return a spinner or other component which shows the user that the page is loading.
Once you get the response from the server, isFetchingUserDetails will be set to false and the rest of the render() will be executed.
Without Store
import React, { Component } from "react";
import ReactDOM from "react-dom";
class App extends Component {
constructor() {
super();
this.state = { isFetchingUserDetails: true };
}
componentDidMount() {
fetch(`https://api.makethecall.com/users/1`)
.then(res => res.json())
.then(() => this.setState({ isFetchingUserDetails: false }));
}
render() {
if (this.state.isFetchingUserDetails) {
return <Spinner />
}
return (
<Home />
);
}
}

Update route and redux state data at same time

I have an app that has user profiles. On the user profile there are a list of friends, and when clicking on a friend it should take you to that other user profile.
Currently, when I click to navigate to the other profile (through redux-router Link) it updates the URL but does not update the profile or render the new route.
Here is a simplified code snippet, I've taken out a lot of code for simplicity sake. There are some more layers underneath but the problem happens at the top layer in my Profile Container. If I can get the userId prop to update for ProfileSections then everything will propagate through.
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) { this.props.getUser(userId) }
}
render() {
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
As you can see, what happens is that I am running the getUser action on componentWillMount, which will happen only once and is the reason the route changes but the profile data does not update.
When I change it to another lifecycle hook like componentWillUpdate to run the getUser action, I get in an endless loop of requests because it will keep updating the state and then update component.
I've also tried using the onEnter hook supplied by react-router on Route component but it doesn't fire when navigating from one profile to another since it's the same route, so that won't work.
I believe I'm thinking about this in the wrong way and am looking for some guidance on how I could handle this situation of navigating from one profile to another while the data is stored in the redux store.
So I would suggest you approach this in the following way:
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) {
// This is the initial fetch for your first user.
this.fetchUserData(userId)
}
}
componentWillReceiveProps(nextProps) {
const { userId } = this.props.params
const { userId: nextUserId } = nextProps.params
if (nextUserId && nextUserId !== userId) {
// This will refetch if the user ID changes.
this.fetchUserData(nextUserId)
}
}
fetchUserData(userId) {
this.props.getUser(userId)
}
render() {
const { user } = this.props
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
Note that I have it set up so in the componentWillMount lifecycle method, you make the request for the initial userId. The code in the componentWillReceiveProps method checks to see if a new user ID has been received (which will happen when you navigate to a different profile) and re-fetches the data if so.
You may consider using componentDidMount and componentDidUpdate instead of componentWillMount and componentWillReceiveProps respectively for the fetchUserData calls, but it could depend on your use case.

Resources