Firestore: enablePersistence() and then using redux with offline database? - reactjs

So, essentially, I'm using Create-React-App and I want to allow users to add data to redux either offline or online. I also want to sync redux with Firestore.
In my main attempt, I initialize my firebase settings:
// ./firebase/firebase.js
var firestoreDatabase;
firebase.initializeApp(config.firebase.config);
firebase.firestore().enablePersistence().then(() => {
firestoreDatabase = firebase.firestore();
});
export { firebase, firestoreDatabase };
Then, to make sure this has fired properly (this is definitely wrong, but I can't figure out the best place to catch the enablePersistence() return... ):
// src/index.js
import { firebase, firestoreDatabase } from "./firebase/firebase";
firebase.auth().onAuthStateChanged(user => {
store.dispatch(setReduxData()).then(() => {
if (firestoreDatabase) {
ReactDOM.render(application, document.getElementById("root"));
}
});
});
ACTIONS FILE
import { firestoreDatabase } from "../firebase/firebase";
export const setReduxData = () => {
return (dispatch, getState) => {
const uid = getState().auth.uid;
const data = { newData: '123' };
return firestoreDatabase
.collection("Users")
.doc(uid)
.collection("data")
.add(data)
.then(ref => {
// so, this never gets fired
dispatch(
addData({
id: ref.id,
...data
})
);
})
So the dispatch never gets fired, however, when I refresh the application, the data I entered { newData: '123' } is added to the store.
I think my entire way of handling this is wrong. I don't like exporting firestoreDatabase as undefined and then updating it when enablePersistence() returns...
I would like to just enablePersistence() once and then use the cache or the server depending on if the user is online or not... Redux should operate the same regardless...
Any thoughts and feedback are welcome!

So, I figured out how to load Firestore properly in my application:
In my firebase.js file:
import * as firebase from "firebase";
import config from "../config";
// https://firebase.google.com/docs/reference/js/
firebase.initializeApp(config.firebase.config);
const database = firebase.database();
const auth = firebase.auth();
const googleAuthProvider = new firebase.auth.GoogleAuthProvider();
export { firebase, googleAuthProvider, auth, database };
Then, I added a firestore.js file:
import { firebase } from "./firebase";
import "firebase/firestore";
import { notification } from "antd";
firebase.firestore().settings({ timestampsInSnapshots: true });
const handleError = error => {
if (error === "failed-precondition") {
notification.open({
message: "Error",
description:
"Multiple tabs open, offline data only works in one tab at a a time."
});
} else if (error === "unimplemented") {
notification.open({
message: "Error",
description: "Cannot save offline on this browser."
});
}
};
export default firebase
.firestore()
.enablePersistence()
.then(() => firebase.firestore())
.catch(err => {
handleError(err.code);
return firebase.firestore();
});
And then I call firestore in my actions file:
import firestore from "../firebase/firestore";
return firestore
.then(db => {
var newData = db
.collection("Users")
.doc(uid)
.collection("userData")
.doc();
newData.set(data);
var id = newData.id;
dispatch(addData({ id, ...data }));
})
.catch(err => {
// notification
});
Essentially, I separated out my redux and Firestore, but ultimately they are connected through the Firestore id.

Related

Avoiding multiple API calls due to rerenders in React with Firebase auth

My web app uses Firebase Auth to handle user authentication along with a backend API, these are provided to the React app as a provider. The idea is that the backend API will verify the user's token when they sign in and deal with any custom claims / data that needs to be sent to the client.
The problem I'm having is that the provider is rerendering multiple times during the login flow, and each rerender is making an API call. I've managed to get the amount of rerenders down to two, but if I add other 'features' to the provider (e.g update the user's state if their access should change) then this adds to the amount of rerenders, sometimes exponentially, which leads me to suspect that the provider is rerendering as a result of setUserState being called, perhaps unnecessarily. Either way, it is clearly indicative of a problem somewhere in my code, which I've included below:
import {useState, useContext, createContext, useEffect} from 'react'
import {auth, provider} from './firebase'
import {getAuth, onAuthStateChanged, signInWithPopup, signOut} from 'firebase/auth'
import {api} from './axios'
export const UserContext = createContext(null)
export const useAuth = () => useContext(UserContext)
const verifyToken = token => {
return api({
method: 'post',
url: '/verifyToken',
headers: {token}
})
}
const UserProvider = props => {
const [userState, setUserState] = useState(null)
const [loading, setLoading] = useState(true)
const userSignedOut = async () => {
setLoading(true)
return await signOut(auth).then(() => {
setUserState(null)
}).catch(e => {
console.error(e)
}).finally(() => {
setLoading(false)
})
}
const userSignIn = async () => {
console.log('userSignIn')
setLoading(true)
try {
return await signInWithPopup(auth, provider)
} catch (e) {
console.error(e)
} finally {
if (!userState) {
setLoading(false)
}
}
}
const handleUserSignIn = async user => {
console.log('handleUserSignIn', user)
if (user && getAuth().currentUser) {
setLoading(true)
const idToken = await getAuth().currentUser.getIdToken(true)
const firebaseJWT = await getAuth().currentUser.getIdTokenResult()
if (!firebaseJWT) {throw(new Error('no jwt'))}
verifyToken(idToken).then(res => {
if (res.data.role !== firebaseJWT.claims.role) {
throw(new Error('role level claims mismatch'))
} else {
user.verifiedToken = res.data
console.log(`user ${user.uid} valid and token verified`, user)
setUserState(user)
setLoading(false)
}
}).catch(e => {
userSignedOut()
console.error('handleUserSignIn', e)
}).finally(() => {
setLoading(false)
})
} else {
console.log('no user')
userSignedOut()
}
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async user => {
console.log('onAuthStateChanged')
if (user?.uid && user.accessToken) {
await handleUserSignIn(user)
} else {
setUserState(null)
setLoading(false)
}
})
return () => unsubscribe()
}, [])
const value = {
signOut: userSignedOut, // for sign out button
signIn: userSignIn, // for sign in button
user: userState
}
return (
<UserContext.Provider value={value}>
{props.children}
</UserContext.Provider>
)
}
export default UserProvider
I tried to create a codesandbox for this, but unfortunately I was unable to simulate the Firebase auth functions.
The login flow is supposed to look like this:
The user signs in using their Google account.
The app is now loading, and the user cannot interact with it yet (they just get a spinning wheel).
The user's data and accessToken are sent to the backend API server. (function verifyToken)
The API server sets any custom claims and returns the verified token in its response, as well as the access that the user is supposed to have.
If the user's role / custom claims do not match what the API says they should be, the user is signed out.
The user's data is set using setUserState()
The app has finished loading, and the user is signed in.
I would like to avoid unnecessary rerenders and API calls and I suspect that some refactoring may be in order, but I'm not really sure what is best to do here.

How to push the data to firestore after stripe checkout session complete using React js frontend

I am trying to push the checkout basket data to firebase firestore after a stripe checkout session complete using react js node js. the checkout session successfully completed without error. However, there are NO user orders data being push to firebase firestore from the piece of code below:
const { error } = await stripe.redirectToCheckout({
sessionId
}).then(()=>{
db
.collection('users')
.doc(user?.uid)
.collection('orders')
.doc()
.set({
basket: basket,
// amount: paymentIntent.amount,
})
});
Below is the whole pieces of codes of backend and frontend
Functions/index.js
const functions = require("firebase-functions");
const express=require("express");
const cors=require("cors");
require('dotenv').config({ path: './.env' });
//API
const stripe=require("stripe")('sk_test_51KM...zIP');
//App config
const app=express();
var admin = require("firebase-admin");
var serviceAccount = require("./serviceAccountKey.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
//middlewares
app.use(cors({origin: true}));
app.use(express.json());
async function createCheckoutSession(req, res) {
const domainUrl = process.env.WEB_APP_URL;
const { line_items, customer_email } = req.body;
// check req body has line items and email
if (!line_items || !customer_email) {
return res.status(400).json({ error: 'missing required session parameters' });
}
let session;
try {
session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
line_items,
customer_email,
success_url: `${domainUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
// success_url: '/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: `${domainUrl}/canceled`,
shipping_address_collection: { allowed_countries: ['GB', 'US'] }
});
res.status(200).json({ sessionId: session.id, });
} catch (error) {
console.log(error);
res.status(400).json({ error: 'an error occured, unable to create session'});
}}
app.get('/',(req, res)=>res.send('Hello World!'));
app.post('/create-checkout-session', createCheckoutSession);
exports.api=functions.https.onRequest(app);
src/strip-checkout.js
import React, { useContext, useState,useEffect } from 'react';
import { useStripe } from '#stripe/react-stripe-js';
import CheckoutProduct from '../CheckoutProduct';
import { useStateValue } from '../StateProvider';
import { db } from '../firebase';
import { fetchFromAPI } from '../helpers';
import "./stripe-checkout.css";
import { useHistory } from 'react-router-dom/cjs/react-router-dom.min';
const StripeCheckout = () => {
const history=useHistory();
const [{basket, user},dispatch]=useStateValue();
const [email, setEmail] = useState('');
const [processing,setProcessing]=useState("");
const stripe = useStripe();
const handleGuestCheckout = async (e) => {
e.preventDefault();
setProcessing(true);
const line_items = basket?.map(item => {
return {
quantity: 1,
price_data: {
currency: 'usd',
unit_amount: item.price*100, // amount is in cents
product_data: {
name: item.title,
description: item.material
images: [item.image[0]],
}}}})
const response = await fetchFromAPI('create-checkout-session', {
body: { line_items, customer_email: user.email },
})
const { sessionId } = response;
const { error } = await stripe.redirectToCheckout({
sessionId
}).then(()=>{
db
.collection('users')
.doc(user?.uid)
.collection('orders')
.doc()
.set({
basket: basket,
})
});
console.log(sessionId);
if (error) {
console.log(error);
}}
return (
<form onSubmit={handleGuestCheckout}>
<div className='submit-btn'>
<button type='submit' >
<span>{processing ?<p>Processing</p>:
"Proceed to Checkout"}</span>
</button>
</div>
</form>
);
}
export default StripeCheckout;
Since there is no shown any error or warning, how to push the data to firestore after stripe checkout session complete in this case?
Stripe Checkout returns to either a success- or a cancel-URL.
It does not send data to both URLs, except the CHECKOUT_SESSION_ID you may add to it when defining these URLs.
The usual way to get data from Stripe Checkout is to use a Firebase Function Webhook. This Webhook is called by Stripe if a transactions is done, or upon failure etc. This Webhook stores the data in Firebase. I had the same problem and found no other, proper solution than using a Webhook.
https://stripe.com/docs/webhooks
There is also a Firebase Extension providing support for Stripe, including the Webhook. Basically. it will add some collections to Firestore, which you then can query.
https://firebase.google.com/products/extensions/stripe-firestore-stripe-payments

How Can I exist Queries When the User refresh the browser in react-query?

When refresh the Browser, inactive state queries have gone.
How Can I maintain queries When I refresh the Browser?
and also, I want to maintain userData when the pages go out
code like this.. (with zustand, react-query)
const {userId} = useParams();
const userData = useStore((state) => state.userData);
const {isLoading, data} = useQuery('user', () => getUser(userId), {
onSuccess: (res) => {
useStore.setState({userData: res.data});
},
onError: (err) => errorMsg(err),
});
getUser func
export const getUser = (userId)=>{
if(!userId) return;
return axios.get(`${API.user(userId)}`};
}
React Query provides experimental (as of June 2022) support for storing data in Local Storage:
import { persistQueryClient } from 'react-query/persistQueryClient-experimental'
import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental'
// probably also supports session storage as the API is the same
const localStoragePersistor = createWebStoragePersistor({storage: window.localStorage})
// the queryClient you pass to QueryClientProvider
persistQueryClient({
queryClient,
persistor: localStoragePersistor,
})
Source: https://react-query.tanstack.com/plugins/persistQueryClient

What's wrong with my Redux Thunk? It's being called alright but fail for some reason I can't get

I'm new to this and read many tutorial and docs about Redux Thunk and Higher Order Components. I am trying to wire this together but I can't understand what's wrong here.
This image show what's happening: my inner Thunk function save() is not called for some reason.
It works if I change the return withFirebase(save); to return save; but I need the Firebase Context so how can I solve this and why does react-thunk care about the withFirebase?
This is the Action:
import { userActionTypes } from './user.types';
import { withFirebase } from '../../firebase';
import * as ROLES from '../../constants/roles';
const saveUserStart = () => ({
type: userActionTypes.SAVE_USER_START,
});
const saveUserSuccess = user => ({
type: userActionTypes.SAVE_USER_SUCCESS,
payload: user,
});
const saveUserFailure = errMsg => ({
type: userActionTypes.SAVE_USER_FAILURE,
payload: errMsg,
});
function saveUser() {
return dispatch => {
function save({ firebase }) {
const userRef = firebase.userDoc(firebase.auth.currentUser.uid);
dispatch(saveUserStart());
firebase.db
.runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(userRef).then(doc => {
if (!doc.exists) {
return Promise.reject('Transaction failed: User dont exist!');
}
const newRoles = doc.data().roles;
// new roll
newRoles.push(ROLES.USER);
// remove roll
newRoles.splice(newRoles.indexOf('ANONYMOUS'), 1);
// save it back
transaction.update(userRef, { roles: newRoles });
return newRoles;
});
})
.then(newRoles => {
dispatch(saveUserSuccess(firebase.auth.currentUser));
console.log(`Transaction successfully committed role(s): ${newRoles}`);
})
.catch(error => {
dispatch(saveUserFailure(error));
console.log(`Transaction failed committed role(s): ${error}`);
});
}
return withFirebase(save);
};
}
export default saveUser;

Can't fetch Firbase webapp database in reactjs

I can't fetch the data from firebase database.I have used these code to import the firebase database below:
import firebase from 'firebase/app'
import "firebase/database";
const firebaseDB = firebase.database();
const firebaseArticle = firebaseDB.ref('articles')
const firebaseTeams = firebaseDB.ref('teams')
const firebaseVideos = firebaseDB.ref('videos')
And in another file where I want to fetch the data from firebase i used these codes
UNSAFE_componentWillMount() {
firebaseArticle
.limitToFirst(3)
.once("value")
.then((snapshot) => {
const news = [];
snapshot.forEach((childSnapshot) => {
news.push({
...childSnapshot.val(),
id: childSnapshot.key,
});
});
this.setState({
news
});
});
I am getting these error messages in the console. Looks like I've got problems in lifecycles even though I've used UNSAFE_lifecyclename which they recommended. What should I do now to fetch data from firebase? & How can I get rid of these warnings?
Thanks
You should use componentDidMount, like so:
componentDidMount() {
firebaseArticle
.limitToFirst(3)
.once("value")
.then((snapshot) => {
const news = [];
snapshot.forEach((childSnapshot) => {
news.push({
...childSnapshot.val(),
id: childSnapshot.key,
});
});
this.setState({
news
});
});
}

Resources