@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 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
+ [![Version](https://img.shields.io/npm/v/@availity/change-password.svg?style=for-the-badge)](https://www.npmjs.com/package/@availity/change-password)
6
+ [![NPM Downloads](https://img.shields.io/npm/dt/@availity/change-password.svg?style=for-the-badge)](https://www.npmjs.com/package/@availity/change-password)
7
+ [![Dependecy Status](https://img.shields.io/librariesio/release/npm/@availity/change-password?style=for-the-badge)](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
@@ -0,0 +1,8 @@
1
+ const global = require('../../jest/global-config');
2
+
3
+ module.exports = {
4
+ ...global,
5
+ displayName: 'change-password',
6
+ coverageDirectory: '../../coverage/change-password',
7
+ };
8
+
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,5 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["."],
4
+ "exclude": ["dist", "build", "node_modules"]
5
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "module": "commonjs",
6
+ "types": ["jest", "node", "@testing-library/jest-dom"],
7
+ "allowJs": true
8
+ },
9
+ "include": ["**/*.test.js", "**/*.test.ts", "**/*.d.ts"]
10
+ }
@@ -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,4 @@
1
+ declare const ChangePasswordFeedback: React.FC<>;
2
+
3
+ export default ChangePasswordFeedback;
4
+
@@ -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;