diff --git a/packages/peregrine/lib/context/user.js b/packages/peregrine/lib/context/user.js index cde6de9a61..84ef97c0e2 100644 --- a/packages/peregrine/lib/context/user.js +++ b/packages/peregrine/lib/context/user.js @@ -42,4 +42,37 @@ export default connect( mapDispatchToProps )(UserContextProvider); +/** + * @typedef {Object} UserState + * + * @property {CurrentUser} currentUser Current user details + * @property {Error} getDetailsError Get Details call related error + * @property {Boolean} isGettingDetails Boolean if true indicates that user details are being fetched. False otherwise. + * @property {Boolean} isResettingPassword Deprecated + * @property {Boolean} isSignedIn Boolean if true indicates that the user is signed in. False otherwise. + * @property {Error} resetPasswordError Deprecated + * + */ + +/** + * @typedef {Object} CurrentUser + * + * @property {String} email Current user's email + * @property {String} firstname Current user's first name + * @property {String} lastname Current user's last name + */ + +/** + * @typedef {Object} UserActions + * + * @property {Function} clearToken Callback to clear user token in browser persistence storage + * @property {Function} getUserDetails Callback to get user details + * @property {Function} resetPassword Deprecated + * @property {Function} setToken Callback to set user token in browser persistence storage + * @property {Function} signOut Callback to sign the user out + */ + +/** + * @returns {[UserState, UserActions]} + */ export const useUserContext = () => useContext(UserContext); diff --git a/packages/peregrine/lib/talons/AuthModal/useAuthModal.js b/packages/peregrine/lib/talons/AuthModal/useAuthModal.js index 03a8734379..e0654a3be2 100644 --- a/packages/peregrine/lib/talons/AuthModal/useAuthModal.js +++ b/packages/peregrine/lib/talons/AuthModal/useAuthModal.js @@ -17,6 +17,10 @@ const UNAUTHED_ONLY = ['CREATE_ACCOUNT', 'FORGOT_PASSWORD', 'SIGN_IN']; * @param {function} props.showForgotPassword - callback that shows forgot password view * @param {function} props.showMainMenu - callback that shows main menu view * @param {function} props.showMyAccount - callback that shows my account view + * @param {function} props.showSignIn - callback that shows signin view + * @param {DocumentNode} props.signOutMutation - mutation to call when signing out + * @param {string} props.view - string that represents the current view + * * @return {{ * handleClose: function, * handleCreateAccount: function, @@ -35,6 +39,7 @@ export const useAuthModal = props => { showForgotPassword, showMainMenu, showMyAccount, + showSignIn, signOutMutation, view } = props; @@ -67,6 +72,10 @@ export const useAuthModal = props => { closeDrawer(); }, [closeDrawer, showMainMenu]); + const handleCancel = useCallback(() => { + showSignIn(); + }, [showSignIn]); + const handleCreateAccount = useCallback(() => { showMyAccount(); }, [showMyAccount]); @@ -86,6 +95,7 @@ export const useAuthModal = props => { }, [apolloClient, history, revokeToken, signOut]); return { + handleCancel, handleClose, handleCreateAccount, handleSignOut, diff --git a/packages/peregrine/lib/talons/ForgotPassword/__tests__/__snapshots__/useForgotPassword.spec.js.snap b/packages/peregrine/lib/talons/ForgotPassword/__tests__/__snapshots__/useForgotPassword.spec.js.snap new file mode 100644 index 0000000000..54a952d56c --- /dev/null +++ b/packages/peregrine/lib/talons/ForgotPassword/__tests__/__snapshots__/useForgotPassword.spec.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render properly 1`] = ` +Object { + "forgotPasswordEmail": null, + "formErrors": Array [ + null, + ], + "handleCancel": [Function], + "handleFormSubmit": [Function], + "hasCompleted": false, + "isResettingPassword": false, +} +`; diff --git a/packages/peregrine/lib/talons/ForgotPassword/__tests__/useForgotPassword.spec.js b/packages/peregrine/lib/talons/ForgotPassword/__tests__/useForgotPassword.spec.js new file mode 100644 index 0000000000..27577a520e --- /dev/null +++ b/packages/peregrine/lib/talons/ForgotPassword/__tests__/useForgotPassword.spec.js @@ -0,0 +1,118 @@ +import React from 'react'; +import { useMutation } from '@apollo/react-hooks'; +import { act } from 'react-test-renderer'; + +import { createTestInstance } from '@magento/peregrine'; + +import { useForgotPassword } from '../useForgotPassword'; + +jest.mock('@apollo/react-hooks', () => ({ + useMutation: jest.fn().mockReturnValue([ + jest.fn().mockResolvedValue(true), + { + error: null, + loading: false + } + ]) +})); + +const Component = props => { + const talonProps = useForgotPassword(props); + + return ; +}; + +const getTalonProps = props => { + const tree = createTestInstance(); + const { root } = tree; + const { talonProps } = root.findByType('i').props; + + const update = newProps => { + act(() => { + tree.update(); + }); + + return root.findByType('i').props.talonProps; + }; + + return { talonProps, tree, update }; +}; + +test('should render properly', () => { + const { talonProps } = getTalonProps({ + mutations: { + requestPasswordResetEmailMutation: + 'requestPasswordResetEmailMutation' + }, + onCancel: jest.fn() + }); + + expect(talonProps).toMatchSnapshot(); +}); + +test('should call onCancel on handleCancel', () => { + const onCancel = jest.fn(); + const { talonProps } = getTalonProps({ + mutations: { + requestPasswordResetEmailMutation: + 'requestPasswordResetEmailMutation' + }, + onCancel + }); + + talonProps.handleCancel(); + + expect(onCancel).toHaveBeenCalled(); +}); + +test('handleFormSubmit should set hasCompleted to true', async () => { + const { talonProps, update } = getTalonProps({ + mutations: { + requestPasswordResetEmailMutation: + 'requestPasswordResetEmailMutation' + }, + onCancel: jest.fn() + }); + + await talonProps.handleFormSubmit({ email: 'gooseton@goosemail.com' }); + const newTalonProps = update(); + + expect(newTalonProps.hasCompleted).toBeTruthy(); +}); + +test('handleFormSubmit should set forgotPasswordEmail', async () => { + const { talonProps, update } = getTalonProps({ + mutations: { + requestPasswordResetEmailMutation: + 'requestPasswordResetEmailMutation' + }, + onCancel: jest.fn() + }); + + await talonProps.handleFormSubmit({ email: 'gooseton@goosemail.com' }); + const newTalonProps = update(); + + expect(newTalonProps.forgotPasswordEmail).toBe('gooseton@goosemail.com'); +}); + +test('handleFormSubmit should set hasCompleted to false if the mutation fails', async () => { + useMutation.mockReturnValueOnce([ + jest.fn().mockRejectedValueOnce(false), + { + error: 'Mutation error', + loading: false + } + ]); + const { talonProps, update } = getTalonProps({ + mutations: { + requestPasswordResetEmailMutation: + 'requestPasswordResetEmailMutation' + }, + onCancel: jest.fn() + }); + + await talonProps.handleFormSubmit({ email: 'gooseton@goosemail.com' }); + const newTalonProps = update(); + + expect(newTalonProps.hasCompleted).toBeFalsy(); +}); diff --git a/packages/peregrine/lib/talons/ForgotPassword/useForgotPassword.js b/packages/peregrine/lib/talons/ForgotPassword/useForgotPassword.js index 1ba2d166ff..7a0fed1e8a 100644 --- a/packages/peregrine/lib/talons/ForgotPassword/useForgotPassword.js +++ b/packages/peregrine/lib/talons/ForgotPassword/useForgotPassword.js @@ -1,37 +1,81 @@ import { useCallback, useState } from 'react'; -import { useUserContext } from '@magento/peregrine/lib/context/user'; +import { useMutation } from '@apollo/react-hooks'; /** * Returns props necessary to render a ForgotPassword form. - * @param {function} props.onClose callback function to invoke when closing the form + * + * @function + * + * @param {Function} props.onCancel - callback function to call when user clicks the cancel button + * @param {RequestPasswordEmailMutations} props.mutations - GraphQL mutations for the forgot password form. + * + * @returns {ForgotPasswordProps} + * + * @example Importing into your project + * import { useForgotPassword } from '@magento/peregrine/lib/talons/ForgotPassword/useForgotPassword.js'; */ export const useForgotPassword = props => { - const [{ isResettingPassword }, { resetPassword }] = useUserContext(); + const { onCancel, mutations } = props; - const { onClose } = props; - - const [inProgress, setInProgress] = useState(false); + const [hasCompleted, setCompleted] = useState(false); const [forgotPasswordEmail, setForgotPasswordEmail] = useState(null); + const [ + requestResetEmail, + { error: requestResetEmailError, loading: isResettingPassword } + ] = useMutation(mutations.requestPasswordResetEmailMutation); + const handleFormSubmit = useCallback( async ({ email }) => { - setInProgress(true); - setForgotPasswordEmail(email); - await resetPassword({ email }); + try { + await requestResetEmail({ variables: { email } }); + setForgotPasswordEmail(email); + setCompleted(true); + } catch (err) { + setCompleted(false); + } }, - [resetPassword] + [requestResetEmail] ); - const handleContinue = useCallback(() => { - setInProgress(false); - onClose(); - }, [onClose]); + const handleCancel = useCallback(() => { + onCancel(); + }, [onCancel]); return { forgotPasswordEmail, - handleContinue, + formErrors: [requestResetEmailError], + handleCancel, handleFormSubmit, - inProgress, + hasCompleted, isResettingPassword }; }; + +/** JSDocs type definitions */ + +/** + * GraphQL mutations for the forgot password form. + * This is a type used by the {@link useForgotPassword} talon. + * + * @typedef {Object} RequestPasswordEmailMutations + * + * @property {GraphQLAST} requestPasswordResetEmailMutation mutation for requesting password reset email + * + * @see [forgotPassword.gql.js]{@link https://github.com/magento/pwa-studio/blob/develop/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.gql.js} + * for the query used in Venia + */ + +/** + * Object type returned by the {@link useForgotPassword} talon. + * It provides props data to use when rendering the forgot password form component. + * + * @typedef {Object} ForgotPasswordProps + * + * @property {String} forgotPasswordEmail email address of the user whose password reset has been requested + * @property {Array} formErrors A list of form errors + * @property {Function} handleCancel Callback function to handle form cancellations + * @property {Function} handleFormSubmit Callback function to handle form submission + * @property {Boolean} hasCompleted True if password reset mutation has completed. False otherwise + * @property {Boolean} isResettingPassword True if password reset mutation is in progress. False otherwise + */ diff --git a/packages/peregrine/lib/talons/Header/__tests__/__snapshots__/useAccountMenu.spec.js.snap b/packages/peregrine/lib/talons/Header/__tests__/__snapshots__/useAccountMenu.spec.js.snap index cb917f1f5d..4f939d3131 100644 --- a/packages/peregrine/lib/talons/Header/__tests__/__snapshots__/useAccountMenu.spec.js.snap +++ b/packages/peregrine/lib/talons/Header/__tests__/__snapshots__/useAccountMenu.spec.js.snap @@ -4,6 +4,7 @@ exports[`should return correct shape 1`] = ` Object { "handleCreateAccount": [Function], "handleForgotPassword": [Function], + "handleForgotPasswordCancel": [Function], "handleSignOut": [Function], "updateUsername": [Function], "username": "", diff --git a/packages/peregrine/lib/talons/Header/__tests__/__snapshots__/useAccountTrigger.spec.js.snap b/packages/peregrine/lib/talons/Header/__tests__/__snapshots__/useAccountTrigger.spec.js.snap index fba9b22712..36af46c1f9 100644 --- a/packages/peregrine/lib/talons/Header/__tests__/__snapshots__/useAccountTrigger.spec.js.snap +++ b/packages/peregrine/lib/talons/Header/__tests__/__snapshots__/useAccountTrigger.spec.js.snap @@ -4,7 +4,7 @@ exports[`should return correct shape 1`] = ` Object { "accountMenuIsOpen": false, "accountMenuRef": "elementRef", - "accountMenuTriggerRef": "triggerRef", + "accountMenuTriggerRef": [MockFunction], "handleTriggerClick": [Function], "setAccountMenuIsOpen": [MockFunction], } diff --git a/packages/peregrine/lib/talons/Header/__tests__/useAccountTrigger.spec.js b/packages/peregrine/lib/talons/Header/__tests__/useAccountTrigger.spec.js index 463ee62227..636112b282 100644 --- a/packages/peregrine/lib/talons/Header/__tests__/useAccountTrigger.spec.js +++ b/packages/peregrine/lib/talons/Header/__tests__/useAccountTrigger.spec.js @@ -8,7 +8,7 @@ jest.mock('@magento/peregrine/lib/hooks/useDropdown', () => ({ elementRef: 'elementRef', expanded: false, setExpanded: jest.fn(), - triggerRef: 'triggerRef' + triggerRef: jest.fn() }) })); diff --git a/packages/peregrine/lib/talons/Header/useAccountMenu.js b/packages/peregrine/lib/talons/Header/useAccountMenu.js index 586b35f706..3390d66bb1 100644 --- a/packages/peregrine/lib/talons/Header/useAccountMenu.js +++ b/packages/peregrine/lib/talons/Header/useAccountMenu.js @@ -56,6 +56,10 @@ export const useAccountMenu = props => { setView('FORGOT_PASSWORD'); }, []); + const handleForgotPasswordCancel = useCallback(() => { + setView('SIGNIN'); + }, []); + const handleCreateAccount = useCallback(() => { setView('CREATE_ACCOUNT'); }, []); @@ -81,6 +85,7 @@ export const useAccountMenu = props => { username, handleSignOut, handleForgotPassword, + handleForgotPasswordCancel, handleCreateAccount, updateUsername: setUsername }; diff --git a/packages/peregrine/lib/talons/MyAccount/__tests__/__snapshots__/useResetPassword.spec.js.snap b/packages/peregrine/lib/talons/MyAccount/__tests__/__snapshots__/useResetPassword.spec.js.snap new file mode 100644 index 0000000000..bedc51c274 --- /dev/null +++ b/packages/peregrine/lib/talons/MyAccount/__tests__/__snapshots__/useResetPassword.spec.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render properly 1`] = ` +Object { + "email": "gooseton@adobe.com", + "formErrors": Array [ + null, + ], + "handleSubmit": [Function], + "hasCompleted": false, + "loading": false, + "token": "eUokxamL1kiElLDjo6AQHYFO4XlK3", +} +`; diff --git a/packages/peregrine/lib/talons/MyAccount/__tests__/useResetPassword.spec.js b/packages/peregrine/lib/talons/MyAccount/__tests__/useResetPassword.spec.js new file mode 100644 index 0000000000..97e86a9dc1 --- /dev/null +++ b/packages/peregrine/lib/talons/MyAccount/__tests__/useResetPassword.spec.js @@ -0,0 +1,100 @@ +import React from 'react'; +import { useMutation } from '@apollo/react-hooks'; +import { act } from 'react-test-renderer'; + +import createTestInstance from '@magento/peregrine/lib/util/createTestInstance'; + +import { useResetPassword } from '../useResetPassword'; + +jest.mock('@apollo/react-hooks', () => ({ + useMutation: jest + .fn() + .mockReturnValue([jest.fn(), { error: null, loading: false }]) +})); +jest.mock('react-router-dom', () => ({ + useLocation: jest.fn().mockReturnValue({ + search: + '?email=gooseton%40adobe.com&token=eUokxamL1kiElLDjo6AQHYFO4XlK3' + }) +})); + +const Component = props => { + const talonProps = useResetPassword(props); + + return ; +}; + +const getTalonProps = props => { + const tree = createTestInstance(); + const { root } = tree; + const { talonProps } = root.findByType('i').props; + + const update = newProps => { + act(() => { + tree.update(); + }); + + return root.findByType('i').props.talonProps; + }; + + return { talonProps, tree, update }; +}; + +test('should render properly', () => { + const { talonProps } = getTalonProps({ + mutations: { + resetPasswordMutation: 'resetPasswordMutation' + } + }); + + expect(talonProps).toMatchSnapshot(); +}); + +test('should set hasCompleted to true if submission is successful', async () => { + const resetPassword = jest.fn().mockResolvedValueOnce({}); + useMutation.mockReturnValueOnce([ + resetPassword, + { + loading: false, + error: null + } + ]); + const { talonProps, update } = getTalonProps({ + mutations: { + resetPasswordMutation: 'resetPasswordMutation' + } + }); + + await talonProps.handleSubmit({ newPassword: 'NEW_PASSWORD' }); + const newTalonProps = update(); + + expect(newTalonProps.hasCompleted).toBeTruthy(); + expect(resetPassword).toHaveBeenCalledWith({ + variables: { + email: 'gooseton@adobe.com', + token: 'eUokxamL1kiElLDjo6AQHYFO4XlK3', + newPassword: 'NEW_PASSWORD' + } + }); +}); + +test('should set hasCompleted to false if submission is not successful', async () => { + const resetPassword = jest.fn().mockRejectedValueOnce(); + useMutation.mockReturnValueOnce([ + resetPassword, + { + loading: false, + error: null + } + ]); + const { talonProps, update } = getTalonProps({ + mutations: { + resetPasswordMutation: 'resetPasswordMutation' + } + }); + + await talonProps.handleSubmit({ newPassword: 'NEW_PASSWORD' }); + const newTalonProps = update(); + + expect(newTalonProps.hasCompleted).toBeFalsy(); +}); diff --git a/packages/peregrine/lib/talons/MyAccount/useResetPassword.js b/packages/peregrine/lib/talons/MyAccount/useResetPassword.js new file mode 100644 index 0000000000..1b092c8c4c --- /dev/null +++ b/packages/peregrine/lib/talons/MyAccount/useResetPassword.js @@ -0,0 +1,84 @@ +import { useState, useMemo, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useMutation } from '@apollo/react-hooks'; + +/** + * Returns props necessary to render a ResetPassword form. + * + * @param {function} props.mutations - mutation to call when the user submits the new password. + * + * @returns {ResetPasswordProps} - GraphQL mutations for the reset password form. + * + * @example Importing into your project + * import { useResetPassword } from '@magento/peregrine/lib/talons/MyAccount/useResetPassword.js'; + */ +export const useResetPassword = props => { + const { mutations } = props; + + const [hasCompleted, setHasCompleted] = useState(false); + const location = useLocation(); + const [ + resetPassword, + { error: resetPasswordErrors, loading } + ] = useMutation(mutations.resetPasswordMutation); + + const searchParams = useMemo(() => new URLSearchParams(location.search), [ + location + ]); + const email = searchParams.get('email'); + const token = searchParams.get('token'); + + const handleSubmit = useCallback( + async ({ newPassword }) => { + try { + if (email && token && newPassword) { + await resetPassword({ + variables: { email, token, newPassword } + }); + + setHasCompleted(true); + } + } catch (err) { + setHasCompleted(false); + } + }, + [resetPassword, email, token] + ); + + return { + email, + formErrors: [resetPasswordErrors], + handleSubmit, + hasCompleted, + loading, + token + }; +}; + +/** JSDocs type definitions */ + +/** + * GraphQL mutations for the reset password form. + * This is a type used by the {@link useResetPassword} talon. + * + * @typedef {Object} ResetPasswordMutations + * + * @property {GraphQLAST} resetPasswordMutation mutation for resetting password + * + * @see [resetPassword.gql.js]{@link https://github.com/magento/pwa-studio/blob/develop/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.gql.js} + * for the query used in Venia + */ + +/** + * Object type returned by the {@link useResetPassword} talon. + * It provides props data to use when rendering the reset password form component. + * + * @typedef {Object} ResetPasswordProps + * + * @property {String} email email address of the user whose password is beeing reset + * @property {Array} formErrors A list of form errors + * @property {Function} handleSubmit Callback function to handle form submission + * @property {Boolean} hasCompleted True if password reset mutation has completed. False otherwise + * @property {Boolean} loading True if password reset mutation is in progress. False otherwise + * @property {String} token token needed for password reset, will be sent in the mutation + */ diff --git a/packages/peregrine/lib/talons/Password/__tests__/__snapshots__/usePassword.spec.js.snap b/packages/peregrine/lib/talons/Password/__tests__/__snapshots__/usePassword.spec.js.snap new file mode 100644 index 0000000000..44076f15a8 --- /dev/null +++ b/packages/peregrine/lib/talons/Password/__tests__/__snapshots__/usePassword.spec.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render properly 1`] = ` +Object { + "togglePasswordVisibility": [Function], + "visible": false, +} +`; diff --git a/packages/peregrine/lib/talons/Password/__tests__/usePassword.spec.js b/packages/peregrine/lib/talons/Password/__tests__/usePassword.spec.js new file mode 100644 index 0000000000..901e4f2dae --- /dev/null +++ b/packages/peregrine/lib/talons/Password/__tests__/usePassword.spec.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { act } from 'react-test-renderer'; + +import createTestInstance from '@magento/peregrine/lib/util/createTestInstance'; + +import { usePassword } from '../usePassword'; + +const Component = props => { + const talonProps = usePassword(props); + + return ; +}; + +const getTalonProps = props => { + const tree = createTestInstance(); + const { root } = tree; + const { talonProps } = root.findByType('i').props; + + const update = newProps => { + act(() => { + tree.update(); + }); + + return root.findByType('i').props.talonProps; + }; + + return { talonProps, tree, update }; +}; + +test('should render properly', () => { + const { talonProps } = getTalonProps(); + + expect(talonProps).toMatchSnapshot(); +}); + +test('togglePasswordVisibility should toggle visible value', () => { + const { talonProps, update } = getTalonProps(); + + expect(talonProps.visible).toBeFalsy(); + + talonProps.togglePasswordVisibility(); + + const newTalonProps = update(); + + expect(newTalonProps.visible).toBeTruthy(); +}); diff --git a/packages/peregrine/lib/talons/Password/usePassword.js b/packages/peregrine/lib/talons/Password/usePassword.js new file mode 100644 index 0000000000..edcc847f86 --- /dev/null +++ b/packages/peregrine/lib/talons/Password/usePassword.js @@ -0,0 +1,34 @@ +import { useState, useCallback } from 'react'; + +/** + * Returns props necessary to render a Password component. + * + * @returns {PasswordProps} + * + * @example Importing into your project + * import { usePassword } from '@magento/peregrine/lib/talons/Password/usePassword.js'; + */ +export const usePassword = () => { + const [visible, setVisbility] = useState(false); + + const togglePasswordVisibility = useCallback(() => { + setVisbility(!visible); + }, [visible]); + + return { + visible, + togglePasswordVisibility + }; +}; + +/** JSDocs type definitions */ + +/** + * Object type returned by the {@link usePassword} talon. + * It provides props data to use when rendering the password component. + * + * @typedef {Object} PasswordProps + * + * @property {Boolean} visible If true password should be visible. Hidden if false. + * @property {Function} togglePasswordVisibility Callback function to toggle password visibility + */ diff --git a/packages/venia-ui/lib/components/AccountMenu/__tests__/__snapshots__/accountMenu.spec.js.snap b/packages/venia-ui/lib/components/AccountMenu/__tests__/__snapshots__/accountMenu.spec.js.snap index 0fca1396d3..e6cea2bea5 100644 --- a/packages/venia-ui/lib/components/AccountMenu/__tests__/__snapshots__/accountMenu.spec.js.snap +++ b/packages/venia-ui/lib/components/AccountMenu/__tests__/__snapshots__/accountMenu.spec.js.snap @@ -23,7 +23,7 @@ exports[`it renders SignIn component when the view is SIGNIN 1`] = ` `; -exports[`it renders forgot password component when the view is CREATE_ACCOUNT 1`] = ` +exports[`it renders create account component when the view is CREATE_ACCOUNT 1`] = ` `; diff --git a/packages/venia-ui/lib/components/AccountMenu/__tests__/accountMenu.spec.js b/packages/venia-ui/lib/components/AccountMenu/__tests__/accountMenu.spec.js index 53ca2ec28a..bcb62d2f6e 100644 --- a/packages/venia-ui/lib/components/AccountMenu/__tests__/accountMenu.spec.js +++ b/packages/venia-ui/lib/components/AccountMenu/__tests__/accountMenu.spec.js @@ -1,12 +1,13 @@ import React from 'react'; -import { createTestInstance } from '@magento/peregrine'; +import { createTestInstance } from '@magento/peregrine'; import { useAccountMenu } from '@magento/peregrine/lib/talons/Header/useAccountMenu'; import AccountMenu from '../accountMenu'; jest.mock('../accountMenuItems', () => 'AccountMenuItems'); jest.mock('../../SignIn/signIn', () => 'SignIn Component'); +jest.mock('../../ForgotPassword', () => 'Forgot Password Component'); jest.mock('@magento/peregrine/lib/talons/Header/useAccountMenu', () => ({ useAccountMenu: jest.fn().mockReturnValue({ @@ -15,6 +16,7 @@ jest.mock('@magento/peregrine/lib/talons/Header/useAccountMenu', () => ({ handleSignOut: jest.fn(), handleForgotPassword: jest.fn(), handleCreateAccount: jest.fn(), + handleForgotPasswordCancel: jest.fn(), updateUsername: jest.fn() }) })); @@ -25,15 +27,16 @@ const defaultTalonProps = { handleSignOut: jest.fn(), handleForgotPassword: jest.fn(), handleCreateAccount: jest.fn(), + handleForgotPasswordCancel: jest.fn(), updateUsername: jest.fn() }; const defaultProps = { accountMenuIsOpen: false, - setAccountMenuIsOpen: jest.fn(), classes: { modal_active: 'modal_active_class' - } + }, + setAccountMenuIsOpen: jest.fn() }; test('it renders AccountMenuItems when the user is signed in', () => { @@ -75,7 +78,7 @@ test('it renders forgot password component when the view is FORGOT_PASSWORD', () expect(instance.toJSON()).toMatchSnapshot(); }); -test('it renders forgot password component when the view is CREATE_ACCOUNT', () => { +test('it renders create account component when the view is CREATE_ACCOUNT', () => { useAccountMenu.mockReturnValueOnce({ ...defaultTalonProps, view: 'CREATE_ACCOUNT' diff --git a/packages/venia-ui/lib/components/AccountMenu/accountMenu.css b/packages/venia-ui/lib/components/AccountMenu/accountMenu.css index 44071b8524..0d969bede7 100644 --- a/packages/venia-ui/lib/components/AccountMenu/accountMenu.css +++ b/packages/venia-ui/lib/components/AccountMenu/accountMenu.css @@ -12,7 +12,7 @@ transition-property: opacity, transform, visibility; visibility: hidden; width: 27.5rem; - min-height: 18rem; + min-height: 10rem; } .root_open { diff --git a/packages/venia-ui/lib/components/AccountMenu/accountMenu.js b/packages/venia-ui/lib/components/AccountMenu/accountMenu.js index 9c0b26bfc7..ca375577bd 100644 --- a/packages/venia-ui/lib/components/AccountMenu/accountMenu.js +++ b/packages/venia-ui/lib/components/AccountMenu/accountMenu.js @@ -10,6 +10,7 @@ import AccountMenuItems from './accountMenuItems'; import SIGN_OUT_MUTATION from '../../queries/signOut.graphql'; import defaultClasses from './accountMenu.css'; +import ForgotPassword from '../ForgotPassword'; const AccountMenu = React.forwardRef((props, ref) => { const { accountMenuIsOpen, setAccountMenuIsOpen } = props; @@ -24,6 +25,7 @@ const AccountMenu = React.forwardRef((props, ref) => { handleSignOut, handleForgotPassword, handleCreateAccount, + handleForgotPasswordCancel, updateUsername } = talonProps; @@ -40,13 +42,10 @@ const AccountMenu = React.forwardRef((props, ref) => { } case 'FORGOT_PASSWORD': { dropdownContents = ( - // username will be used by forgot password component -
- To be handled in PWA-77 -
+ onCancel={handleForgotPasswordCancel} + /> ); break; diff --git a/packages/venia-ui/lib/components/AccountMenu/accountMenuItems.js b/packages/venia-ui/lib/components/AccountMenu/accountMenuItems.js index c5d33a7de1..88b690e4f6 100644 --- a/packages/venia-ui/lib/components/AccountMenu/accountMenuItems.js +++ b/packages/venia-ui/lib/components/AccountMenu/accountMenuItems.js @@ -40,9 +40,7 @@ const AccountMenuItems = props => { className={classes.signOut} onClick={handleSignOut} type="button" - > - {`Sign Out`} - + >{`Sign Out`} ); }; diff --git a/packages/venia-ui/lib/components/AuthModal/authModal.js b/packages/venia-ui/lib/components/AuthModal/authModal.js index b9d382a517..f4ca704ba4 100644 --- a/packages/venia-ui/lib/components/AuthModal/authModal.js +++ b/packages/venia-ui/lib/components/AuthModal/authModal.js @@ -12,7 +12,7 @@ import SIGN_OUT_MUTATION from '../../queries/signOut.graphql'; const AuthModal = props => { const { - handleClose, + handleCancel, handleCreateAccount, handleSignOut, setUsername, @@ -40,7 +40,7 @@ const AuthModal = props => { child = ( ); break; @@ -72,9 +72,10 @@ AuthModal.propTypes = { classes: shape({ root: string }), + closeDrawer: func.isRequired, showCreateAccount: func.isRequired, showForgotPassword: func.isRequired, - showMainMenu: func.isRequired, showMyAccount: func.isRequired, - view: string.isRequired + showMainMenu: func.isRequired, + showSignIn: func.isRequired }; diff --git a/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/__tests__/__snapshots__/forgotPasswordForm.spec.js.snap b/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/__tests__/__snapshots__/forgotPasswordForm.spec.js.snap index 080047048a..463ab20e22 100644 --- a/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/__tests__/__snapshots__/forgotPasswordForm.spec.js.snap +++ b/packages/venia-ui/lib/components/ForgotPassword/ForgotPasswordForm/__tests__/__snapshots__/forgotPasswordForm.spec.js.snap @@ -13,7 +13,7 @@ exports[`renders correctly 1`] = ` + + - ); }; @@ -26,10 +22,8 @@ export default FormSubmissionSuccessful; FormSubmissionSuccessful.propTypes = { classes: shape({ - buttonContainer: string, root: string, text: string }), - email: string, - onContinue: func.isRequired + email: string }; diff --git a/packages/venia-ui/lib/components/ForgotPassword/__tests__/__snapshots__/forgotPassword.spec.js.snap b/packages/venia-ui/lib/components/ForgotPassword/__tests__/__snapshots__/forgotPassword.spec.js.snap new file mode 100644 index 0000000000..78f57cc531 --- /dev/null +++ b/packages/venia-ui/lib/components/ForgotPassword/__tests__/__snapshots__/forgotPassword.spec.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render properly 1`] = ` +
+

+ Recover Password +

+

+ Please enter the email address associated with this account. +

+
+ Forgot Password Form +
+
+`; + +exports[`should render successful view if hasCompleted is true 1`] = ` +
+
+ Form Submission Successful Component +
+
+`; diff --git a/packages/venia-ui/lib/components/ForgotPassword/__tests__/forgotPassword.spec.js b/packages/venia-ui/lib/components/ForgotPassword/__tests__/forgotPassword.spec.js new file mode 100644 index 0000000000..b8e67f26c5 --- /dev/null +++ b/packages/venia-ui/lib/components/ForgotPassword/__tests__/forgotPassword.spec.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { createTestInstance } from '@magento/peregrine'; + +import { useForgotPassword } from '@magento/peregrine/lib/talons/ForgotPassword/useForgotPassword'; + +import ForgotPassword from '../forgotPassword'; + +jest.mock('../FormSubmissionSuccessful', () => props => ( +
Form Submission Successful Component
+)); +jest.mock('../ForgotPasswordForm', () => props => ( +
Forgot Password Form
+)); +jest.mock( + '@magento/peregrine/lib/talons/ForgotPassword/useForgotPassword', + () => ({ + useForgotPassword: jest.fn().mockReturnValue({ + forgotPasswordEmail: 'gooseton@goosemail.com', + formErrors: [], + handleCancel: jest.fn(), + handleFormSubmit: jest.fn(), + hasCompleted: false, + isResettingPassword: false + }) + }) +); + +test('should render properly', () => { + const tree = createTestInstance( + + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +test('should render successful view if hasCompleted is true', () => { + useForgotPassword.mockReturnValueOnce({ + forgotPasswordEmail: 'gooseton@goosemail.com', + formErrors: [], + handleCancel: jest.fn(), + handleFormSubmit: jest.fn(), + hasCompleted: true, + isResettingPassword: false + }); + + const tree = createTestInstance( + + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); diff --git a/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.css b/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.css index c8d31c82e4..391aed5c53 100644 --- a/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.css +++ b/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.css @@ -2,14 +2,15 @@ display: grid; gap: 1.5rem; justify-items: stretch; - padding: 1rem 1.5rem; + padding: 1.5rem; +} + +.title { + padding-top: 0.5rem; + text-transform: capitalize; } .instructions { - background-color: rgb(var(--venia-global-color-gray)); - border-radius: 4px; - font-size: 0.875rem; font-weight: 300; line-height: 1.25rem; - padding: 1rem; } diff --git a/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.gql.js b/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.gql.js new file mode 100644 index 0000000000..dd3f3e2543 --- /dev/null +++ b/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.gql.js @@ -0,0 +1,15 @@ +import gql from 'graphql-tag'; + +export const REQUEST_PASSWORD_RESET_EMAIL_MUTATION = gql` + mutation requestPasswordResetEmail($email: String!) { + requestPasswordResetEmail(email: $email) + @connection(key: "requestPasswordResetEmail") + } +`; + +export default { + queries: {}, + mutations: { + requestPasswordResetEmailMutation: REQUEST_PASSWORD_RESET_EMAIL_MUTATION + } +}; diff --git a/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.js b/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.js index 4dda8c863d..a41ace0e1a 100644 --- a/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.js +++ b/packages/venia-ui/lib/components/ForgotPassword/forgotPassword.js @@ -1,44 +1,52 @@ import React, { Fragment } from 'react'; import { func, shape, string } from 'prop-types'; +import { useForgotPassword } from '@magento/peregrine/lib/talons/ForgotPassword/useForgotPassword'; + +import FormErrors from '../FormError'; import { mergeClasses } from '../../classify'; import ForgotPasswordForm from './ForgotPasswordForm'; import FormSubmissionSuccessful from './FormSubmissionSuccessful'; + +import forgotPasswordOperations from './forgotPassword.gql'; + import defaultClasses from './forgotPassword.css'; -import { useForgotPassword } from '@magento/peregrine/lib/talons/ForgotPassword/useForgotPassword'; -const INSTRUCTIONS = 'Enter your email below to receive a password reset link.'; +const INSTRUCTIONS = + 'Please enter the email address associated with this account.'; const ForgotPassword = props => { - const { initialValues, onClose } = props; + const { initialValues, onCancel } = props; const talonProps = useForgotPassword({ - onClose + onCancel, + ...forgotPasswordOperations }); const { forgotPasswordEmail, - handleContinue, + formErrors, + handleCancel, handleFormSubmit, - inProgress, + hasCompleted, isResettingPassword } = talonProps; const classes = mergeClasses(defaultClasses, props.classes); - const children = inProgress ? ( - + const children = hasCompleted ? ( + ) : ( +

Recover Password

{INSTRUCTIONS}

+
); @@ -52,9 +60,12 @@ ForgotPassword.propTypes = { instructions: string, root: string }), - email: string, initialValues: shape({ email: string }), - onClose: func.isRequired + onCancel: func +}; + +ForgotPassword.defaultProps = { + onCancel: () => {} }; diff --git a/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/__snapshots__/resetPassword.spec.js.snap b/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/__snapshots__/resetPassword.spec.js.snap new file mode 100644 index 0000000000..9a5083c979 --- /dev/null +++ b/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/__snapshots__/resetPassword.spec.js.snap @@ -0,0 +1,217 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render error message if email is falsy 1`] = ` +
+
+ Head Component +
+

+ Reset Password +

+
+
+ Uh oh, something went wrong. Check the link or try again. +
+
+
+`; + +exports[`should render error message if token is falsy 1`] = ` +
+
+ Head Component +
+

+ Reset Password +

+
+
+ Uh oh, something went wrong. Check the link or try again. +
+
+
+`; + +exports[`should render formErrors 1`] = ` +
+
+ Head Component +
+

+ Reset Password +

+ +
+ Please enter your new password. +
+
+ + + + + + + + + + +

+

+ + +
+`; + +exports[`should render properly 1`] = ` +
+
+ Head Component +
+

+ Reset Password +

+
+
+ Please enter your new password. +
+
+ + + + + + + + + + +

+

+ +
+
+`; + +exports[`should render success message if hasCompleted is true 1`] = ` +
+
+ Head Component +
+

+ Reset Password +

+
+
+ Your new password has been saved. Please use this password to sign into your Account. +
+
+
+`; diff --git a/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/resetPassword.spec.js b/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/resetPassword.spec.js new file mode 100644 index 0000000000..52b812c2df --- /dev/null +++ b/packages/venia-ui/lib/components/MyAccount/ResetPassword/__tests__/resetPassword.spec.js @@ -0,0 +1,117 @@ +import React from 'react'; + +import { useToasts } from '@magento/peregrine'; +import createTestInstance from '@magento/peregrine/lib/util/createTestInstance'; +import { useResetPassword } from '@magento/peregrine/lib/talons/MyAccount/useResetPassword'; + +import ResetPassword from '../resetPassword'; + +jest.mock('@magento/peregrine', () => ({ + useToasts: jest.fn().mockReturnValue([{}, { addToast: jest.fn() }]) +})); +jest.mock('@magento/peregrine/lib/talons/MyAccount/useResetPassword', () => ({ + useResetPassword: jest.fn().mockReturnValue({ + hasCompleted: false, + loading: false, + email: 'gooseton@goosemail.com', + token: '********', + formErrors: [], + handleSubmit: jest.fn() + }) +})); +jest.mock('../../../Head', () => ({ + Title: props =>
Head Component
+})); + +test('should render properly', () => { + const tree = createTestInstance(); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +test('should render error message if token is falsy', () => { + useResetPassword.mockReturnValueOnce({ + hasCompleted: false, + loading: false, + email: 'gooseton@goosemail.com', + token: null, + formErrors: [], + handleSubmit: jest.fn() + }); + + const tree = createTestInstance(); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +test('should render error message if email is falsy', () => { + useResetPassword.mockReturnValueOnce({ + hasCompleted: false, + loading: false, + email: null, + token: '**********', + formErrors: [], + handleSubmit: jest.fn() + }); + + const tree = createTestInstance(); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +test('should render formErrors', () => { + useResetPassword.mockReturnValueOnce({ + hasCompleted: false, + loading: false, + email: 'gooseton@goosemail.com', + token: '**********', + formErrors: [ + { + graphQLErrors: { + message: 'This is an error.' + } + } + ], + handleSubmit: jest.fn() + }); + + const tree = createTestInstance(); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +test('should render success message if hasCompleted is true', () => { + useResetPassword.mockReturnValueOnce({ + hasCompleted: true, + loading: false, + email: 'gooseton@goosemail.com', + token: '**********', + formErrors: [], + handleSubmit: jest.fn() + }); + + const tree = createTestInstance(); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +test('should render toast if hasCompleted is true', () => { + const addToast = jest.fn(); + useToasts.mockReturnValueOnce([{}, { addToast }]); + useResetPassword.mockReturnValueOnce({ + hasCompleted: true, + loading: false, + email: 'gooseton@goosemail.com', + token: '**********', + formErrors: [], + handleSubmit: jest.fn() + }); + + createTestInstance(); + + expect(addToast).toHaveBeenCalledWith({ + type: 'info', + message: 'Your new password has been saved.', + timeout: 5000 + }); +}); diff --git a/packages/venia-ui/lib/components/MyAccount/ResetPassword/index.js b/packages/venia-ui/lib/components/MyAccount/ResetPassword/index.js new file mode 100644 index 0000000000..5ad983ef52 --- /dev/null +++ b/packages/venia-ui/lib/components/MyAccount/ResetPassword/index.js @@ -0,0 +1 @@ +export { default } from './resetPassword'; diff --git a/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.css b/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.css new file mode 100644 index 0000000000..acda72ec45 --- /dev/null +++ b/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.css @@ -0,0 +1,135 @@ +.root { + padding: 2.5rem 3rem; + max-width: 1080px; + margin: 0 auto; +} + +.heading { + text-transform: capitalize; + line-height: 1.25em; + margin-bottom: 2.5rem; + text-align: center; +} + +.container { + display: grid; + gap: 1.5rem; + grid-template-columns: 2fr auto; + margin: 2rem 7rem; + padding: 3rem; + border: 2px solid rgb(var(--venia-global-color-gray-400)); + border-radius: 0.375rem; +} + +.description { + grid-row: 1 / span 1; + grid-column: 1 / span 2; + padding-bottom: 0.5rem; + font-size: var(--venia-global-typography-heading-M-fontSize); + line-height: var(--venia-global-typography-heading-lineHeight); +} + +.password { + grid-row: 2 / span 1; + grid-column: 1 / span 1; +} + +.submitButton { + composes: root_highPriority from '../../Button/button.css'; + + grid-row: 2 / span 1; + grid-column: 2 / span 1; + margin: auto; + margin-top: 2rem; +} + +.invalidTokenContainer { + border: 2px solid rgb(var(--venia-global-color-gray-400)); + border-radius: 0.375rem; + margin: auto; + padding: 3rem 5rem; + padding-left: 3rem; + width: fit-content; +} + +.invalidToken { + padding: 0.5rem 1rem; + border-left: 4px solid rgb(var(--venia-global-color-error)); + color: rgb(var(--venia-global-color-error)); +} + +.successMessageContainer { + border: 2px solid rgb(var(--venia-global-color-gray-400)); + border-radius: 0.375rem; + margin: auto; + padding: 3rem 5rem; + padding-left: 3rem; + width: fit-content; +} + +.successMessage { + padding: 0.5rem 1rem; + text-align: center; +} + +.errorMessage { + padding-top: 1rem; +} + +/* + * Mobile-specific styles. + */ + +@media (max-width: 960px) { + .root { + padding-left: 1.5rem; + padding-right: 1.5rem; + margin: 0 auto; + } + + .container { + display: grid; + border: none; + margin: 0rem; + padding: 0rem; + } + + .description { + grid-row: 1 / span 1; + grid-column: 1 / span 2; + padding-bottom: 0.5rem; + } + + .password { + grid-row: 2 / span 1; + grid-column: 1 / span 2; + min-height: 5rem; + } + + .submitButton { + composes: root_highPriority from '../../Button/button.css'; + + grid-row: 3 / span 1; + grid-column: 1 / span 2; + margin: auto; + margin-top: 1rem; + } + + .invalidTokenContainer { + border: none; + margin: auto; + padding: 0rem; + } + + .invalidToken { + border-left: 4px solid rgb(var(--venia-global-color-error)); + padding: 0.5rem; + text-align: left; + } + + .successMessageContainer { + border: none; + margin: auto; + padding: 0rem; + } +} diff --git a/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.gql.js b/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.gql.js new file mode 100644 index 0000000000..e881457651 --- /dev/null +++ b/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.gql.js @@ -0,0 +1,22 @@ +import gql from 'graphql-tag'; + +export const RESET_PASSWORD_MUTATION = gql` + mutation resetPassword( + $email: String! + $token: String! + $newPassword: String! + ) { + resetPassword( + email: $email + resetPasswordToken: $token + newPassword: $newPassword + ) @connection(key: "resetPassword") + } +`; + +export default { + queries: {}, + mutations: { + resetPasswordMutation: RESET_PASSWORD_MUTATION + } +}; diff --git a/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.js b/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.js new file mode 100644 index 0000000000..3d565e339d --- /dev/null +++ b/packages/venia-ui/lib/components/MyAccount/ResetPassword/resetPassword.js @@ -0,0 +1,115 @@ +import React, { useEffect } from 'react'; +import { shape, string } from 'prop-types'; +import { Form } from 'informed'; + +import { useToasts } from '@magento/peregrine'; +import { useResetPassword } from '@magento/peregrine/lib/talons/MyAccount/useResetPassword'; +import { mergeClasses } from '@magento/venia-ui/lib/classify'; + +import { Title } from '../../Head'; + +import Button from '../../Button'; +import FormErrors from '../../FormError'; + +import resetPasswordOperations from './resetPassword.gql'; + +import defaultClasses from './resetPassword.css'; +import Password from '../../Password'; + +const PAGE_TITLE = `Reset Password`; + +const ResetPassword = props => { + const { classes: propClasses } = props; + const classes = mergeClasses(defaultClasses, propClasses); + const talonProps = useResetPassword({ + ...resetPasswordOperations + }); + const { + hasCompleted, + loading, + email, + token, + formErrors, + handleSubmit + } = talonProps; + + const tokenMissing = ( +
+
+ {'Uh oh, something went wrong. Check the link or try again.'} +
+
+ ); + + const [, { addToast }] = useToasts(); + + useEffect(() => { + if (hasCompleted) { + addToast({ + type: 'info', + message: 'Your new password has been saved.', + timeout: 5000 + }); + } + }, [addToast, hasCompleted]); + + const recoverPassword = hasCompleted ? ( +
+
+ { + 'Your new password has been saved. Please use this password to sign into your Account.' + } +
+
+ ) : ( +
+
+ Please enter your new password. +
+ + + + + ); + + return ( +
+ {`${PAGE_TITLE} - ${STORE_NAME}`} +

{PAGE_TITLE}

+ {token && email ? recoverPassword : tokenMissing} +
+ ); +}; + +export default ResetPassword; + +ResetPassword.propTypes = { + classes: shape({ + container: string, + description: string, + errorMessage: string, + heading: string, + invalidToken: string, + invalidTokenContainer: string, + password: string, + root: string, + submitButton: string, + successMessage: string, + successMessageContainer: string + }) +}; diff --git a/packages/venia-ui/lib/components/MyAccount/myAccount.js b/packages/venia-ui/lib/components/MyAccount/myAccount.js index 3dcc3c144e..aefbd80100 100644 --- a/packages/venia-ui/lib/components/MyAccount/myAccount.js +++ b/packages/venia-ui/lib/components/MyAccount/myAccount.js @@ -8,16 +8,18 @@ import AccountMenuItems from '../AccountMenu/accountMenuItems'; import defaultClasses from './myAccount.css'; const MyAccount = props => { - const { onSignOut } = props; + const { classes: propClasses, onSignOut, onClose } = props; + const classes = mergeClasses(defaultClasses, propClasses); - const talonProps = useMyAccount({ onSignOut }); - const { handleSignOut } = talonProps; - - const classes = mergeClasses(defaultClasses, props.classes); + const talonProps = useMyAccount({ + onSignOut: onSignOut, + onClose: onClose + }); + const { handleSignOut, handleClose } = talonProps; return (
- +
); }; diff --git a/packages/venia-ui/lib/components/Password/__tests__/__snapshots__/password.spec.js.snap b/packages/venia-ui/lib/components/Password/__tests__/__snapshots__/password.spec.js.snap new file mode 100644 index 0000000000..3259059b24 --- /dev/null +++ b/packages/venia-ui/lib/components/Password/__tests__/__snapshots__/password.spec.js.snap @@ -0,0 +1,150 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render properly 1`] = ` +
+ + + + + + + + +

+

+`; + +exports[`should render show button if visible is false 1`] = ` +
+ + + + + + + + + + +

+

+`; + +exports[`should render toggle button if isToggleButtonHidden is false 1`] = ` +
+ + + + + + + + + + +

+

+`; diff --git a/packages/venia-ui/lib/components/Password/__tests__/password.spec.js b/packages/venia-ui/lib/components/Password/__tests__/password.spec.js new file mode 100644 index 0000000000..3cae7f595b --- /dev/null +++ b/packages/venia-ui/lib/components/Password/__tests__/password.spec.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { createTestInstance } from '@magento/peregrine'; + +import { usePassword } from '@magento/peregrine/lib/talons/Password/usePassword'; + +import Password from '../password'; + +jest.mock('@magento/peregrine/lib/talons/Password/usePassword', () => ({ + usePassword: jest.fn().mockReturnValue({ + visible: false, + togglePasswordVisibility: jest.fn() + }) +})); + +test('should render properly', () => { + const tree = createTestInstance( + + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +test('should render toggle button if isToggleButtonHidden is false', () => { + usePassword.mockReturnValue({ + visible: false, + togglePasswordVisibility: jest.fn() + }); + + const tree = createTestInstance( + + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); + +test('should render show button if visible is false', () => { + usePassword.mockReturnValue({ + visible: true, + togglePasswordVisibility: jest.fn() + }); + + const tree = createTestInstance( + + ); + + expect(tree.toJSON()).toMatchSnapshot(); +}); diff --git a/packages/venia-ui/lib/components/Password/index.js b/packages/venia-ui/lib/components/Password/index.js new file mode 100644 index 0000000000..1d2ddc886f --- /dev/null +++ b/packages/venia-ui/lib/components/Password/index.js @@ -0,0 +1 @@ +export { default } from './password'; diff --git a/packages/venia-ui/lib/components/Password/password.css b/packages/venia-ui/lib/components/Password/password.css new file mode 100644 index 0000000000..6c395bb49c --- /dev/null +++ b/packages/venia-ui/lib/components/Password/password.css @@ -0,0 +1,24 @@ +.passwordButton { + composes: root from '../Button/button.css'; + + --stroke: var(--venia-global-color-gray-500); + background: none; + border-radius: 0px; + border-style: none; + border-width: 0px; + padding: 0px; + min-width: 0px; +} + +.passwordButton:hover { + --stroke: var(--venia-global-color-gray-700); +} + +.passwordButton:focus { + box-shadow: none; + --stroke: var(--venia-global-color-gray-700); +} + +.root:active { + --stroke: var(--venia-global-color-gray-700); +} diff --git a/packages/venia-ui/lib/components/Password/password.js b/packages/venia-ui/lib/components/Password/password.js new file mode 100644 index 0000000000..39d0c49cf6 --- /dev/null +++ b/packages/venia-ui/lib/components/Password/password.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { string, bool, shape, func } from 'prop-types'; +import { Eye, EyeOff } from 'react-feather'; + +import { mergeClasses } from '@magento/venia-ui/lib/classify'; +import { usePassword } from '@magento/peregrine/lib/talons/Password/usePassword'; + +import Button from '../Button'; +import Field from '../Field'; +import TextInput from '../TextInput'; +import { isRequired } from '../../util/formValidators'; + +import defaultClasses from './password.css'; + +const Password = props => { + const { + classes: propClasses, + label, + fieldName, + isToggleButtonHidden, + autoComplete, + validate, + ...otherProps + } = props; + const talonProps = usePassword(); + const { visible, togglePasswordVisibility } = talonProps; + const classes = mergeClasses(defaultClasses, propClasses); + + const passwordButton = ( + + ); + + const fieldType = visible ? 'text' : 'password'; + + return ( + + + + ); +}; + +Password.propTypes = { + autoComplete: string, + classes: shape({ + root: string + }), + label: string, + fieldName: string, + isToggleButtonHidden: bool, + validate: func +}; + +Password.defaultProps = { + isToggleButtonHidden: true, + validate: isRequired +}; + +export default Password; diff --git a/packages/venia-ui/lib/components/SignIn/__tests__/__snapshots__/signIn.spec.js.snap b/packages/venia-ui/lib/components/SignIn/__tests__/__snapshots__/signIn.spec.js.snap index a30de0b26f..a8b6258be5 100644 --- a/packages/venia-ui/lib/components/SignIn/__tests__/__snapshots__/signIn.spec.js.snap +++ b/packages/venia-ui/lib/components/SignIn/__tests__/__snapshots__/signIn.spec.js.snap @@ -81,7 +81,7 @@ exports[`displays an error message if there is a sign in error 1`] = ` className="root" style={ Object { - "--iconsAfter": 0, + "--iconsAfter": 1, "--iconsBefore": 0, } } @@ -104,7 +104,9 @@ exports[`displays an error message if there is a sign in error 1`] = ` /> + > + +

+ > + +

{ @@ -70,14 +71,13 @@ const SignIn = props => { validate={isRequired} /> - - - +

{ exact: true, path: '../../RootComponents/Search' }, + { + /** + * This path is configured in the forgot password + * email template in the admin panel. + */ + name: 'Reset Password', + pattern: '/customer/account/createPassword', + exact: true, + path: '../MyAccount/ResetPassword' + }, { name: 'CommunicationsPage', pattern: '/communications',