Skip to content

Commit

Permalink
My Account - Forgot Password (#2619)
Browse files Browse the repository at this point in the history
* Added singin trigger content.

* Tests update.

* Removed forgot password and create account comps.

* Reverting unnecessary changes.

* Converted VIEWS to const export instead of prop.

* Updated account menu tests.

* Added useAccountTrigger tests.

* Added formErrors support.

* Minor UX changes.

* Implemented forgot password component.

* Updated forgot password buttons css.

* Wired in forgot password mutation.

* Added reser password route.

* Update packages/peregrine/lib/talons/Header/useAccountTrigger.js

Co-authored-by: Stephen <[email protected]>

* Added reset password UI.

* Added new password submit logic.

* Minor CSS changes.

* Loading state CSS update.

* Prop names change.

* Removing form errors. Tobe handled in 2588.

* Moving account menu logic to account menu trigger.

* Minor.

* Added notes.

* Updated account menu CSS.

* Doc updates.

* Updated tests.

* Snapshot updates.

* Updated docs.

* Mobile specific CSS.

* Minor CSS changes.

* Update packages/venia-ui/lib/components/AccountMenu/__tests__/accountMenu.spec.js

Co-authored-by: Stephen <[email protected]>

* Update packages/peregrine/lib/talons/Header/useAccountMenu.js

Co-authored-by: Stephen <[email protected]>

* Updated docs.

* Fixed tests.

* Minor CSS change.

* Removed continue shopping button in recover password UI.

* Added new password component.

* Added password snapshot tests.

* Test updates.

* Minor.

* Snapshot update.

* Proptypes update.

* Added success toast on password reset.

* Added error message UI.

* Form errors UI CSS update.

* Updated reset password css.

* Updated CSS for mobile.

* Prettier changes.

* Snapshot updates.

* Minor es lint change.

* Minor.

* Switching eye drop icons.

* Added border around success message.

* Added text color for error message.

* Trigger Build

* Added initial resetPassword.js tests.

* Added resetPassword tests.

* Added useResetPassword tests.

* Added password tests.

* Added usePassword tests.

* Using new password component in signin form.

* Addressed PR comments.

* Minor CSS change.

* Added forgot password docs and minor modifications.

* Added forgot password component and talon tests.

* Added new js docs.

* Added user context docs.

* Minor.

* Tests update.

Co-authored-by: Stephen <[email protected]>
Co-authored-by: Andy Terranova <[email protected]>
  • Loading branch information
3 people committed Aug 25, 2020
1 parent 35868d3 commit b942aff
Show file tree
Hide file tree
Showing 48 changed files with 1,694 additions and 123 deletions.
33 changes: 33 additions & 0 deletions packages/peregrine/lib/context/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
10 changes: 10 additions & 0 deletions packages/peregrine/lib/talons/AuthModal/useAuthModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,6 +39,7 @@ export const useAuthModal = props => {
showForgotPassword,
showMainMenu,
showMyAccount,
showSignIn,
signOutMutation,
view
} = props;
Expand Down Expand Up @@ -67,6 +72,10 @@ export const useAuthModal = props => {
closeDrawer();
}, [closeDrawer, showMainMenu]);

const handleCancel = useCallback(() => {
showSignIn();
}, [showSignIn]);

const handleCreateAccount = useCallback(() => {
showMyAccount();
}, [showMyAccount]);
Expand All @@ -86,6 +95,7 @@ export const useAuthModal = props => {
}, [apolloClient, history, revokeToken, signOut]);

return {
handleCancel,
handleClose,
handleCreateAccount,
handleSignOut,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
`;
Original file line number Diff line number Diff line change
@@ -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 <i talonProps={talonProps} />;
};

const getTalonProps = props => {
const tree = createTestInstance(<Component {...props} />);
const { root } = tree;
const { talonProps } = root.findByType('i').props;

const update = newProps => {
act(() => {
tree.update(<Component {...{ ...props, ...newProps }} />);
});

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: '[email protected]' });
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: '[email protected]' });
const newTalonProps = update();

expect(newTalonProps.forgotPasswordEmail).toBe('[email protected]');
});

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: '[email protected]' });
const newTalonProps = update();

expect(newTalonProps.hasCompleted).toBeFalsy();
});
76 changes: 60 additions & 16 deletions packages/peregrine/lib/talons/ForgotPassword/useForgotPassword.js
Original file line number Diff line number Diff line change
@@ -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 <caption>Importing into your project</caption>
* 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
*/
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports[`should return correct shape 1`] = `
Object {
"handleCreateAccount": [Function],
"handleForgotPassword": [Function],
"handleForgotPasswordCancel": [Function],
"handleSignOut": [Function],
"updateUsername": [Function],
"username": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`should return correct shape 1`] = `
Object {
"accountMenuIsOpen": false,
"accountMenuRef": "elementRef",
"accountMenuTriggerRef": "triggerRef",
"accountMenuTriggerRef": [MockFunction],
"handleTriggerClick": [Function],
"setAccountMenuIsOpen": [MockFunction],
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jest.mock('@magento/peregrine/lib/hooks/useDropdown', () => ({
elementRef: 'elementRef',
expanded: false,
setExpanded: jest.fn(),
triggerRef: 'triggerRef'
triggerRef: jest.fn()
})
}));

Expand Down
5 changes: 5 additions & 0 deletions packages/peregrine/lib/talons/Header/useAccountMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export const useAccountMenu = props => {
setView('FORGOT_PASSWORD');
}, []);

const handleForgotPasswordCancel = useCallback(() => {
setView('SIGNIN');
}, []);

const handleCreateAccount = useCallback(() => {
setView('CREATE_ACCOUNT');
}, []);
Expand All @@ -81,6 +85,7 @@ export const useAccountMenu = props => {
username,
handleSignOut,
handleForgotPassword,
handleForgotPasswordCancel,
handleCreateAccount,
updateUsername: setUsername
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render properly 1`] = `
Object {
"email": "[email protected]",
"formErrors": Array [
null,
],
"handleSubmit": [Function],
"hasCompleted": false,
"loading": false,
"token": "eUokxamL1kiElLDjo6AQHYFO4XlK3",
}
`;
Loading

0 comments on commit b942aff

Please sign in to comment.