diff --git a/CHANGELOG.md b/CHANGELOG.md index b69a65183..94f57dbf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/babel.config.js b/babel.config.js index 723c15517..0db43fbd4 100644 --- a/babel.config.js +++ b/babel.config.js @@ -37,7 +37,8 @@ module.exports = api => { extractFromFormatMessageCall: true, messagesDir: "build/locale/" } - ] + ], + "macros" ]; return { diff --git a/locale/messages.pot b/locale/messages.pot index d7bc3641c..375f88ea9 100644 --- a/locale/messages.pot +++ b/locale/messages.pot @@ -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 you’ll receive a message with instructions on how to reset your password. +msgctxt "description" +msgid "Success! In a few minutes you’ll 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: diff --git a/package-lock.json b/package-lock.json index cdfa829e5..eedf6a666 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6c8830060..d18a61efb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index 147dec6b0..b09edcf96 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -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) => ( {(...tokenVerify) => ( - + {children} )} @@ -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; - tokenVerify: PartialMutationProviderOutput; + children: (props: { + hasToken: boolean; + isAuthenticated: boolean; + tokenAuthLoading: boolean; + tokenVerifyLoading: boolean; + user: User; + }) => React.ReactNode; + tokenAuth: [ + MutationFunction, + MutationResult + ]; + tokenVerify: [ + MutationFunction, + MutationResult + ]; } 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 ( {children({ hasToken: !!getAuthToken(), isAuthenticated, - tokenAuthLoading: tokenAuth.opts.loading, - tokenVerifyLoading: tokenVerify.opts.loading, + tokenAuthLoading: tokenAuthOpts.loading, + tokenVerifyLoading: tokenVerifyOpts.loading, user })} diff --git a/src/auth/components/Layout.tsx b/src/auth/components/Layout.tsx new file mode 100644 index 000000000..3d217293b --- /dev/null +++ b/src/auth/components/Layout.tsx @@ -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 ( +
+
+ +
+
+
+ + {children} +
+
+
+ ); +}; + +Layout.displayName = "Layout"; +export default Layout; diff --git a/src/auth/components/LoginPage/LoginPage.tsx b/src/auth/components/LoginPage/LoginPage.tsx index c361257d1..9f3345e54 100644 --- a/src/auth/components/LoginPage/LoginPage.tsx +++ b/src/auth/components/LoginPage/LoginPage.tsx @@ -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 { } 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 }) => ( -
-
- -
-
-
- - {error && ( -
- - - -
- )} - - - - -
- - - -
- {/* - - {i18n.t("Reset your password")} - */} + <> + {error && ( +
+ + +
+ )} + + + + +
+ + +
-
+ + + + + )} ); diff --git a/src/auth/components/NewPasswordPage/NewPasswordPage.stories.tsx b/src/auth/components/NewPasswordPage/NewPasswordPage.stories.tsx new file mode 100644 index 000000000..4f6359937 --- /dev/null +++ b/src/auth/components/NewPasswordPage/NewPasswordPage.stories.tsx @@ -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", () => ( + undefined} /> + )) + .add("loading", () => ( + undefined} /> + )); diff --git a/src/auth/components/NewPasswordPage/NewPasswordPage.tsx b/src/auth/components/NewPasswordPage/NewPasswordPage.tsx new file mode 100644 index 000000000..48aa0ada7 --- /dev/null +++ b/src/auth/components/NewPasswordPage/NewPasswordPage.tsx @@ -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 = props => { + const { disabled, onSubmit } = props; + + const classes = useStyles(props); + const intl = useIntl(); + + return ( +
+ {({ change: handleChange, data, submit: handleSubmit }) => { + const passwordError = + data.password !== data.confirmPassword && data.password.length > 0; + + return ( + <> + + + + + + + + + + + ); + }} + + ); +}; + +NewPasswordPage.displayName = "NewPasswordPage"; +export default NewPasswordPage; diff --git a/src/auth/components/NewPasswordPage/index.ts b/src/auth/components/NewPasswordPage/index.ts new file mode 100644 index 000000000..6f9c96fb5 --- /dev/null +++ b/src/auth/components/NewPasswordPage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./NewPasswordPage"; +export * from "./NewPasswordPage"; diff --git a/src/auth/components/ResetPasswordPage/ResetPasswordPage.stories.tsx b/src/auth/components/ResetPasswordPage/ResetPasswordPage.stories.tsx new file mode 100644 index 000000000..4c4a495a8 --- /dev/null +++ b/src/auth/components/ResetPasswordPage/ResetPasswordPage.stories.tsx @@ -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", () => ) + .add("loading", () => ) + .add("error", () => ( + + )); diff --git a/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx b/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx new file mode 100644 index 000000000..38e18d00b --- /dev/null +++ b/src/auth/components/ResetPasswordPage/ResetPasswordPage.tsx @@ -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 = props => { + const { disabled, error, onSubmit } = props; + + const classes = useStyles(props); + const intl = useIntl(); + + return ( +
+ {({ change: handleChange, data, submit: handleSubmit }) => ( + <> + {!!error && ( +
+ {error} +
+ )} + + + + + + + + + )} + + ); +}; + +ResetPasswordPage.displayName = "ResetPasswordPage"; +export default ResetPasswordPage; diff --git a/src/auth/components/ResetPasswordPage/index.ts b/src/auth/components/ResetPasswordPage/index.ts new file mode 100644 index 000000000..6abfb8cd4 --- /dev/null +++ b/src/auth/components/ResetPasswordPage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./ResetPasswordPage"; +export * from "./ResetPasswordPage"; diff --git a/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.stories.tsx b/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.stories.tsx new file mode 100644 index 000000000..3e263e929 --- /dev/null +++ b/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.stories.tsx @@ -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", () => undefined} />); diff --git a/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.tsx b/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.tsx new file mode 100644 index 000000000..744737373 --- /dev/null +++ b/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.tsx @@ -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 ( + <> + + + + + + + ); +}; + +ResetPasswordSuccessPage.displayName = "ResetPasswordSuccessPage"; +export default ResetPasswordSuccessPage; diff --git a/src/auth/components/ResetPasswordSuccessPage/index.ts b/src/auth/components/ResetPasswordSuccessPage/index.ts new file mode 100644 index 000000000..0943b57cc --- /dev/null +++ b/src/auth/components/ResetPasswordSuccessPage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./ResetPasswordSuccessPage"; +export * from "./ResetPasswordSuccessPage"; diff --git a/src/auth/index.tsx b/src/auth/index.tsx index 5af2ad33e..74fb61b65 100644 --- a/src/auth/index.tsx +++ b/src/auth/index.tsx @@ -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({ 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 = () => ( + + + + + + + + +); -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"; diff --git a/src/auth/mutations.ts b/src/auth/mutations.ts index 6566fee3f..6791e4b0d 100644 --- a/src/auth/mutations.ts +++ b/src/auth/mutations.ts @@ -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); diff --git a/src/auth/types/RequestPasswordReset.ts b/src/auth/types/RequestPasswordReset.ts new file mode 100644 index 000000000..6508afa3a --- /dev/null +++ b/src/auth/types/RequestPasswordReset.ts @@ -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; +} diff --git a/src/auth/types/SetPassword.ts b/src/auth/types/SetPassword.ts new file mode 100644 index 000000000..708560696 --- /dev/null +++ b/src/auth/types/SetPassword.ts @@ -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; +} diff --git a/src/auth/urls.ts b/src/auth/urls.ts new file mode 100644 index 000000000..c0ac7c68b --- /dev/null +++ b/src/auth/urls.ts @@ -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); diff --git a/src/auth/utils.ts b/src/auth/utils.ts new file mode 100644 index 000000000..f2ed57e85 --- /dev/null +++ b/src/auth/utils.ts @@ -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); +}; diff --git a/src/auth/views/Login.tsx b/src/auth/views/Login.tsx index 5c61f1269..28d8a7868 100644 --- a/src/auth/views/Login.tsx +++ b/src/auth/views/Login.tsx @@ -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 = ({ loading }) => ( - - {({ login, user }) => { - const handleSubmit = (data: FormData) => - login(data.email, data.password, data.rememberMe); - return ( - - ); - }} - -); + const handleSubmit = (data: FormData) => + login(data.email, data.password, data.rememberMe); + + return ( + navigate(passwordResetUrl)} + onSubmit={handleSubmit} + /> + ); +}; LoginView.displayName = "LoginView"; export default LoginView; diff --git a/src/auth/views/NewPassword.tsx b/src/auth/views/NewPassword.tsx new file mode 100644 index 000000000..c667f92d0 --- /dev/null +++ b/src/auth/views/NewPassword.tsx @@ -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 = ({ 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 ( + + {(setPassword, setPasswordOpts) => { + const handleSubmit = (data: NewPasswordPageFormData) => + setPassword({ + variables: { + email: params.email, + password: data.password, + token: params.token + } + }); + + return ( + + ); + }} + + ); +}; + +NewPassword.displayName = "NewPassword"; +export default NewPassword; diff --git a/src/auth/views/ResetPassword.tsx b/src/auth/views/ResetPassword.tsx new file mode 100644 index 000000000..69b130f92 --- /dev/null +++ b/src/auth/views/ResetPassword.tsx @@ -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(); + 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 ( + + {(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 ( + + ); + }} + + ); +}; +ResetPasswordView.displayName = "ResetPasswordView"; +export default ResetPasswordView; diff --git a/src/auth/views/ResetPasswordSuccess.tsx b/src/auth/views/ResetPasswordSuccess.tsx new file mode 100644 index 000000000..650a22cd4 --- /dev/null +++ b/src/auth/views/ResetPasswordSuccess.tsx @@ -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 navigate("/")} />; +}; +ResetPasswordSuccessView.displayName = "ResetPasswordSuccessView"; +export default ResetPasswordSuccessView; diff --git a/src/index.tsx b/src/index.tsx index 02ec5af46..bf8e504e8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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 ? ( ) : ( - + ) } diff --git a/src/intl.ts b/src/intl.ts index 0934376b4..254e4ac44 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -50,6 +50,9 @@ export const commonMessages = defineMessages({ savedChanges: { defaultMessage: "Saved changes" }, + somethingWentWrong: { + defaultMessage: "Saleor ran into an unexpected problem" + }, startDate: { defaultMessage: "Start Date" }, diff --git a/src/mutations.tsx b/src/mutations.tsx index f6611b837..5fcda0308 100644 --- a/src/mutations.tsx +++ b/src/mutations.tsx @@ -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 { children: ( @@ -30,17 +31,10 @@ export function TypedMutation( { - 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); } diff --git a/src/queries.tsx b/src/queries.tsx index bde4cdb04..e3bdda8cd 100644 --- a/src/queries.tsx +++ b/src/queries.tsx @@ -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 { @@ -83,16 +84,9 @@ export function TypedQuery( > {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 = ( diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index eda2d6d28..389c823cc 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -11910,152 +11910,147 @@ exports[`Storyshots Views / Authentication / Log in default 1`] = `
-
+
-
- -
-
+
- -
- -
- - -
-
+ E-mail Address +
-
- -
- - -
-
-
-
- -
+ ​ + + + + -
-
+
+
+ +
+ + +
+
+
+
+ +
+ +
+
+ +
- +
`; @@ -12063,161 +12058,156 @@ exports[`Storyshots Views / Authentication / Log in error 1`] = `
-
+
-
- -
-
+
-
- - Sorry, your username and/or password are incorrect. Please try again. - -
-
+
+
+
+ E-mail Address +
-
- -
- - -
-
-
-
- -
+ ​ + + + + -
-
+
+
+ +
+ + +
+
+
+
+ +
+ +
+
+ +
- +
`; @@ -12225,152 +12215,666 @@ exports[`Storyshots Views / Authentication / Log in loading 1`] = `
-
+
-
- -
-
+
- -
- -
- - -
-
+ E-mail Address +
-
- -
- - -
-
-
-
- -
+ ​ + + + + -
-
+
+
+ +
+ + +
+
+
+
+ +
+ +
+
+ +
- +
+
+`; + +exports[`Storyshots Views / Authentication / Reset password default 1`] = ` +
+
+
+
+

+ Forgot your password? Don't worry, we'll reset it for you. +

+
+
+ +
+ + +
+
+
+ + +
+
+
+`; + +exports[`Storyshots Views / Authentication / Reset password error 1`] = ` +
+
+
+
+
+ + Generic form error + +
+

+ Forgot your password? Don't worry, we'll reset it for you. +

+
+
+ +
+ + +
+
+
+ + +
+
+
+`; + +exports[`Storyshots Views / Authentication / Reset password loading 1`] = ` +
+
+
+
+

+ Forgot your password? Don't worry, we'll reset it for you. +

+
+
+ +
+ + +
+
+
+ + +
+
+
+`; + +exports[`Storyshots Views / Authentication / Reset password success default 1`] = ` +
+
+
+

+ Success! In a few minutes you’ll receive a message with instructions on how to reset your password. +

+
+ +
+
+
+`; + +exports[`Storyshots Views / Authentication / Set up a new password default 1`] = ` +
+
+
+
+

+ Please set up a new password. +

+
+
+ +
+ + +
+
+
+
+ +
+ + +
+
+
+ + +
+
+
+`; + +exports[`Storyshots Views / Authentication / Set up a new password loading 1`] = ` +
+
+
+
+

+ Please set up a new password. +

+
+
+ +
+ + +
+
+
+
+ +
+ + +
+
+
+ + +
+
`; diff --git a/src/storybook/config.js b/src/storybook/config.js index 78aff494a..89b3a57cf 100644 --- a/src/storybook/config.js +++ b/src/storybook/config.js @@ -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"); diff --git a/src/storybook/stories/auth/LoginPage.tsx b/src/storybook/stories/auth/LoginPage.tsx index 65185dc28..68a3d0add 100644 --- a/src/storybook/stories/auth/LoginPage.tsx +++ b/src/storybook/stories/auth/LoginPage.tsx @@ -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 = { @@ -13,6 +14,7 @@ const props: Omit = { }; storiesOf("Views / Authentication / Log in", module) + .addDecorator(CardDecorator) .addDecorator(Decorator) .add("default", () => ) .add("error", () => )