@digihmis/esm-login-app 1.0.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.
Files changed (186) hide show
  1. package/README.md +35 -0
  2. package/__mocks__/config.mock.ts +34 -0
  3. package/__mocks__/locations.mock.ts +540 -0
  4. package/__mocks__/react-i18next.js +49 -0
  5. package/dist/1091.js +1 -0
  6. package/dist/1091.js.map +1 -0
  7. package/dist/119.js +1 -0
  8. package/dist/119.js.map +1 -0
  9. package/dist/1339.js +1 -0
  10. package/dist/1339.js.map +1 -0
  11. package/dist/1465.js +1 -0
  12. package/dist/1576.js +1 -0
  13. package/dist/1799.js +1 -0
  14. package/dist/1827.js +1 -0
  15. package/dist/204.js +1 -0
  16. package/dist/204.js.map +1 -0
  17. package/dist/2197.js +1 -0
  18. package/dist/2254.js +1 -0
  19. package/dist/2273.js +1 -0
  20. package/dist/2573.js +1 -0
  21. package/dist/2573.js.map +1 -0
  22. package/dist/2603.js +1 -0
  23. package/dist/2802.js +1 -0
  24. package/dist/3040.js +1 -0
  25. package/dist/3074.js +1 -0
  26. package/dist/3116.js +1 -0
  27. package/dist/3408.js +1 -0
  28. package/dist/3472.js +1 -0
  29. package/dist/3472.js.map +1 -0
  30. package/dist/3549.js +1 -0
  31. package/dist/3566.js +1 -0
  32. package/dist/3631.js +1 -0
  33. package/dist/3663.js +15 -0
  34. package/dist/3663.js.map +1 -0
  35. package/dist/3667.js +1 -0
  36. package/dist/3667.js.map +1 -0
  37. package/dist/3720.js +1 -0
  38. package/dist/3720.js.map +1 -0
  39. package/dist/3809.js +1 -0
  40. package/dist/3890.js +1 -0
  41. package/dist/3890.js.map +1 -0
  42. package/dist/4316.js +1 -0
  43. package/dist/4523.js +1 -0
  44. package/dist/4523.js.map +1 -0
  45. package/dist/4571.js +1 -0
  46. package/dist/4618.js +1 -0
  47. package/dist/4641.js +1 -0
  48. package/dist/466.js +1 -0
  49. package/dist/466.js.map +1 -0
  50. package/dist/4904.js +1 -0
  51. package/dist/5138.js +1 -0
  52. package/dist/5170.js +1 -0
  53. package/dist/5170.js.map +1 -0
  54. package/dist/5177.js +1 -0
  55. package/dist/5882.js +1 -0
  56. package/dist/6339.js +1 -0
  57. package/dist/6666.js +1 -0
  58. package/dist/689.js +1 -0
  59. package/dist/689.js.map +1 -0
  60. package/dist/6989.js +1 -0
  61. package/dist/6989.js.map +1 -0
  62. package/dist/7085.js +1 -0
  63. package/dist/7085.js.map +1 -0
  64. package/dist/7127.js +1 -0
  65. package/dist/720.js +1 -0
  66. package/dist/7240.js +1 -0
  67. package/dist/7240.js.map +1 -0
  68. package/dist/7329.js +1 -0
  69. package/dist/7329.js.map +1 -0
  70. package/dist/760.js +1 -0
  71. package/dist/760.js.map +1 -0
  72. package/dist/7751.js +1 -0
  73. package/dist/7874.js +1 -0
  74. package/dist/7874.js.map +1 -0
  75. package/dist/7925.js +43 -0
  76. package/dist/7925.js.map +1 -0
  77. package/dist/8037.js +1 -0
  78. package/dist/8381.js +1 -0
  79. package/dist/8474.js +1 -0
  80. package/dist/8674.js +1 -0
  81. package/dist/868.js +1 -0
  82. package/dist/8925.js +1 -0
  83. package/dist/8958.js +1 -0
  84. package/dist/8958.js.map +1 -0
  85. package/dist/900.js +1 -0
  86. package/dist/9001.js +1 -0
  87. package/dist/9001.js.map +1 -0
  88. package/dist/916.js +1 -0
  89. package/dist/918.js +1 -0
  90. package/dist/954.js +1 -0
  91. package/dist/970.js +1 -0
  92. package/dist/9754.js +1 -0
  93. package/dist/998.js +1 -0
  94. package/dist/998.js.map +1 -0
  95. package/dist/digihmis-esm-login-app.js +6 -0
  96. package/dist/digihmis-esm-login-app.js.buildmanifest.json +1704 -0
  97. package/dist/digihmis-esm-login-app.js.map +1 -0
  98. package/dist/main.js +6 -0
  99. package/dist/main.js.map +1 -0
  100. package/dist/routes.json +1 -0
  101. package/jest.config.js +20 -0
  102. package/package.json +73 -0
  103. package/rspack.config.js +1 -0
  104. package/src/background-wrapper/background-wrapper.component.tsx +58 -0
  105. package/src/background-wrapper/background-wrapper.scss +45 -0
  106. package/src/change-location-link/change-location-link.extension.tsx +32 -0
  107. package/src/change-location-link/change-location-link.scss +17 -0
  108. package/src/change-location-link/change-location-link.test.tsx +36 -0
  109. package/src/change-password/change-password-link.extension.tsx +30 -0
  110. package/src/change-password/change-password-link.test.tsx +27 -0
  111. package/src/change-password/change-password-modal.scss +11 -0
  112. package/src/change-password/change-password.component.tsx +159 -0
  113. package/src/change-password/change-password.modal.tsx +175 -0
  114. package/src/change-password/change-password.resource.ts +12 -0
  115. package/src/change-password/change-password.scss +51 -0
  116. package/src/change-password/change-password.test.tsx +53 -0
  117. package/src/config-schema.ts +166 -0
  118. package/src/declarations.d.ts +1 -0
  119. package/src/flag/flag.component.tsx +19 -0
  120. package/src/footer.component.tsx +41 -0
  121. package/src/footer.scss +105 -0
  122. package/src/index.ts +25 -0
  123. package/src/loading/loading.component.tsx +11 -0
  124. package/src/loading/loading.scss +7 -0
  125. package/src/location-picker/location-picker-view.component.tsx +174 -0
  126. package/src/location-picker/location-picker.resource.ts +111 -0
  127. package/src/location-picker/location-picker.scss +94 -0
  128. package/src/location-picker/location-picker.test.tsx +341 -0
  129. package/src/login/login.component.tsx +256 -0
  130. package/src/login/login.scss +174 -0
  131. package/src/login/login.test.tsx +289 -0
  132. package/src/login.resource.ts +147 -0
  133. package/src/logo.component.tsx +24 -0
  134. package/src/logout/logout.extension.tsx +23 -0
  135. package/src/logout/logout.scss +12 -0
  136. package/src/redirect-logout/logout.resource.ts +21 -0
  137. package/src/redirect-logout/redirect-logout.component.tsx +34 -0
  138. package/src/redirect-logout/redirect-logout.test.tsx +117 -0
  139. package/src/root.component.tsx +22 -0
  140. package/src/routes.json +63 -0
  141. package/src/setupTests.ts +15 -0
  142. package/src/test-helpers/render-with-router.tsx +17 -0
  143. package/src/types.ts +34 -0
  144. package/translations/am.json +39 -0
  145. package/translations/ar.json +41 -0
  146. package/translations/ar_SY.json +41 -0
  147. package/translations/bn.json +41 -0
  148. package/translations/cs.json +41 -0
  149. package/translations/de.json +41 -0
  150. package/translations/en.json +39 -0
  151. package/translations/en_US.json +41 -0
  152. package/translations/es.json +41 -0
  153. package/translations/es_MX.json +41 -0
  154. package/translations/fr.json +41 -0
  155. package/translations/he.json +41 -0
  156. package/translations/hi.json +41 -0
  157. package/translations/hi_IN.json +41 -0
  158. package/translations/id.json +41 -0
  159. package/translations/it.json +41 -0
  160. package/translations/ka.json +41 -0
  161. package/translations/km.json +41 -0
  162. package/translations/ku.json +41 -0
  163. package/translations/ky.json +41 -0
  164. package/translations/lg.json +41 -0
  165. package/translations/ne.json +41 -0
  166. package/translations/pl.json +41 -0
  167. package/translations/pt.json +41 -0
  168. package/translations/pt_BR.json +41 -0
  169. package/translations/qu.json +41 -0
  170. package/translations/ro_RO.json +41 -0
  171. package/translations/ru_RU.json +41 -0
  172. package/translations/si.json +41 -0
  173. package/translations/sq.json +41 -0
  174. package/translations/sw.json +39 -0
  175. package/translations/sw_KE.json +41 -0
  176. package/translations/tr.json +41 -0
  177. package/translations/tr_TR.json +41 -0
  178. package/translations/uk.json +41 -0
  179. package/translations/uz.json +41 -0
  180. package/translations/uz@Latn.json +41 -0
  181. package/translations/uz_UZ.json +41 -0
  182. package/translations/vi.json +41 -0
  183. package/translations/zh.json +41 -0
  184. package/translations/zh_CN.json +41 -0
  185. package/translations/zh_TW.json +41 -0
  186. package/tsconfig.json +25 -0
@@ -0,0 +1 @@
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.2.0"},"pages":[{"component":"root","route":"login","online":true,"offline":true},{"component":"root","route":"logout","online":true,"offline":true},{"component":"root","route":"change-password","online":true,"offline":true}],"extensions":[{"name":"location-picker","slot":"location-picker","component":"locationPicker","online":true,"offline":true},{"name":"logout-button","slot":"user-panel-bottom-slot","component":"logoutButton","online":true,"offline":true},{"name":"password-changer","slot":"user-panel-slot","component":"changePasswordLink","online":true,"offline":true},{"name":"location-changer","slot":"top-nav-info-slot","component":"changeLocationLink","online":true,"offline":true,"order":1}],"modals":[{"name":"change-password-modal","component":"changePasswordModal"}],"version":"1.0.0"}
package/jest.config.js ADDED
@@ -0,0 +1,20 @@
1
+ /** @type {import('jest').Config} */
2
+ module.exports = {
3
+ clearMocks: true,
4
+ transform: {
5
+ '^.+\\.[jt]sx?$': ['@swc/jest'],
6
+ },
7
+ moduleNameMapper: {
8
+ '@openmrs/esm-framework': '@openmrs/esm-framework/mock',
9
+ '\\.(s?css)$': 'identity-obj-proxy',
10
+ '^lodash-es/(.*)$': 'lodash/$1',
11
+ 'lodash-es': 'lodash',
12
+ dexie: require.resolve('dexie'),
13
+ },
14
+ setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
15
+ testEnvironment: 'jsdom',
16
+ testEnvironmentOptions: {
17
+ url: 'http://localhost/',
18
+ },
19
+ transformIgnorePatterns: ['/node_modules/', '\\.pnp\\.[^\\/]+$'],
20
+ };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@digihmis/esm-login-app",
3
+ "version": "1.0.0",
4
+ "license": "MPL-2.0",
5
+ "description": "The login microfrontend for the OpenMRS SPA",
6
+ "browser": "dist/digihmis-esm-login-app.js",
7
+ "main": "src/index.ts",
8
+ "source": true,
9
+ "scripts": {
10
+ "start": "openmrs develop",
11
+ "serve": "rspack serve --mode=development",
12
+ "debug": "npm run serve",
13
+ "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color",
14
+ "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color",
15
+ "build": "rspack --mode=production",
16
+ "build:development": "rspack --mode=development",
17
+ "analyze": "rspack --mode=production --env analyze=true",
18
+ "typescript": "tsc",
19
+ "lint": "eslint src --ext ts,tsx",
20
+ "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.extension.tsx' 'src/**/*.modal.tsx' 'src/**/*.resource.ts' --config='../../../tools/i18next-parser.config.js'"
21
+ },
22
+ "keywords": [
23
+ "openmrs",
24
+ "digihmis",
25
+ "microfrontends",
26
+ "healthcare",
27
+ "spa"
28
+ ],
29
+ "browserslist": [
30
+ "extends browserslist-config-openmrs"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/its-kios09/openmrs-esm-core.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/its-kios09/openmrs-esm-core/issues"
38
+ },
39
+ "homepage": "https://github.com/its-kios09/openmrs-esm-core#readme",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@carbon/react": "^1.92.1",
45
+ "@hookform/resolvers": "^3.6.0",
46
+ "lodash-es": "^4.17.21",
47
+ "react-hook-form": "^7.52.2",
48
+ "zod": "^3.23.8"
49
+ },
50
+ "peerDependencies": {
51
+ "@carbon/react": "1.x",
52
+ "@openmrs/esm-framework": "8.x",
53
+ "react": "18.x",
54
+ "react-dom": "18.x",
55
+ "react-i18next": "16.x",
56
+ "react-router-dom": "6.x",
57
+ "rxjs": "6.x",
58
+ "swr": "2.x"
59
+ },
60
+ "devDependencies": {
61
+ "@rspack/cli": "^1.3.11",
62
+ "@rspack/core": "^1.3.11",
63
+ "cross-env": "^7.0.3",
64
+ "jest": "^29.7.0",
65
+ "react": "^18.3.1",
66
+ "react-dom": "^18.3.1",
67
+ "react-i18next": "^16.0.0",
68
+ "react-router-dom": "^6.3.0",
69
+ "rxjs": "^6.5.3",
70
+ "swr": "2.2.5",
71
+ "webpack": "^5.99.9"
72
+ }
73
+ }
@@ -0,0 +1 @@
1
+ module.exports = require('../../tools/rs-pack-code-spliting');
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import styles from './background-wrapper.scss';
3
+ import { useConfig } from '@openmrs/esm-framework';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ interface BackgroundWrapperProps {
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ const BackgroundWrapper: React.FC<BackgroundWrapperProps> = ({ children }) => {
11
+ const { t } = useTranslation();
12
+ const { backgroundImage } = useConfig();
13
+
14
+ const backgroundSrc = backgroundImage?.src;
15
+ const backgroundAlt = backgroundImage?.alt || '';
16
+ const showCenteredLogin = backgroundImage?.showCenteredLogin;
17
+
18
+ function renderBackground() {
19
+ if (backgroundSrc) {
20
+ return <img src={backgroundSrc} alt={backgroundAlt} className={styles.backgroundImage} />;
21
+ } else {
22
+ return (
23
+ <svg role="img" className={styles.backgroundImage}>
24
+ <title>{t('openmrsLogo', 'OpenMRS logo')}</title>
25
+ <use href="#omrs-logo-full-color"></use>
26
+ </svg>
27
+ );
28
+ }
29
+ }
30
+
31
+ return showCenteredLogin ? (
32
+ <div className={styles.centeredLoginBackgroundContainer}>
33
+ <div
34
+ style={{
35
+ backgroundImage: backgroundSrc ? `url(${backgroundSrc})` : undefined,
36
+ }}
37
+ className={styles.centeredLoginOverlay}
38
+ >
39
+ {!backgroundSrc && (
40
+ <svg role="img" className={styles.backgroundSvg}>
41
+ <title>{t('openmrsLogo', 'OpenMRS logo')}</title>
42
+ <use href="#omrs-logo-full-color"></use>
43
+ </svg>
44
+ )}
45
+ </div>
46
+ <div className={styles.contentOverlay}>{children}</div>
47
+ </div>
48
+ ) : (
49
+ <>
50
+ {renderBackground()}
51
+ <div className={styles.backgroundContainer}>
52
+ <div className={styles.contentOverlay}>{children}</div>
53
+ </div>
54
+ </>
55
+ );
56
+ };
57
+
58
+ export default BackgroundWrapper;
@@ -0,0 +1,45 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ .backgroundContainer,
7
+ .centeredLoginBackgroundContainer {
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ height: 100vh;
12
+ width: 100%;
13
+ position: relative;
14
+ overflow: hidden;
15
+ }
16
+
17
+ .backgroundImage {
18
+ position: absolute;
19
+ top: 0;
20
+ left: 0;
21
+ width: 100%;
22
+ height: 100%;
23
+ object-fit: cover;
24
+ opacity: 1;
25
+ }
26
+
27
+ .contentOverlay {
28
+ position: relative;
29
+ z-index: 1;
30
+ display: flex;
31
+ justify-content: center;
32
+ align-items: center;
33
+ }
34
+
35
+ @media screen and (min-width: 1150px) {
36
+ .backgroundContainer {
37
+ max-width: 90%;
38
+ justify-content: right;
39
+ }
40
+
41
+ .backgroundImage {
42
+ object-fit: fit;
43
+ max-width: 55%;
44
+ }
45
+ }
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { HeaderGlobalAction } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { LocationIcon, navigate, useSession } from '@openmrs/esm-framework';
5
+ import styles from './change-location-link.scss';
6
+
7
+ const ChangeLocationLink: React.FC = () => {
8
+ const { t } = useTranslation();
9
+ const session = useSession();
10
+ const currentLocation = session?.sessionLocation?.display;
11
+
12
+ const changeLocation = () => {
13
+ // update=true is passed as a query param for updating the location preference,
14
+ // The location picker won't redirect with default location on finding the update=true param.
15
+ navigate({
16
+ to: `\${openmrsSpaBase}/login/location?returnToUrl=${window.location.pathname}&update=true`,
17
+ });
18
+ };
19
+
20
+ return (
21
+ <HeaderGlobalAction
22
+ aria-label={t('changeLocation', 'Change location')}
23
+ className={styles.changeLocationButton}
24
+ onClick={changeLocation}
25
+ >
26
+ <LocationIcon size={16} />
27
+ <span className={styles.currentLocationText}>{currentLocation}</span>
28
+ </HeaderGlobalAction>
29
+ );
30
+ };
31
+
32
+ export default ChangeLocationLink;
@@ -0,0 +1,17 @@
1
+ @use '@carbon/layout';
2
+
3
+ .changeLocationButton {
4
+ width: fit-content;
5
+ background-color: transparent;
6
+ color: white;
7
+ font-size: 14px;
8
+ padding: layout.$spacing-04 !important; // this gets unset in rtl language without !important
9
+
10
+ &:hover {
11
+ color: white;
12
+ }
13
+ }
14
+
15
+ .currentLocationText {
16
+ padding-inline: layout.$spacing-04;
17
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { navigate, type Session, useSession } from '@openmrs/esm-framework';
5
+ import ChangeLocationLink from './change-location-link.extension';
6
+
7
+ const mockNavigate = jest.mocked(navigate);
8
+ const mockUseSession = jest.mocked(useSession);
9
+
10
+ delete window.location;
11
+ (window.location as Location) = new URL('https://dev3.openmrs.org/openmrs/spa/home') as unknown as Location;
12
+
13
+ describe('ChangeLocationLink', () => {
14
+ beforeEach(() => {
15
+ mockUseSession.mockReturnValue({
16
+ sessionLocation: {
17
+ display: 'Waffle House',
18
+ },
19
+ } as Session);
20
+ });
21
+
22
+ it('should display the `Change location` link', async () => {
23
+ render(<ChangeLocationLink />);
24
+
25
+ const user = userEvent.setup();
26
+ const changeLocationButton = await screen.findByRole('button', {
27
+ name: /Change/i,
28
+ });
29
+
30
+ await user.click(changeLocationButton);
31
+
32
+ expect(mockNavigate).toHaveBeenCalledWith({
33
+ to: '${openmrsSpaBase}/login/location?returnToUrl=/openmrs/spa/home&update=true',
34
+ });
35
+ });
36
+ });
@@ -0,0 +1,30 @@
1
+ import React, { useCallback } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, SwitcherItem } from '@carbon/react';
4
+ import { PasswordIcon, showModal } from '@openmrs/esm-framework';
5
+ import styles from './change-password.scss';
6
+
7
+ const ChangePasswordLink: React.FC = () => {
8
+ const { t } = useTranslation();
9
+
10
+ const launchChangePasswordModal = useCallback(() => {
11
+ const dispose = showModal('change-password-modal', {
12
+ closeModal: () => dispose(),
13
+ size: 'sm',
14
+ });
15
+ }, []);
16
+
17
+ return (
18
+ <SwitcherItem aria-label={t('changePassword', 'ChangePassword')} className={styles.panelItemContainer}>
19
+ <div>
20
+ <PasswordIcon size={20} />
21
+ <p>{t('password', 'Password')}</p>
22
+ </div>
23
+ <Button kind="ghost" onClick={launchChangePasswordModal}>
24
+ {t('change', 'Change')}
25
+ </Button>
26
+ </SwitcherItem>
27
+ );
28
+ };
29
+
30
+ export default ChangePasswordLink;
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { showModal } from '@openmrs/esm-framework';
5
+ import ChangePasswordLink from './change-password-link.extension';
6
+
7
+ const mockShowModal = jest.mocked(showModal);
8
+
9
+ describe('ChangePasswordLink', () => {
10
+ it('should launch the change password modal', async () => {
11
+ const user = userEvent.setup();
12
+
13
+ render(<ChangePasswordLink />);
14
+
15
+ const changePasswordLink = screen.getByRole('button', {
16
+ name: /Change/i,
17
+ });
18
+
19
+ await user.click(changePasswordLink);
20
+
21
+ expect(mockShowModal).toHaveBeenCalledTimes(1);
22
+ expect(mockShowModal).toHaveBeenCalledWith('change-password-modal', {
23
+ size: 'sm',
24
+ closeModal: expect.any(Function),
25
+ });
26
+ });
27
+ });
@@ -0,0 +1,11 @@
1
+ @use '@carbon/layout';
2
+
3
+ .submitButton {
4
+ :global(.cds--inline-loading) {
5
+ min-height: layout.$spacing-05;
6
+ }
7
+
8
+ :global(.cds--inline-loading__text) {
9
+ font-size: unset;
10
+ }
11
+ }
@@ -0,0 +1,159 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { z } from 'zod';
4
+ import { zodResolver } from '@hookform/resolvers/zod';
5
+ import { Controller, FieldError, useForm, type SubmitHandler } from 'react-hook-form';
6
+ import { Button, Form, PasswordInput, InlineLoading, Tile } from '@carbon/react';
7
+ import { showSnackbar } from '@openmrs/esm-framework';
8
+ import { changeUserPassword } from './change-password.resource';
9
+ import Logo from '../logo.component';
10
+ import styles from './change-password.scss';
11
+
12
+ const ChangePassword: React.FC = () => {
13
+ const { t } = useTranslation();
14
+ const [isChangingPassword, setIsChangingPassword] = useState(false);
15
+
16
+ const oldPasswordValidation = z.string({
17
+ required_error: t('oldPasswordRequired', 'Old password is required'),
18
+ });
19
+
20
+ const newPasswordValidation = z.string({
21
+ required_error: t('newPasswordRequired', 'New password is required'),
22
+ });
23
+
24
+ const passwordConfirmationValidation = z.string({
25
+ required_error: t('passwordConfirmationRequired', 'Password confirmation is required'),
26
+ });
27
+
28
+ const changePasswordFormSchema = z
29
+ .object({
30
+ oldPassword: oldPasswordValidation,
31
+ newPassword: newPasswordValidation,
32
+ passwordConfirmation: passwordConfirmationValidation,
33
+ })
34
+ .refine((data) => data.newPassword === data.passwordConfirmation, {
35
+ message: t('passwordsDoNotMatch', 'Passwords do not match'),
36
+ path: ['passwordConfirmation'],
37
+ });
38
+
39
+ const {
40
+ handleSubmit,
41
+ control,
42
+ formState: { errors },
43
+ } = useForm({
44
+ resolver: zodResolver(changePasswordFormSchema),
45
+ });
46
+
47
+ const onSubmit: SubmitHandler<z.infer<typeof changePasswordFormSchema>> = useCallback(
48
+ (data) => {
49
+ setIsChangingPassword(true);
50
+
51
+ const { oldPassword, newPassword } = data;
52
+
53
+ changeUserPassword(oldPassword, newPassword)
54
+ .then(() => {
55
+ showSnackbar({
56
+ title: t('passwordChangedSuccessfully', 'Password changed successfully'),
57
+ kind: 'success',
58
+ });
59
+ })
60
+ .catch((error) => {
61
+ showSnackbar({
62
+ kind: 'error',
63
+ subtitle: error?.message,
64
+ title: t('errorChangingPassword', 'Error changing password'),
65
+ });
66
+ })
67
+ .finally(() => {
68
+ setIsChangingPassword(false);
69
+ });
70
+ },
71
+ [t],
72
+ );
73
+
74
+ const onError = useCallback(() => setIsChangingPassword(false), []);
75
+
76
+ return (
77
+ <div className={styles.container}>
78
+ <Tile className={styles.changePasswordCard}>
79
+ <div className={styles.alignCenter}>
80
+ <Logo />
81
+ </div>
82
+ <Form onSubmit={handleSubmit(onSubmit, onError)}>
83
+ <Controller
84
+ name="oldPassword"
85
+ control={control}
86
+ render={({ field: { onChange, value } }) => (
87
+ <PasswordInput
88
+ id="oldPassword"
89
+ invalid={!!errors?.oldPassword}
90
+ invalidText={
91
+ (errors &&
92
+ errors.oldPassword &&
93
+ errors.oldPassword.message &&
94
+ typeof errors.oldPassword.message === 'string' &&
95
+ errors.oldPassword.message) ??
96
+ ''
97
+ }
98
+ labelText={t('oldPassword', 'Old password')}
99
+ onChange={onChange}
100
+ value={value}
101
+ />
102
+ )}
103
+ />
104
+ <Controller
105
+ name="newPassword"
106
+ control={control}
107
+ render={({ field: { onChange, value } }) => (
108
+ <PasswordInput
109
+ id="newPassword"
110
+ invalid={!!errors?.newPassword}
111
+ invalidText={
112
+ (errors &&
113
+ errors.newPassword &&
114
+ errors.newPassword.message &&
115
+ typeof errors.newPassword.message === 'string' &&
116
+ errors.newPassword.message) ??
117
+ ''
118
+ }
119
+ labelText={t('newPassword', 'New password')}
120
+ onChange={onChange}
121
+ value={value}
122
+ />
123
+ )}
124
+ />
125
+ <Controller
126
+ name="passwordConfirmation"
127
+ control={control}
128
+ render={({ field: { onChange, value } }) => (
129
+ <PasswordInput
130
+ id="passwordConfirmation"
131
+ invalid={!!errors?.passwordConfirmation}
132
+ invalidText={
133
+ (errors &&
134
+ errors.passwordConfirmation &&
135
+ errors.passwordConfirmation.message &&
136
+ typeof errors.passwordConfirmation.message === 'string' &&
137
+ errors.passwordConfirmation.message) ??
138
+ ''
139
+ }
140
+ labelText={t('confirmPassword', 'Confirm new password')}
141
+ onChange={onChange}
142
+ value={value}
143
+ />
144
+ )}
145
+ />
146
+ <Button className={styles.submitButton} disabled={isChangingPassword} type="submit">
147
+ {isChangingPassword ? (
148
+ <InlineLoading description={t('changingPassword', 'Changing password') + '...'} />
149
+ ) : (
150
+ <span>{t('change', 'Change Password')}</span>
151
+ )}
152
+ </Button>
153
+ </Form>
154
+ </Tile>
155
+ </div>
156
+ );
157
+ };
158
+
159
+ export default ChangePassword;