Merge pull request #147 from mirumee/add/reset-password

Password reset flow
This commit is contained in:
Dominik Żegleń 2019-09-06 15:57:35 +02:00 committed by GitHub
commit 0f4bdabc19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1935 additions and 685 deletions

View file

@ -16,3 +16,4 @@ All notable, unreleased changes to this project will be documented in this file.
- Fix table cell padding - #143 by @dominik-zeglen
- Add fallback locale - #153 by @dominik-zeglen
- Replace checkbox with switch component in "product type has variants" - #152 by @dominik-zeglen
- Add password reset flow - #147 by @dominik-zeglen

View file

@ -37,7 +37,8 @@ module.exports = api => {
extractFromFormatMessageCall: true,
messagesDir: "build/locale/"
}
]
],
"macros"
];
return {

View file

@ -1,6 +1,6 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2019-09-04T11:51:21.421Z\n"
"POT-Creation-Date: 2019-09-06T13:56:36.897Z\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"MIME-Version: 1.0\n"
@ -1487,6 +1487,14 @@ msgctxt "button"
msgid "Back to home"
msgstr ""
#: build/locale/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.json
#. [src.auth.components.ResetPasswordSuccessPage.2245157344] - button
#. defaultMessage is:
#. Back to login
msgctxt "button"
msgid "Back to login"
msgstr ""
#: build/locale/src/categories/components/CategoryBackground/CategoryBackground.json
#. [src.categories.components.CategoryBackground.1849089820] - section header
#. defaultMessage is:
@ -1943,6 +1951,14 @@ msgctxt "button"
msgid "Confirm"
msgstr ""
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
#. [src.auth.components.NewPasswordPage.2799926859]
#. defaultMessage is:
#. Confirm Password
msgctxt "description"
msgid "Confirm Password"
msgstr ""
#: build/locale/src/orders/components/OrderCustomer/OrderCustomer.json
#. [src.orders.components.OrderCustomer.1111991638] - subheader
#. defaultMessage is:
@ -3363,6 +3379,14 @@ msgctxt "tax rate"
msgid "Foodstuffs"
msgstr ""
#: build/locale/src/auth/components/ResetPasswordPage/ResetPasswordPage.json
#. [src.auth.components.ResetPasswordPage.2370700732]
#. defaultMessage is:
#. Forgot your password? Don't worry, we'll reset it for you.
msgctxt "description"
msgid "Forgot your password? Don't worry, we'll reset it for you."
msgstr ""
#: build/locale/src/discounts/components/VoucherTypes/VoucherTypes.json
#. [src.discounts.components.VoucherTypes.3289659291] - voucher discount type
#. defaultMessage is:
@ -4179,6 +4203,14 @@ msgctxt "navigation section name"
msgid "Navigation"
msgstr ""
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
#. [src.auth.components.NewPasswordPage.1254879564]
#. defaultMessage is:
#. New Password
msgctxt "description"
msgid "New Password"
msgstr ""
#: build/locale/src/products/views/ProductCreate.json
#. [src.products.views.1591632382] - page header
#. defaultMessage is:
@ -5111,6 +5143,14 @@ msgctxt "description"
msgid "Password"
msgstr ""
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
#. [src.auth.components.NewPasswordPage.4253911811]
#. defaultMessage is:
#. Passwords do not match
msgctxt "description"
msgid "Passwords do not match"
msgstr ""
#: build/locale/src/orders/components/OrderList/OrderList.json
#. [src.orders.components.OrderList.2743232155] - payment status
#. defaultMessage is:
@ -5259,6 +5299,14 @@ msgctxt "product type"
msgid "Physical"
msgstr ""
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
#. [src.auth.components.NewPasswordPage.1915811227]
#. defaultMessage is:
#. Please set up a new password.
msgctxt "description"
msgid "Please set up a new password."
msgstr ""
#: build/locale/src/plugins/components/PluginInfo/PluginInfo.json
#. [src.plugins.components.PluginInfo.3425535100] - section header
#. defaultMessage is:
@ -5675,6 +5723,14 @@ msgctxt "tax rate"
msgid "Property renovations"
msgstr ""
#: build/locale/src/auth/views/ResetPassword.json
#. [src.auth.views.2388238158]
#. defaultMessage is:
#. Provided email address does not exist in our database.
msgctxt "description"
msgid "Provided email address does not exist in our database."
msgstr ""
#: build/locale/src/collections/views/CollectionList.json
#. [src.collections.views.1547167026] - publish collections
#. defaultMessage is:
@ -6031,6 +6087,14 @@ msgctxt "button"
msgid "Reset"
msgstr ""
#: build/locale/src/auth/components/LoginPage/LoginPage.json
#. [src.auth.components.LoginPage.4028609483] - button
#. defaultMessage is:
#. Reset your password
msgctxt "button"
msgid "Reset your password"
msgstr ""
#: build/locale/src/shipping/components/ShippingZoneCountriesAssignDialog/ShippingZoneCountriesAssignDialog.json
#. [src.shipping.components.ShippingZoneCountriesAssignDialog.1003092716]
#. defaultMessage is:
@ -6107,6 +6171,14 @@ msgctxt "description"
msgid "Sale Name"
msgstr ""
#: build/locale/src/intl.json
#. [src.somethingWentWrong]
#. defaultMessage is:
#. Saleor ran into an unexpected problem
msgctxt "description"
msgid "Saleor ran into an unexpected problem"
msgstr ""
#: build/locale/src/intl.json
#. [src.sales] - sales section name
#. defaultMessage is:
@ -6447,6 +6519,14 @@ msgctxt "add order note, button"
msgid "Send"
msgstr ""
#: build/locale/src/auth/components/ResetPasswordPage/ResetPasswordPage.json
#. [src.auth.components.ResetPasswordPage.3663311080] - password reset, button
#. defaultMessage is:
#. Send Instructions
msgctxt "password reset, button"
msgid "Send Instructions"
msgstr ""
#: build/locale/src/staff/components/StaffAddMemberDialog/StaffAddMemberDialog.json
#. [src.staff.components.StaffAddMemberDialog.449055697] - button
#. defaultMessage is:
@ -6479,6 +6559,14 @@ msgctxt "voucher end date, switch button"
msgid "Set end date"
msgstr ""
#: build/locale/src/auth/components/NewPasswordPage/NewPasswordPage.json
#. [src.auth.components.NewPasswordPage.2342634351] - button
#. defaultMessage is:
#. Set new password
msgctxt "button"
msgid "Set new password"
msgstr ""
#: build/locale/src/plugins/components/PluginInfo/PluginInfo.json
#. [src.plugins.components.PluginInfo.4013064767]
#. defaultMessage is:
@ -6683,18 +6771,6 @@ msgctxt "description"
msgid "Some products require shipping, but no method provided"
msgstr ""
#: build/locale/src/mutations.json
#. [src.1395232177] - error message
#. defaultMessage is:
#. Something went wrong. {errorMessage}
#: build/locale/src/queries.json
#. [src.1395232177] - error message
#. defaultMessage is:
#. Something went wrong. {errorMessage}
msgctxt "error message"
msgid "Something went wrong. {errorMessage}"
msgstr ""
#: build/locale/src/components/NotFoundPage/NotFoundPage.json
#. [src.components.NotFoundPage.4036415297]
#. defaultMessage is:
@ -6943,6 +7019,14 @@ msgctxt "plugin success message"
msgid "Succesfully updated plugin settings"
msgstr ""
#: build/locale/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.json
#. [src.auth.components.ResetPasswordSuccessPage.4066297200]
#. defaultMessage is:
#. Success! In a few minutes youll receive a message with instructions on how to reset your password.
msgctxt "description"
msgid "Success! In a few minutes youll receive a message with instructions on how to reset your password."
msgstr ""
#: build/locale/src/attributes/views/AttributeCreate/AttributeCreate.json
#. [src.attributes.views.AttributeCreate.11941964]
#. defaultMessage is:

50
package-lock.json generated
View file

@ -9409,8 +9409,7 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"aproba": {
"version": "1.2.0",
@ -9428,13 +9427,11 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"optional": true
"bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -9447,18 +9444,15 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
@ -9561,8 +9555,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
@ -9572,7 +9565,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -9585,20 +9577,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"optional": true
"bundled": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -9615,7 +9604,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -9688,8 +9676,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
@ -9699,7 +9686,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -9775,8 +9761,7 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"optional": true
"bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@ -9806,7 +9791,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -9824,7 +9808,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -9863,13 +9846,11 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"optional": true
"bundled": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"optional": true
"bundled": true
}
}
},
@ -16962,6 +16943,15 @@
"tough-cookie": "^2.3.3"
}
},
"require-context.macro": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/require-context.macro/-/require-context.macro-1.1.1.tgz",
"integrity": "sha512-l1XH5HruDyG+Iwo5pz39EGbOFVtoYQt8cl7mJ6KJFWzARNJnpb+XUui+jBQUGlJ8SJOdx+QDh784e8b42PxLXA==",
"dev": true,
"requires": {
"@types/webpack-env": "^1.14.0"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

View file

@ -120,6 +120,7 @@
"react-intl-po": "^2.2.2",
"react-test-renderer": "^16.8.6",
"regenerator-runtime": "^0.11.1",
"require-context.macro": "^1.1.1",
"rimraf": "^2.7.0",
"testcafe": "^1.3.3",
"ts-jest": "^23.10.5",

View file

@ -1,23 +1,21 @@
import React from "react";
import { getMutationProviderData } from "../misc";
import { PartialMutationProviderOutput } from "../types";
import { getAuthToken, removeAuthToken, setAuthToken, UserContext } from "./";
import { MutationFunction, MutationResult } from "react-apollo";
import { UserContext } from "./";
import { TypedTokenAuthMutation, TypedVerifyTokenMutation } from "./mutations";
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
import { User } from "./types/User";
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
import { getAuthToken, removeAuthToken, setAuthToken } from "./utils";
interface AuthProviderOperationsProps {
children: (
props: {
hasToken: boolean;
isAuthenticated: boolean;
tokenAuthLoading: boolean;
tokenVerifyLoading: boolean;
user: User;
}
) => React.ReactNode;
children: (props: {
hasToken: boolean;
isAuthenticated: boolean;
tokenAuthLoading: boolean;
tokenVerifyLoading: boolean;
user: User;
}) => React.ReactNode;
}
const AuthProviderOperations: React.StatelessComponent<
AuthProviderOperationsProps
@ -27,10 +25,7 @@ const AuthProviderOperations: React.StatelessComponent<
{(...tokenAuth) => (
<TypedVerifyTokenMutation>
{(...tokenVerify) => (
<AuthProvider
tokenAuth={getMutationProviderData(...tokenAuth)}
tokenVerify={getMutationProviderData(...tokenVerify)}
>
<AuthProvider tokenAuth={tokenAuth} tokenVerify={tokenVerify}>
{children}
</AuthProvider>
)}
@ -41,17 +36,21 @@ const AuthProviderOperations: React.StatelessComponent<
};
interface AuthProviderProps {
children: (
props: {
hasToken: boolean;
isAuthenticated: boolean;
tokenAuthLoading: boolean;
tokenVerifyLoading: boolean;
user: User;
}
) => React.ReactNode;
tokenAuth: PartialMutationProviderOutput<TokenAuth, TokenAuthVariables>;
tokenVerify: PartialMutationProviderOutput<VerifyToken, VerifyTokenVariables>;
children: (props: {
hasToken: boolean;
isAuthenticated: boolean;
tokenAuthLoading: boolean;
tokenVerifyLoading: boolean;
user: User;
}) => React.ReactNode;
tokenAuth: [
MutationFunction<TokenAuth, TokenAuthVariables>,
MutationResult<TokenAuth>
];
tokenVerify: [
MutationFunction<VerifyToken, VerifyTokenVariables>,
MutationResult<VerifyToken>
];
}
interface AuthProviderState {
@ -70,23 +69,26 @@ class AuthProvider extends React.Component<
componentWillReceiveProps(props: AuthProviderProps) {
const { tokenAuth, tokenVerify } = props;
if (tokenAuth.opts.error || tokenVerify.opts.error) {
const tokenAuthOpts = tokenAuth[1];
const tokenVerifyOpts = tokenVerify[1];
if (tokenAuthOpts.error || tokenVerifyOpts.error) {
this.logout();
}
if (tokenAuth.opts.data) {
const user = tokenAuth.opts.data.tokenCreate.user;
if (tokenAuthOpts.data) {
const user = tokenAuthOpts.data.tokenCreate.user;
// FIXME: Now we set state also when auth fails and returned user is
// `null`, because the LoginView uses this `null` to display error.
this.setState({ user });
if (user) {
setAuthToken(
tokenAuth.opts.data.tokenCreate.token,
tokenAuthOpts.data.tokenCreate.token,
this.state.persistToken
);
}
} else {
if (tokenVerify.opts.data && tokenVerify.opts.data.tokenVerify.user) {
const user = tokenVerify.opts.data.tokenVerify.user;
if (tokenVerifyOpts.data && tokenVerifyOpts.data.tokenVerify.user) {
const user = tokenVerifyOpts.data.tokenVerify.user;
this.setState({ user });
}
}
@ -94,17 +96,23 @@ class AuthProvider extends React.Component<
componentDidMount() {
const { user } = this.state;
const { tokenVerify } = this.props;
const token = getAuthToken();
if (!!token && !user) {
tokenVerify.mutate({ token });
this.verifyToken(token);
}
}
login = (email: string, password: string, persistToken: boolean) => {
const { tokenAuth } = this.props;
const [tokenAuthFn] = tokenAuth;
this.setState({ persistToken });
tokenAuth.mutate({ email, password });
tokenAuthFn({ variables: { email, password } });
};
loginByToken = (token: string, user: User) => {
this.setState({ user });
setAuthToken(token, this.state.persistToken);
};
logout = () => {
@ -112,19 +120,36 @@ class AuthProvider extends React.Component<
removeAuthToken();
};
verifyToken = (token: string) => {
const { tokenVerify } = this.props;
const [tokenVerifyFn] = tokenVerify;
return tokenVerifyFn({ variables: { token } });
};
render() {
const { children, tokenAuth, tokenVerify } = this.props;
const tokenAuthOpts = tokenAuth[1];
const tokenVerifyOpts = tokenVerify[1];
const { user } = this.state;
const isAuthenticated = !!user;
return (
<UserContext.Provider
value={{ user, login: this.login, logout: this.logout }}
value={{
login: this.login,
loginByToken: this.loginByToken,
logout: this.logout,
tokenAuthLoading: tokenAuthOpts.loading,
tokenVerifyLoading: tokenVerifyOpts.loading,
user
}}
>
{children({
hasToken: !!getAuthToken(),
isAuthenticated,
tokenAuthLoading: tokenAuth.opts.loading,
tokenVerifyLoading: tokenVerify.opts.loading,
tokenAuthLoading: tokenAuthOpts.loading,
tokenVerifyLoading: tokenVerifyOpts.loading,
user
})}
</UserContext.Provider>

View file

@ -0,0 +1,100 @@
import { Theme } from "@material-ui/core/styles";
import { makeStyles } from "@material-ui/styles";
import React from "react";
import SVG from "react-inlinesvg";
import backgroundArt from "@assets/images/login-background.svg";
import saleorDarkLogo from "@assets/images/logo-dark.svg";
import saleorLightLogo from "@assets/images/logo-light.svg";
import useTheme from "@saleor/hooks/useTheme";
const useStyles = makeStyles(
(theme: Theme) => ({
logo: {
"& svg": {
display: "block",
height: 40,
marginBottom: theme.spacing.unit * 4
}
},
mainPanel: {
[theme.breakpoints.down("sm")]: {
padding: theme.spacing.unit * 2
},
background: theme.palette.background.paper,
display: "flex",
flexDirection: "column",
height: "100vh",
justifyContent: "center",
padding: theme.spacing.unit * 6,
width: "100%"
},
mainPanelContent: {
[theme.breakpoints.up("xs")]: {
width: "100%"
},
[theme.breakpoints.up("sm")]: {
width: 328
},
"@media (min-width: 1440px)": {
width: 464
},
margin: "auto",
width: "100%"
},
root: {
[theme.breakpoints.up("lg")]: {
gridTemplateColumns: "376px 1fr"
},
"@media (min-width: 1440px)": {
gridTemplateColumns: "520px 1fr"
},
display: "grid",
gridTemplateColumns: "1fr",
height: "100vh",
overflow: "hidden",
width: "100vw"
},
sidebar: {
[theme.breakpoints.up("lg")]: {
display: "block"
},
display: "none"
},
sidebarArt: {
"& svg": {
width: "100%"
}
}
}),
{
name: "Layout"
}
);
const Layout: React.FC = props => {
const { children } = props;
const classes = useStyles(props);
const { isDark } = useTheme();
return (
<div className={classes.root}>
<div className={classes.sidebar}>
<SVG className={classes.sidebarArt} src={backgroundArt} />
</div>
<div className={classes.mainPanel}>
<div className={classes.mainPanelContent}>
<SVG
className={classes.logo}
src={isDark ? saleorDarkLogo : saleorLightLogo}
/>
{children}
</div>
</div>
</div>
);
};
Layout.displayName = "Layout";
export default Layout;

View file

@ -8,16 +8,11 @@ import {
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import React from "react";
import SVG from "react-inlinesvg";
import { FormattedMessage, useIntl } from "react-intl";
import backgroundArt from "@assets/images/login-background.svg";
import saleorDarkLogo from "@assets/images/logo-dark.svg";
import saleorLightLogo from "@assets/images/logo-light.svg";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form";
import { FormSpacer } from "@saleor/components/FormSpacer";
import useTheme from "@saleor/hooks/useTheme";
import { commonMessages } from "@saleor/intl";
export interface FormData {
@ -41,38 +36,6 @@ const styles = (theme: Theme) =>
loginButton: {
width: 140
},
logo: {
"& svg": {
display: "block",
height: 40,
marginBottom: theme.spacing.unit * 4
}
},
mainPanel: {
[theme.breakpoints.down("sm")]: {
padding: theme.spacing.unit * 2
},
background: theme.palette.background.paper,
display: "flex",
flexDirection: "column",
height: "100vh",
justifyContent: "center",
padding: theme.spacing.unit * 6,
width: "100%"
},
mainPanelContent: {
[theme.breakpoints.up("xs")]: {
width: "100%"
},
[theme.breakpoints.up("sm")]: {
width: 328
},
"@media (min-width: 1440px)": {
width: 464
},
margin: "auto",
width: "100%"
},
panel: {
"& span": {
color: theme.palette.error.contrastText
@ -81,30 +44,6 @@ const styles = (theme: Theme) =>
borderRadius: theme.spacing.unit,
marginBottom: theme.spacing.unit * 3,
padding: theme.spacing.unit * 1.5
},
root: {
[theme.breakpoints.up("lg")]: {
gridTemplateColumns: "376px 1fr"
},
"@media (min-width: 1440px)": {
gridTemplateColumns: "520px 1fr"
},
display: "grid",
gridTemplateColumns: "1fr",
height: "100vh",
overflow: "hidden",
width: "100vw"
},
sidebar: {
[theme.breakpoints.up("lg")]: {
display: "block"
},
display: "none"
},
sidebarArt: {
"& svg": {
width: "100%"
}
}
});
@ -116,8 +55,13 @@ export interface LoginCardProps extends WithStyles<typeof styles> {
}
const LoginCard = withStyles(styles, { name: "LoginCard" })(
({ classes, error, disableLoginButton, onSubmit }: LoginCardProps) => {
const { isDark } = useTheme();
({
classes,
error,
disableLoginButton,
onPasswordRecovery,
onSubmit
}: LoginCardProps) => {
const intl = useIntl();
return (
@ -126,82 +70,71 @@ const LoginCard = withStyles(styles, { name: "LoginCard" })(
onSubmit={onSubmit}
>
{({ change: handleChange, data, submit: handleSubmit }) => (
<div className={classes.root}>
<div className={classes.sidebar}>
<SVG className={classes.sidebarArt} src={backgroundArt} />
</div>
<div className={classes.mainPanel}>
<div className={classes.mainPanelContent}>
<SVG
className={classes.logo}
src={isDark ? saleorDarkLogo : saleorLightLogo}
/>
{error && (
<div className={classes.panel}>
<Typography variant="caption">
<FormattedMessage defaultMessage="Sorry, your username and/or password are incorrect. Please try again." />
</Typography>
</div>
)}
<TextField
autoFocus
fullWidth
autoComplete="username"
label={intl.formatMessage(commonMessages.email)}
name="email"
onChange={handleChange}
value={data.email}
inputProps={{
"data-tc": "email"
}}
/>
<FormSpacer />
<TextField
fullWidth
autoComplete="current-password"
label={intl.formatMessage({
defaultMessage: "Password"
})}
name="password"
onChange={handleChange}
type="password"
value={data.password}
data-tc="password"
/>
<FormSpacer />
<div className={classes.buttonContainer}>
<ControlledCheckbox
checked={data.rememberMe}
label={intl.formatMessage({
defaultMessage: "Remember me",
description: "login"
})}
name="rememberMe"
onChange={handleChange}
/>
<FormSpacer />
<Button
className={classes.loginButton}
color="primary"
disabled={disableLoginButton}
variant="contained"
onClick={handleSubmit}
type="submit"
data-tc="submit"
>
<FormattedMessage
defaultMessage="Login"
description="button"
/>
</Button>
</div>
{/* <FormSpacer />
<Typography className={classes.link}>
{i18n.t("Reset your password")}
</Typography> */}
<>
{error && (
<div className={classes.panel}>
<Typography variant="caption">
<FormattedMessage defaultMessage="Sorry, your username and/or password are incorrect. Please try again." />
</Typography>
</div>
)}
<TextField
autoFocus
fullWidth
autoComplete="username"
label={intl.formatMessage(commonMessages.email)}
name="email"
onChange={handleChange}
value={data.email}
inputProps={{
"data-tc": "email"
}}
/>
<FormSpacer />
<TextField
fullWidth
autoComplete="current-password"
label={intl.formatMessage({
defaultMessage: "Password"
})}
name="password"
onChange={handleChange}
type="password"
value={data.password}
data-tc="password"
/>
<FormSpacer />
<div className={classes.buttonContainer}>
<ControlledCheckbox
checked={data.rememberMe}
label={intl.formatMessage({
defaultMessage: "Remember me",
description: "login"
})}
name="rememberMe"
onChange={handleChange}
/>
<FormSpacer />
<Button
className={classes.loginButton}
color="primary"
disabled={disableLoginButton}
variant="contained"
onClick={handleSubmit}
type="submit"
data-tc="submit"
>
<FormattedMessage defaultMessage="Login" description="button" />
</Button>
</div>
</div>
<FormSpacer />
<Typography className={classes.link} onClick={onPasswordRecovery}>
<FormattedMessage
defaultMessage="Reset your password"
description="button"
/>
</Typography>
</>
)}
</Form>
);

View file

@ -0,0 +1,16 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import CardDecorator from "@saleor/storybook//CardDecorator";
import Decorator from "@saleor/storybook//Decorator";
import NewPasswordPage from "./NewPasswordPage";
storiesOf("Views / Authentication / Set up a new password", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => (
<NewPasswordPage disabled={false} onSubmit={() => undefined} />
))
.add("loading", () => (
<NewPasswordPage disabled={true} onSubmit={() => undefined} />
));

View file

@ -0,0 +1,115 @@
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/styles";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer";
const useStyles = makeStyles(
{
submit: {
width: "100%"
}
},
{
name: "NewPasswordPage"
}
);
export interface NewPasswordPageFormData {
password: string;
confirmPassword: string;
}
export interface NewPasswordPageProps {
disabled: boolean;
onSubmit: (data: NewPasswordPageFormData) => void;
}
const initialForm: NewPasswordPageFormData = {
confirmPassword: "",
password: ""
};
const NewPasswordPage: React.FC<NewPasswordPageProps> = props => {
const { disabled, onSubmit } = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Form initial={initialForm} onSubmit={onSubmit}>
{({ change: handleChange, data, submit: handleSubmit }) => {
const passwordError =
data.password !== data.confirmPassword && data.password.length > 0;
return (
<>
<Typography>
<FormattedMessage defaultMessage="Please set up a new password." />
</Typography>
<FormSpacer />
<TextField
autoFocus
fullWidth
autoComplete="none"
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "New Password"
})}
name="password"
onChange={handleChange}
type="password"
value={data.password}
inputProps={{
"data-tc": "password"
}}
/>
<FormSpacer />
<TextField
fullWidth
error={passwordError}
autoComplete="none"
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Confirm Password"
})}
name="confirmPassword"
onChange={handleChange}
type="password"
value={data.confirmPassword}
helperText={
passwordError &&
intl.formatMessage({
defaultMessage: "Passwords do not match"
})
}
inputProps={{
"data-tc": "confirm-password"
}}
/>
<FormSpacer />
<Button
className={classes.submit}
color="primary"
disabled={(passwordError && data.password.length > 0) || disabled}
variant="contained"
onClick={handleSubmit}
type="submit"
>
<FormattedMessage
defaultMessage="Set new password"
description="button"
/>
</Button>
</>
);
}}
</Form>
);
};
NewPasswordPage.displayName = "NewPasswordPage";
export default NewPasswordPage;

View file

@ -0,0 +1,2 @@
export { default } from "./NewPasswordPage";
export * from "./NewPasswordPage";

View file

@ -0,0 +1,21 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { formError } from "@saleor/storybook/misc";
import ResetPasswordPage, { ResetPasswordPageProps } from "./ResetPasswordPage";
const props: ResetPasswordPageProps = {
disabled: false,
error: undefined,
onSubmit: () => undefined
};
storiesOf("Views / Authentication / Reset password", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => <ResetPasswordPage {...props} />)
.add("loading", () => <ResetPasswordPage {...props} disabled={true} />)
.add("error", () => (
<ResetPasswordPage {...props} error={formError("").message} />
));

View file

@ -0,0 +1,95 @@
import Button from "@material-ui/core/Button";
import { Theme } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/styles";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer";
import { commonMessages } from "@saleor/intl";
const useStyles = makeStyles(
(theme: Theme) => ({
panel: {
"& span": {
color: theme.palette.error.contrastText
},
background: theme.palette.error.main,
borderRadius: theme.spacing.unit,
marginBottom: theme.spacing.unit * 3,
padding: theme.spacing.unit * 1.5
},
submit: {
width: "100%"
}
}),
{
name: "ResetPasswordPage"
}
);
export interface ResetPasswordPageFormData {
email: string;
}
export interface ResetPasswordPageProps {
disabled: boolean;
error: string;
onSubmit: (data: ResetPasswordPageFormData) => void;
}
const ResetPasswordPage: React.FC<ResetPasswordPageProps> = props => {
const { disabled, error, onSubmit } = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Form initial={{ email: "" }} onSubmit={onSubmit}>
{({ change: handleChange, data, submit: handleSubmit }) => (
<>
{!!error && (
<div className={classes.panel}>
<Typography variant="caption">{error}</Typography>
</div>
)}
<Typography>
<FormattedMessage defaultMessage="Forgot your password? Don't worry, we'll reset it for you." />
</Typography>
<FormSpacer />
<TextField
autoFocus
disabled={disabled}
fullWidth
autoComplete="username"
label={intl.formatMessage(commonMessages.email)}
name="email"
onChange={handleChange}
value={data.email}
inputProps={{
"data-tc": "email"
}}
/>
<FormSpacer />
<Button
className={classes.submit}
color="primary"
disabled={disabled}
variant="contained"
onClick={handleSubmit}
type="submit"
>
<FormattedMessage
defaultMessage="Send Instructions"
description="password reset, button"
/>
</Button>
</>
)}
</Form>
);
};
ResetPasswordPage.displayName = "ResetPasswordPage";
export default ResetPasswordPage;

View file

@ -0,0 +1,2 @@
export { default } from "./ResetPasswordPage";
export * from "./ResetPasswordPage";

View file

@ -0,0 +1,11 @@
import { storiesOf } from "@storybook/react";
import React from "react";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import ResetPasswordSuccessPage from "./ResetPasswordSuccessPage";
storiesOf("Views / Authentication / Reset password success", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => <ResetPasswordSuccessPage onBack={() => undefined} />);

View file

@ -0,0 +1,54 @@
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/styles";
import React from "react";
import { FormattedMessage } from "react-intl";
import FormSpacer from "@saleor/components/FormSpacer";
const useStyles = makeStyles(
{
submit: {
width: "100%"
}
},
{
name: "ResetPasswordSuccessPage"
}
);
export interface ResetPasswordSuccessPageFormData {
email: string;
}
export interface ResetPasswordSuccessPageProps {
onBack: () => void;
}
const ResetPasswordSuccessPage: React.FC<
ResetPasswordSuccessPageProps
> = props => {
const { onBack } = props;
const classes = useStyles(props);
return (
<>
<Typography>
<FormattedMessage defaultMessage="Success! In a few minutes youll receive a message with instructions on how to reset your password." />
</Typography>
<FormSpacer />
<Button
className={classes.submit}
color="primary"
variant="contained"
onClick={onBack}
type="submit"
>
<FormattedMessage defaultMessage="Back to login" description="button" />
</Button>
</>
);
};
ResetPasswordSuccessPage.displayName = "ResetPasswordSuccessPage";
export default ResetPasswordSuccessPage;

View file

@ -0,0 +1,2 @@
export { default } from "./ResetPasswordSuccessPage";
export * from "./ResetPasswordSuccessPage";

View file

@ -1,30 +1,47 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import Layout from "./components/Layout";
import { User } from "./types/User";
const TOKEN_STORAGE_KEY = "dashboardAuth";
import {
newPasswordPath,
passwordResetPath,
passwordResetSuccessPath
} from "./urls";
import LoginView from "./views/Login";
import NewPassword from "./views/NewPassword";
import ResetPassword from "./views/ResetPassword";
import ResetPasswordSuccess from "./views/ResetPasswordSuccess";
interface UserContext {
login: (username: string, password: string, persist: boolean) => void;
loginByToken: (token: string, user: User) => void;
logout: () => void;
tokenAuthLoading: boolean;
tokenVerifyLoading: boolean;
user?: User;
}
export const UserContext = React.createContext<UserContext>({
login: undefined,
logout: undefined
loginByToken: undefined,
logout: undefined,
tokenAuthLoading: false,
tokenVerifyLoading: false
});
export const getAuthToken = () =>
localStorage.getItem(TOKEN_STORAGE_KEY) ||
sessionStorage.getItem(TOKEN_STORAGE_KEY);
const AuthRouter: React.FC = () => (
<Layout>
<Switch>
<Route path={passwordResetSuccessPath} component={ResetPasswordSuccess} />
<Route path={passwordResetPath} component={ResetPassword} />
<Route path={newPasswordPath} component={NewPassword} />
<Route component={LoginView} />
</Switch>
</Layout>
);
export const setAuthToken = (token: string, persist: boolean) =>
persist
? localStorage.setItem(TOKEN_STORAGE_KEY, token)
: sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
AuthRouter.displayName = "AuthRouter";
export default AuthRouter;
export const removeAuthToken = () => {
localStorage.removeItem(TOKEN_STORAGE_KEY);
sessionStorage.removeItem(TOKEN_STORAGE_KEY);
};
export * from "./utils";

View file

@ -1,6 +1,11 @@
import gql from "graphql-tag";
import { TypedMutation } from "../mutations";
import {
RequestPasswordReset,
RequestPasswordResetVariables
} from "./types/RequestPasswordReset";
import { SetPassword, SetPasswordVariables } from "./types/SetPassword";
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
@ -59,3 +64,38 @@ export const TypedVerifyTokenMutation = TypedMutation<
VerifyToken,
VerifyTokenVariables
>(tokenVerifyMutation);
export const requestPasswordReset = gql`
mutation RequestPasswordReset($email: String!, $redirectUrl: String!) {
requestPasswordReset(email: $email, redirectUrl: $redirectUrl) {
errors {
field
message
}
}
}
`;
export const RequestPasswordResetMutation = TypedMutation<
RequestPasswordReset,
RequestPasswordResetVariables
>(requestPasswordReset);
export const setPassword = gql`
${fragmentUser}
mutation SetPassword($email: String!, $password: String!, $token: String!) {
setPassword(email: $email, password: $password, token: $token) {
token
errors {
field
message
}
user {
...User
}
}
}
`;
export const SetPasswordMutation = TypedMutation<
SetPassword,
SetPasswordVariables
>(setPassword);

View file

@ -0,0 +1,27 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: RequestPasswordReset
// ====================================================
export interface RequestPasswordReset_requestPasswordReset_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface RequestPasswordReset_requestPasswordReset {
__typename: "RequestPasswordReset";
errors: RequestPasswordReset_requestPasswordReset_errors[] | null;
}
export interface RequestPasswordReset {
requestPasswordReset: RequestPasswordReset_requestPasswordReset | null;
}
export interface RequestPasswordResetVariables {
email: string;
redirectUrl: string;
}

View file

@ -0,0 +1,55 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { PermissionEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: SetPassword
// ====================================================
export interface SetPassword_setPassword_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface SetPassword_setPassword_user_permissions {
__typename: "PermissionDisplay";
code: PermissionEnum;
name: string;
}
export interface SetPassword_setPassword_user_avatar {
__typename: "Image";
url: string;
}
export interface SetPassword_setPassword_user {
__typename: "User";
id: string;
email: string;
firstName: string;
lastName: string;
isStaff: boolean;
note: string | null;
permissions: (SetPassword_setPassword_user_permissions | null)[] | null;
avatar: SetPassword_setPassword_user_avatar | null;
}
export interface SetPassword_setPassword {
__typename: "SetPassword";
token: string | null;
errors: (SetPassword_setPassword_errors | null)[];
user: SetPassword_setPassword_user | null;
}
export interface SetPassword {
setPassword: SetPassword_setPassword | null;
}
export interface SetPasswordVariables {
email: string;
password: string;
token: string;
}

15
src/auth/urls.ts Normal file
View file

@ -0,0 +1,15 @@
import { stringify as stringifyQs } from "qs";
export const passwordResetPath = "/reset-password/";
export const passwordResetUrl = passwordResetPath;
export const passwordResetSuccessPath = "/reset-password/success/";
export const passwordResetSuccessUrl = passwordResetSuccessPath;
export const newPasswordPath = "/new-password/";
export interface NewPasswordUrlQueryParams {
email: string;
token: string;
}
export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) =>
newPasswordPath + "?" + stringifyQs(params);

15
src/auth/utils.ts Normal file
View file

@ -0,0 +1,15 @@
const TOKEN_STORAGE_KEY = "dashboardAuth";
export const getAuthToken = () =>
localStorage.getItem(TOKEN_STORAGE_KEY) ||
sessionStorage.getItem(TOKEN_STORAGE_KEY);
export const setAuthToken = (token: string, persist: boolean) =>
persist
? localStorage.setItem(TOKEN_STORAGE_KEY, token)
: sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
export const removeAuthToken = () => {
localStorage.removeItem(TOKEN_STORAGE_KEY);
sessionStorage.removeItem(TOKEN_STORAGE_KEY);
};

View file

@ -1,27 +1,25 @@
import React from "react";
import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser";
import LoginPage, { FormData } from "../components/LoginPage";
import { UserContext } from "../index";
import { passwordResetUrl } from "../urls";
interface LoginViewProps {
loading: boolean;
}
const LoginView: React.FC = () => {
const navigate = useNavigator();
const { login, user, tokenAuthLoading } = useUser();
const LoginView: React.StatelessComponent<LoginViewProps> = ({ loading }) => (
<UserContext.Consumer>
{({ login, user }) => {
const handleSubmit = (data: FormData) =>
login(data.email, data.password, data.rememberMe);
return (
<LoginPage
error={user === null}
disableLoginButton={loading}
onPasswordRecovery={undefined}
onSubmit={handleSubmit}
/>
);
}}
</UserContext.Consumer>
);
const handleSubmit = (data: FormData) =>
login(data.email, data.password, data.rememberMe);
return (
<LoginPage
error={user === null}
disableLoginButton={tokenAuthLoading}
onPasswordRecovery={() => navigate(passwordResetUrl)}
onSubmit={handleSubmit}
/>
);
};
LoginView.displayName = "LoginView";
export default LoginView;

View file

@ -0,0 +1,51 @@
import { parse as parseQs } from "qs";
import React from "react";
import { RouteComponentProps } from "react-router";
import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser";
import NewPasswordPage, {
NewPasswordPageFormData
} from "../components/NewPasswordPage";
import { SetPasswordMutation } from "../mutations";
import { SetPassword } from "../types/SetPassword";
import { NewPasswordUrlQueryParams } from "../urls";
const NewPassword: React.FC<RouteComponentProps> = ({ location }) => {
const navigate = useNavigator();
const { loginByToken } = useUser();
const params: NewPasswordUrlQueryParams = parseQs(location.search.substr(1));
const handleSetPassword = async (data: SetPassword) => {
if (data.setPassword.errors.length === 0) {
loginByToken(data.setPassword.token, data.setPassword.user);
navigate("/", true);
}
};
return (
<SetPasswordMutation onCompleted={handleSetPassword}>
{(setPassword, setPasswordOpts) => {
const handleSubmit = (data: NewPasswordPageFormData) =>
setPassword({
variables: {
email: params.email,
password: data.password,
token: params.token
}
});
return (
<NewPasswordPage
disabled={setPasswordOpts.loading}
onSubmit={handleSubmit}
/>
);
}}
</SetPasswordMutation>
);
};
NewPassword.displayName = "NewPassword";
export default NewPassword;

View file

@ -0,0 +1,64 @@
import React from "react";
import { useIntl } from "react-intl";
import urlJoin from "url-join";
import { APP_MOUNT_URI } from "@saleor/config";
import useNavigator from "@saleor/hooks/useNavigator";
import { commonMessages } from "@saleor/intl";
import ResetPasswordPage, {
ResetPasswordPageFormData
} from "../components/ResetPasswordPage";
import { RequestPasswordResetMutation } from "../mutations";
import { RequestPasswordReset } from "../types/RequestPasswordReset";
import { newPasswordUrl, passwordResetSuccessUrl } from "../urls";
const ResetPasswordView: React.FC = () => {
const [error, setError] = React.useState<string>();
const navigate = useNavigator();
const intl = useIntl();
const handleRequestPasswordReset = (data: RequestPasswordReset) => {
if (data.requestPasswordReset.errors.length === 0) {
navigate(passwordResetSuccessUrl);
} else {
if (data.requestPasswordReset.errors.find(err => err.field === "email")) {
setError(
intl.formatMessage({
defaultMessage:
"Provided email address does not exist in our database."
})
);
} else {
setError(intl.formatMessage(commonMessages.somethingWentWrong));
}
}
};
return (
<RequestPasswordResetMutation onCompleted={handleRequestPasswordReset}>
{(requestPasswordReset, requestPasswordResetOpts) => {
const handleSubmit = (data: ResetPasswordPageFormData) =>
requestPasswordReset({
variables: {
email: data.email,
redirectUrl: urlJoin(
window.location.origin,
APP_MOUNT_URI === "/" ? "" : APP_MOUNT_URI,
newPasswordUrl().replace(/\?/, "")
)
}
});
return (
<ResetPasswordPage
disabled={requestPasswordResetOpts.loading}
error={error}
onSubmit={handleSubmit}
/>
);
}}
</RequestPasswordResetMutation>
);
};
ResetPasswordView.displayName = "ResetPasswordView";
export default ResetPasswordView;

View file

@ -0,0 +1,12 @@
import React from "react";
import useNavigator from "@saleor/hooks/useNavigator";
import ResetPasswordSuccessPage from "../components/ResetPasswordSuccessPage";
const ResetPasswordSuccessView: React.FC = () => {
const navigate = useNavigator();
return <ResetPasswordSuccessPage onBack={() => navigate("/")} />;
};
ResetPasswordSuccessView.displayName = "ResetPasswordSuccessView";
export default ResetPasswordSuccessView;

View file

@ -13,12 +13,11 @@ import { BrowserRouter, Route, Switch } from "react-router-dom";
import AttributeSection from "./attributes";
import { attributeSection } from "./attributes/urls";
import { getAuthToken, removeAuthToken } from "./auth";
import Auth, { getAuthToken, removeAuthToken } from "./auth";
import AuthProvider from "./auth/AuthProvider";
import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
import SectionRoute from "./auth/components/SectionRoute";
import { hasPermission } from "./auth/misc";
import Login from "./auth/views/Login";
import CategorySection from "./categories";
import CollectionSection from "./collections";
import { AppProgressProvider } from "./components/AppProgress";
@ -64,6 +63,7 @@ const invalidTokenLink = onError((error: ResponseError) => {
const authLink = setContext((_, context) => {
const authToken = getAuthToken();
return {
...context,
headers: {
@ -241,7 +241,7 @@ const Routes: React.FC = () => {
) : hasToken && tokenVerifyLoading ? (
<LoginLoading />
) : (
<Login loading={tokenAuthLoading} />
<Auth />
)
}
</AuthProvider>

View file

@ -50,6 +50,9 @@ export const commonMessages = defineMessages({
savedChanges: {
defaultMessage: "Saved changes"
},
somethingWentWrong: {
defaultMessage: "Saleor ran into an unexpected problem"
},
startDate: {
defaultMessage: "Start Date"
},

View file

@ -5,6 +5,7 @@ import { Mutation, MutationFunction, MutationResult } from "react-apollo";
import { useIntl } from "react-intl";
import useNotifier from "./hooks/useNotifier";
import { commonMessages } from "./intl";
export interface TypedMutationInnerProps<TData, TVariables> {
children: (
@ -30,17 +31,10 @@ export function TypedMutation<TData, TVariables>(
<Mutation
mutation={mutation}
onCompleted={onCompleted}
onError={err => {
const msg = intl.formatMessage(
{
defaultMessage: "Something went wrong. {errorMessage}",
description: "error message"
},
{
errorMessage: err.message
}
);
notify({ text: msg });
onError={(err: ApolloError) => {
notify({
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
if (onError) {
onError(err);
}

View file

@ -9,6 +9,7 @@ import AppProgress from "./components/AppProgress";
import ErrorPage from "./components/ErrorPage/ErrorPage";
import useNavigator from "./hooks/useNavigator";
import useNotifier from "./hooks/useNotifier";
import { commonMessages } from "./intl";
import { RequireAtLeastOne } from "./misc";
export interface LoadMore<TData, TVariables> {
@ -83,16 +84,9 @@ export function TypedQuery<TData, TVariables>(
>
{queryData => {
if (queryData.error) {
const msg = intl.formatMessage(
{
defaultMessage: "Something went wrong. {errorMessage}",
description: "error message"
},
{
message: queryData.error.message
}
);
pushMessage({ text: msg });
pushMessage({
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
const loadMore = (

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,13 @@
/* eslint-disable */
const { configure } = require("@storybook/react");
import requireContext from "require-context.macro";
import { configure } from "@storybook/react";
const req = requireContext("../", true, /.stories.tsx$/);
function loadStories() {
// Story autodiscovery
req.keys().forEach(filename => req(filename));
// Components
require("./stories/components/ActionDialog");
require("./stories/components/AddressEdit");

View file

@ -3,6 +3,7 @@ import { storiesOf } from "@storybook/react";
import React from "react";
import LoginPage, { LoginCardProps } from "../../../auth/components/LoginPage";
import CardDecorator from "../../CardDecorator";
import Decorator from "../../Decorator";
const props: Omit<LoginCardProps, "classes"> = {
@ -13,6 +14,7 @@ const props: Omit<LoginCardProps, "classes"> = {
};
storiesOf("Views / Authentication / Log in", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => <LoginPage {...props} />)
.add("error", () => <LoginPage {...props} error={true} />)