@availity/change-password 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +26 -0
- package/index.d.ts +10 -0
- package/index.js +10 -0
- package/jest.config.js +8 -0
- package/package.json +52 -0
- package/project.json +36 -0
- package/src/ChangePassword.js +57 -0
- package/src/ChangePasswordFeedback.js +44 -0
- package/src/ChangePasswordFeedback.scss +43 -0
- package/src/ChangePasswordForm.js +203 -0
- package/src/ChangePasswordForm.scss +44 -0
- package/stories/change-password.stories.tsx +186 -0
- package/tests/ChangePasswordFeedback.test.js +76 -0
- package/tests/ChangePasswordForm.test.js +272 -0
- package/tsconfig.json +5 -0
- package/tsconfig.spec.json +10 -0
- package/types/ChangePassword.d.ts +45 -0
- package/types/ChangePasswordFeedback.d.ts +4 -0
- package/types/ChangePasswordForm.d.ts +14 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
|
+
|
|
5
|
+
# 0.1.0 (2023-07-20)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **change-password:** documentation fixes ([7cfa372](https://github.com/Availity/availity-react/commit/7cfa372ec65c002ecfa0e57c1025207e6bac2d05))
|
|
11
|
+
* rename confirmPassword to confirmNewPassword, fix icon styling ([75b8167](https://github.com/Availity/availity-react/commit/75b8167416adecea2ef0b9db73875906c93a851a))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* add change password component ([47e7041](https://github.com/Availity/availity-react/commit/47e70411c459a0651e60c605bffe2ccfc6d0e872))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Changelog
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @availity/change-password
|
|
2
|
+
|
|
3
|
+
> Easy to use change password component
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@availity/change-password)
|
|
6
|
+
[](https://www.npmjs.com/package/@availity/change-password)
|
|
7
|
+
[](https://github.com/Availity/availity-react/blob/master/packages/change-password/package.json)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### NPM
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @availity/change-password
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Yarn
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
yarn add @availity/change-password
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
Check out more documentation at [availity.github.io](https://availity.github.io/availity-react/components/change-password/index)
|
|
26
|
+
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import ChangePassword, { useChangePasswordContext, ChangePasswordContext } from './types/ChangePassword';
|
|
2
|
+
import ChangePasswordForm from './types/ChangePasswordForm';
|
|
3
|
+
|
|
4
|
+
export default ChangePassword;
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
ChangePasswordForm,
|
|
8
|
+
useChangePasswordContext,
|
|
9
|
+
ChangePasswordContext,
|
|
10
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import ChangePassword, { useChangePasswordContext, ChangePasswordContext } from './src/ChangePassword';
|
|
2
|
+
import ChangePasswordForm from './src/ChangePasswordForm';
|
|
3
|
+
|
|
4
|
+
export default ChangePassword;
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
useChangePasswordContext,
|
|
8
|
+
ChangePasswordContext,
|
|
9
|
+
ChangePasswordForm,
|
|
10
|
+
};
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@availity/change-password",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Easy to use change-password components",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"availity",
|
|
8
|
+
"change-password"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://availity.github.io/availity-react/components/change-password/index",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Availity/availity-react/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/Availity/availity-react.git",
|
|
17
|
+
"directory": "packages/change-password"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Tyson Warner <tyson.warner@availity.com>",
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"types": "index.d.ts",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"publish": "yarn npm publish --tolerate-republish --access public",
|
|
25
|
+
"publish:canary": "yarn npm publish --access public --tag canary"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@availity/form": "1.7.3",
|
|
29
|
+
"@availity/icon": "0.11.3",
|
|
30
|
+
"classnames": "^2.3.1",
|
|
31
|
+
"dayjs": "^1.11.2",
|
|
32
|
+
"lodash": "^4.17.21",
|
|
33
|
+
"prop-types": "^15.8.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@availity/api-axios": "^7.0.1",
|
|
37
|
+
"axios": "^0.21.4",
|
|
38
|
+
"react": "^17.0.2",
|
|
39
|
+
"react-dom": "^17.0.2",
|
|
40
|
+
"reactstrap": "^8.10.1"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@availity/api-axios": "^6.0.0 || ^7.0.0",
|
|
44
|
+
"axios": "^0.21.1",
|
|
45
|
+
"react": ">=16.8.6",
|
|
46
|
+
"react-query": "^3.29.0",
|
|
47
|
+
"reactstrap": "^8.0.0"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": "packages/change-password",
|
|
3
|
+
"projectType": "library",
|
|
4
|
+
"targets": {
|
|
5
|
+
"test": {
|
|
6
|
+
"executor": "@nrwl/jest:jest",
|
|
7
|
+
"outputs": ["coverage/change-password"],
|
|
8
|
+
"options": { "jestConfig": "packages/change-password/jest.config.js", "passWithNoTests": true }
|
|
9
|
+
},
|
|
10
|
+
"version": {
|
|
11
|
+
"executor": "@jscutlery/semver:version",
|
|
12
|
+
"options": {
|
|
13
|
+
"preset": "angular",
|
|
14
|
+
"commitMessageFormat": "chore(${projectName}): release version ${version} [skip ci]",
|
|
15
|
+
"tagPrefix": "@availity/${projectName}@",
|
|
16
|
+
"baseBranch": "master"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"lint": {
|
|
20
|
+
"executor": "@nrwl/linter:eslint",
|
|
21
|
+
"options": {
|
|
22
|
+
"eslintConfig": ".eslintrc.yaml",
|
|
23
|
+
"lintFilePatterns": ["packages/change-password/**/*.{js,ts}"],
|
|
24
|
+
"silent": false,
|
|
25
|
+
"fix": false,
|
|
26
|
+
"cache": true,
|
|
27
|
+
"cacheLocation": "./node_modules/.cache/change-password/.eslintcache",
|
|
28
|
+
"maxWarnings": -1,
|
|
29
|
+
"quiet": false,
|
|
30
|
+
"noEslintrc": false,
|
|
31
|
+
"hasTypeAwareRules": true,
|
|
32
|
+
"cacheStrategy": "metadata"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useRef } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import AvApi, { AvMicroserviceApi } from '@availity/api-axios';
|
|
4
|
+
|
|
5
|
+
export const ChangePasswordContext = createContext();
|
|
6
|
+
|
|
7
|
+
export const useChangePasswordContext = () => useContext(ChangePasswordContext);
|
|
8
|
+
|
|
9
|
+
const ChangePassword = ({ resource, schema, conditions, children }) => {
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
const [success, setSuccess] = useState(null);
|
|
13
|
+
|
|
14
|
+
const currentPasswordIconRef = useRef(null);
|
|
15
|
+
const newPasswordIconRef = useRef(null);
|
|
16
|
+
const confirmNewPasswordIconRef = useRef(null);
|
|
17
|
+
|
|
18
|
+
const [currentPasswordVisible, setCurrentPasswordVisible] = useState(false);
|
|
19
|
+
const [newPasswordVisible, setNewPasswordVisible] = useState(false);
|
|
20
|
+
const [confirmNewPasswordVisible, setConfirmNewPasswordVisible] = useState(false);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<ChangePasswordContext.Provider
|
|
24
|
+
value={{
|
|
25
|
+
loading,
|
|
26
|
+
setLoading,
|
|
27
|
+
error,
|
|
28
|
+
setError,
|
|
29
|
+
success,
|
|
30
|
+
setSuccess,
|
|
31
|
+
resource,
|
|
32
|
+
schema,
|
|
33
|
+
conditions,
|
|
34
|
+
currentPasswordIconRef,
|
|
35
|
+
newPasswordIconRef,
|
|
36
|
+
confirmNewPasswordIconRef,
|
|
37
|
+
currentPasswordVisible,
|
|
38
|
+
setCurrentPasswordVisible,
|
|
39
|
+
newPasswordVisible,
|
|
40
|
+
setNewPasswordVisible,
|
|
41
|
+
confirmNewPasswordVisible,
|
|
42
|
+
setConfirmNewPasswordVisible,
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</ChangePasswordContext.Provider>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
ChangePassword.propTypes = {
|
|
51
|
+
resource: PropTypes.oneOfType([PropTypes.instanceOf(AvApi), PropTypes.instanceOf(AvMicroserviceApi)]).isRequired,
|
|
52
|
+
schema: PropTypes.object,
|
|
53
|
+
conditions: PropTypes.arrayOf(PropTypes.shape({ message: PropTypes.string, passes: PropTypes.func })),
|
|
54
|
+
children: PropTypes.node,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default ChangePassword;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Icon from '@availity/icon';
|
|
3
|
+
import { useFormikContext } from 'formik';
|
|
4
|
+
import { useChangePasswordContext } from './ChangePassword';
|
|
5
|
+
import './ChangePasswordFeedback.scss';
|
|
6
|
+
|
|
7
|
+
const ChangePasswordFeedback = () => {
|
|
8
|
+
const { conditions = [] } = useChangePasswordContext();
|
|
9
|
+
|
|
10
|
+
const { values, touched } = useFormikContext();
|
|
11
|
+
const { currentPassword, newPassword, confirmNewPassword } = values;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div>
|
|
15
|
+
<strong>Your new password must contain</strong>
|
|
16
|
+
<div>
|
|
17
|
+
{conditions.map(({ message, passes }) => {
|
|
18
|
+
const conditionPasses = passes({
|
|
19
|
+
currentPassword,
|
|
20
|
+
newPassword,
|
|
21
|
+
confirmNewPassword,
|
|
22
|
+
currentPasswordTouched: currentPassword || touched.currentPassword === true,
|
|
23
|
+
newPasswordTouched: newPassword || touched.newPassword === true,
|
|
24
|
+
confirmNewPasswordTouched: confirmNewPassword || touched.confirmNewPassword === true,
|
|
25
|
+
});
|
|
26
|
+
return (
|
|
27
|
+
<div key={message} className="passwordElement pt-3">
|
|
28
|
+
{conditionPasses === true ? (
|
|
29
|
+
<Icon data-testid={`ok-circle-${message}`} name="ok-circle" className="complies" />
|
|
30
|
+
) : conditionPasses === false ? (
|
|
31
|
+
<Icon data-testid={`cancel-circle-${message}`} name="cancel-circle" className="text-danger" />
|
|
32
|
+
) : (
|
|
33
|
+
<Icon data-testid={`circle-empty-${message}`} name="circle-empty" />
|
|
34
|
+
)}
|
|
35
|
+
{message}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
})}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default ChangePasswordFeedback;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
.passwordElement {
|
|
2
|
+
padding-bottom: 3px;
|
|
3
|
+
}
|
|
4
|
+
.complies {
|
|
5
|
+
color: #29a232;
|
|
6
|
+
vertical-align: middle;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.password-input {
|
|
10
|
+
background-image: none !important;
|
|
11
|
+
}
|
|
12
|
+
.password-with-icon {
|
|
13
|
+
position: relative;
|
|
14
|
+
.icon {
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 27px;
|
|
17
|
+
right: 0;
|
|
18
|
+
margin: 0.5rem 0.75rem;
|
|
19
|
+
padding: 1px;
|
|
20
|
+
outline: none;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// get rid of Edge browser's show/hide password eye it adds automatically
|
|
24
|
+
::-ms-reveal {
|
|
25
|
+
display: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#current-password-eye::before,
|
|
29
|
+
#new-password-eye::before,
|
|
30
|
+
#confirm-new-password-eye::before {
|
|
31
|
+
border: solid 1px rgb(255, 255, 255, 0);
|
|
32
|
+
border-radius: 15%;
|
|
33
|
+
padding: 2px;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
}
|
|
36
|
+
#current-password-eye:focus::before,
|
|
37
|
+
#new-password-eye:focus::before,
|
|
38
|
+
#confirm-new-password-eye:focus::before {
|
|
39
|
+
border: solid 1px rgb(116, 116, 116);
|
|
40
|
+
border-radius: 15%;
|
|
41
|
+
padding: 2px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import BlockUI from 'react-block-ui';
|
|
4
|
+
import Icon from '@availity/icon';
|
|
5
|
+
import { Alert, Button, CardBody, CardFooter, Col, Row } from 'reactstrap';
|
|
6
|
+
import { Form, Field } from '@availity/form';
|
|
7
|
+
import { useChangePasswordContext } from './ChangePassword';
|
|
8
|
+
import ChangePasswordFeedback from './ChangePasswordFeedback';
|
|
9
|
+
import './ChangePasswordForm.scss';
|
|
10
|
+
|
|
11
|
+
const ChangePasswordForm = ({
|
|
12
|
+
onHandleSubmit,
|
|
13
|
+
onError,
|
|
14
|
+
additionalButtons,
|
|
15
|
+
header,
|
|
16
|
+
maxLength = 30,
|
|
17
|
+
currentPasswordProps,
|
|
18
|
+
newPasswordProps,
|
|
19
|
+
confirmNewPasswordProps,
|
|
20
|
+
}) => {
|
|
21
|
+
const {
|
|
22
|
+
loading,
|
|
23
|
+
setLoading,
|
|
24
|
+
error,
|
|
25
|
+
setError,
|
|
26
|
+
success,
|
|
27
|
+
setSuccess,
|
|
28
|
+
resource,
|
|
29
|
+
schema,
|
|
30
|
+
currentPasswordIconRef,
|
|
31
|
+
newPasswordIconRef,
|
|
32
|
+
confirmNewPasswordIconRef,
|
|
33
|
+
currentPasswordVisible,
|
|
34
|
+
setCurrentPasswordVisible,
|
|
35
|
+
newPasswordVisible,
|
|
36
|
+
setNewPasswordVisible,
|
|
37
|
+
confirmNewPasswordVisible,
|
|
38
|
+
setConfirmNewPasswordVisible,
|
|
39
|
+
} = useChangePasswordContext();
|
|
40
|
+
|
|
41
|
+
const handleSubmit = async ({ currentPassword, newPassword }) => {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
try {
|
|
44
|
+
const result = await resource.changePassword({ currentPassword, newPassword });
|
|
45
|
+
setSuccess('Your password was successfully changed');
|
|
46
|
+
if (onHandleSubmit) {
|
|
47
|
+
await onHandleSubmit({ result });
|
|
48
|
+
}
|
|
49
|
+
setLoading(false);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
setError('An error occurred changing your password');
|
|
52
|
+
if (onError) {
|
|
53
|
+
await onError({ error });
|
|
54
|
+
}
|
|
55
|
+
setLoading(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<BlockUI blocking={loading}>
|
|
61
|
+
<Form
|
|
62
|
+
initialValues={{ currentPassword: '', newPassword: '', confirmNewPassword: '' }}
|
|
63
|
+
validationSchema={schema}
|
|
64
|
+
onSubmit={handleSubmit}
|
|
65
|
+
>
|
|
66
|
+
<CardBody>
|
|
67
|
+
{header}
|
|
68
|
+
<Row>
|
|
69
|
+
<Col>
|
|
70
|
+
<Alert isOpen={!!error} color="danger" toggle={() => setError(null)}>
|
|
71
|
+
{error}
|
|
72
|
+
</Alert>
|
|
73
|
+
<Alert isOpen={!!success} color="success" toggle={() => setSuccess(null)}>
|
|
74
|
+
{success}
|
|
75
|
+
</Alert>
|
|
76
|
+
<div className="password-with-icon">
|
|
77
|
+
<Field
|
|
78
|
+
name="currentPassword"
|
|
79
|
+
data-testid="current-password-input"
|
|
80
|
+
label="Current Password"
|
|
81
|
+
maxLength={maxLength}
|
|
82
|
+
placeholder="Input your current password"
|
|
83
|
+
type={currentPasswordVisible ? 'text' : 'password'}
|
|
84
|
+
{...currentPasswordProps}
|
|
85
|
+
/>
|
|
86
|
+
<Icon
|
|
87
|
+
name={`eye${currentPasswordVisible ? '' : '-off'}`}
|
|
88
|
+
data-testid="current-password-icon"
|
|
89
|
+
id="current-password-eye"
|
|
90
|
+
role="button"
|
|
91
|
+
label="password-visibility"
|
|
92
|
+
onMouseDown={(e) => {
|
|
93
|
+
currentPasswordIconRef?.current?.focus();
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
setCurrentPasswordVisible(!currentPasswordVisible);
|
|
96
|
+
}}
|
|
97
|
+
onKeyDown={(e) => {
|
|
98
|
+
if (e.keyCode === 13 || e.keyCode === 32) {
|
|
99
|
+
setCurrentPasswordVisible(!currentPasswordVisible);
|
|
100
|
+
}
|
|
101
|
+
}}
|
|
102
|
+
tabIndex={0}
|
|
103
|
+
aria-label={currentPasswordVisible ? 'Hide Password' : 'Show Password'}
|
|
104
|
+
ref={currentPasswordIconRef}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="password-with-icon">
|
|
109
|
+
<Field
|
|
110
|
+
name="newPassword"
|
|
111
|
+
data-testid="new-password-input"
|
|
112
|
+
label="New Password"
|
|
113
|
+
maxLength={maxLength}
|
|
114
|
+
placeholder="Input your new password"
|
|
115
|
+
className="password-input"
|
|
116
|
+
type={newPasswordVisible ? 'text' : 'password'}
|
|
117
|
+
{...newPasswordProps}
|
|
118
|
+
/>
|
|
119
|
+
<Icon
|
|
120
|
+
name={`eye${newPasswordVisible ? '' : '-off'}`}
|
|
121
|
+
data-testid="new-password-icon"
|
|
122
|
+
id="new-password-eye"
|
|
123
|
+
role="button"
|
|
124
|
+
label="password-visibility"
|
|
125
|
+
onMouseDown={(e) => {
|
|
126
|
+
newPasswordIconRef?.current?.focus();
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
setNewPasswordVisible(!newPasswordVisible);
|
|
129
|
+
}}
|
|
130
|
+
onKeyDown={(e) => {
|
|
131
|
+
if (e.keyCode === 13 || e.keyCode === 32) {
|
|
132
|
+
setNewPasswordVisible(!newPasswordVisible);
|
|
133
|
+
}
|
|
134
|
+
}}
|
|
135
|
+
tabIndex={0}
|
|
136
|
+
aria-label={newPasswordVisible ? 'Hide Password' : 'Show Password'}
|
|
137
|
+
ref={newPasswordIconRef}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="password-with-icon">
|
|
142
|
+
<Field
|
|
143
|
+
name="confirmNewPassword"
|
|
144
|
+
data-testid="confirm-new-password-input"
|
|
145
|
+
label="Confirm New Password"
|
|
146
|
+
maxLength={maxLength}
|
|
147
|
+
placeholder="Confirm your new password"
|
|
148
|
+
className="password-input"
|
|
149
|
+
type={confirmNewPasswordVisible ? 'text' : 'password'}
|
|
150
|
+
{...confirmNewPasswordProps}
|
|
151
|
+
/>
|
|
152
|
+
<Icon
|
|
153
|
+
name={`eye${confirmNewPasswordVisible ? '' : '-off'}`}
|
|
154
|
+
data-testid="confirm-new-password-icon"
|
|
155
|
+
id="confirm-new-password-eye"
|
|
156
|
+
role="button"
|
|
157
|
+
label="password-visibility"
|
|
158
|
+
onMouseDown={(e) => {
|
|
159
|
+
confirmNewPasswordIconRef?.current?.focus();
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
setConfirmNewPasswordVisible(!confirmNewPasswordVisible);
|
|
162
|
+
}}
|
|
163
|
+
onKeyDown={(e) => {
|
|
164
|
+
if (e.keyCode === 13 || e.keyCode === 32) {
|
|
165
|
+
setConfirmNewPasswordVisible(!confirmNewPasswordVisible);
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
tabIndex={0}
|
|
169
|
+
aria-label={confirmNewPasswordVisible ? 'Hide Password' : 'Show Password'}
|
|
170
|
+
ref={confirmNewPasswordIconRef}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
</Col>
|
|
174
|
+
<Col>
|
|
175
|
+
<ChangePasswordFeedback />
|
|
176
|
+
</Col>
|
|
177
|
+
</Row>
|
|
178
|
+
</CardBody>
|
|
179
|
+
<CardFooter>
|
|
180
|
+
<div className="text-left">
|
|
181
|
+
<Button className="mx-3" color="primary" type="submit" data-testid="submit-btn">
|
|
182
|
+
Submit
|
|
183
|
+
</Button>
|
|
184
|
+
{additionalButtons}
|
|
185
|
+
</div>
|
|
186
|
+
</CardFooter>
|
|
187
|
+
</Form>
|
|
188
|
+
</BlockUI>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
ChangePasswordForm.propTypes = {
|
|
193
|
+
onHandleSubmit: PropTypes.func,
|
|
194
|
+
onError: PropTypes.func,
|
|
195
|
+
additionalButtons: PropTypes.node,
|
|
196
|
+
header: PropTypes.node,
|
|
197
|
+
maxLength: PropTypes.number,
|
|
198
|
+
currentPasswordProps: PropTypes.object,
|
|
199
|
+
newPasswordProps: PropTypes.object,
|
|
200
|
+
confirmNewPasswordProps: PropTypes.object,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export default ChangePasswordForm;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.passwordElement {
|
|
2
|
+
padding-bottom: 3px;
|
|
3
|
+
}
|
|
4
|
+
.complies {
|
|
5
|
+
color: #29a232;
|
|
6
|
+
vertical-align: middle;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.password-input {
|
|
10
|
+
background-image: none !important;
|
|
11
|
+
}
|
|
12
|
+
.password-with-icon {
|
|
13
|
+
position: relative;
|
|
14
|
+
.icon {
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 27px;
|
|
17
|
+
right: 0;
|
|
18
|
+
margin: 0.5rem 0.75rem;
|
|
19
|
+
padding: 1px;
|
|
20
|
+
outline: none;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// get rid of Edge browser's show/hide password eye it adds automatically
|
|
24
|
+
::-ms-reveal {
|
|
25
|
+
display: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#current-password-eye::before,
|
|
29
|
+
#new-password-eye::before,
|
|
30
|
+
#confirm-new-password-eye::before {
|
|
31
|
+
border: solid 1px rgb(255, 255, 255, 0);
|
|
32
|
+
border-radius: 15%;
|
|
33
|
+
padding: 2px;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
}
|
|
36
|
+
#current-password-eye:focus::before,
|
|
37
|
+
#new-password-eye:focus::before,
|
|
38
|
+
#confirm-new-password-eye:focus::before {
|
|
39
|
+
border: solid 1px rgb(116, 116, 116);
|
|
40
|
+
border-radius: 15%;
|
|
41
|
+
padding: 2px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Story, Meta } from '@storybook/react';
|
|
3
|
+
import { ArgsTable } from '@storybook/addon-docs';
|
|
4
|
+
import * as yup from 'yup';
|
|
5
|
+
import AvApi from '@availity/api-axios';
|
|
6
|
+
import ChangePassword, { ChangePasswordForm } from '..';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: 'Components/Change Password',
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: `This is the provider component needed for @availity/change-password components to work.
|
|
14
|
+
All @availity/change-password components must be children of a ChangePassword provider.
|
|
15
|
+
The change-password provider is used manage state surrounding the change-password component`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} as Meta;
|
|
20
|
+
|
|
21
|
+
const me = { userId: 'youguessedme' };
|
|
22
|
+
|
|
23
|
+
class AvChangePasswordApi extends AvApi {
|
|
24
|
+
constructor(config) {
|
|
25
|
+
super({
|
|
26
|
+
name: 'change-password',
|
|
27
|
+
...config,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
changePassword() {
|
|
32
|
+
window.alert('Your password change was submitted');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Default: Story = () => {
|
|
37
|
+
const resource = new AvChangePasswordApi();
|
|
38
|
+
|
|
39
|
+
const [submitted, setSubmitted] = useState(false);
|
|
40
|
+
|
|
41
|
+
const onHandleSubmit = () => {
|
|
42
|
+
setSubmitted(true);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const changePasswordSchema = yup.object().shape({
|
|
46
|
+
currentPassword: yup.string().required('Input your current password'),
|
|
47
|
+
newPassword: yup
|
|
48
|
+
.string()
|
|
49
|
+
.min(8, 'Your password must be between 8 and 15 characters')
|
|
50
|
+
.max(15, 'Your password must be between 8 and 15 characters')
|
|
51
|
+
.required('Input your current password')
|
|
52
|
+
.matches(
|
|
53
|
+
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?!.*\s)(?=.*[!"#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-])[\w!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~-]{8,15}$/,
|
|
54
|
+
'Input your current password'
|
|
55
|
+
)
|
|
56
|
+
.test('passDoesntContainUserId', 'Input your current password', (value) => {
|
|
57
|
+
const passDoesntContainUserId = new RegExp(`^((?!${me.userId}).)*$`).test(value); // true when password doesnt contain userId
|
|
58
|
+
// fail test with false when the userId is blank, since we cant really check if the pass contains it if its not there yet
|
|
59
|
+
return me.userId ? passDoesntContainUserId : false;
|
|
60
|
+
}),
|
|
61
|
+
confirmNewPassword: yup
|
|
62
|
+
.string()
|
|
63
|
+
.test('passMatches', 'Passwords do not match', function (value) {
|
|
64
|
+
const { resolve } = this;
|
|
65
|
+
const password = resolve(yup.ref('newPassword'));
|
|
66
|
+
const passwordsMatch = password === value; // true when confirmNewPassword === password
|
|
67
|
+
return password ? passwordsMatch : false;
|
|
68
|
+
})
|
|
69
|
+
.min(8, 'Input your current password')
|
|
70
|
+
.max(15, 'Input your current password')
|
|
71
|
+
.required('Input your current password')
|
|
72
|
+
.matches(
|
|
73
|
+
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?!.*\s)(?=.*[!"#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-])[\w!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~-]{8,15}$/,
|
|
74
|
+
'Input your current password'
|
|
75
|
+
)
|
|
76
|
+
.test('passDoesntContainUserId', 'Input your current password', (value) => {
|
|
77
|
+
const passDoesntContainUserId = new RegExp(`^((?!${me.userId}).)*$`).test(value); // true when password doesnt contain userId
|
|
78
|
+
// fail test with false when the userId is blank, since we cant really check if the pass contains it if its not there yet
|
|
79
|
+
return me.userId ? passDoesntContainUserId : false;
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const conditions = [
|
|
84
|
+
{
|
|
85
|
+
message: 'Have 8 to 15 characters',
|
|
86
|
+
passes: ({ newPassword, newPasswordTouched }) => {
|
|
87
|
+
if (new RegExp(/^.{8,15}$/).test(newPassword)) {
|
|
88
|
+
if (newPasswordTouched) return true;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (submitted) return false;
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
message: 'Have a number',
|
|
97
|
+
passes: ({ newPassword, newPasswordTouched }) => {
|
|
98
|
+
if (new RegExp(/\d/).test(newPassword)) {
|
|
99
|
+
if (newPasswordTouched) return true;
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (submitted) return false;
|
|
103
|
+
return null;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
message: 'Have an uppercase letter',
|
|
108
|
+
passes: ({ newPassword, newPasswordTouched }) => {
|
|
109
|
+
if (new RegExp('[A-Z]').test(newPassword)) {
|
|
110
|
+
if (newPasswordTouched) return true;
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (submitted) return false;
|
|
114
|
+
return null;
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
message: 'Have a lowercase letter',
|
|
119
|
+
passes: ({ newPassword, newPasswordTouched }) => {
|
|
120
|
+
if (new RegExp('[a-z]').test(newPassword)) {
|
|
121
|
+
if (newPasswordTouched) return true;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (submitted) return false;
|
|
125
|
+
return null;
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
message: 'Have a special character',
|
|
130
|
+
passes: ({ newPassword, newPasswordTouched }) => {
|
|
131
|
+
if (new RegExp(/[!"#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]/).test(newPassword)) {
|
|
132
|
+
if (newPasswordTouched) return true;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (submitted) return false;
|
|
136
|
+
return null;
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
message: 'Not contain your user Id',
|
|
141
|
+
passes: ({ newPassword, newPasswordTouched }) => {
|
|
142
|
+
if (new RegExp(`^((?!${me.userId}).)*$`).test(newPassword)) {
|
|
143
|
+
if (newPasswordTouched) return true;
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (submitted) return false;
|
|
147
|
+
return null;
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
message: 'Not contain spaces',
|
|
152
|
+
passes: ({ newPassword, newPasswordTouched }) => {
|
|
153
|
+
if (new RegExp('^\\S*$').test(newPassword)) {
|
|
154
|
+
if (newPasswordTouched) return true;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (submitted) return false;
|
|
158
|
+
return null;
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<ChangePassword resource={resource} schema={changePasswordSchema} conditions={conditions}>
|
|
165
|
+
<ChangePasswordForm onHandleSubmit={onHandleSubmit} />
|
|
166
|
+
</ChangePassword>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
Default.storyName = 'default';
|
|
171
|
+
|
|
172
|
+
export const ChangePasswordProps: Story = () => (
|
|
173
|
+
<>
|
|
174
|
+
<h4>Availity Props</h4>
|
|
175
|
+
<h5>ChangePassword</h5>
|
|
176
|
+
<ArgsTable of={ChangePassword} />
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
export const ChangePasswordFormProps: Story = () => (
|
|
181
|
+
<>
|
|
182
|
+
<h4>Availity Props</h4>
|
|
183
|
+
<h5>ChangePasswordForm</h5>
|
|
184
|
+
<ArgsTable of={ChangePasswordForm} />
|
|
185
|
+
</>
|
|
186
|
+
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent, cleanup } from '@testing-library/react';
|
|
3
|
+
import ChangePassword, { ChangePasswordForm } from '..';
|
|
4
|
+
|
|
5
|
+
jest.mock('@availity/api-axios');
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
cleanup();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('ChangePasswordFeedback', () => {
|
|
13
|
+
it('renders change password feedback', async () => {
|
|
14
|
+
const me = { userId: 'twarner' };
|
|
15
|
+
const conditions = [
|
|
16
|
+
{
|
|
17
|
+
message: 'Have 8 to 16 characters',
|
|
18
|
+
passes: ({ newPassword }) => new RegExp(/^.{8,16}$/).test(newPassword),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
message: 'Have a number',
|
|
22
|
+
passes: ({ newPassword }) => new RegExp(/\d/).test(newPassword),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
message: 'Have an uppercase letter',
|
|
26
|
+
passes: ({ newPassword }) => new RegExp('[A-Z]').test(newPassword),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
message: 'Have a lowercase letter',
|
|
30
|
+
passes: ({ newPassword }) => new RegExp('[a-z]').test(newPassword),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
message: 'Have a special character',
|
|
34
|
+
passes: ({ newPassword }) => new RegExp(/[!"#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]/).test(newPassword),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
message: 'Not contain your user Id',
|
|
38
|
+
passes: ({ newPassword }) => new RegExp(`^((?!${me.userId}).)*$`).test(newPassword),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
message: 'Not contain spaces',
|
|
42
|
+
passes: ({ newPassword }) => new RegExp('^\\S*$').test(newPassword),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
message: 'This one should always display the empty circle',
|
|
46
|
+
passes: () => null,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const resource = { changePassword: jest.fn() };
|
|
51
|
+
const { getByTestId } = render(
|
|
52
|
+
<ChangePassword resource={resource} conditions={conditions}>
|
|
53
|
+
<ChangePasswordForm />
|
|
54
|
+
</ChangePassword>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const newPasswordInput = getByTestId('new-password-input');
|
|
58
|
+
|
|
59
|
+
await fireEvent.change(newPasswordInput, {
|
|
60
|
+
target: {
|
|
61
|
+
name: 'newPassword',
|
|
62
|
+
value: 'twarnerpassword1',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Ensure appropriate circles render
|
|
67
|
+
expect(getByTestId('ok-circle-Have 8 to 16 characters')).toBeDefined();
|
|
68
|
+
expect(getByTestId('ok-circle-Have a number')).toBeDefined();
|
|
69
|
+
expect(getByTestId('cancel-circle-Have an uppercase letter')).toBeDefined();
|
|
70
|
+
expect(getByTestId('ok-circle-Have a lowercase letter')).toBeDefined();
|
|
71
|
+
expect(getByTestId('cancel-circle-Have a special character')).toBeDefined();
|
|
72
|
+
expect(getByTestId('cancel-circle-Not contain your user Id')).toBeDefined();
|
|
73
|
+
expect(getByTestId('ok-circle-Not contain spaces')).toBeDefined();
|
|
74
|
+
expect(getByTestId('circle-empty-This one should always display the empty circle')).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, waitFor, fireEvent, cleanup } from '@testing-library/react';
|
|
3
|
+
import ChangePassword, { ChangePasswordForm, useChangePasswordContext } from '..';
|
|
4
|
+
|
|
5
|
+
jest.mock('@availity/api-axios');
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
cleanup();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('ChangePasswordForm', () => {
|
|
13
|
+
it('renders custom header', async () => {
|
|
14
|
+
const header = 'This is a custom header';
|
|
15
|
+
const { getByText } = render(
|
|
16
|
+
<ChangePassword>
|
|
17
|
+
<ChangePasswordForm header={header} />
|
|
18
|
+
</ChangePassword>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(getByText('This is a custom header')).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('submits api request with new password', async () => {
|
|
25
|
+
const resource = {
|
|
26
|
+
changePassword: jest.fn().mockResolvedValue('Your password was successfully updated'),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const onHandleSubmit = jest.fn();
|
|
30
|
+
|
|
31
|
+
const { getByTestId, getByText } = render(
|
|
32
|
+
<ChangePassword resource={resource}>
|
|
33
|
+
<ChangePasswordForm onHandleSubmit={onHandleSubmit} />
|
|
34
|
+
</ChangePassword>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const currentPasswordInput = getByTestId('current-password-input');
|
|
38
|
+
const newPasswordInput = getByTestId('new-password-input');
|
|
39
|
+
const confirmNewPasswordInput = getByTestId('confirm-new-password-input');
|
|
40
|
+
|
|
41
|
+
await fireEvent.change(currentPasswordInput, {
|
|
42
|
+
target: {
|
|
43
|
+
name: 'currentPassword',
|
|
44
|
+
value: 'my_current_password',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await fireEvent.change(newPasswordInput, {
|
|
49
|
+
target: {
|
|
50
|
+
name: 'newPassword',
|
|
51
|
+
value: 'my_new_password',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await fireEvent.change(confirmNewPasswordInput, {
|
|
56
|
+
target: {
|
|
57
|
+
name: 'confirmNewPassword',
|
|
58
|
+
value: 'my_new_password',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await fireEvent.click(getByTestId('submit-btn'));
|
|
63
|
+
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
// Test that resource was called
|
|
66
|
+
expect(resource.changePassword).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(resource.changePassword.mock.calls[0][0].currentPassword).toBe('my_current_password');
|
|
68
|
+
expect(resource.changePassword.mock.calls[0][0].newPassword).toBe('my_new_password');
|
|
69
|
+
|
|
70
|
+
// Test that onHandleSubmit was called
|
|
71
|
+
expect(onHandleSubmit).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(onHandleSubmit.mock.calls[0][0].result).toBe('Your password was successfully updated');
|
|
73
|
+
|
|
74
|
+
expect(getByText('Your password was successfully changed')).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('renders custom on success message', async () => {
|
|
79
|
+
const resource = {
|
|
80
|
+
changePassword: jest.fn().mockResolvedValue('Your password was successfully updated'),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const CustomPasswordComponent = () => {
|
|
84
|
+
const { setSuccess } = useChangePasswordContext();
|
|
85
|
+
|
|
86
|
+
const onHandleSubmit = () => {
|
|
87
|
+
setSuccess('Congratulations! Your password was successfully changed.');
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return <ChangePasswordForm onHandleSubmit={onHandleSubmit} />;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const { getByTestId, getByText } = render(
|
|
94
|
+
<ChangePassword resource={resource}>
|
|
95
|
+
<CustomPasswordComponent />
|
|
96
|
+
</ChangePassword>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const currentPasswordInput = getByTestId('current-password-input');
|
|
100
|
+
const newPasswordInput = getByTestId('new-password-input');
|
|
101
|
+
const confirmNewPasswordInput = getByTestId('confirm-new-password-input');
|
|
102
|
+
|
|
103
|
+
await fireEvent.change(currentPasswordInput, {
|
|
104
|
+
target: {
|
|
105
|
+
name: 'currentPassword',
|
|
106
|
+
value: 'my_current_password',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await fireEvent.change(newPasswordInput, {
|
|
111
|
+
target: {
|
|
112
|
+
name: 'newPassword',
|
|
113
|
+
value: 'my_new_password',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await fireEvent.change(confirmNewPasswordInput, {
|
|
118
|
+
target: {
|
|
119
|
+
name: 'confirmNewPassword',
|
|
120
|
+
value: 'my_new_password',
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await fireEvent.click(getByTestId('submit-btn'));
|
|
125
|
+
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
// Test that resource was called
|
|
128
|
+
expect(resource.changePassword).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(resource.changePassword.mock.calls[0][0].currentPassword).toBe('my_current_password');
|
|
130
|
+
expect(resource.changePassword.mock.calls[0][0].newPassword).toBe('my_new_password');
|
|
131
|
+
|
|
132
|
+
expect(getByText('Congratulations! Your password was successfully changed.')).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('renders error message', async () => {
|
|
137
|
+
const resource = {
|
|
138
|
+
changePassword: jest.fn().mockRejectedValue('Failed to change password'),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const { getByTestId, getByText } = render(
|
|
142
|
+
<ChangePassword resource={resource}>
|
|
143
|
+
<ChangePasswordForm />
|
|
144
|
+
</ChangePassword>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const currentPasswordInput = getByTestId('current-password-input');
|
|
148
|
+
const newPasswordInput = getByTestId('new-password-input');
|
|
149
|
+
const confirmNewPasswordInput = getByTestId('confirm-new-password-input');
|
|
150
|
+
|
|
151
|
+
await fireEvent.change(currentPasswordInput, {
|
|
152
|
+
target: {
|
|
153
|
+
name: 'currentPassword',
|
|
154
|
+
value: 'my_current_password',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await fireEvent.change(newPasswordInput, {
|
|
159
|
+
target: {
|
|
160
|
+
name: 'newPassword',
|
|
161
|
+
value: 'my_new_password',
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await fireEvent.change(confirmNewPasswordInput, {
|
|
166
|
+
target: {
|
|
167
|
+
name: 'confirmNewPassword',
|
|
168
|
+
value: 'my_new_password',
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await fireEvent.click(getByTestId('submit-btn'));
|
|
173
|
+
|
|
174
|
+
await waitFor(() => {
|
|
175
|
+
// Test that resource was called
|
|
176
|
+
expect(resource.changePassword).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(resource.changePassword.mock.calls[0][0].currentPassword).toBe('my_current_password');
|
|
178
|
+
expect(resource.changePassword.mock.calls[0][0].newPassword).toBe('my_new_password');
|
|
179
|
+
|
|
180
|
+
expect(getByText('An error occurred changing your password')).toBeDefined();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('renders custom error message', async () => {
|
|
185
|
+
const resource = {
|
|
186
|
+
changePassword: jest.fn().mockRejectedValue('Failed to change password'),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const CustomPasswordComponent = () => {
|
|
190
|
+
const { setError } = useChangePasswordContext();
|
|
191
|
+
|
|
192
|
+
const onError = () => {
|
|
193
|
+
setError('Whoopsies! We were unable to update your password.');
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return <ChangePasswordForm onError={onError} />;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const { getByTestId, getByText } = render(
|
|
200
|
+
<ChangePassword resource={resource}>
|
|
201
|
+
<CustomPasswordComponent />
|
|
202
|
+
</ChangePassword>
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const currentPasswordInput = getByTestId('current-password-input');
|
|
206
|
+
const newPasswordInput = getByTestId('new-password-input');
|
|
207
|
+
const confirmNewPasswordInput = getByTestId('confirm-new-password-input');
|
|
208
|
+
|
|
209
|
+
await fireEvent.change(currentPasswordInput, {
|
|
210
|
+
target: {
|
|
211
|
+
name: 'currentPassword',
|
|
212
|
+
value: 'my_current_password',
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await fireEvent.change(newPasswordInput, {
|
|
217
|
+
target: {
|
|
218
|
+
name: 'newPassword',
|
|
219
|
+
value: 'my_new_password',
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await fireEvent.change(confirmNewPasswordInput, {
|
|
224
|
+
target: {
|
|
225
|
+
name: 'confirmNewPassword',
|
|
226
|
+
value: 'my_new_password',
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await fireEvent.click(getByTestId('submit-btn'));
|
|
231
|
+
|
|
232
|
+
await waitFor(() => {
|
|
233
|
+
// Test that resource was called
|
|
234
|
+
expect(resource.changePassword).toHaveBeenCalledTimes(1);
|
|
235
|
+
expect(resource.changePassword.mock.calls[0][0].currentPassword).toBe('my_current_password');
|
|
236
|
+
expect(resource.changePassword.mock.calls[0][0].newPassword).toBe('my_new_password');
|
|
237
|
+
|
|
238
|
+
expect(getByText('Whoopsies! We were unable to update your password.')).toBeDefined();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('toggles whether passwords are visible', async () => {
|
|
243
|
+
const resource = {
|
|
244
|
+
changePassword: jest.fn(),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const { getByTestId } = render(
|
|
248
|
+
<ChangePassword resource={resource}>
|
|
249
|
+
<ChangePasswordForm />
|
|
250
|
+
</ChangePassword>
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const currentPasswordInput = getByTestId('current-password-input');
|
|
254
|
+
const newPasswordInput = getByTestId('new-password-input');
|
|
255
|
+
const confirmNewPasswordInput = getByTestId('confirm-new-password-input');
|
|
256
|
+
const currentPasswordIcon = getByTestId('current-password-icon');
|
|
257
|
+
const newPasswordIcon = getByTestId('new-password-icon');
|
|
258
|
+
const confirmNewPasswordIcon = getByTestId('confirm-new-password-icon');
|
|
259
|
+
|
|
260
|
+
expect(currentPasswordInput.type).toBe('password');
|
|
261
|
+
expect(newPasswordInput.type).toBe('password');
|
|
262
|
+
expect(confirmNewPasswordInput.type).toBe('password');
|
|
263
|
+
|
|
264
|
+
await fireEvent.mouseDown(currentPasswordIcon);
|
|
265
|
+
await fireEvent.mouseDown(newPasswordIcon);
|
|
266
|
+
await fireEvent.mouseDown(confirmNewPasswordIcon);
|
|
267
|
+
|
|
268
|
+
expect(currentPasswordInput.type).toBe('text');
|
|
269
|
+
expect(newPasswordInput.type).toBe('text');
|
|
270
|
+
expect(confirmNewPasswordInput.type).toBe('text');
|
|
271
|
+
});
|
|
272
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface Condition {
|
|
2
|
+
message: string;
|
|
3
|
+
passes: (passwords: {
|
|
4
|
+
currentPassword: string;
|
|
5
|
+
newPassword: string;
|
|
6
|
+
confirmNewPassword: string;
|
|
7
|
+
currentPasswordTouched: boolean;
|
|
8
|
+
newPasswordTouched: boolean;
|
|
9
|
+
confirmNewPasswordTouched: boolean;
|
|
10
|
+
}) => boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ChangePasswordProps {
|
|
14
|
+
resource: any;
|
|
15
|
+
schema?: object;
|
|
16
|
+
conditions?: Condition[];
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ChangePasswordContext {
|
|
21
|
+
loading: boolean;
|
|
22
|
+
setLoading: () => boolean;
|
|
23
|
+
error: () => boolean;
|
|
24
|
+
setError: () => boolean;
|
|
25
|
+
success: () => boolean;
|
|
26
|
+
setSuccess: () => boolean;
|
|
27
|
+
resource: any;
|
|
28
|
+
schema: object;
|
|
29
|
+
conditions: Condition[];
|
|
30
|
+
currentPasswordIconRef: React.Ref;
|
|
31
|
+
newPasswordIconRef: React.Ref;
|
|
32
|
+
confirmNewPasswordIconRef: React.Ref;
|
|
33
|
+
currentPasswordVisible: boolean;
|
|
34
|
+
setCurrentPasswordVisible: () => boolean;
|
|
35
|
+
newPasswordVisible: boolean;
|
|
36
|
+
setNewPasswordVisible: () => boolean;
|
|
37
|
+
confirmNewPasswordVisible: boolean;
|
|
38
|
+
setConfirmNewPasswordVisible: () => boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare function useChangePasswordContext(): ChangePasswordContext;
|
|
42
|
+
|
|
43
|
+
declare const ChangePassword: React.FC<ChangePasswordProps>;
|
|
44
|
+
|
|
45
|
+
export default ChangePassword;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ChangePasswordFormProps {
|
|
2
|
+
onHandleSubmit?: (arg: { result: any }) => void;
|
|
3
|
+
onError?: (arg: { error: Error }) => void;
|
|
4
|
+
additionalButtons?: React.ReactNode;
|
|
5
|
+
header?: React.ReactNode;
|
|
6
|
+
maxLength?: number;
|
|
7
|
+
currentPasswordProps?: any;
|
|
8
|
+
newPasswordProps?: any;
|
|
9
|
+
confirmNewPasswordProps?: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare const ChangePasswordForm: React.FC<ChangePasswordFormProps>;
|
|
13
|
+
|
|
14
|
+
export default ChangePasswordForm;
|