@digitaldefiance/express-suite-react-components 2.9.37 → 2.9.38

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 (257) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +4 -5
  3. package/src/auth/Private.tsx +17 -0
  4. package/src/auth/PrivateRoute.tsx +28 -0
  5. package/src/auth/UnAuth.tsx +16 -0
  6. package/src/auth/UnAuthRoute.tsx +30 -0
  7. package/src/auth/{index.d.ts → index.ts} +1 -2
  8. package/src/components/ApiAccess.tsx +174 -0
  9. package/src/components/BackupCodeLoginForm.tsx +488 -0
  10. package/src/components/BackupCodesForm.tsx +286 -0
  11. package/src/components/ChangePasswordForm.tsx +272 -0
  12. package/src/components/ConfirmationDialog.tsx +48 -0
  13. package/src/components/CurrencyCodeSelector.tsx +60 -0
  14. package/src/components/CurrencyInput.tsx +80 -0
  15. package/src/components/DashboardPage.tsx +24 -0
  16. package/src/components/DropdownMenu.tsx +92 -0
  17. package/src/components/ExpirationSecondsSelector.tsx +60 -0
  18. package/src/components/Flag.tsx +52 -0
  19. package/src/components/ForgotPasswordForm.tsx +173 -0
  20. package/src/components/LoginForm.tsx +455 -0
  21. package/src/components/LogoutPage.tsx +21 -0
  22. package/src/components/RegisterForm.tsx +602 -0
  23. package/src/components/ResetPasswordForm.tsx +246 -0
  24. package/src/components/SideMenu.tsx +46 -0
  25. package/src/components/SideMenuListItem.tsx +74 -0
  26. package/src/components/TopMenu.tsx +145 -0
  27. package/src/components/TranslatedTitle.tsx +29 -0
  28. package/src/components/UserLanguageSelector.tsx +45 -0
  29. package/src/components/UserMenu.tsx +15 -0
  30. package/src/components/UserSettingsForm.tsx +505 -0
  31. package/src/components/VerifyEmailPage.tsx +184 -0
  32. package/src/components/{index.d.ts → index.ts} +1 -1
  33. package/src/contexts/AuthProvider.spec.tsx +1195 -0
  34. package/src/contexts/AuthProvider.tsx +924 -0
  35. package/src/contexts/I18nProvider.tsx +114 -0
  36. package/src/contexts/MenuContext.tsx +398 -0
  37. package/src/contexts/SuiteConfigProvider.tsx +93 -0
  38. package/src/contexts/ThemeProvider.tsx +67 -0
  39. package/src/contexts/{index.d.ts → index.ts} +0 -1
  40. package/src/hooks/{index.d.ts → index.ts} +0 -1
  41. package/src/hooks/useBackupCodes.ts +105 -0
  42. package/src/hooks/useEmailVerification.ts +49 -0
  43. package/src/hooks/useExpiringValue.ts +78 -0
  44. package/src/hooks/useLocalStorage.ts +18 -0
  45. package/src/hooks/useUserSettings.ts +269 -0
  46. package/src/{index.d.ts → index.ts} +1 -1
  47. package/src/interfaces/IAppConfig.ts +5 -0
  48. package/src/interfaces/IMenuConfig.ts +11 -0
  49. package/src/interfaces/IMenuOption.ts +55 -0
  50. package/src/interfaces/index.ts +3 -0
  51. package/src/services/__mocks__/authService.ts +14 -0
  52. package/src/services/api.ts +13 -0
  53. package/src/services/authService.ts +500 -0
  54. package/src/services/authenticatedApi.ts +17 -0
  55. package/src/services/index.ts +3 -0
  56. package/src/types/MenuType.ts +15 -0
  57. package/src/types/expirationSeconds.ts +18 -0
  58. package/src/types/index.ts +1 -0
  59. package/src/types/translation.ts +20 -0
  60. package/src/wrappers/BackupCodeLoginWrapper.tsx +34 -0
  61. package/src/wrappers/BackupCodesWrapper.tsx +28 -0
  62. package/src/wrappers/ChangePasswordFormWrapper.tsx +34 -0
  63. package/src/wrappers/LoginFormWrapper.tsx +59 -0
  64. package/src/wrappers/LogoutPageWrapper.tsx +30 -0
  65. package/src/wrappers/RegisterFormWrapper.tsx +61 -0
  66. package/src/wrappers/UserSettingsFormWrapper.tsx +39 -0
  67. package/src/wrappers/VerifyEmailPageWrapper.tsx +27 -0
  68. package/src/wrappers/{index.d.ts → index.tsx} +8 -1
  69. package/src/auth/Private.d.ts +0 -6
  70. package/src/auth/Private.d.ts.map +0 -1
  71. package/src/auth/Private.js +0 -14
  72. package/src/auth/PrivateRoute.d.ts +0 -8
  73. package/src/auth/PrivateRoute.d.ts.map +0 -1
  74. package/src/auth/PrivateRoute.js +0 -23
  75. package/src/auth/UnAuth.d.ts +0 -6
  76. package/src/auth/UnAuth.d.ts.map +0 -1
  77. package/src/auth/UnAuth.js +0 -14
  78. package/src/auth/UnAuthRoute.d.ts +0 -8
  79. package/src/auth/UnAuthRoute.d.ts.map +0 -1
  80. package/src/auth/UnAuthRoute.js +0 -22
  81. package/src/auth/index.d.ts.map +0 -1
  82. package/src/auth/index.js +0 -10
  83. package/src/components/ApiAccess.d.ts +0 -16
  84. package/src/components/ApiAccess.d.ts.map +0 -1
  85. package/src/components/ApiAccess.js +0 -77
  86. package/src/components/BackupCodeLoginForm.d.ts +0 -43
  87. package/src/components/BackupCodeLoginForm.d.ts.map +0 -1
  88. package/src/components/BackupCodeLoginForm.js +0 -139
  89. package/src/components/BackupCodesForm.d.ts +0 -26
  90. package/src/components/BackupCodesForm.d.ts.map +0 -1
  91. package/src/components/BackupCodesForm.js +0 -120
  92. package/src/components/ChangePasswordForm.d.ts +0 -26
  93. package/src/components/ChangePasswordForm.d.ts.map +0 -1
  94. package/src/components/ChangePasswordForm.js +0 -78
  95. package/src/components/ConfirmationDialog.d.ts +0 -13
  96. package/src/components/ConfirmationDialog.d.ts.map +0 -1
  97. package/src/components/ConfirmationDialog.js +0 -10
  98. package/src/components/CurrencyCodeSelector.d.ts +0 -9
  99. package/src/components/CurrencyCodeSelector.d.ts.map +0 -1
  100. package/src/components/CurrencyCodeSelector.js +0 -31
  101. package/src/components/CurrencyInput.d.ts +0 -13
  102. package/src/components/CurrencyInput.d.ts.map +0 -1
  103. package/src/components/CurrencyInput.js +0 -22
  104. package/src/components/DashboardPage.d.ts +0 -8
  105. package/src/components/DashboardPage.d.ts.map +0 -1
  106. package/src/components/DashboardPage.js +0 -10
  107. package/src/components/DropdownMenu.d.ts +0 -9
  108. package/src/components/DropdownMenu.d.ts.map +0 -1
  109. package/src/components/DropdownMenu.js +0 -56
  110. package/src/components/ExpirationSecondsSelector.d.ts +0 -13
  111. package/src/components/ExpirationSecondsSelector.d.ts.map +0 -1
  112. package/src/components/ExpirationSecondsSelector.js +0 -32
  113. package/src/components/Flag.d.ts +0 -20
  114. package/src/components/Flag.d.ts.map +0 -1
  115. package/src/components/Flag.js +0 -43
  116. package/src/components/ForgotPasswordForm.d.ts +0 -18
  117. package/src/components/ForgotPasswordForm.d.ts.map +0 -1
  118. package/src/components/ForgotPasswordForm.js +0 -61
  119. package/src/components/LoginForm.d.ts +0 -44
  120. package/src/components/LoginForm.d.ts.map +0 -1
  121. package/src/components/LoginForm.js +0 -122
  122. package/src/components/LogoutPage.d.ts +0 -8
  123. package/src/components/LogoutPage.d.ts.map +0 -1
  124. package/src/components/LogoutPage.js +0 -16
  125. package/src/components/RegisterForm.d.ts +0 -56
  126. package/src/components/RegisterForm.d.ts.map +0 -1
  127. package/src/components/RegisterForm.js +0 -140
  128. package/src/components/ResetPasswordForm.d.ts +0 -23
  129. package/src/components/ResetPasswordForm.d.ts.map +0 -1
  130. package/src/components/ResetPasswordForm.js +0 -78
  131. package/src/components/SideMenu.d.ts +0 -8
  132. package/src/components/SideMenu.d.ts.map +0 -1
  133. package/src/components/SideMenu.js +0 -25
  134. package/src/components/SideMenuListItem.d.ts +0 -13
  135. package/src/components/SideMenuListItem.d.ts.map +0 -1
  136. package/src/components/SideMenuListItem.js +0 -44
  137. package/src/components/TopMenu.d.ts +0 -24
  138. package/src/components/TopMenu.d.ts.map +0 -1
  139. package/src/components/TopMenu.js +0 -35
  140. package/src/components/TranslatedTitle.d.ts +0 -7
  141. package/src/components/TranslatedTitle.d.ts.map +0 -1
  142. package/src/components/TranslatedTitle.js +0 -15
  143. package/src/components/UserLanguageSelector.d.ts +0 -4
  144. package/src/components/UserLanguageSelector.d.ts.map +0 -1
  145. package/src/components/UserLanguageSelector.js +0 -31
  146. package/src/components/UserMenu.d.ts +0 -4
  147. package/src/components/UserMenu.d.ts.map +0 -1
  148. package/src/components/UserMenu.js +0 -12
  149. package/src/components/UserSettingsForm.d.ts +0 -57
  150. package/src/components/UserSettingsForm.d.ts.map +0 -1
  151. package/src/components/UserSettingsForm.js +0 -126
  152. package/src/components/VerifyEmailPage.d.ts +0 -23
  153. package/src/components/VerifyEmailPage.d.ts.map +0 -1
  154. package/src/components/VerifyEmailPage.js +0 -70
  155. package/src/components/index.d.ts.map +0 -1
  156. package/src/components/index.js +0 -28
  157. package/src/contexts/AuthProvider.d.ts +0 -152
  158. package/src/contexts/AuthProvider.d.ts.map +0 -1
  159. package/src/contexts/AuthProvider.js +0 -502
  160. package/src/contexts/I18nProvider.d.ts +0 -16
  161. package/src/contexts/I18nProvider.d.ts.map +0 -1
  162. package/src/contexts/I18nProvider.js +0 -46
  163. package/src/contexts/MenuContext.d.ts +0 -20
  164. package/src/contexts/MenuContext.d.ts.map +0 -1
  165. package/src/contexts/MenuContext.js +0 -273
  166. package/src/contexts/SuiteConfigProvider.d.ts +0 -44
  167. package/src/contexts/SuiteConfigProvider.d.ts.map +0 -1
  168. package/src/contexts/SuiteConfigProvider.js +0 -43
  169. package/src/contexts/ThemeProvider.d.ts +0 -15
  170. package/src/contexts/ThemeProvider.d.ts.map +0 -1
  171. package/src/contexts/ThemeProvider.js +0 -36
  172. package/src/contexts/index.d.ts.map +0 -1
  173. package/src/contexts/index.js +0 -8
  174. package/src/hooks/index.d.ts.map +0 -1
  175. package/src/hooks/index.js +0 -8
  176. package/src/hooks/useBackupCodes.d.ts +0 -15
  177. package/src/hooks/useBackupCodes.d.ts.map +0 -1
  178. package/src/hooks/useBackupCodes.js +0 -74
  179. package/src/hooks/useEmailVerification.d.ts +0 -10
  180. package/src/hooks/useEmailVerification.d.ts.map +0 -1
  181. package/src/hooks/useEmailVerification.js +0 -40
  182. package/src/hooks/useExpiringValue.d.ts +0 -14
  183. package/src/hooks/useExpiringValue.d.ts.map +0 -1
  184. package/src/hooks/useExpiringValue.js +0 -53
  185. package/src/hooks/useLocalStorage.d.ts +0 -2
  186. package/src/hooks/useLocalStorage.d.ts.map +0 -1
  187. package/src/hooks/useLocalStorage.js +0 -15
  188. package/src/hooks/useUserSettings.d.ts +0 -48
  189. package/src/hooks/useUserSettings.d.ts.map +0 -1
  190. package/src/hooks/useUserSettings.js +0 -169
  191. package/src/index.d.ts.map +0 -1
  192. package/src/index.js +0 -12
  193. package/src/interfaces/IAppConfig.d.ts +0 -6
  194. package/src/interfaces/IAppConfig.d.ts.map +0 -1
  195. package/src/interfaces/IAppConfig.js +0 -2
  196. package/src/interfaces/IMenuConfig.d.ts +0 -11
  197. package/src/interfaces/IMenuConfig.d.ts.map +0 -1
  198. package/src/interfaces/IMenuConfig.js +0 -2
  199. package/src/interfaces/IMenuOption.d.ts +0 -58
  200. package/src/interfaces/IMenuOption.d.ts.map +0 -1
  201. package/src/interfaces/IMenuOption.js +0 -2
  202. package/src/interfaces/index.d.ts +0 -4
  203. package/src/interfaces/index.d.ts.map +0 -1
  204. package/src/interfaces/index.js +0 -6
  205. package/src/services/__mocks__/authService.d.ts +0 -21
  206. package/src/services/__mocks__/authService.d.ts.map +0 -1
  207. package/src/services/__mocks__/authService.js +0 -15
  208. package/src/services/api.d.ts +0 -3
  209. package/src/services/api.d.ts.map +0 -1
  210. package/src/services/api.js +0 -14
  211. package/src/services/authService.d.ts +0 -72
  212. package/src/services/authService.d.ts.map +0 -1
  213. package/src/services/authService.js +0 -335
  214. package/src/services/authenticatedApi.d.ts +0 -3
  215. package/src/services/authenticatedApi.d.ts.map +0 -1
  216. package/src/services/authenticatedApi.js +0 -18
  217. package/src/services/index.d.ts +0 -4
  218. package/src/services/index.d.ts.map +0 -1
  219. package/src/services/index.js +0 -6
  220. package/src/types/MenuType.d.ts +0 -11
  221. package/src/types/MenuType.d.ts.map +0 -1
  222. package/src/types/MenuType.js +0 -12
  223. package/src/types/expirationSeconds.d.ts +0 -3
  224. package/src/types/expirationSeconds.d.ts.map +0 -1
  225. package/src/types/expirationSeconds.js +0 -17
  226. package/src/types/index.d.ts +0 -2
  227. package/src/types/index.d.ts.map +0 -1
  228. package/src/types/index.js +0 -4
  229. package/src/types/translation.d.ts +0 -10
  230. package/src/types/translation.d.ts.map +0 -1
  231. package/src/types/translation.js +0 -9
  232. package/src/wrappers/BackupCodeLoginWrapper.d.ts +0 -8
  233. package/src/wrappers/BackupCodeLoginWrapper.d.ts.map +0 -1
  234. package/src/wrappers/BackupCodeLoginWrapper.js +0 -20
  235. package/src/wrappers/BackupCodesWrapper.d.ts +0 -7
  236. package/src/wrappers/BackupCodesWrapper.d.ts.map +0 -1
  237. package/src/wrappers/BackupCodesWrapper.js +0 -17
  238. package/src/wrappers/ChangePasswordFormWrapper.d.ts +0 -8
  239. package/src/wrappers/ChangePasswordFormWrapper.d.ts.map +0 -1
  240. package/src/wrappers/ChangePasswordFormWrapper.js +0 -21
  241. package/src/wrappers/LoginFormWrapper.d.ts +0 -9
  242. package/src/wrappers/LoginFormWrapper.d.ts.map +0 -1
  243. package/src/wrappers/LoginFormWrapper.js +0 -43
  244. package/src/wrappers/LogoutPageWrapper.d.ts +0 -9
  245. package/src/wrappers/LogoutPageWrapper.d.ts.map +0 -1
  246. package/src/wrappers/LogoutPageWrapper.js +0 -21
  247. package/src/wrappers/RegisterFormWrapper.d.ts +0 -9
  248. package/src/wrappers/RegisterFormWrapper.d.ts.map +0 -1
  249. package/src/wrappers/RegisterFormWrapper.js +0 -31
  250. package/src/wrappers/UserSettingsFormWrapper.d.ts +0 -8
  251. package/src/wrappers/UserSettingsFormWrapper.d.ts.map +0 -1
  252. package/src/wrappers/UserSettingsFormWrapper.js +0 -24
  253. package/src/wrappers/VerifyEmailPageWrapper.d.ts +0 -8
  254. package/src/wrappers/VerifyEmailPageWrapper.d.ts.map +0 -1
  255. package/src/wrappers/VerifyEmailPageWrapper.js +0 -20
  256. package/src/wrappers/index.d.ts.map +0 -1
  257. package/src/wrappers/index.js +0 -20
@@ -0,0 +1,505 @@
1
+ import { CurrencyCode } from '@digitaldefiance/i18n-lib';
2
+ import {
3
+ SuiteCoreComponentId,
4
+ SuiteCoreStringKey,
5
+ } from '@digitaldefiance/suite-core-lib';
6
+ import {
7
+ Alert,
8
+ Box,
9
+ Button,
10
+ Container,
11
+ FormControl,
12
+ FormControlLabel,
13
+ InputLabel,
14
+ MenuItem,
15
+ Select,
16
+ Switch,
17
+ TextField,
18
+ Typography,
19
+ } from '@mui/material';
20
+ import { useFormik } from 'formik';
21
+ import moment from 'moment-timezone';
22
+ import { FC, useMemo, useState } from 'react';
23
+ import * as Yup from 'yup';
24
+ import { useI18n } from '../contexts';
25
+
26
+ export interface UserSettingsFormValues {
27
+ email: string;
28
+ timezone: string;
29
+ siteLanguage: string;
30
+ currency: string;
31
+ darkMode: boolean;
32
+ directChallenge: boolean;
33
+ [key: string]: string | boolean;
34
+ }
35
+
36
+ export interface UserSettingsFormProps {
37
+ initialValues: UserSettingsFormValues;
38
+ onSubmit: (values: UserSettingsFormValues) => Promise<
39
+ | { success: boolean; message: string }
40
+ | {
41
+ error: string;
42
+ errorType?: string;
43
+ field?: string;
44
+ errors?: Array<{ path: string; msg: string }>;
45
+ }
46
+ >;
47
+ languages: Array<{ code: string; label: string }>;
48
+ emailValidation?: Yup.StringSchema;
49
+ timezoneValidation?: Yup.StringSchema;
50
+ siteLanguageValidation?: Yup.StringSchema;
51
+ currencyValidation?: Yup.StringSchema;
52
+ darkModeValidation?: Yup.BooleanSchema;
53
+ directChallengeValidation?: Yup.BooleanSchema;
54
+ additionalFields?: (
55
+ formik: ReturnType<typeof useFormik<UserSettingsFormValues>>
56
+ ) => React.ReactNode;
57
+ additionalInitialValues?: Record<string, string | boolean>;
58
+ additionalValidation?: Record<string, Yup.Schema>;
59
+ labels?: {
60
+ title?: string;
61
+ email?: string;
62
+ emailHelper?: string;
63
+ timezone?: string;
64
+ siteLanguage?: string;
65
+ currency?: string;
66
+ darkMode?: string;
67
+ directChallenge?: string;
68
+ directChallengeHelper?: string;
69
+ saving?: string;
70
+ save?: string;
71
+ successMessage?: string;
72
+ };
73
+ }
74
+
75
+ export const UserSettingsForm: FC<UserSettingsFormProps> = ({
76
+ initialValues,
77
+ onSubmit,
78
+ languages,
79
+ emailValidation,
80
+ timezoneValidation,
81
+ siteLanguageValidation,
82
+ currencyValidation,
83
+ darkModeValidation,
84
+ directChallengeValidation,
85
+ additionalFields,
86
+ additionalInitialValues = {},
87
+ additionalValidation = {},
88
+ labels = {},
89
+ }) => {
90
+ const { tComponent } = useI18n();
91
+ const [apiErrors, setApiErrors] = useState<Record<string, string>>({});
92
+ const [saving, setSaving] = useState(false);
93
+ const [successMessage, setSuccessMessage] = useState<string | null>(null);
94
+
95
+ const timezones = useMemo(() => moment.tz.names(), []);
96
+ const currencies = useMemo(
97
+ () =>
98
+ CurrencyCode.getAllData().map((c) => ({
99
+ code: c.code,
100
+ label: `${c.code} - ${c.currency}`,
101
+ })),
102
+ []
103
+ );
104
+
105
+ const validation = {
106
+ email:
107
+ emailValidation ||
108
+ Yup.string()
109
+ .email(
110
+ tComponent<SuiteCoreStringKey>(
111
+ SuiteCoreComponentId,
112
+ SuiteCoreStringKey.Validation_InvalidEmail
113
+ )
114
+ )
115
+ .required(
116
+ tComponent<SuiteCoreStringKey>(
117
+ SuiteCoreComponentId,
118
+ SuiteCoreStringKey.Validation_Required
119
+ )
120
+ ),
121
+ timezone:
122
+ timezoneValidation ||
123
+ Yup.string()
124
+ .required(
125
+ tComponent<SuiteCoreStringKey>(
126
+ SuiteCoreComponentId,
127
+ SuiteCoreStringKey.Validation_TimezoneRequired
128
+ )
129
+ )
130
+ .test(
131
+ 'valid-timezone',
132
+ tComponent<SuiteCoreStringKey>(
133
+ SuiteCoreComponentId,
134
+ SuiteCoreStringKey.Validation_TimezoneInvalid
135
+ ),
136
+ (value) => !value || moment.tz.zone(value) !== null
137
+ ),
138
+ siteLanguage:
139
+ siteLanguageValidation ||
140
+ Yup.string().required(
141
+ tComponent<SuiteCoreStringKey>(
142
+ SuiteCoreComponentId,
143
+ SuiteCoreStringKey.Validation_Required
144
+ )
145
+ ),
146
+ currency:
147
+ currencyValidation ||
148
+ Yup.string()
149
+ .required(
150
+ tComponent<SuiteCoreStringKey>(
151
+ SuiteCoreComponentId,
152
+ SuiteCoreStringKey.Validation_Required
153
+ )
154
+ )
155
+ .test(
156
+ 'valid-currency',
157
+ tComponent<SuiteCoreStringKey>(
158
+ SuiteCoreComponentId,
159
+ SuiteCoreStringKey.Validation_Required
160
+ ),
161
+ (value) => !value || CurrencyCode.isValid(value)
162
+ ),
163
+ darkMode:
164
+ darkModeValidation ||
165
+ Yup.boolean().required(
166
+ tComponent<SuiteCoreStringKey>(
167
+ SuiteCoreComponentId,
168
+ SuiteCoreStringKey.Validation_Required
169
+ )
170
+ ),
171
+ directChallenge:
172
+ directChallengeValidation ||
173
+ Yup.boolean().required(
174
+ tComponent<SuiteCoreStringKey>(
175
+ SuiteCoreComponentId,
176
+ SuiteCoreStringKey.Validation_Required
177
+ )
178
+ ),
179
+ };
180
+
181
+ const formik = useFormik<UserSettingsFormValues>({
182
+ initialValues: {
183
+ ...initialValues,
184
+ ...additionalInitialValues,
185
+ },
186
+ enableReinitialize: true,
187
+ validationSchema: Yup.object({
188
+ email: validation.email,
189
+ timezone: validation.timezone,
190
+ siteLanguage: validation.siteLanguage,
191
+ currency: validation.currency,
192
+ darkMode: validation.darkMode,
193
+ directChallenge: validation.directChallenge,
194
+ ...additionalValidation,
195
+ }),
196
+ onSubmit: async (values, { setSubmitting, setFieldError, setTouched }) => {
197
+ setSaving(true);
198
+ setSuccessMessage(null);
199
+ const result = await onSubmit(values);
200
+
201
+ if ('success' in result && result.success) {
202
+ setSuccessMessage(
203
+ result.message ||
204
+ labels.successMessage ||
205
+ tComponent<SuiteCoreStringKey>(
206
+ SuiteCoreComponentId,
207
+ SuiteCoreStringKey.Settings_SaveSuccess
208
+ )
209
+ );
210
+ setApiErrors({});
211
+ } else {
212
+ const newApiErrors: Record<string, string> = {};
213
+ const fieldsToTouch: Record<string, boolean> = {};
214
+
215
+ if ('field' in result && result.field) {
216
+ setFieldError(result.field, result.error);
217
+ fieldsToTouch[result.field] = true;
218
+ }
219
+
220
+ if ('errors' in result && result.errors) {
221
+ result.errors.forEach((err) => {
222
+ if (err.path && err.msg) {
223
+ setFieldError(err.path, err.msg);
224
+ fieldsToTouch[err.path] = true;
225
+ }
226
+ });
227
+ }
228
+
229
+ if (
230
+ 'error' in result &&
231
+ result.error &&
232
+ !Object.keys(newApiErrors).length
233
+ ) {
234
+ newApiErrors.general = result.error;
235
+ }
236
+
237
+ setApiErrors(newApiErrors);
238
+ setTouched(fieldsToTouch, false);
239
+ }
240
+ setSubmitting(false);
241
+ setSaving(false);
242
+ },
243
+ });
244
+
245
+ return (
246
+ <Container maxWidth="sm">
247
+ <Box
248
+ sx={{
249
+ mt: 4,
250
+ display: 'flex',
251
+ flexDirection: 'column',
252
+ alignItems: 'center',
253
+ }}
254
+ >
255
+ <Typography variant="h4" component="h1" gutterBottom>
256
+ {labels.title ||
257
+ tComponent<SuiteCoreStringKey>(
258
+ SuiteCoreComponentId,
259
+ SuiteCoreStringKey.Settings_Title
260
+ )}
261
+ </Typography>
262
+
263
+ <Box
264
+ component="form"
265
+ onSubmit={formik.handleSubmit}
266
+ sx={{ mt: 1, width: '100%' }}
267
+ >
268
+ <TextField
269
+ fullWidth
270
+ id="email"
271
+ name="email"
272
+ label={
273
+ labels.email ||
274
+ tComponent<SuiteCoreStringKey>(
275
+ SuiteCoreComponentId,
276
+ SuiteCoreStringKey.Common_Email
277
+ )
278
+ }
279
+ value={formik.values.email}
280
+ onChange={formik.handleChange}
281
+ onBlur={formik.handleBlur}
282
+ error={Boolean(
283
+ formik.touched.email && (formik.errors.email || apiErrors.email)
284
+ )}
285
+ helperText={
286
+ (formik.touched.email &&
287
+ (formik.errors.email || apiErrors.email)) ||
288
+ labels.emailHelper ||
289
+ tComponent<SuiteCoreStringKey>(
290
+ SuiteCoreComponentId,
291
+ SuiteCoreStringKey.Settings_EmailHelper
292
+ )
293
+ }
294
+ margin="normal"
295
+ />
296
+
297
+ <FormControl fullWidth margin="normal">
298
+ <InputLabel id="timezone-label">
299
+ {labels.timezone ||
300
+ tComponent<SuiteCoreStringKey>(
301
+ SuiteCoreComponentId,
302
+ SuiteCoreStringKey.Common_Timezone
303
+ )}
304
+ </InputLabel>
305
+ <Select
306
+ labelId="timezone-label"
307
+ id="timezone"
308
+ name="timezone"
309
+ value={formik.values.timezone}
310
+ onChange={formik.handleChange}
311
+ onBlur={formik.handleBlur}
312
+ error={formik.touched.timezone && Boolean(formik.errors.timezone)}
313
+ label={
314
+ labels.timezone ||
315
+ tComponent<SuiteCoreStringKey>(
316
+ SuiteCoreComponentId,
317
+ SuiteCoreStringKey.Common_Timezone
318
+ )
319
+ }
320
+ >
321
+ {timezones.map((tz) => (
322
+ <MenuItem key={tz} value={tz}>
323
+ {tz}
324
+ </MenuItem>
325
+ ))}
326
+ </Select>
327
+ {formik.touched.timezone &&
328
+ (formik.errors.timezone || apiErrors.timezone) && (
329
+ <Typography color="error" variant="caption">
330
+ {formik.errors.timezone || apiErrors.timezone}
331
+ </Typography>
332
+ )}
333
+ </FormControl>
334
+
335
+ <FormControl fullWidth margin="normal">
336
+ <InputLabel id="language-label">
337
+ {labels.siteLanguage ||
338
+ tComponent<SuiteCoreStringKey>(
339
+ SuiteCoreComponentId,
340
+ SuiteCoreStringKey.Settings_SiteLanguage
341
+ )}
342
+ </InputLabel>
343
+ <Select
344
+ labelId="language-label"
345
+ id="siteLanguage"
346
+ name="siteLanguage"
347
+ value={formik.values.siteLanguage}
348
+ onChange={formik.handleChange}
349
+ onBlur={formik.handleBlur}
350
+ error={
351
+ formik.touched.siteLanguage &&
352
+ Boolean(formik.errors.siteLanguage)
353
+ }
354
+ label={
355
+ labels.siteLanguage ||
356
+ tComponent<SuiteCoreStringKey>(
357
+ SuiteCoreComponentId,
358
+ SuiteCoreStringKey.Settings_SiteLanguage
359
+ )
360
+ }
361
+ >
362
+ {languages.map((lang) => (
363
+ <MenuItem key={lang.code} value={lang.code}>
364
+ {lang.label}
365
+ </MenuItem>
366
+ ))}
367
+ </Select>
368
+ {formik.touched.siteLanguage &&
369
+ (formik.errors.siteLanguage || apiErrors.siteLanguage) && (
370
+ <Typography color="error" variant="caption">
371
+ {formik.errors.siteLanguage || apiErrors.siteLanguage}
372
+ </Typography>
373
+ )}
374
+ </FormControl>
375
+
376
+ <FormControl fullWidth margin="normal">
377
+ <InputLabel id="currency-label">
378
+ {labels.currency ||
379
+ tComponent<SuiteCoreStringKey>(
380
+ SuiteCoreComponentId,
381
+ SuiteCoreStringKey.Settings_Currency
382
+ )}
383
+ </InputLabel>
384
+ <Select
385
+ labelId="currency-label"
386
+ id="currency"
387
+ name="currency"
388
+ value={formik.values.currency}
389
+ onChange={formik.handleChange}
390
+ onBlur={formik.handleBlur}
391
+ error={formik.touched.currency && Boolean(formik.errors.currency)}
392
+ label={
393
+ labels.currency ||
394
+ tComponent<SuiteCoreStringKey>(
395
+ SuiteCoreComponentId,
396
+ SuiteCoreStringKey.Settings_Currency
397
+ )
398
+ }
399
+ >
400
+ {currencies.map((curr) => (
401
+ <MenuItem key={curr.code} value={curr.code}>
402
+ {curr.label}
403
+ </MenuItem>
404
+ ))}
405
+ </Select>
406
+ {formik.touched.currency &&
407
+ (formik.errors.currency || apiErrors.currency) && (
408
+ <Typography color="error" variant="caption">
409
+ {formik.errors.currency || apiErrors.currency}
410
+ </Typography>
411
+ )}
412
+ </FormControl>
413
+
414
+ <FormControl fullWidth margin="normal">
415
+ <FormControlLabel
416
+ control={
417
+ <Switch
418
+ id="darkMode"
419
+ name="darkMode"
420
+ checked={formik.values.darkMode}
421
+ onChange={formik.handleChange}
422
+ />
423
+ }
424
+ label={
425
+ labels.darkMode ||
426
+ tComponent<SuiteCoreStringKey>(
427
+ SuiteCoreComponentId,
428
+ SuiteCoreStringKey.Settings_DarkMode
429
+ )
430
+ }
431
+ />
432
+ </FormControl>
433
+
434
+ <FormControl fullWidth margin="normal">
435
+ <FormControlLabel
436
+ control={
437
+ <Switch
438
+ id="directChallenge"
439
+ name="directChallenge"
440
+ checked={formik.values.directChallenge}
441
+ onChange={formik.handleChange}
442
+ />
443
+ }
444
+ label={
445
+ labels.directChallenge ||
446
+ tComponent<SuiteCoreStringKey>(
447
+ SuiteCoreComponentId,
448
+ SuiteCoreStringKey.Registration_DirectChallengeLabel
449
+ )
450
+ }
451
+ />
452
+ <Typography
453
+ variant="caption"
454
+ color="text.secondary"
455
+ sx={{ ml: 4, mt: -1 }}
456
+ >
457
+ {labels.directChallengeHelper ||
458
+ tComponent<SuiteCoreStringKey>(
459
+ SuiteCoreComponentId,
460
+ SuiteCoreStringKey.Registration_DirectChallengeHelper
461
+ )}
462
+ </Typography>
463
+ </FormControl>
464
+
465
+ {additionalFields && additionalFields(formik)}
466
+
467
+ {apiErrors.general && (
468
+ <Alert severity="error" sx={{ mt: 2, mb: 2 }}>
469
+ {apiErrors.general}
470
+ </Alert>
471
+ )}
472
+
473
+ {successMessage && (
474
+ <Alert severity="success" sx={{ mt: 2, mb: 2 }}>
475
+ {successMessage}
476
+ </Alert>
477
+ )}
478
+
479
+ <Button
480
+ type="submit"
481
+ fullWidth
482
+ variant="contained"
483
+ color="primary"
484
+ sx={{ mt: 3, mb: 2 }}
485
+ disabled={formik.isSubmitting}
486
+ >
487
+ {saving
488
+ ? labels.saving ||
489
+ tComponent<SuiteCoreStringKey>(
490
+ SuiteCoreComponentId,
491
+ SuiteCoreStringKey.Settings_Saving
492
+ )
493
+ : labels.save ||
494
+ tComponent<SuiteCoreStringKey>(
495
+ SuiteCoreComponentId,
496
+ SuiteCoreStringKey.Settings_Save
497
+ )}
498
+ </Button>
499
+ </Box>
500
+ </Box>
501
+ </Container>
502
+ );
503
+ };
504
+
505
+ export default UserSettingsForm;
@@ -0,0 +1,184 @@
1
+ import {
2
+ SuiteCoreComponentId,
3
+ SuiteCoreStringKey,
4
+ } from '@digitaldefiance/suite-core-lib';
5
+ import {
6
+ Alert,
7
+ Box,
8
+ Button,
9
+ CircularProgress,
10
+ Container,
11
+ Link,
12
+ Typography,
13
+ } from '@mui/material';
14
+ import { FC, useEffect, useMemo, useState } from 'react';
15
+ import { useI18n } from '../contexts';
16
+
17
+ export interface VerifyEmailPageProps {
18
+ token: string | null;
19
+ onVerify: (token: string) => Promise<{ success: boolean; message?: string }>;
20
+ labels?: {
21
+ title?: string;
22
+ verifying?: string;
23
+ success?: string;
24
+ failed?: string;
25
+ noToken?: string;
26
+ proceedToLogin?: string;
27
+ contactSupport?: string;
28
+ requestNewEmail?: string;
29
+ };
30
+ loginLink?: string;
31
+ resendLink?: string;
32
+ }
33
+
34
+ export const VerifyEmailPage: FC<VerifyEmailPageProps> = ({
35
+ token,
36
+ onVerify,
37
+ labels = {},
38
+ loginLink = '/login',
39
+ resendLink = '/resend-verification',
40
+ }) => {
41
+ const { tComponent } = useI18n();
42
+ const [message, setMessage] = useState('');
43
+ const [loading, setLoading] = useState(true);
44
+ const [verificationStatus, setVerificationStatus] = useState<
45
+ 'pending' | 'success' | 'error'
46
+ >('pending');
47
+
48
+ const translatedLabels = useMemo<{
49
+ title: string;
50
+ success: string;
51
+ failed: string;
52
+ noToken: string;
53
+ proceedToLogin: string;
54
+ contactSupport: string;
55
+ requestNewEmail: string;
56
+ }>(() => {
57
+ return {
58
+ title:
59
+ labels.title ||
60
+ tComponent<SuiteCoreStringKey>(
61
+ SuiteCoreComponentId,
62
+ SuiteCoreStringKey.Common_EmailVerification
63
+ ),
64
+ success:
65
+ labels.success ||
66
+ tComponent<SuiteCoreStringKey>(
67
+ SuiteCoreComponentId,
68
+ SuiteCoreStringKey.EmailVerification_Success
69
+ ),
70
+ failed:
71
+ labels.failed ||
72
+ tComponent<SuiteCoreStringKey>(
73
+ SuiteCoreComponentId,
74
+ SuiteCoreStringKey.EmailVerification_Failed
75
+ ),
76
+ noToken:
77
+ labels.noToken ||
78
+ tComponent<SuiteCoreStringKey>(
79
+ SuiteCoreComponentId,
80
+ SuiteCoreStringKey.NoVerificationTokenProvided
81
+ ),
82
+ proceedToLogin:
83
+ labels.proceedToLogin ||
84
+ tComponent<SuiteCoreStringKey>(
85
+ SuiteCoreComponentId,
86
+ SuiteCoreStringKey.ProceedToLogin
87
+ ),
88
+ contactSupport:
89
+ labels.contactSupport ||
90
+ tComponent<SuiteCoreStringKey>(
91
+ SuiteCoreComponentId,
92
+ SuiteCoreStringKey.HavingTroubleContactSupport
93
+ ),
94
+ requestNewEmail:
95
+ labels.requestNewEmail ||
96
+ tComponent<SuiteCoreStringKey>(
97
+ SuiteCoreComponentId,
98
+ SuiteCoreStringKey.RequestNewVerificationEmail
99
+ ),
100
+ };
101
+ }, [labels, tComponent]);
102
+
103
+ useEffect(() => {
104
+ const verifyEmail = async (verificationToken: string) => {
105
+ try {
106
+ const result = await onVerify(verificationToken);
107
+ if (result.success) {
108
+ setMessage(result.message || translatedLabels.success);
109
+ setVerificationStatus('success');
110
+ } else {
111
+ setMessage(result.message || translatedLabels.failed);
112
+ setVerificationStatus('error');
113
+ }
114
+ } catch {
115
+ setMessage(translatedLabels.failed);
116
+ setVerificationStatus('error');
117
+ } finally {
118
+ setLoading(false);
119
+ }
120
+ };
121
+
122
+ if (token) {
123
+ verifyEmail(token);
124
+ } else {
125
+ setLoading(false);
126
+ setMessage(translatedLabels.noToken);
127
+ setVerificationStatus('error');
128
+ }
129
+ }, [token, onVerify, translatedLabels]);
130
+
131
+ return (
132
+ <Container maxWidth="sm">
133
+ <Box
134
+ sx={{
135
+ mt: 8,
136
+ display: 'flex',
137
+ flexDirection: 'column',
138
+ alignItems: 'center',
139
+ }}
140
+ >
141
+ <Typography variant="h4" component="h1" gutterBottom>
142
+ {translatedLabels.title}
143
+ </Typography>
144
+
145
+ {loading ? (
146
+ <CircularProgress />
147
+ ) : (
148
+ <Box sx={{ width: '100%', mt: 2 }}>
149
+ <Alert
150
+ severity={verificationStatus === 'success' ? 'success' : 'error'}
151
+ sx={{ mb: 2 }}
152
+ >
153
+ {message}
154
+ </Alert>
155
+
156
+ {verificationStatus === 'success' && (
157
+ <Button
158
+ variant="contained"
159
+ color="primary"
160
+ component={Link}
161
+ href={loginLink}
162
+ fullWidth
163
+ >
164
+ {translatedLabels.proceedToLogin}
165
+ </Button>
166
+ )}
167
+
168
+ {verificationStatus === 'error' && (
169
+ <Typography variant="body1">
170
+ {translatedLabels.contactSupport}{' '}
171
+ <Link href={resendLink} color="primary">
172
+ {translatedLabels.requestNewEmail}
173
+ </Link>
174
+ .
175
+ </Typography>
176
+ )}
177
+ </Box>
178
+ )}
179
+ </Box>
180
+ </Container>
181
+ );
182
+ };
183
+
184
+ export default VerifyEmailPage;
@@ -1,3 +1,4 @@
1
+ // Component exports
1
2
  export * from './ApiAccess';
2
3
  export * from './BackupCodeLoginForm';
3
4
  export * from './BackupCodesForm';
@@ -22,4 +23,3 @@ export * from './TranslatedTitle';
22
23
  export * from './UserMenu';
23
24
  export * from './UserLanguageSelector';
24
25
  export * from './VerifyEmailPage';
25
- //# sourceMappingURL=index.d.ts.map