I have a React ecommerce site that is currently integrated with Stripe. When a successful payment is submitted, the cart is emptied (managed in localStorage), however the Cart quantity in the Navbar isn't resetting to 0.
The cart quantity is being managed in state in the <App /> component, setQty. The stripe payment is submitted in the <PaymentForm> component which is nested 4 components deep:
<App /> > <Checkout /> > <PaymentForm /> > <CheckoutForm />
In CheckoutForm, I'm using setQty({quantity: 0}); which I thought would pass "0" up to <App /> and reset the Cart quantity, instead I get an error of "Unhandled Rejection (TypeError): setQty is not a function". Why is this? How can I get this to work? Also, is there an easier way of resetting the Cart without passing props through so many components?
A breakdown of each component so you can see how I'm passing setQty through each component:
App
import React, { useState, useEffect } from 'react';
import './App.css';
import Nav from './Nav';
import Shop from './Components/Shop';
import Info from './Components/Info';
import Cart from './Components/Cart';
import Item from './Components/Item';
import Checkout from './Components/CheckoutForm/Checkout';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import { getQuantity } from './helpers/helperTools';
function App() {
const storageItems = JSON.parse(localStorage.getItem('product'));
const [qty, setQty] = useState({quantity: getQuantity(storageItems || [])});
console.log("Apppp", qty)
return (
<Router>
<div className="App">
<Nav qty={qty.quantity} />
<Route path="/" exact component={Shop} />
<Route path="/Info" component={Info} />
<Route path="/Cart/" render={(props) => <Cart {...props} setQty={setQty} />} />
<Route path="/Item/:item" component={Item} />
<Route path="/Checkout" component={Checkout} setQty={setQty} />
</div>
</Router>
)
}
export default App;
Checkout
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import {
Paper,
Stepper,
Step,
StepLabel,
Typography,
CircularProgress,
Divider,
Button,
} from '#material-ui/core';
import useStyles from './styles';
import AddressForm from './AddressForm';
import PaymentForm from './PaymentForm';
const steps = ['Shipping address', 'Payment details'];
function Checkout({setQty}) {
const classes = useStyles();
const [activeStep, setActiveStep] = useState(0);
const [shippingData, setShippingData] = useState({});
const nextStep = () => setActiveStep((prevActiveStep) => prevActiveStep + 1);
const backStep = () => setActiveStep((prevActiveStep) => prevActiveStep - 1);
const next = (data) => {
setShippingData(data);
nextStep();
};
const Form = () =>
activeStep === 0 ? (
<AddressForm next={next} />
) : (
<PaymentForm shippingData={shippingData} backStep={backStep} nextStep={nextStep} setQty={setQty} />
);
const Confirmation = () => <div>Confirmation</div>;
return (
<div>
<div className={classes.toolbar} />
<main className={classes.layout}>
<Paper className={classes.paper}>
<Typography variant='h4' align='center'>
Checkout
</Typography>
<Stepper activeStep={activeStep} className={classes.stepper}>
{steps.map((step) => (
<Step key={step}>
<StepLabel>{step}</StepLabel>
</Step>
))}
</Stepper>
{activeStep === steps.length ? <Confirmation /> : <Form />}
</Paper>
</main>
</div>
);
}
export default Checkout;
PaymentForm AND CheckoutForm, both in the same file
import React, { useState } from 'react';
import { Typography, Button, Divider } from '#material-ui/core';
import {
Elements,
CardElement,
ElementsConsumer,
useStripe,
useElements,
} from '#stripe/react-stripe-js';
import { loadStripe } from '#stripe/stripe-js';
import axios from 'axios';
import { getTotal } from '../../helpers/helperTools';
import Review from './Review';
const stripePromise = loadStripe(
'pk_HIDDEN_FOR_NOW'
);
const CheckoutForm = ({ shippingData, backStep, nextStep, setQty }) => {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) {
return;
}
const storageItems = JSON.parse(localStorage.getItem('product'));
const products = storageItems || [];
const totalPrice = getTotal(products);
let productTitle = '';
products.map((item, index) => {
productTitle = `${productTitle} | ${item.title}`;
});
const cardElement = elements.getElement(CardElement);
const { error, source } = await stripe.createSource(cardElement);
console.log(error, source);
const order = await axios.post('http://localhost:7000/api/stripe/charge', {
amount: totalPrice * 100,
source: source.id,
receipt_email: shippingData.email,
title: productTitle,
customerName: `${shippingData.firstName} ${shippingData.lastName}`,
address: {
city: shippingData.City,
country: shippingData.shippingCountry,
line1: shippingData.address1,
postal_code: shippingData.ZIP,
state: shippingData.shippingState,
},
});
if (error) {
console.log('[error]', error);
} else {
console.log('[PaymentMethod]', order);
localStorage.setItem('product', JSON.stringify([]));
nextStep();
setQty({quantity: 0});
}
};
return (
<form onSubmit={handleSubmit}>
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4',
},
},
invalid: {
color: '#9e2146',
},
},
}}
/>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button variant='outlined' onClick={backStep}>
Back
</Button>
<Button type='submit' variant='contained' disabled={!stripe} color='primary'>
Pay
</Button>
</div>
</form>
);
};
function PaymentForm({ shippingData, backStep, nextStep, setQty }) {
return (
<Elements stripe={stripePromise}>
<Review />
<br />
<br />
<CheckoutForm shippingData={shippingData} nextStep={nextStep} backStep={backStep} setQty={setQty} />
</Elements>
);
}
export default PaymentForm;
Screenshot of my file structure
In the App component, you need to pass setQty as below. Props that are mentioned in the Route component would not be transmitted to the component by itself, and we need to use the render function to pass props.
<Route path="/Checkout" render={(props) => <Checkout setQty={setQty} {...props}/>
Related
I am new to react, i have this code which i got from my manager, i have to add an event, actually on button click i have to add a class to other component, the button is in Header component, and the class i have to add is in LeftNav.
Here is my header.js
export class Header extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
anchorEl: null,
gettingUser: false,
loggingOut: false,
menuOpen: false
};
this.cookies = new Cookies();
}
i made a state menuOpen.
and tried setting state to true like this in header.js
<Button className="menu-btn-wrapper header-button" onClick={() => self.setState({ menuOpen: true })}>
<i className="icon circle zf-cs-icon-burger"></i>
</Button>
and moving on to my app.js, i have this
<Header history={history} />
<LeftNav className="leftFixed leftNav" />
How can i get the state of menuOpen from Header here?
i tried
<Header history={history} menuOpen={this.state.menuOpen} />
but i am getting a stateless error.
There is no class present in app.js
Here is my full app.js
import React, { useEffect } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import IdleTimer from 'react-idle-timer';
import Cookies from 'universal-cookie';
import { LinearProgress, IconButton, Tooltip } from '#material-ui/core';
import { Cancel as CancelIcon } from '#material-ui/icons';
import { getToken, updateMaxAge, expirySet } from '../auth';
import { cancelFileUpload, setSnackbar } from '../actions/appActions';
import { logoutUser, saveUserSettings, setAccountActivationCode, setShowConnectingDialog } from '../actions/userActions';
import { privateRoutes, publicRoutes } from '../config/routes';
import { onboardingSteps } from '../constants/onboardingConstants';
import SharedSnackbar from '../components/common/sharedsnackbar';
import ErrorBoundary from '../errorboundary';
import { getOnboardingState } from '../selectors';
import { colors } from '../themes/mender-theme';
import Tracking from '../tracking';
import { getOnboardingComponentFor } from '../utils/onboardingmanager';
import LiveChatBox from './livechatbox';
import ConfirmDismissHelptips from './common/dialogs/confirmdismisshelptips';
import DeviceConnectionDialog from './common/dialogs/deviceconnectiondialog';
import Header from './header/header';
import LeftNav from './leftnav';
import 'react-perfect-scrollbar/dist/css/styles.css';
import PerfectScrollbar from 'react-perfect-scrollbar';
const activationPath = '/activate';
const timeout = 900000; // 15 minutes idle time
const cookies = new Cookies();
export const AppRoot = ({
cancelFileUpload,
currentUser,
history,
logoutUser,
onboardingState,
setAccountActivationCode,
setShowConnectingDialog,
showDeviceConnectionDialog,
showDismissHelptipsDialog,
setSnackbar,
snackbar,
trackingCode,
uploadProgress
}) => {
useEffect(() => {
if (trackingCode) {
if (!cookies.get('_ga')) {
Tracking.cookieconsent().then(({ trackingConsentGiven }) => {
if (trackingConsentGiven) {
Tracking.initialize(trackingCode);
Tracking.pageview();
}
});
} else {
Tracking.initialize(trackingCode);
}
}
history.listen(trackLocationChange);
trackLocationChange(history.location);
}, []);
const trackLocationChange = location => {
// if we're on page whose path might contain sensitive device/ group/ deployment names etc. we sanitize the sent information before submission
let page = location.pathname || '';
if (page.includes('=') && (page.startsWith('/devices') || page.startsWith('/deployments'))) {
const splitter = page.lastIndexOf('/');
const filters = page.slice(splitter + 1);
const keyOnlyFilters = filters.split('&').reduce((accu, item) => `${accu}:${item.split('=')[0]}&`, ''); // assume the keys to filter by are not as revealing as the values things are filtered by
page = `${page.substring(0, splitter)}?${keyOnlyFilters.substring(0, keyOnlyFilters.length - 1)}`; // cut off the last & of the reduced filters string
} else if (page.startsWith(activationPath)) {
setAccountActivationCode(page.substring(activationPath.length + 1));
history.replace('/settings/my-profile');
}
Tracking.pageview(page);
};
const onIdle = () => {
if (expirySet() && currentUser) {
// logout user and warn
return logoutUser('Your session has expired. You have been automatically logged out due to inactivity.').catch(() => updateMaxAge());
}
};
const onboardingComponent = getOnboardingComponentFor(onboardingSteps.ARTIFACT_CREATION_DIALOG, onboardingState);
const containerProps = getToken() ? { id: 'app' } : { className: 'flexbox centered', style: { minHeight: '100vh' } };
return (
<div {...containerProps}>
{getToken() ? (
<>
<IdleTimer element={document} onAction={updateMaxAge} onIdle={onIdle} timeout={timeout} />
<Header history={history} />
<LeftNav className="leftFixed leftNav" />
<PerfectScrollbar className="rightFluid container main">
<ErrorBoundary>{privateRoutes}</ErrorBoundary>
</PerfectScrollbar>
{onboardingComponent ? onboardingComponent : null}
{showDismissHelptipsDialog && <ConfirmDismissHelptips />}
{showDeviceConnectionDialog && <DeviceConnectionDialog onCancel={() => setShowConnectingDialog(false)} />}
{
// eslint-disable-next-line no-undef
ENV === 'production' && <LiveChatBox />
}
</>
) : (
publicRoutes
)}
<SharedSnackbar snackbar={snackbar} setSnackbar={setSnackbar} />
{Boolean(uploadProgress) && (
<div id="progressBarContainer">
<p className="align-center">Upload in progress ({Math.round(uploadProgress)}%)</p>
<LinearProgress variant="determinate" style={{ backgroundColor: colors.grey, gridColumn: 1, margin: '15px 0' }} value={uploadProgress} />
<Tooltip title="Abort" placement="top">
<IconButton onClick={cancelFileUpload}>
<CancelIcon />
</IconButton>
</Tooltip>
</div>
)}
</div>
);
};
const actionCreators = { cancelFileUpload, logoutUser, saveUserSettings, setAccountActivationCode, setShowConnectingDialog, setSnackbar };
const mapStateToProps = state => {
return {
currentUser: state.users.currentUser,
onboardingState: getOnboardingState(state),
showDismissHelptipsDialog: !state.onboarding.complete && state.onboarding.showTipsDialog,
showDeviceConnectionDialog: state.users.showConnectDeviceDialog,
snackbar: state.app.snackbar,
trackingCode: state.app.trackerCode,
uploadProgress: state.app.uploadProgress
};
};
export default compose(withRouter, connect(mapStateToProps, actionCreators))(AppRoot);
and in main.js
export const Main = () =>
render(
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<ErrorBoundary>
<Router basename="/ui/#">
<App />
</Router>
</ErrorBoundary>
</MuiThemeProvider>
</Provider>,
document.getElementById('main') || document.createElement('div')
);
Main();
The root component is AppRoot, that is a Functional Component.
define the menuOpen state in AppRoot with useState:
const [menuOpen,setMenuOpen]=useState(false)
then define a function in AppRoot to handle menuOpen changes:
const handleMenuOpen=()=>{
setMenuOpen(true)
}
then pass the handler function to the Header via props:
<Header history={history} onButtonClick={handleMenuOpen} />
inside the Header:
<Button className="menu-btn-wrapper header-button" onClick={() => this.props.onButtonClick()}>
<i className="icon circle zf-cs-icon-burger"></i>
</Button>
AppRoot after all above steps:
export const AppRoot = ({
cancelFileUpload,
currentUser,
history,
logoutUser,
onboardingState,
setAccountActivationCode,
setShowConnectingDialog,
showDeviceConnectionDialog,
showDismissHelptipsDialog,
setSnackbar,
snackbar,
trackingCode,
uploadProgress
}) => {
const [menuOpen,setMenuOpen]=useState(false)
const handleMenuOpen=()=>{
setMenuOpen(true)
}
useEffect(() => {
if (trackingCode) {
if (!cookies.get('_ga')) {
Tracking.cookieconsent().then(({ trackingConsentGiven }) => {
if (trackingConsentGiven) {
Tracking.initialize(trackingCode);
Tracking.pageview();
}
});
} else {
Tracking.initialize(trackingCode);
}
}
history.listen(trackLocationChange);
trackLocationChange(history.location);
}, []);
const trackLocationChange = location => {
// if we're on page whose path might contain sensitive device/ group/ deployment names etc. we sanitize the sent information before submission
let page = location.pathname || '';
if (page.includes('=') && (page.startsWith('/devices') || page.startsWith('/deployments'))) {
const splitter = page.lastIndexOf('/');
const filters = page.slice(splitter + 1);
const keyOnlyFilters = filters.split('&').reduce((accu, item) => `${accu}:${item.split('=')[0]}&`, ''); // assume the keys to filter by are not as revealing as the values things are filtered by
page = `${page.substring(0, splitter)}?${keyOnlyFilters.substring(0, keyOnlyFilters.length - 1)}`; // cut off the last & of the reduced filters string
} else if (page.startsWith(activationPath)) {
setAccountActivationCode(page.substring(activationPath.length + 1));
history.replace('/settings/my-profile');
}
Tracking.pageview(page);
};
const onIdle = () => {
if (expirySet() && currentUser) {
// logout user and warn
return logoutUser('Your session has expired. You have been automatically logged out due to inactivity.').catch(() => updateMaxAge());
}
};
const onboardingComponent =
getOnboardingComponentFor(onboardingSteps.ARTIFACT_CREATION_DIALOG, onboardingState);
const containerProps = getToken() ? { id: 'app' } : { className: 'flexbox centered', style: { minHeight: '100vh' } };
return (
<div {...containerProps}>
{getToken() ? (
<>
<IdleTimer element={document} onAction={updateMaxAge} onIdle={onIdle} timeout={timeout} />
<Header history={history} onButtonClick={handleMenuOpen} />
<LeftNav className="leftFixed leftNav" />
<PerfectScrollbar className="rightFluid container main">
<ErrorBoundary>{privateRoutes}</ErrorBoundary>
</PerfectScrollbar>
{onboardingComponent ? onboardingComponent : null}
{showDismissHelptipsDialog && <ConfirmDismissHelptips />}
{showDeviceConnectionDialog && <DeviceConnectionDialog onCancel={() => setShowConnectingDialog(false)} />}
{
// eslint-disable-next-line no-undef
ENV === 'production' && <LiveChatBox />
}
</>
) : (
publicRoutes
)}
<SharedSnackbar snackbar={snackbar} setSnackbar={setSnackbar} />
{Boolean(uploadProgress) && (
<div id="progressBarContainer">
<p className="align-center">Upload in progress ({Math.round(uploadProgress)}%)</p>
<LinearProgress variant="determinate" style={{ backgroundColor: colors.grey, gridColumn: 1, margin: '15px 0' }} value={uploadProgress} />
<Tooltip title="Abort" placement="top">
<IconButton onClick={cancelFileUpload}>
<CancelIcon />
</IconButton>
</Tooltip>
</div>
)}
</div>
);
};
I have a page that I need to refresh, but only one time after it loads. If I put window.location.reload in useeffect(), then it refreshes an infinite amount of times. How do I make it stop reloading the page after one time? I tried boolean values and other stuff but nothing works. It keeps refreshing an infinite amount of times. The route which is linking to page is market.
app.tsx
import React from "react";
import Market from "containers/Market";
import Coins from "containers/Coins";
import PoweredBy from "components/PoweredBy";
import MarketProvider from "store/MarketProvider";
import Exchanges from "components/Exchanges";
import Exchange from "components/Exchange";
import { BrowserRouter as Router, Route, Redirect, Switch, Link, LinkProps } from 'react-router-dom';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import { default as Tab, TabProps } from '#material-ui/core/Tab';
import HomeIcon from '#material-ui/icons/Home';
import CodeIcon from '#material-ui/icons/Code';
import TimelineIcon from '#material-ui/icons/Timeline';
const LinkTab: React.ComponentType<TabProps & LinkProps> = Tab as React.ComponentType<TabProps & LinkProps>;
function NavBar() {
const [value, setValue] = React.useState(0);
// console.log(value);
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};
return (
<div >
<AppBar position="static">
<Tabs>
<Tab label="Exchanges" to="/exchange" component={Link} />
<Tab label="Coins" to="/" component={Link} />
<Tab label="Home" to="/pm" component={Link} />
</Tabs>
</AppBar>
</div>
)
};
const App = () => {
return (
<Router>
<div>
<NavBar />
<Switch>
<Route exact path="/" component={Coins} />
<Route exact path="/exchange" component={Exchanges} />
<Route exact path="/market" >
<MarketProvider>
<Market />
</MarketProvider>
</Route>
<Redirect from="/" to="/" />
</Switch>
</div>
</Router>
);
};
file.tsx
import React from "react";
import { Grid, Snackbar, SnackbarCloseReason } from "#material-ui/core";
import { Skeleton, Alert } from "#material-ui/lab";
import useAxios from "axios-hooks";
import PrimaryChart from "components/PrimaryChart";
import SecondaryChart from "components/SecondaryChart";
import TimeFilterButtons from "components/TimeFilterButtons";
import { SC } from "./styled";
import { DataProps } from "interfaces/DataProps";
import useWindowDimensions from "hooks/useWindowDimensions";
import { useQueryParams, StringParam } from "use-query-params";
import { MarketContext } from "store/MarketProvider";
var x = true;
const Market = () => {
const {
filteredDataState: { filteredData },
} = React.useContext(MarketContext);
const [queryParams] = useQueryParams({
id: StringParam,
name: StringParam,
});
const [timeFilter, setTimeFilter] = React.useState<string>("1");
const [isErrorMessage, setIsErrorMessage] = React.useState<string>("");
const [boxWidth, setBoxWidth] = React.useState<number>(0);
const { height } = useWindowDimensions();
const [{ data, loading, error }, fetch] = useAxios(
{
url: `https://api.coingecko.com/api/v3/coins/${queryParams?.id}/market_chart?vs_currency=usd&days=${timeFilter}`,
method: "GET",
},
{ manual: true }
);
const gridItemRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (queryParams.id && queryParams.name) {
fetch();
}
}, [fetch, queryParams, queryParams.id, queryParams.name]);
React.useEffect(() => {
if (error) {
setIsErrorMessage(error.message);
}
}, [error]);
React.useEffect(() => {
const handleResize = (width?: number) => {
setBoxWidth(width || 0);
};
handleResize(gridItemRef.current?.clientWidth || 0);
window.addEventListener("resize", () =>
handleResize(gridItemRef?.current?.clientWidth || 0)
);
return () => {
window.removeEventListener("resize", () => handleResize());
};
}, [gridItemRef]);
const mappedData: DataProps[] = React.useMemo(() => {
return data
? data?.prices.map((ele: any) => ({
date: new Date(ele[0]),
price: ele[1],
}))
: [];
}, [data]);
const handleError = (
e: React.SyntheticEvent<any>,
reason?: SnackbarCloseReason
) => {
setIsErrorMessage("");
};
return (
<Grid container justify="center">
<Grid ref={gridItemRef} item xs={12} md={10} lg={8}>
<SC.MarketHeader>
<SC.Title>{queryParams?.name}</SC.Title>
<TimeFilterButtons
value={timeFilter}
onChange={(v) => setTimeFilter(v || "")}
/>
</SC.MarketHeader>
{loading ? (
<Skeleton
variant="rect"
height={Math.floor(height * 0.6)}
width={boxWidth}
/>
) : mappedData?.length ? (
<>
<PrimaryChart
data={filteredData ?? []}
height={Math.floor(height * 0.4)}
width={boxWidth}
margin={{
top: 16,
right: 16,
bottom: 40,
left: 48,
}}
/>
<SecondaryChart
data={mappedData ?? []}
height={Math.floor(height * 0.1)}
width={boxWidth}
margin={{
top: 0,
right: 16,
bottom: 24,
left: 48,
}}
/>
</>
) : null}
</Grid>
<Snackbar open={!!isErrorMessage} onClose={handleError}>
<Alert onClose={handleError} severity="error">
{isErrorMessage}
</Alert>
</Snackbar>
</Grid>
);
};
export default Market;
I can't see any reason of reloading bth but I'm thinking if you do conditional rendering and handle with boolean when should reload that's would be ok
something like:
const [hasAlreadyReload, setHasAlreadyReload] = React.useState(false);
...
hasAlreadyReload && <Redirect from="/" to="/" />
need help making private route with react redux and firebase
the issue is:
when i click on sign in the user is sign it correctly and the state is updated on redux store but the user is not redirected to dashboard but typing "/dashboard" URL works after clicking the button signing in also trying to go "/dashboard" when the user state user null works
App.js
import React, { useEffect } from "react";
import { connect, Provider } from "react-redux";
import store from "./store";
import { setUser, clearUser } from "./actions/userActions";
import {
BrowserRouter as Router,
Route,
Switch,
withRouter,
} from "react-router-dom";
import Login from "./components/auth/Login";
import Register from "./components/auth/Register";
import Home from "./components/pages/Home";
import Dashboard from "./components/pages/Dashboard";
import firebase from "./firebase";
import PrivateRoute from "./components/PrivateRoute";
function App({ setUser, clearUser, currentUser, history, isLoading }) {
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
clearUser();
}
});
return () => {
unsubscribe();
clearUser();
};
}, [clearUser, history, setUser]);
return (
<Switch>
<Route path="/" component={Home} exact />
<PrivateRoute
path="/dashboard"
component={Dashboard}
currentUser={currentUser}
isLoading={isLoading}
/>
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
</Switch>
);
}
const mapStateToProps = (state) => ({
isLoading: state.user.isLoading,
currentUser: state.user.currentUser,
});
const AppWithRouter = withRouter(
connect(mapStateToProps, { setUser, clearUser })(App)
);
const AppWithAuth = () => (
<Provider store={store}>
<Router>
<AppWithRouter />
</Router>
</Provider>
);
export default AppWithAuth;
PrivateRoute.js
import React from "react";
import { Redirect, Route } from "react-router-dom";
const PrivateRoute = ({
component: Component,
currentUser,
isLoading,
...rest
}) => {
return isLoading ? (
<h1>Spinner</h1>
) : (
<Route
{...rest}
render={(props) =>
currentUser ? <Component {...props} /> : <Redirect to="login" />
}
/>
);
};
export default PrivateRoute;
Login.js
import React, { useState } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
//firebase
import firebase from "../../firebase";
import {
Grid,
Box,
TextField,
CssBaseline,
Avatar,
Container,
Typography,
LinearProgress,
Button,
makeStyles,
} from "#material-ui/core";
const Login = (props) => {
const [form, setForm] = useState({
email: "",
password: "",
});
const [errors, setErrors] = useState([]);
const [loading, setLoading] = useState(false);
const handleChange = (event) =>
setForm({ ...form, [event.target.name]: event.target.value });
const handleSubmit = (e) => {
e.preventDefault();
if (!form.email || !form.password) {
return setErrors(["Fill in all fields"]);
}
setErrors([]);
setLoading(true);
firebase
.auth()
.signInWithEmailAndPassword(form.email, form.password)
.then((signedInUser) => {
setLoading(false);
props.history.push("dashboard");
})
.catch((err) => {
setLoading(false);
setErrors([err.message]);
});
};
const useStyles = makeStyles((theme) => ({
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center",
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}));
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}></Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<form className={classes.form} noValidate onsubmit={handleSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
onChange={handleChange}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
onChange={handleChange}
/>
<Box color="red">
{errors.length > 0 && errors.map((err, i) => <span>{err}</span>)}
</Box>
<Grid justify="center"></Grid>
{loading ? <LinearProgress size={14} /> : ""}
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={handleSubmit}
>
Sign in
</Button>
<Grid container>
<Grid item>
<Link to="/register" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</form>
</div>
<Box mt={8}></Box>
</Container>
);
};
const mapStateToProps = (state) => ({
isLoading: state.user.isLoading,
currentUser: state.user.currentUser,
});
export default connect(mapStateToProps)(Login);
I transitioned from a previous React app to a new template. Issue is i am quite confused about how redux is setup and how i can implement authentication.
LoginForm
// validation functions
const required = value => (value === null ? 'Required' : undefined);
const email = value =>
value && !/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value) ? 'Invalid email' : undefined;
const LinkBtn = React.forwardRef(function LinkBtn(props, ref) {
// eslint-disable-line
return <NavLink to={props.to} {...props} innerRef={ref} />; // eslint-disable-line
});
// eslint-disable-next-line
class LoginForm extends React.Component {
// state = {
// showPassword: false,
// };
constructor() {
super();
this.state = {
email: '',
password: '',
errors: {},
showPassword: false,
};
}
handleClickShowPassword = () => {
const { showPassword } = this.state;
this.setState({ showPassword: !showPassword });
};
handleMouseDownPassword = event => {
event.preventDefault();
};
onSubmit = e => {
e.preventDefault();
const userData = {
email: this.state.email,
password: this.state.password,
};
loginUser(userData);
};
render() {
console.log(this.props);
const { classes, handleSubmit, pristine, submitting, deco } = this.props;
const { showPassword } = this.state;
return (
<Fragment>
<Hidden mdUp>
<NavLink to="/" className={classNames(classes.brand, classes.outer)}>
<img src={logo} alt={brand.name} />
{brand.name}
</NavLink>
</Hidden>
<Paper className={classNames(classes.paperWrap, deco && classes.petal)}>
<Hidden smDown>
<div className={classes.topBar}>
<NavLink to="/" className={classes.brand}>
<img src={logo} alt={brand.name} />
{brand.name}
</NavLink>
<Button
size="small"
className={classes.buttonLink}
component={LinkBtn}
to="/register"
>
<Icon className={classes.icon}>arrow_forward</Icon>
Create new account
</Button>
</div>
</Hidden>
<Typography variant="h4" className={classes.title} gutterBottom>
Sign In
</Typography>
<Typography variant="caption" className={classes.subtitle} gutterBottom align="center">
Lorem ipsum dolor sit amet
</Typography>
<section className={classes.socmedLogin}>
<div className={classes.btnArea}>
<Button variant="outlined" size="small" className={classes.redBtn} type="button">
<AllInclusive className={classNames(classes.leftIcon, classes.iconSmall)} />
Socmed 1
</Button>
<Button variant="outlined" size="small" className={classes.blueBtn} type="button">
<Brightness5 className={classNames(classes.leftIcon, classes.iconSmall)} />
Socmed 2
</Button>
<Button variant="outlined" size="small" className={classes.cyanBtn} type="button">
<People className={classNames(classes.leftIcon, classes.iconSmall)} />
Socmed 3
</Button>
</div>
<ContentDivider content="Or sign in with email" />
</section>
<section className={classes.formWrap}>
<form onSubmit={handleSubmit}>
<div>
<FormControl className={classes.formControl}>
<Field
name="email"
component={TextFieldRedux}
placeholder="Your Email"
label="Your Email"
required
validate={[required, email]}
className={classes.field}
/>
</FormControl>
</div>
<div>
<FormControl className={classes.formControl}>
<Field
name="password"
component={TextFieldRedux}
type={showPassword ? 'text' : 'password'}
label="Your Password"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
onClick={this.handleClickShowPassword}
onMouseDown={this.handleMouseDownPassword}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
required
validate={required}
className={classes.field}
/>
</FormControl>
</div>
<div className={classes.optArea}>
<FormControlLabel
className={classes.label}
control={<Field name="checkbox" component={CheckboxRedux} />}
label="Remember"
/>
<Button
size="small"
component={LinkBtn}
to="/reset-password"
className={classes.buttonLink}
>
Forgot Password
</Button>
</div>
<div className={classes.btnArea}>
<Button variant="contained" color="primary" size="large" type="submit">
Continue
<ArrowForward
className={classNames(classes.rightIcon, classes.iconSmall)}
disabled={submitting || pristine}
/>
</Button>
</div>
</form>
</section>
</Paper>
</Fragment>
);
}
}
const mapDispatchToProps = dispatch => ({
init: bindActionCreators(loginUser, dispatch),
loginUser:
});
LoginForm.propTypes = {
classes: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired,
loginUser: PropTypes.func.isRequired,
deco: PropTypes.bool.isRequired,
};
const LoginFormReduxed = reduxForm({
form: 'immutableExample',
enableReinitialize: true,
})(LoginForm);
const reducerLogin = 'login';
const reducerUi = 'ui';
const FormInit = connect(
state => ({
force: state,
initialValues: state.getIn([reducerLogin, 'usersLogin']),
deco: state.getIn([reducerUi, 'decoration']),
}),
mapDispatchToProps,
)(LoginFormReduxed);
export default withStyles(styles)(FormInit);
login.js
import React from 'react';
import { Helmet } from 'react-helmet';
import brand from 'dan-api/dummy/brand';
import PropTypes from 'prop-types';
import { withStyles } from '#material-ui/core/styles';
import { LoginForm } from 'dan-components';
import styles from 'dan-components/Forms/user-jss';
class Login extends React.Component {
state = {
valueForm: [],
};
submitForm(values) {
const { valueForm } = this.state;
setTimeout(() => {
this.setState({ valueForm: values });
console.log(`You submitted:\n\n${valueForm}`);
window.location.href = '/app';
}, 500); // simulate server latency
}
render() {
const title = brand.name + ' - Login';
const description = brand.desc;
const { classes } = this.props;
return (
<div className={classes.root}>
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
</Helmet>
<div className={classes.container}>
<div className={classes.userFormWrap}>
<LoginForm onSubmit={values => this.submitForm(values)} />
</div>
</div>
</div>
);
}
}
Login.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(Login);
My authactions i am trying to add.
import axios from 'axios';
import jwt_decode from 'jwt-decode';
import setAuthToken from '../../utils/setAuthToken';
import { GET_ERRORS, SET_CURRENT_USER, USER_LOADING } from '../constants/authConstants';
// Login - get user token
export const loginUser = userData => dispatch => {
axios
.post('/api/total/users/login', userData)
.then(res => {
// Save to localStorage
// Set token to localStorage
const { token } = res.data;
localStorage.setItem('jwtToken', JSON.stringify(token));
// Set token to Auth header
setAuthToken(token);
// Decode token to get user data
const decoded = jwt_decode(token);
// Set current user
dispatch(setCurrentUser(decoded));
})
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data,
}),
);
};
// Set logged in user
export const setCurrentUser = decoded => {
return {
type: SET_CURRENT_USER,
payload: decoded,
};
};
// User loading
export const setUserLoading = () => {
return {
type: USER_LOADING,
};
};
// Log user out
export const logoutUser = history => dispatch => {
// Remove token from local storage
localStorage.removeItem('jwtTokenTeams');
// Remove auth header for future requests
setAuthToken(false);
// Set current user to empty object {} which will set isAuthenticated to false
dispatch(setCurrentUser({}));
history.push('/dashboard');
};
and the authreducer
import { Map, fromJS } from 'immutable';
import { INIT } from '../constants/reduxFormConstants';
import { SET_CURRENT_USER, USER_LOADING } from '../constants/authConstants';
const isEmpty = require('is-empty');
const initialState = {
usersLogin: Map({
isAuthenticated: false,
user: {},
loading: false,
remember: false,
}),
};
const initialImmutableState = fromJS(initialState);
export default function reducer(state = initialImmutableState, action = {}) {
switch (action.type) {
case INIT:
return state;
case SET_CURRENT_USER:
return {
...state,
isAuthenticated: !isEmpty(action.payload),
user: action.payload,
};
case USER_LOADING:
return {
...state,
loading: true,
};
default:
return state;
}
}
I'm having a really hard time understaning how i can make this work together.
adding app.js
/**
* app.js
*
* This is the entry file for the application, only setup and boilerplate
* code.
*/
// Needed for redux-saga es6 generator support
import '#babel/polyfill';
// Import all the third party stuff
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router/immutable';
import FontFaceObserver from 'fontfaceobserver';
import history from 'utils/history';
import 'sanitize.css/sanitize.css';
// Import root app
import App from 'containers/App';
import './styles/layout/base.scss';
// Import Language Provider
import LanguageProvider from 'containers/LanguageProvider';
// Load the favicon and the .htaccess file
import '!file-loader?name=[name].[ext]!../public/favicons/favicon.ico'; // eslint-disable-line
import 'file-loader?name=.htaccess!./.htaccess'; // eslint-disable-line
import configureStore from './redux/configureStore';
// Import i18n messages
import { translationMessages } from './i18n';
// Observe loading of Open Sans (to remove open sans, remove the <link> tag in
// the index.html file and this observer)
const openSansObserver = new FontFaceObserver('Open Sans', {});
// When Open Sans is loaded, add a font-family using Open Sans to the body
openSansObserver.load().then(() => {
document.body.classList.add('fontLoaded');
});
// Create redux store with history
const initialState = {};
const store = configureStore(initialState, history);
const MOUNT_NODE = document.getElementById('app');
const render = messages => {
ReactDOM.render(
<Provider store={store}>
<LanguageProvider messages={messages}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</LanguageProvider>
</Provider>,
MOUNT_NODE,
);
};
if (module.hot) {
// Hot reloadable React components and translation json files
// modules.hot.accept does not accept dynamic dependencies,
// have to be constants at compile-time
module.hot.accept(['./i18n', 'containers/App'], () => {
ReactDOM.unmountComponentAtNode(MOUNT_NODE);
render(translationMessages);
});
}
// Chunked polyfill for browsers without Intl support
if (!window.Intl) {
new Promise(resolve => {
resolve(import('intl'));
})
.then(() =>
Promise.all([import('intl/locale-data/jsonp/en.js'), import('intl/locale-data/jsonp/de.js')]),
) // eslint-disable-line prettier/prettier
.then(() => render(translationMessages))
.catch(err => {
throw err;
});
} else {
render(translationMessages);
}
// Install ServiceWorker and AppCache in the end since
// it's not most important operation and if main code fails,
// we do not want it installed
if (process.env.NODE_ENV === 'production') {
require('offline-plugin/runtime').install(); // eslint-disable-line global-require
}
the app component
import React from 'react';
import jwt_decode from 'jwt-decode';
import { Switch, Route } from 'react-router-dom';
import NotFound from 'containers/Pages/Standalone/NotFoundDedicated';
import store from '../../redux/configureStore';
import { setCurrentUser, logoutUser } from '../../redux/actions/authActions';
import setAuthToken from '../../utils/setAuthToken';
import Auth from './Auth';
import Application from './Application';
import ThemeWrapper, { AppContext } from './ThemeWrapper';
window.__MUI_USE_NEXT_TYPOGRAPHY_VARIANTS__ = true;
class App extends React.Component {
render() {
console.log(this.props);
return (
<ThemeWrapper>
<AppContext.Consumer>
{changeMode => (
<Switch>
<Route path="/" exact component={Auth} />
<Route
path="/app"
render={props => <Application {...props} changeMode={changeMode} />}
/>
<Route component={Auth} />
<Route component={NotFound} />
</Switch>
)}
</AppContext.Consumer>
</ThemeWrapper>
);
}
}
export default App;
the auth component
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import Outer from '../Templates/Outer';
import {
Login,
LoginV2,
LoginV3,
Register,
RegisterV2,
RegisterV3,
ResetPassword,
LockScreen,
ComingSoon,
Maintenance,
NotFound,
} from '../pageListAsync';
class Auth extends React.Component {
render() {
return (
<Outer>
<Switch>
<Route path="/login" component={Login} />
{/* <Route path="/login-v2" component={LoginV2} />
<Route path="/login-v3" component={LoginV3} />
<Route path="/register" component={Register} />
<Route path="/register-v2" component={RegisterV2} />
<Route path="/register-v3" component={RegisterV3} /> */}
<Route path="/reset-password" component={ResetPassword} />
<Route path="/lock-screen" component={LockScreen} />
{/* <Route path="/maintenance" component={Maintenance} />
<Route path="/coming-soon" component={ComingSoon} /> */}
<Route component={NotFound} />
</Switch>
</Outer>
);
}
}
export default Auth;
To maintain authentication using PLain redux is not quite possible because when ever you reload, the store get refreshed . However, redux has a functionality called Persisted store
Persisted Store store the data in memory and will not be refreshed with page reload or anything like that.
You can check this Link
Update with out persisted store:
In that case, Get the IsLoggedin state from store.
In App component
const App = () => {
const isLoggedIn = localStorage.getItem("jwtToken") !== null ? true: false
return (
<Router>
<Switch>
<PrivateRoute isLoggedIn={isLoggedIn} path="/dashboard">
<Dashboard />
</PrivateRoute>
<Route path="/login">
<Login authToken={authToken} />
</Route>
</Switch>
</Router>
);
and then in private route component:
const PrivateRoute = (props) => {
const { children, IsLoggedin, ...rest } = props;
return (
<Route
{...rest}
render={({ location }) =>
IsLoggedin ? (
children
) : (
<Redirect
to={{
pathname: '/login',
state: { from: location },
}}
/>
)
}
/>
);
};
I have custom user menu
import React from "react";
import { UserMenu, MenuItemLink} from "react-admin";
import SettingsIcon from "#material-ui/icons/Settings";
import jwt from "jwt-decode";
const AdminUserMenu = ({ logout }) => {
var user = jwt(localStorage.getItem("token"));
return (
<UserMenu>
<MenuItemLink
to={"/Admins/" + user.id}
primaryText="Profile"
leftIcon={<SettingsIcon />}
/>
{logout}
</UserMenu>
);
};
export default AdminUserMenu;
const AdminAppBar = (props) => (
<AppBar {...props} userMenu={<AdminUserMenu />} />
);
Profile link works. But logout button does nothing. How can I make it work so that user could logout?
Try this:
const AdminUserMenu = (props) => {
var user = jwt(localStorage.getItem("token"));
return (
<UserMenu {...props} >
<MenuItemLink
to={"/Admins/" + user.id}
primaryText="Profile"
leftIcon={<SettingsIcon />}
/>
</UserMenu>
);
};
I had the same behavior that you did using the latest version. The above code from MaxAlex also didn't work for some reason.
I was able to get it work work as follows. Notice the placement of the {logout} in the ConfigurationMenu object versus in the CustomUserMenu.
import * as React from 'react';
import { forwardRef } from 'react';
import { AppBar, UserMenu, MenuItemLink, useTranslate } from 'react-admin';
import Typography from '#material-ui/core/Typography';
import SettingsIcon from '#material-ui/icons/Settings';
import { makeStyles } from '#material-ui/core/styles';
import Logo from '../Logo';
const useStyles = makeStyles({
title: {
flex: 1,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
},
spacer: {
flex: 1,
}
});
const ConfigurationMenu = ({logout, props}) => {
const translate = useTranslate();
return (
<div>
<MenuItemLink
to="/profile"
primaryText={translate("cw.profile.title")}
leftIcon={<SettingsIcon />}
{...props}
/>
{logout}
</div>
);
};
const CustomUserMenu = (props) => (
<UserMenu {...props} >
<ConfigurationMenu />
</UserMenu>
);
const CustomAppBar = (props) => {
const classes = useStyles();
return (
<AppBar {...props} elevation={1} userMenu={<CustomUserMenu />}>
<Typography
variant="h6"
color="inherit"
className={classes.title}
id="react-admin-title"
/>
<Logo />
<span className={classes.spacer} />
</AppBar>
);
};
export default CustomAppBar;