@ampath/esm-login-app 8.0.0-next.10

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 (202) hide show
  1. package/.editorconfig +8 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc +76 -0
  4. package/.gitattributes +4 -0
  5. package/.prettierignore +9 -0
  6. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  7. package/.yarn/versions/643d2b70.yml +21 -0
  8. package/README.md +4 -0
  9. package/__mocks__/config.mock.ts +26 -0
  10. package/__mocks__/locations.mock.ts +540 -0
  11. package/__mocks__/react-i18next.js +49 -0
  12. package/dist/1128.js +1 -0
  13. package/dist/1128.js.map +1 -0
  14. package/dist/1480.js +1 -0
  15. package/dist/1578.js +1 -0
  16. package/dist/1578.js.map +1 -0
  17. package/dist/1646.js +1 -0
  18. package/dist/1800.js +1 -0
  19. package/dist/1800.js.map +1 -0
  20. package/dist/1869.js +1 -0
  21. package/dist/1877.js +1 -0
  22. package/dist/2317.js +1 -0
  23. package/dist/2416.js +1 -0
  24. package/dist/2489.js +1 -0
  25. package/dist/2489.js.map +1 -0
  26. package/dist/282.js +1 -0
  27. package/dist/2881.js +1 -0
  28. package/dist/2997.js +1 -0
  29. package/dist/2997.js.map +1 -0
  30. package/dist/3219.js +1 -0
  31. package/dist/3219.js.map +1 -0
  32. package/dist/3378.js +1 -0
  33. package/dist/3963.js +1 -0
  34. package/dist/4106.js +1 -0
  35. package/dist/4111.js +1 -0
  36. package/dist/4169.js +1 -0
  37. package/dist/4169.js.map +1 -0
  38. package/dist/434.js +1 -0
  39. package/dist/4348.js +1 -0
  40. package/dist/4378.js +1 -0
  41. package/dist/4378.js.map +1 -0
  42. package/dist/4383.js +1 -0
  43. package/dist/4658.js +1 -0
  44. package/dist/4668.js +1 -0
  45. package/dist/4668.js.map +1 -0
  46. package/dist/4870.js +1 -0
  47. package/dist/4870.js.map +1 -0
  48. package/dist/4928.js +1 -0
  49. package/dist/4bc56e5b0b0e91da.png +0 -0
  50. package/dist/5098.js +1 -0
  51. package/dist/5098.js.map +1 -0
  52. package/dist/5117.js +1 -0
  53. package/dist/5132.js +1 -0
  54. package/dist/5145.js +1 -0
  55. package/dist/5503.js +1 -0
  56. package/dist/556.js +1 -0
  57. package/dist/5644.js +1 -0
  58. package/dist/5898.js +1 -0
  59. package/dist/5898.js.map +1 -0
  60. package/dist/5940.js +1 -0
  61. package/dist/5976.js +1 -0
  62. package/dist/5976.js.map +1 -0
  63. package/dist/6047.js +1 -0
  64. package/dist/6237.js +1 -0
  65. package/dist/6237.js.map +1 -0
  66. package/dist/6362.js +1 -0
  67. package/dist/6362.js.map +1 -0
  68. package/dist/6371.js +1 -0
  69. package/dist/6377.js +1 -0
  70. package/dist/6444.js +1 -0
  71. package/dist/647e55b5cedf5df2.png +0 -0
  72. package/dist/6508.js +1 -0
  73. package/dist/6724.js +1 -0
  74. package/dist/6904.js +1 -0
  75. package/dist/7045.js +1 -0
  76. package/dist/7144.js +43 -0
  77. package/dist/7144.js.map +1 -0
  78. package/dist/7175.js +1 -0
  79. package/dist/7182.js +1 -0
  80. package/dist/7251.js +1 -0
  81. package/dist/7251.js.map +1 -0
  82. package/dist/749.js +1 -0
  83. package/dist/749.js.map +1 -0
  84. package/dist/7742.js +1 -0
  85. package/dist/7912.js +1 -0
  86. package/dist/8358.js +1 -0
  87. package/dist/8359.js +1 -0
  88. package/dist/8695.js +1 -0
  89. package/dist/903.js +1 -0
  90. package/dist/9072.js +1 -0
  91. package/dist/9510.js +15 -0
  92. package/dist/9510.js.map +1 -0
  93. package/dist/9806.js +1 -0
  94. package/dist/a6792134b9df70c4.png +0 -0
  95. package/dist/acd6ab71c5f6bcb6.jpg +0 -0
  96. package/dist/d0bf081185f017f3.jpg +0 -0
  97. package/dist/d48e253df6a333a7.png +0 -0
  98. package/dist/esm-login-app.js +6 -0
  99. package/dist/esm-login-app.js.buildmanifest.json +1598 -0
  100. package/dist/esm-login-app.js.map +1 -0
  101. package/dist/main.js +6 -0
  102. package/dist/main.js.map +1 -0
  103. package/dist/routes.json +1 -0
  104. package/jest.config.js +20 -0
  105. package/package.json +111 -0
  106. package/prettier.config.js +8 -0
  107. package/rspack.config.js +1 -0
  108. package/src/assets/Taifa-Care.png +0 -0
  109. package/src/assets/ampath-logo.png +0 -0
  110. package/src/assets/dha.png +0 -0
  111. package/src/assets/gok.png +0 -0
  112. package/src/assets/medicine.jpg +0 -0
  113. package/src/assets/openmrs.jpg +0 -0
  114. package/src/change-location-link/change-location-link.extension.tsx +32 -0
  115. package/src/change-location-link/change-location-link.scss +17 -0
  116. package/src/change-location-link/change-location-link.test.tsx +36 -0
  117. package/src/change-password/change-password-link.extension.tsx +30 -0
  118. package/src/change-password/change-password-link.test.tsx +27 -0
  119. package/src/change-password/change-password-modal.scss +11 -0
  120. package/src/change-password/change-password.component.tsx +159 -0
  121. package/src/change-password/change-password.modal.tsx +175 -0
  122. package/src/change-password/change-password.resource.ts +12 -0
  123. package/src/change-password/change-password.scss +51 -0
  124. package/src/change-password/change-password.test.tsx +53 -0
  125. package/src/common/otp/otp.component.tsx +54 -0
  126. package/src/common/otp/otp.scss +13 -0
  127. package/src/common/resend-timer/resend-timer.component.tsx +56 -0
  128. package/src/common/resend-timer/resend-timer.scss +7 -0
  129. package/src/config-schema.ts +140 -0
  130. package/src/declarations.d.ts +5 -0
  131. package/src/footer.component.tsx +60 -0
  132. package/src/footer.scss +113 -0
  133. package/src/index.ts +27 -0
  134. package/src/loading/loading.component.tsx +11 -0
  135. package/src/loading/loading.scss +7 -0
  136. package/src/location-picker/location-picker-view.component.tsx +174 -0
  137. package/src/location-picker/location-picker.resource.ts +111 -0
  138. package/src/location-picker/location-picker.scss +94 -0
  139. package/src/location-picker/location-picker.test.tsx +341 -0
  140. package/src/login/login.component.tsx +350 -0
  141. package/src/login/login.scss +246 -0
  142. package/src/login/login.test.tsx +288 -0
  143. package/src/login.resource.ts +147 -0
  144. package/src/logo.component.tsx +22 -0
  145. package/src/logout/logout.extension.tsx +23 -0
  146. package/src/logout/logout.scss +12 -0
  147. package/src/otp/otp.component.tsx +108 -0
  148. package/src/otp/otp.module.scss +44 -0
  149. package/src/redirect-logout/logout.resource.ts +15 -0
  150. package/src/redirect-logout/redirect-logout.component.tsx +42 -0
  151. package/src/redirect-logout/redirect-logout.test.tsx +180 -0
  152. package/src/resources/otp.resource.ts +90 -0
  153. package/src/root.component.tsx +24 -0
  154. package/src/routes.json +63 -0
  155. package/src/setupTests.ts +15 -0
  156. package/src/test-helpers/render-with-router.tsx +17 -0
  157. package/src/types.ts +34 -0
  158. package/src/utils/get-base-url.ts +17 -0
  159. package/translations/am.json +41 -0
  160. package/translations/ar.json +41 -0
  161. package/translations/ar_SY.json +41 -0
  162. package/translations/bn.json +41 -0
  163. package/translations/cs.json +41 -0
  164. package/translations/de.json +41 -0
  165. package/translations/en.json +41 -0
  166. package/translations/en_US.json +41 -0
  167. package/translations/es.json +41 -0
  168. package/translations/es_MX.json +41 -0
  169. package/translations/fr.json +41 -0
  170. package/translations/he.json +41 -0
  171. package/translations/hi.json +41 -0
  172. package/translations/hi_IN.json +41 -0
  173. package/translations/id.json +41 -0
  174. package/translations/it.json +41 -0
  175. package/translations/ka.json +41 -0
  176. package/translations/km.json +41 -0
  177. package/translations/ku.json +41 -0
  178. package/translations/ky.json +41 -0
  179. package/translations/lg.json +41 -0
  180. package/translations/ne.json +41 -0
  181. package/translations/pl.json +41 -0
  182. package/translations/pt.json +41 -0
  183. package/translations/pt_BR.json +41 -0
  184. package/translations/qu.json +41 -0
  185. package/translations/ro_RO.json +41 -0
  186. package/translations/ru_RU.json +41 -0
  187. package/translations/si.json +41 -0
  188. package/translations/sq.json +41 -0
  189. package/translations/sw.json +41 -0
  190. package/translations/sw_KE.json +41 -0
  191. package/translations/tr.json +41 -0
  192. package/translations/tr_TR.json +41 -0
  193. package/translations/uk.json +41 -0
  194. package/translations/uz.json +41 -0
  195. package/translations/uz@Latn.json +41 -0
  196. package/translations/uz_UZ.json +41 -0
  197. package/translations/vi.json +41 -0
  198. package/translations/zh.json +41 -0
  199. package/translations/zh_CN.json +41 -0
  200. package/translations/zh_TW.json +41 -0
  201. package/tsconfig.json +25 -0
  202. package/yarnrc.yml +14 -0
@@ -0,0 +1,341 @@
1
+ import { screen, waitFor } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import {
4
+ openmrsFetch,
5
+ setSessionLocation,
6
+ setUserProperties,
7
+ showSnackbar,
8
+ useConfig,
9
+ useSession,
10
+ type LoggedInUser,
11
+ type Session,
12
+ type FetchResponse,
13
+ } from '@openmrs/esm-framework';
14
+ import {
15
+ mockLoginLocations,
16
+ validatingLocationFailureResponse,
17
+ validatingLocationSuccessResponse,
18
+ } from '../../__mocks__/locations.mock';
19
+ import { mockConfig } from '../../__mocks__/config.mock';
20
+ import renderWithRouter from '../test-helpers/render-with-router';
21
+ import LocationPickerView from './location-picker-view.component';
22
+
23
+ const fistLocation = {
24
+ uuid: 'uuid_1',
25
+ name: 'location_1',
26
+ };
27
+
28
+ const secondLocation = {
29
+ uuid: 'uuid_2',
30
+ name: 'location_2',
31
+ };
32
+
33
+ const invalidLocationUuid = '2gf1b7d4-c865-4178-82b0-5932e51503d6';
34
+ const userUuid = '90bd24b3-e700-46b0-a5ef-c85afdfededd';
35
+
36
+ const mockOpenmrsFetch = jest.mocked(openmrsFetch);
37
+ const mockUseConfig = jest.mocked(useConfig);
38
+ const mockUseSession = jest.mocked(useSession);
39
+ const mockSetSessionLocation = jest.mocked(setSessionLocation);
40
+ const mockSetUserProperties = jest.mocked(setUserProperties);
41
+ const mockShowSnackbar = jest.mocked(showSnackbar);
42
+
43
+ describe('LocationPickerView', () => {
44
+ beforeEach(() => {
45
+ mockUseConfig.mockReturnValue(mockConfig);
46
+
47
+ mockUseSession.mockReturnValue({
48
+ user: {
49
+ display: 'Testy McTesterface',
50
+ uuid: '90bd24b3-e700-46b0-a5ef-c85afdfededd',
51
+ userProperties: {},
52
+ } as LoggedInUser,
53
+ } as Session);
54
+
55
+ const urlResponseMap: Record<string, FetchResponse<unknown>> = {
56
+ [`/ws/fhir2/R4/Location?_id=${fistLocation.uuid}`]: validatingLocationSuccessResponse as FetchResponse<unknown>,
57
+ [`/ws/fhir2/R4/Location?_id=${invalidLocationUuid}`]: validatingLocationFailureResponse as FetchResponse<unknown>,
58
+ };
59
+
60
+ mockOpenmrsFetch.mockImplementation(
61
+ async (url) => urlResponseMap[url] ?? (mockLoginLocations as FetchResponse<unknown>),
62
+ );
63
+
64
+ mockSetSessionLocation.mockResolvedValue(undefined);
65
+ });
66
+
67
+ it('renders the welcome message and location selection form', () => {
68
+ renderWithRouter(LocationPickerView, {
69
+ currentLocationUuid: 'some-location-uuid',
70
+ hideWelcomeMessage: false,
71
+ });
72
+
73
+ expect(screen.getByText(/welcome testy mctesterface/i)).toBeInTheDocument();
74
+ expect(
75
+ screen.getByText(/select your location from the list below. use the search bar to find your location/i),
76
+ ).toBeInTheDocument();
77
+ });
78
+
79
+ it('disables the confirm button when no location is selected', () => {
80
+ renderWithRouter(LocationPickerView, {});
81
+
82
+ const confirmButton = screen.getByRole('button', { name: /confirm/i });
83
+ expect(confirmButton).toBeDisabled();
84
+ });
85
+
86
+ it('enables the confirm button when a location is selected', async () => {
87
+ const user = userEvent.setup();
88
+ renderWithRouter(LocationPickerView, {});
89
+
90
+ const confirmButton = screen.getByRole('button', { name: /confirm/i });
91
+ expect(confirmButton).toBeDisabled();
92
+
93
+ const location = await screen.findByRole('radio', { name: fistLocation.name });
94
+ await user.click(location);
95
+
96
+ expect(confirmButton).toBeEnabled();
97
+ });
98
+
99
+ describe('Saving location preference', () => {
100
+ it('allows user to save their preferred location for future logins', async () => {
101
+ const user = userEvent.setup();
102
+
103
+ renderWithRouter(LocationPickerView, {});
104
+
105
+ const location = await screen.findByRole('radio', { name: fistLocation.name });
106
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
107
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
108
+
109
+ await user.click(location);
110
+ expect(submitButton).toBeEnabled();
111
+
112
+ await user.click(checkbox);
113
+ expect(checkbox).toBeChecked();
114
+
115
+ await user.click(submitButton);
116
+
117
+ await waitFor(() => {
118
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
119
+ });
120
+
121
+ expect(mockSetUserProperties).toHaveBeenCalledWith(userUuid, {
122
+ defaultLocation: fistLocation.uuid,
123
+ });
124
+
125
+ await waitFor(() => {
126
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
127
+ expect.objectContaining({
128
+ kind: 'success',
129
+ title: 'Location saved',
130
+ subtitle: 'Your preferred location has been saved for future logins',
131
+ }),
132
+ );
133
+ });
134
+ });
135
+
136
+ it('does not save preference when user submits without checking the checkbox', async () => {
137
+ const user = userEvent.setup();
138
+
139
+ renderWithRouter(LocationPickerView, {});
140
+
141
+ const location = await screen.findByRole('radio', { name: fistLocation.name });
142
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
143
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
144
+
145
+ await user.click(location);
146
+ expect(checkbox).not.toBeChecked();
147
+
148
+ await user.click(submitButton);
149
+
150
+ await waitFor(() => {
151
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
152
+ });
153
+
154
+ expect(mockSetUserProperties).not.toHaveBeenCalled();
155
+ expect(mockShowSnackbar).not.toHaveBeenCalled();
156
+ });
157
+
158
+ it('automatically redirects when user has a valid saved location preference', async () => {
159
+ const validLocationUuid = fistLocation.uuid;
160
+ mockUseSession.mockReturnValue({
161
+ user: {
162
+ display: 'Testy McTesterface',
163
+ uuid: userUuid,
164
+ userProperties: {
165
+ defaultLocation: validLocationUuid,
166
+ },
167
+ } as LoggedInUser,
168
+ } as Session);
169
+
170
+ renderWithRouter(LocationPickerView, {});
171
+
172
+ await waitFor(() => {
173
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(validLocationUuid, expect.anything());
174
+ });
175
+
176
+ expect(mockSetUserProperties).not.toHaveBeenCalled();
177
+ });
178
+
179
+ it('shows location picker when saved location preference is invalid', async () => {
180
+ mockUseSession.mockReturnValue({
181
+ user: {
182
+ display: 'Testy McTesterface',
183
+ uuid: userUuid,
184
+ userProperties: {
185
+ defaultLocation: invalidLocationUuid,
186
+ },
187
+ } as LoggedInUser,
188
+ } as Session);
189
+
190
+ renderWithRouter(LocationPickerView, {});
191
+
192
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
193
+ expect(checkbox).toBeChecked();
194
+
195
+ expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument();
196
+ expect(mockSetSessionLocation).not.toHaveBeenCalled();
197
+ });
198
+ });
199
+
200
+ describe('Updating location preference', () => {
201
+ it('shows location picker when update=true is in URL params', () => {
202
+ mockUseSession.mockReturnValue({
203
+ user: {
204
+ display: 'Testy McTesterface',
205
+ uuid: userUuid,
206
+ userProperties: {
207
+ defaultLocation: fistLocation.uuid,
208
+ },
209
+ } as LoggedInUser,
210
+ } as Session);
211
+
212
+ renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
213
+
214
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
215
+ expect(checkbox).toBeChecked();
216
+
217
+ expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument();
218
+ expect(mockSetSessionLocation).not.toHaveBeenCalled();
219
+ });
220
+
221
+ it('allows user to remove saved preference by unchecking the checkbox', async () => {
222
+ const user = userEvent.setup();
223
+
224
+ mockUseSession.mockReturnValue({
225
+ user: {
226
+ display: 'Testy McTesterface',
227
+ uuid: userUuid,
228
+ userProperties: {
229
+ defaultLocation: '1ce1b7d4-c865-4178-82b0-5932e51503d6',
230
+ },
231
+ } as LoggedInUser,
232
+ } as Session);
233
+
234
+ renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
235
+
236
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
237
+ expect(checkbox).toBeChecked();
238
+
239
+ const location = screen.getByRole('radio', { name: fistLocation.name });
240
+ await user.click(location);
241
+
242
+ expect(mockSetSessionLocation).not.toHaveBeenCalled();
243
+
244
+ await user.click(checkbox);
245
+ expect(checkbox).not.toBeChecked();
246
+
247
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
248
+ await user.click(submitButton);
249
+
250
+ await waitFor(() => {
251
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
252
+ });
253
+
254
+ expect(mockSetUserProperties).toHaveBeenCalledWith(userUuid, {});
255
+
256
+ await waitFor(() => {
257
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ kind: 'success',
260
+ title: 'Location preference removed',
261
+ subtitle: 'You will need to select a location on each login',
262
+ }),
263
+ );
264
+ });
265
+ });
266
+
267
+ it('allows user to update their preferred location', async () => {
268
+ const user = userEvent.setup();
269
+
270
+ mockUseSession.mockReturnValue({
271
+ user: {
272
+ display: 'Testy McTesterface',
273
+ uuid: userUuid,
274
+ userProperties: {
275
+ defaultLocation: fistLocation.uuid,
276
+ },
277
+ } as LoggedInUser,
278
+ } as Session);
279
+
280
+ renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
281
+
282
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
283
+ expect(checkbox).toBeChecked();
284
+
285
+ const location = await screen.findByRole('radio', { name: secondLocation.name });
286
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
287
+
288
+ await user.click(location);
289
+ await user.click(submitButton);
290
+
291
+ await waitFor(() => {
292
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(secondLocation.uuid, expect.anything());
293
+ });
294
+
295
+ expect(mockSetUserProperties).toHaveBeenCalledWith(userUuid, {
296
+ defaultLocation: secondLocation.uuid,
297
+ });
298
+
299
+ await waitFor(() => {
300
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
301
+ expect.objectContaining({
302
+ kind: 'success',
303
+ title: 'Location updated',
304
+ subtitle: 'Your preferred login location has been updated',
305
+ }),
306
+ );
307
+ });
308
+ });
309
+
310
+ it('does not update preference when user selects the same location', async () => {
311
+ const user = userEvent.setup();
312
+
313
+ mockUseSession.mockReturnValue({
314
+ user: {
315
+ display: 'Testy McTesterface',
316
+ uuid: userUuid,
317
+ userProperties: {
318
+ defaultLocation: fistLocation.uuid,
319
+ },
320
+ } as LoggedInUser,
321
+ } as Session);
322
+
323
+ renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
324
+
325
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
326
+ expect(checkbox).toBeChecked();
327
+
328
+ const location = await screen.findByRole('radio', { name: fistLocation.name });
329
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
330
+
331
+ await user.click(location);
332
+ await user.click(submitButton);
333
+
334
+ await waitFor(() => {
335
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
336
+ });
337
+
338
+ expect(mockSetUserProperties).not.toHaveBeenCalled();
339
+ });
340
+ });
341
+ });
@@ -0,0 +1,350 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { useLocation, useNavigate } from 'react-router-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, InlineLoading, InlineNotification, PasswordInput, TextInput, Tile } from '@carbon/react';
5
+ import {
6
+ ArrowRightIcon,
7
+ getCoreTranslation,
8
+ refetchCurrentUser,
9
+ navigate as openmrsNavigate,
10
+ useConfig,
11
+ useConnectivity,
12
+ useSession,
13
+ } from '@openmrs/esm-framework';
14
+ import { type ConfigSchema } from '../config-schema';
15
+ import Logo from '../logo.component';
16
+ import styles from './login.scss';
17
+ import { getEmail, getOtp } from '../resources/otp.resource';
18
+ import { getOtpEnabledStatus } from '../utils/get-base-url';
19
+ import image from '../assets/medicine.jpg';
20
+ import openmrsLogo from '../assets/openmrs.jpg';
21
+ import dhaLogo from '../assets/dha.png';
22
+ import gokLogo from '../assets/gok.png';
23
+
24
+ export interface LoginReferrer {
25
+ referrer?: string;
26
+ }
27
+
28
+ const Login: React.FC = () => {
29
+ const { showPasswordOnSeparateScreen, provider: loginProvider, links: loginLinks } = useConfig<ConfigSchema>();
30
+ const isLoginEnabled = useConnectivity();
31
+ const { t } = useTranslation();
32
+ const { user } = useSession();
33
+ const location = useLocation() as unknown as Omit<Location, 'state'> & {
34
+ state: LoginReferrer;
35
+ };
36
+ const navigate = useNavigate();
37
+
38
+ const [errorMessage, setErrorMessage] = useState('');
39
+ const [isLoggingIn, setIsLoggingIn] = useState(false);
40
+ const [password, setPassword] = useState('');
41
+ const [username, setUsername] = useState('');
42
+ const [showPasswordField, setShowPasswordField] = useState(false);
43
+ const passwordInputRef = useRef<HTMLInputElement>(null);
44
+ const usernameInputRef = useRef<HTMLInputElement>(null);
45
+
46
+ useEffect(() => {
47
+ if (!user) {
48
+ if (loginProvider.type === 'oauth2') {
49
+ openmrsNavigate({ to: loginProvider.loginUrl });
50
+ } else if (!username && location.pathname === '/login/confirm') {
51
+ navigate('/login');
52
+ }
53
+ }
54
+ }, [username, navigate, location, user, loginProvider]);
55
+
56
+ useEffect(() => {
57
+ if (showPasswordOnSeparateScreen) {
58
+ if (showPasswordField) {
59
+ if (!passwordInputRef.current?.value) {
60
+ passwordInputRef.current?.focus();
61
+ }
62
+ } else {
63
+ usernameInputRef.current?.focus();
64
+ }
65
+ }
66
+ }, [showPasswordField, showPasswordOnSeparateScreen]);
67
+
68
+ const continueLogin = useCallback(() => {
69
+ const currentUsername = usernameInputRef.current?.value?.trim();
70
+ if (currentUsername) {
71
+ // If credentials were autofilled, input onChange might not have been called
72
+ setUsername(currentUsername);
73
+ setShowPasswordField(true);
74
+ } else {
75
+ usernameInputRef.current?.focus();
76
+ }
77
+ }, []);
78
+
79
+ const changeUsername = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setUsername(evt.target.value), []);
80
+ const changePassword = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setPassword(evt.target.value), []);
81
+
82
+ const handleSubmit = useCallback(
83
+ async (evt: React.FormEvent<HTMLFormElement>) => {
84
+ evt.preventDefault();
85
+ evt.stopPropagation();
86
+
87
+ // If credentials were autofilled, input onChange might not have been called
88
+ const currentUsername = usernameInputRef.current?.value?.trim() || username;
89
+ const currentPassword = passwordInputRef.current?.value || password;
90
+ const isOtpEnabled: boolean = await getOtpEnabledStatus();
91
+
92
+ if (showPasswordOnSeparateScreen && !showPasswordField) {
93
+ continueLogin();
94
+ return false;
95
+ }
96
+
97
+ if (!currentPassword || !currentPassword.trim()) {
98
+ passwordInputRef.current?.focus();
99
+ return false;
100
+ }
101
+
102
+ try {
103
+ setIsLoggingIn(true);
104
+ const sessionStore = await refetchCurrentUser(currentUsername, currentPassword);
105
+ const session = sessionStore.session;
106
+ const authenticated = sessionStore?.session?.authenticated;
107
+
108
+ if (isOtpEnabled === true) {
109
+ if (authenticated) {
110
+ if (session.sessionLocation) {
111
+ let to = loginLinks?.loginSuccess || '/home';
112
+ if (location?.state?.referrer) {
113
+ if (location.state.referrer.startsWith('/')) {
114
+ to = `\${openmrsSpaBase}${location.state.referrer}`;
115
+ } else {
116
+ to = location.state.referrer;
117
+ }
118
+ }
119
+ const uuid = session.user.person.uuid;
120
+ try {
121
+ const email = await getEmail(uuid, username, password);
122
+ await getOtp(username, password, email);
123
+ navigate('otp', {
124
+ state: {
125
+ username,
126
+ password,
127
+ referrer: location?.state?.referrer,
128
+ },
129
+ });
130
+ } catch (err: any) {
131
+ setErrorMessage(err.message);
132
+ return;
133
+ }
134
+ } else if (!session.sessionLocation) {
135
+ const uuid = session.user.person.uuid;
136
+ try {
137
+ await getOtp(username, password, uuid);
138
+ navigate('otp', {
139
+ state: {
140
+ username,
141
+ password,
142
+ referrer: location?.state?.referrer,
143
+ },
144
+ });
145
+ } catch (err: any) {
146
+ setErrorMessage(err.message);
147
+ return;
148
+ }
149
+ }
150
+ } else {
151
+ setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
152
+ setUsername('');
153
+ setPassword('');
154
+ if (showPasswordOnSeparateScreen) {
155
+ setShowPasswordField(false);
156
+ }
157
+ }
158
+ } else {
159
+ if (authenticated) {
160
+ if (session.sessionLocation) {
161
+ let to = loginLinks?.loginSuccess || '/home';
162
+ if (location?.state?.referrer) {
163
+ if (location.state.referrer.startsWith('/')) {
164
+ to = `\${openmrsSpaBase}${location.state.referrer}`;
165
+ } else {
166
+ to = location.state.referrer;
167
+ }
168
+ }
169
+
170
+ openmrsNavigate({ to });
171
+ } else {
172
+ navigate('/login/location');
173
+ }
174
+ } else {
175
+ setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
176
+ setUsername('');
177
+ setPassword('');
178
+ if (showPasswordOnSeparateScreen) {
179
+ setShowPasswordField(false);
180
+ }
181
+ }
182
+ }
183
+
184
+ return true;
185
+ } catch (error: unknown) {
186
+ if (error instanceof Error) {
187
+ setErrorMessage(error.message);
188
+ } else {
189
+ setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
190
+ }
191
+ setUsername('');
192
+ setPassword('');
193
+ if (showPasswordOnSeparateScreen) {
194
+ setShowPasswordField(false);
195
+ }
196
+ } finally {
197
+ setIsLoggingIn(false);
198
+ }
199
+ },
200
+ [
201
+ username,
202
+ password,
203
+ navigate,
204
+ showPasswordOnSeparateScreen,
205
+ showPasswordField,
206
+ loginLinks,
207
+ location,
208
+ t,
209
+ continueLogin,
210
+ ],
211
+ );
212
+
213
+ if (!loginProvider || loginProvider.type === 'basic') {
214
+ return (
215
+ <>
216
+ <div className={styles.wrapperContainer}>
217
+ <div className={styles.logoContainer}>
218
+ <div className={styles.container}>
219
+ <Tile className={styles.loginCard}>
220
+ {errorMessage && (
221
+ <div className={styles.errorMessage}>
222
+ <InlineNotification
223
+ kind="error"
224
+ subtitle={errorMessage}
225
+ title={getCoreTranslation('error')}
226
+ onClick={() => setErrorMessage('')}
227
+ />
228
+ </div>
229
+ )}
230
+ <div className={styles.center}>
231
+ <Logo t={t} />
232
+ </div>
233
+ <form onSubmit={handleSubmit}>
234
+ <div className={styles.inputGroup}>
235
+ <TextInput
236
+ id="username"
237
+ type="text"
238
+ name="username"
239
+ autoComplete="username"
240
+ labelText={t('username', 'Username')}
241
+ value={username}
242
+ onChange={changeUsername}
243
+ ref={usernameInputRef}
244
+ required
245
+ autoFocus
246
+ />
247
+ {showPasswordOnSeparateScreen ? (
248
+ <>
249
+ <div className={showPasswordField ? undefined : styles.hiddenPasswordField}>
250
+ <PasswordInput
251
+ id="password"
252
+ labelText={t('password', 'Password')}
253
+ name="password"
254
+ autoComplete="current-password"
255
+ onChange={changePassword}
256
+ ref={passwordInputRef}
257
+ required
258
+ value={password}
259
+ showPasswordLabel={t('showPassword', 'Show password')}
260
+ invalidText={t('validValueRequired', 'A valid value is required')}
261
+ aria-hidden={!showPasswordField}
262
+ tabIndex={showPasswordField ? 0 : -1}
263
+ />
264
+ </div>
265
+ {showPasswordField ? (
266
+ <Button
267
+ type="submit"
268
+ className={styles.continueButton}
269
+ renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
270
+ iconDescription={t('loginButtonIconDescription', 'Log in button')}
271
+ disabled={!isLoginEnabled || isLoggingIn}
272
+ >
273
+ {isLoggingIn ? (
274
+ <InlineLoading
275
+ className={styles.loader}
276
+ description={t('loggingIn', 'Logging in') + '...'}
277
+ />
278
+ ) : (
279
+ t('login', 'Log in')
280
+ )}
281
+ </Button>
282
+ ) : (
283
+ <Button
284
+ type="submit"
285
+ className={styles.continueButton}
286
+ renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
287
+ iconDescription="Continue to password"
288
+ onClick={(evt) => {
289
+ evt.preventDefault();
290
+ continueLogin();
291
+ }}
292
+ disabled={!isLoginEnabled}
293
+ >
294
+ {t('continue', 'Continue')}
295
+ </Button>
296
+ )}
297
+ </>
298
+ ) : (
299
+ <>
300
+ <PasswordInput
301
+ id="password"
302
+ labelText={t('password', 'Password')}
303
+ name="password"
304
+ autoComplete="current-password"
305
+ onChange={changePassword}
306
+ ref={passwordInputRef}
307
+ required
308
+ value={password}
309
+ showPasswordLabel={t('showPassword', 'Show password')}
310
+ invalidText={t('validValueRequired', 'A valid value is required')}
311
+ />
312
+ <Button
313
+ type="submit"
314
+ className={styles.continueButton}
315
+ renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
316
+ iconDescription="Log in"
317
+ disabled={!isLoginEnabled || isLoggingIn}
318
+ >
319
+ {isLoggingIn ? (
320
+ <InlineLoading
321
+ className={styles.loader}
322
+ description={t('loggingIn', 'Logging in') + '...'}
323
+ />
324
+ ) : (
325
+ t('login', 'Log in')
326
+ )}
327
+ </Button>
328
+ </>
329
+ )}
330
+ </div>
331
+ </form>
332
+ </Tile>
333
+ </div>
334
+ <div className={styles.logoSection}>
335
+ <div className={styles.leftLogos}>
336
+ <img src={gokLogo} alt="GOK logo" className={styles.supportingLogos} />
337
+ <img src={dhaLogo} alt="DHA logo" className={styles.dhaLogo} />
338
+ </div>
339
+ <img src={openmrsLogo} alt="OpenMRS logo" className={styles.openmsrsLogo} />
340
+ </div>
341
+ </div>
342
+ <img className={styles.image} src={image} alt="TAIFA CARE" />
343
+ </div>
344
+ </>
345
+ );
346
+ }
347
+ return null;
348
+ };
349
+
350
+ export default Login;