Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(account-settings): ChangePassword component #2885

Merged
merged 34 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
855a3b0
Initial commit
wlee221 Nov 2, 2022
f967b43
Merge branch 'account-settings/main' into account-settings/change-pas…
wlee221 Nov 3, 2022
b0f023c
Implement form handling
wlee221 Nov 3, 2022
13c8c24
Reorder component
wlee221 Nov 3, 2022
f9f4644
Add unit test
wlee221 Nov 3, 2022
4c796d5
Add button and error as default components
wlee221 Nov 3, 2022
c1a69e5
Add error message test
wlee221 Nov 3, 2022
8b1e7c1
TypeName initial change
wlee221 Nov 3, 2022
517ed84
Update exports snapshot
wlee221 Nov 3, 2022
906160f
Remove unrelated types
wlee221 Nov 3, 2022
6301e18
Clean up rendered DOM
wlee221 Nov 3, 2022
0486c3b
Default error to error variation
wlee221 Nov 3, 2022
1174e12
Merge branch 'account-settings/change-password' of github.com:aws-amp…
wlee221 Nov 3, 2022
2377b8e
Revert unrelated changes
wlee221 Nov 3, 2022
3ed87af
Move all primitive props to parent component
wlee221 Nov 3, 2022
9646425
setError to null on success
wlee221 Nov 3, 2022
95caea4
Update packages/react/src/components/AccountSettings/types.ts
wlee221 Nov 3, 2022
1efd39f
Remove unrelated change
wlee221 Nov 3, 2022
7f52d4c
Merge branch 'account-settings/change-password' of github.com:aws-amp…
wlee221 Nov 3, 2022
fb36407
Add comment about generics
wlee221 Nov 3, 2022
9ed25b2
Fix exports
wlee221 Nov 3, 2022
05472b6
Apply comments from @dbanksdesign
wlee221 Nov 3, 2022
a6fbeb7
Update packages/react/src/components/AccountSettings/types.ts
wlee221 Nov 3, 2022
f2e29b8
this shouldn't be here yet
wlee221 Nov 3, 2022
ccddcbc
Merge branch 'account-settings/change-password' of github.com:aws-amp…
wlee221 Nov 3, 2022
d871d2f
Update packages/react/src/components/AccountSettings/ChangePassword/_…
wlee221 Nov 3, 2022
97936ee
Apply comments from @calebpollman
wlee221 Nov 3, 2022
4ea9fa4
Pass errorMessage as children
wlee221 Nov 3, 2022
6a9d385
Add logger around Auth call
wlee221 Nov 4, 2022
12a0292
null-check errorMessage before setting it
wlee221 Nov 4, 2022
891014f
Add 'Notifications' to LoggerCategory
wlee221 Nov 4, 2022
b1796f0
getCategoryLogger -> getLogger
wlee221 Nov 4, 2022
975f89f
Move around unit test comment
wlee221 Nov 4, 2022
e1ba995
Merge branch 'account-settings/change-password' of github.com:aws-amp…
wlee221 Nov 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Array [
"Button",
"ButtonGroup",
"Card",
"ChangePassword",
"CheckboxField",
"Collection",
"ComponentClassNames",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from 'react';

import { Logger } from 'aws-amplify';
import { changePassword, translate } from '@aws-amplify/ui';

import { useAuth } from '../../../internal';
import { View, Flex } from '../../../primitives';
import {
DefaultCurrentPassword,
DefaultError,
DefaultNewPassword,
DefaultSubmitButton,
} from './defaultComponents';
import { ChangePasswordProps } from './types';
import { FormValues } from '../types';
wlee221 marked this conversation as resolved.
Show resolved Hide resolved

const logger = new Logger('ChangePassword');

function ChangePassword({
onSuccess,
onError,
}: ChangePasswordProps): JSX.Element | null {
const [errorMessage, setErrorMessage] = React.useState<string>(null);
const [formValues, setFormValues] = React.useState<FormValues>({});

const { user, isLoading } = useAuth();

/** Return null if Auth.getCurrentAuthenticatedUser is still in progress */
if (isLoading) {
return null;
}

/** Return null if user isn't authenticated in the first place */
if (!user) {
logger.warn('<ChangePassword /> requires user to be authenticated.');
return null;
}

/** Translations */
// TODO: add AccountSettingsTextUtil to collect these strings
const currentPasswordLabel = translate('Current Password');
const newPasswordLabel = translate('New Password');
const updatePasswordText = translate('Update password');

/** Event Handlers */
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();

const { name, value } = event.target;
setFormValues({ ...formValues, [name]: value });
};

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { currentPassword, newPassword } = formValues;
if (errorMessage) {
setErrorMessage(null);
}
try {
await changePassword({ user, currentPassword, newPassword });

onSuccess?.(); // notify success to the parent
} catch (e) {
const error = e as Error;
if (error.message) setErrorMessage(error.message);

onError?.(error); // notify error to the parent
}
};

return (
<View as="form" className="amplify-changepassword" onSubmit={handleSubmit}>
<Flex direction="column">
Comment on lines +72 to +73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could simplify these 2 lines I think:

Suggested change
<View as="form" className="amplify-changepassword" onSubmit={handleSubmit}>
<Flex direction="column">
<Flex as="form" direction="column" className="amplify-changepassword" onSubmit={handleSubmit}>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've initially tried this, but onSubmit handler for Flex as="form" had TS errors:

Type '(event: React.FormEvent<HTMLFormElement>) => void' is not assignable to type 'FormEventHandler<HTMLDivElement>'.

I'll create a repro and submit an issue. I think we can address this later after then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI created #2891

<DefaultCurrentPassword
autoComplete="current-password"
isRequired
label={currentPasswordLabel}
name="currentPassword"
onChange={handleChange}
/>
<DefaultNewPassword
autoComplete="new-password"
isRequired
label={newPasswordLabel}
name="newPassword"
onChange={handleChange}
/>
<DefaultSubmitButton type="submit">
{updatePasswordText}
</DefaultSubmitButton>
{errorMessage ? <DefaultError>{errorMessage}</DefaultError> : null}
</Flex>
</View>
);
}

export default ChangePassword;
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

import * as UIModule from '@aws-amplify/ui';

import ChangePassword from '../ChangePassword';

const user = {} as unknown as UIModule.AmplifyUser;
jest.mock('../../../../internal', () => ({
useAuth: () => ({
user,
isLoading: false,
}),
}));

const changePasswordSpy = jest.spyOn(UIModule, 'changePassword');

describe('ChangePassword', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders as expected', () => {
const { container } = render(<ChangePassword />);
expect(container).toMatchSnapshot();
});

it('calls changePassword with expected arguments', async () => {
changePasswordSpy.mockResolvedValue();

const onSuccess = jest.fn();
render(<ChangePassword onSuccess={onSuccess} />);

const currentPassword = await screen.findByLabelText('Current Password');
const newPassword = await screen.findByLabelText('New Password');
const submitButton = await screen.findByRole('button', {
name: 'Update password',
});

fireEvent.input(currentPassword, {
target: { name: 'currentPassword', value: 'oldpassword' },
});

fireEvent.input(newPassword, {
target: { name: 'newPassword', value: 'newpassword' },
});

fireEvent.submit(submitButton);

expect(changePasswordSpy).toBeCalledWith({
user,
currentPassword: 'oldpassword',
newPassword: 'newpassword',
});
});

it('onSuccess is called after successful sign up', async () => {
changePasswordSpy.mockResolvedValue();

const onSuccess = jest.fn();
render(<ChangePassword onSuccess={onSuccess} />);

const submitButton = await screen.findByRole('button', {
name: 'Update password',
});
fireEvent.submit(submitButton);

// submit handling is async, wait for onSuccess to be called
// https://testing-library.com/docs/dom-testing-library/api-async/#waitfor
await waitFor(() => expect(onSuccess).toBeCalledTimes(1));
});

it('onError is called after unsuccessful submit', async () => {
changePasswordSpy.mockRejectedValue(new Error('Mock Error'));

const onError = jest.fn();
render(<ChangePassword onError={onError} />);

const submitButton = await screen.findByRole('button', {
name: 'Update password',
});

fireEvent.submit(submitButton);

// submit handling is async, wait for onError to be called
await waitFor(() => expect(onError).toBeCalledTimes(1));
wlee221 marked this conversation as resolved.
Show resolved Hide resolved
});

it('error message is displayed after unsuccessful submit', async () => {
changePasswordSpy.mockRejectedValue(new Error('Mock Error'));

const onError = jest.fn();
render(<ChangePassword onError={onError} />);

const submitButton = await screen.findByRole('button', {
name: 'Update password',
});

fireEvent.submit(submitButton);

// submit handling is async, wait for error to be displayed
await waitFor(() => expect(screen.findByText('Mock Error')).toBeDefined());
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ChangePassword renders as expected 1`] = `
<div>
<form
class="amplify-changepassword"
>
<div
class="amplify-flex"
style="flex-direction: column;"
>
<div
class="amplify-flex amplify-field amplify-textfield amplify-passwordfield"
>
<label
class="amplify-label"
for="amplify-id-0"
>
Current Password
</label>
<div
class="amplify-flex amplify-field-group amplify-field-group--horizontal"
data-orientation="horizontal"
>
<div
class="amplify-field-group__field-wrapper amplify-field-group__field-wrapper--horizontal"
data-orientation="horizontal"
>
<input
aria-invalid="false"
autocomplete="current-password"
class="amplify-input amplify-field-group__control"
id="amplify-id-0"
name="currentPassword"
required=""
type="password"
/>
</div>
<div
class="amplify-field-group__outer-end"
>
<button
aria-checked="false"
aria-label="Show password"
class="amplify-button amplify-field-group__control amplify-field__show-password"
data-fullwidth="false"
role="switch"
type="button"
>
<span
aria-live="polite"
class="amplify-visually-hidden"
>
Password is hidden
</span>
<span
class="amplify-icon"
style="width: 1em; height: 1em;"
>
<svg
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 6C15.79 6 19.17 8.13 20.82 11.5C19.17 14.87 15.79 17 12 17C8.21 17 4.83 14.87 3.18 11.5C4.83 8.13 8.21 6 12 6ZM12 4C7 4 2.73 7.11 1 11.5C2.73 15.89 7 19 12 19C17 19 21.27 15.89 23 11.5C21.27 7.11 17 4 12 4ZM12 9C13.38 9 14.5 10.12 14.5 11.5C14.5 12.88 13.38 14 12 14C10.62 14 9.5 12.88 9.5 11.5C9.5 10.12 10.62 9 12 9ZM12 7C9.52 7 7.5 9.02 7.5 11.5C7.5 13.98 9.52 16 12 16C14.48 16 16.5 13.98 16.5 11.5C16.5 9.02 14.48 7 12 7Z"
fill="currentColor"
/>
</svg>
</span>
</button>
</div>
</div>
</div>
<div
class="amplify-flex amplify-field amplify-textfield amplify-passwordfield"
>
<label
class="amplify-label"
for="amplify-id-1"
>
New Password
</label>
<div
class="amplify-flex amplify-field-group amplify-field-group--horizontal"
data-orientation="horizontal"
>
<div
class="amplify-field-group__field-wrapper amplify-field-group__field-wrapper--horizontal"
data-orientation="horizontal"
>
<input
aria-invalid="false"
autocomplete="new-password"
class="amplify-input amplify-field-group__control"
id="amplify-id-1"
name="newPassword"
required=""
type="password"
/>
</div>
<div
class="amplify-field-group__outer-end"
>
<button
aria-checked="false"
aria-label="Show password"
class="amplify-button amplify-field-group__control amplify-field__show-password"
data-fullwidth="false"
role="switch"
type="button"
>
<span
aria-live="polite"
class="amplify-visually-hidden"
>
Password is hidden
</span>
<span
class="amplify-icon"
style="width: 1em; height: 1em;"
>
<svg
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 6C15.79 6 19.17 8.13 20.82 11.5C19.17 14.87 15.79 17 12 17C8.21 17 4.83 14.87 3.18 11.5C4.83 8.13 8.21 6 12 6ZM12 4C7 4 2.73 7.11 1 11.5C2.73 15.89 7 19 12 19C17 19 21.27 15.89 23 11.5C21.27 7.11 17 4 12 4ZM12 9C13.38 9 14.5 10.12 14.5 11.5C14.5 12.88 13.38 14 12 14C10.62 14 9.5 12.88 9.5 11.5C9.5 10.12 10.62 9 12 9ZM12 7C9.52 7 7.5 9.02 7.5 11.5C7.5 13.98 9.52 16 12 16C14.48 16 16.5 13.98 16.5 11.5C16.5 9.02 14.48 7 12 7Z"
fill="currentColor"
/>
</svg>
</span>
</button>
</div>
</div>
</div>
<button
class="amplify-button amplify-field-group__control"
data-fullwidth="false"
type="submit"
>
Update password
</button>
</div>
</form>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { Alert, Button, PasswordField } from '../../../primitives';
import {
AccountSettingsError,
AccountSettingsPasswordField,
AccountSettingsSubmitButton,
} from '../types';

/** ChangePassword subcomponents */
// TODO: enable component override
export const DefaultCurrentPassword: AccountSettingsPasswordField = (props) => {
return <PasswordField {...props} />;
};

export const DefaultNewPassword: AccountSettingsPasswordField = (props) => {
return <PasswordField {...props} />;
};

export const DefaultSubmitButton: AccountSettingsSubmitButton = ({
children,
...rest
}) => {
return <Button {...rest}>{children}</Button>;
};

export const DefaultError: AccountSettingsError = ({ children, ...rest }) => {
return (
<Alert variation="error" {...rest}>
{children}
</Alert>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ChangePassword } from './ChangePassword';
Loading