@digitaldefiance/express-suite-react-components 2.9.1 → 2.9.2

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 +10 -6
  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 +134 -0
  9. package/src/components/BackupCodeLoginForm.tsx +314 -0
  10. package/src/components/BackupCodesForm.tsx +198 -0
  11. package/src/components/ChangePasswordForm.tsx +182 -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 +65 -0
  18. package/src/components/Flag.tsx +53 -0
  19. package/src/components/ForgotPasswordForm.tsx +120 -0
  20. package/src/components/LoginForm.tsx +307 -0
  21. package/src/components/LogoutPage.tsx +21 -0
  22. package/src/components/RegisterForm.tsx +354 -0
  23. package/src/components/ResetPasswordForm.tsx +164 -0
  24. package/src/components/SideMenu.tsx +46 -0
  25. package/src/components/SideMenuListItem.tsx +74 -0
  26. package/src/components/TopMenu.tsx +134 -0
  27. package/src/components/TranslatedTitle.tsx +22 -0
  28. package/src/components/UserLanguageSelector.tsx +45 -0
  29. package/src/components/UserMenu.tsx +15 -0
  30. package/src/components/UserSettingsForm.tsx +328 -0
  31. package/src/components/VerifyEmailPage.tsx +133 -0
  32. package/src/components/{index.d.ts → index.ts} +1 -1
  33. package/src/contexts/AuthProvider.spec.tsx +1060 -0
  34. package/src/contexts/AuthProvider.tsx +741 -0
  35. package/src/contexts/I18nProvider.tsx +85 -0
  36. package/src/contexts/MenuContext.tsx +310 -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 +85 -0
  42. package/src/hooks/useEmailVerification.ts +39 -0
  43. package/src/hooks/useExpiringValue.ts +78 -0
  44. package/src/hooks/useLocalStorage.ts +18 -0
  45. package/src/hooks/useUserSettings.ts +216 -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 +422 -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 +35 -0
  61. package/src/wrappers/BackupCodesWrapper.tsx +28 -0
  62. package/src/wrappers/ChangePasswordFormWrapper.tsx +31 -0
  63. package/src/wrappers/LoginFormWrapper.tsx +59 -0
  64. package/src/wrappers/LogoutPageWrapper.tsx +30 -0
  65. package/src/wrappers/RegisterFormWrapper.tsx +48 -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 -70
  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 -106
  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 -108
  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 -66
  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 -54
  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 -99
  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 -54
  126. package/src/components/RegisterForm.d.ts.map +0 -1
  127. package/src/components/RegisterForm.js +0 -105
  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 -68
  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 -36
  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 -56
  150. package/src/components/UserSettingsForm.d.ts.map +0 -1
  151. package/src/components/UserSettingsForm.js +0 -93
  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 -61
  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 -446
  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 -244
  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 -70
  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 -36
  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 -46
  189. package/src/hooks/useUserSettings.d.ts.map +0 -1
  190. package/src/hooks/useUserSettings.js +0 -152
  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 -347
  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 -21
  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 -26
  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,134 @@
1
+ import {
2
+ SuiteCoreComponentId,
3
+ SuiteCoreStringKey,
4
+ } from '@digitaldefiance/suite-core-lib';
5
+ import MenuIcon from '@mui/icons-material/Menu';
6
+ import {
7
+ AppBar,
8
+ Box,
9
+ Button,
10
+ IconButton,
11
+ Toolbar,
12
+ Typography,
13
+ } from '@mui/material';
14
+ import React, { FC, ReactElement, useContext, useState } from 'react';
15
+ import { Link } from 'react-router-dom';
16
+ import { AuthContext } from '../contexts/AuthProvider';
17
+ import { useI18n } from '../contexts/I18nProvider';
18
+ import { useMenu } from '../contexts/MenuContext';
19
+ import { MenuType } from '../types/MenuType';
20
+ import { DropdownMenu } from './DropdownMenu';
21
+ import { SideMenu } from './SideMenu';
22
+ import { UserLanguageSelector } from './UserLanguageSelector';
23
+ import { UserMenu } from './UserMenu';
24
+
25
+ // Extend Window interface for APP_CONFIG
26
+ declare global {
27
+ interface Window {
28
+ APP_CONFIG?: {
29
+ hostname: string;
30
+ siteTitle: string;
31
+ server: string;
32
+ [key: string]: unknown;
33
+ };
34
+ }
35
+ }
36
+
37
+ export interface AdditionalDropdownMenu {
38
+ menuType: MenuType;
39
+ menuIcon: ReactElement;
40
+ priority?: number;
41
+ }
42
+
43
+ export interface TopMenuProps {
44
+ Logo: React.ReactNode;
45
+ additionalMenus?: Array<AdditionalDropdownMenu>;
46
+ }
47
+
48
+ export const TopMenu: FC<TopMenuProps> = ({ Logo, additionalMenus }) => {
49
+ const { isAuthenticated } = useContext(AuthContext);
50
+ const { getTopMenus } = useMenu();
51
+ const [isSideMenuOpen, setIsSideMenuOpen] = useState(false);
52
+
53
+ const handleOpenSideMenu = () => setIsSideMenuOpen(true);
54
+ const handleCloseSideMenu = () => setIsSideMenuOpen(false);
55
+ const { t, tComponent } = useI18n();
56
+ const appConfig = window.APP_CONFIG;
57
+ const siteTitle = tComponent<SuiteCoreStringKey>(
58
+ SuiteCoreComponentId,
59
+ SuiteCoreStringKey.Common_SiteTemplate
60
+ );
61
+
62
+ return (
63
+ <AppBar position="fixed" sx={{ top: 10 }}>
64
+ <Toolbar>
65
+ <IconButton
66
+ size="large"
67
+ edge="start"
68
+ color="inherit"
69
+ aria-label="menu"
70
+ sx={{ mr: 2 }}
71
+ onClick={handleOpenSideMenu}
72
+ >
73
+ <MenuIcon />
74
+ </IconButton>
75
+ <Box
76
+ sx={{
77
+ height: 40,
78
+ width: 40,
79
+ marginRight: 2,
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ }}
83
+ >
84
+ {Logo}
85
+ </Box>
86
+ <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
87
+ {siteTitle}
88
+ </Typography>
89
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
90
+ {isAuthenticated ? (
91
+ <>
92
+ <Button color="inherit" component={Link} to="/dashboard">
93
+ {tComponent<SuiteCoreStringKey>(
94
+ SuiteCoreComponentId,
95
+ SuiteCoreStringKey.Common_Dashboard
96
+ )}
97
+ </Button>
98
+ {getTopMenus().map((menu, index) =>
99
+ menu.isUserMenu ? (
100
+ <UserMenu key={`user-menu`} />
101
+ ) : (
102
+ <DropdownMenu
103
+ key={`menu-${index}`}
104
+ menuType={menu.menuType}
105
+ menuIcon={menu.menuIcon as ReactElement}
106
+ />
107
+ )
108
+ )}
109
+ </>
110
+ ) : (
111
+ <>
112
+ <Button color="inherit" component={Link} to="/login">
113
+ {tComponent<SuiteCoreStringKey>(
114
+ SuiteCoreComponentId,
115
+ SuiteCoreStringKey.Login_LoginButton
116
+ )}
117
+ </Button>
118
+ <Button color="inherit" component={Link} to="/register">
119
+ {tComponent<SuiteCoreStringKey>(
120
+ SuiteCoreComponentId,
121
+ SuiteCoreStringKey.RegisterButton
122
+ )}
123
+ </Button>
124
+ </>
125
+ )}
126
+ <UserLanguageSelector />
127
+ </Box>
128
+ </Toolbar>
129
+ <SideMenu isOpen={isSideMenuOpen} onClose={handleCloseSideMenu} />
130
+ </AppBar>
131
+ );
132
+ };
133
+
134
+ export default TopMenu;
@@ -0,0 +1,22 @@
1
+ // src/app/components/TranslatedTitle.tsx
2
+
3
+ import { useEffect } from 'react';
4
+ import { useI18n } from '../contexts';
5
+ import { SuiteCoreStringKey } from '@digitaldefiance/suite-core-lib';
6
+
7
+ interface FCParams<TEnum extends string> {
8
+ componentId: string;
9
+ stringKey: TEnum;
10
+ }
11
+
12
+ export const TranslatedTitle = <TEnum extends string>({ componentId, stringKey }: FCParams<TEnum>): null => {
13
+ const { tComponent, currentLanguage } = useI18n();
14
+
15
+ useEffect(() => {
16
+ document.title = tComponent<TEnum>(componentId, stringKey, undefined, currentLanguage);
17
+ }, [tComponent, componentId, stringKey, currentLanguage]);
18
+
19
+ return null;
20
+ };
21
+
22
+ export default TranslatedTitle;
@@ -0,0 +1,45 @@
1
+ import { LanguageRegistry, LanguageDefinition } from '@digitaldefiance/i18n-lib';
2
+ import { Button, Menu, MenuItem } from '@mui/material';
3
+ import { FC, MouseEvent, useState, useMemo } from 'react';
4
+ import { Flag } from './Flag';
5
+ import { useUserSettings } from '../hooks';
6
+ import { createAuthenticatedApiClient } from '../services';
7
+ import { useAuth, useSuiteConfig } from '../contexts';
8
+
9
+ export const UserLanguageSelector: FC = () => {
10
+ const { baseUrl } = useSuiteConfig();
11
+ const authenticatedApi = useMemo(() => createAuthenticatedApiClient(baseUrl), [baseUrl]);
12
+ const { isAuthenticated } = useAuth();
13
+ const { currentLanguage, changeLanguage } = useUserSettings({ authenticatedApi, isAuthenticated });
14
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
15
+
16
+ const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
17
+ setAnchorEl(event.currentTarget);
18
+ };
19
+
20
+ const handleClose = () => {
21
+ setAnchorEl(null);
22
+ };
23
+
24
+ const handleLanguageChange = (newLanguage: string) => {
25
+ changeLanguage(newLanguage);
26
+ handleClose();
27
+ };
28
+
29
+ return (
30
+ <>
31
+ <Button onClick={handleClick}>
32
+ <Flag language={currentLanguage} />
33
+ </Button>
34
+ <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
35
+ {Object.values(LanguageRegistry.getAllLanguages()).map((lang: LanguageDefinition) => (
36
+ <MenuItem key={lang.code} onClick={() => handleLanguageChange(lang.code)}>
37
+ <Flag language={lang.code} sx={{ mr: 1 }} /> {lang.name}
38
+ </MenuItem>
39
+ ))}
40
+ </Menu>
41
+ </>
42
+ );
43
+ };
44
+
45
+ export default UserLanguageSelector;
@@ -0,0 +1,15 @@
1
+ import { FC } from 'react';
2
+ import { AccountCircle } from '@mui/icons-material';
3
+ import { MenuTypes } from '../types/MenuType';
4
+ import { DropdownMenu } from './DropdownMenu';
5
+
6
+ export const UserMenu: FC = () => {
7
+ return (
8
+ <DropdownMenu
9
+ menuType={MenuTypes.UserMenu}
10
+ menuIcon={<AccountCircle />}
11
+ />
12
+ );
13
+ };
14
+
15
+ export default UserMenu;
@@ -0,0 +1,328 @@
1
+ import {
2
+ Alert,
3
+ Box,
4
+ Button,
5
+ Container,
6
+ FormControl,
7
+ FormControlLabel,
8
+ InputLabel,
9
+ MenuItem,
10
+ Select,
11
+ Switch,
12
+ TextField,
13
+ Typography,
14
+ } from '@mui/material';
15
+ import { useFormik } from 'formik';
16
+ import { FC, useState, useMemo } from 'react';
17
+ import * as Yup from 'yup';
18
+ import moment from 'moment-timezone';
19
+ import { CurrencyCode } from '@digitaldefiance/i18n-lib';
20
+ import { SuiteCoreComponentId, SuiteCoreStringKey } from '@digitaldefiance/suite-core-lib';
21
+ import { useI18n } from '../contexts';
22
+
23
+ export interface UserSettingsFormValues {
24
+ email: string;
25
+ timezone: string;
26
+ siteLanguage: string;
27
+ currency: string;
28
+ darkMode: boolean;
29
+ directChallenge: boolean;
30
+ [key: string]: any;
31
+ }
32
+
33
+ export interface UserSettingsFormProps {
34
+ initialValues: UserSettingsFormValues;
35
+ onSubmit: (values: UserSettingsFormValues) => Promise<
36
+ | { success: boolean; message: string }
37
+ | { error: string; errorType?: string; field?: string; errors?: Array<{ path: string; msg: string }> }
38
+ >;
39
+ languages: Array<{ code: string; label: string }>;
40
+ emailValidation?: Yup.StringSchema;
41
+ timezoneValidation?: Yup.StringSchema;
42
+ siteLanguageValidation?: Yup.StringSchema;
43
+ currencyValidation?: Yup.StringSchema;
44
+ darkModeValidation?: Yup.BooleanSchema;
45
+ directChallengeValidation?: Yup.BooleanSchema;
46
+ additionalFields?: (formik: any) => React.ReactNode;
47
+ additionalInitialValues?: Record<string, any>;
48
+ additionalValidation?: Record<string, Yup.Schema>;
49
+ labels?: {
50
+ title?: string;
51
+ email?: string;
52
+ emailHelper?: string;
53
+ timezone?: string;
54
+ siteLanguage?: string;
55
+ currency?: string;
56
+ darkMode?: string;
57
+ directChallenge?: string;
58
+ directChallengeHelper?: string;
59
+ saving?: string;
60
+ save?: string;
61
+ successMessage?: string;
62
+ };
63
+ }
64
+
65
+ export const UserSettingsForm: FC<UserSettingsFormProps> = ({
66
+ initialValues,
67
+ onSubmit,
68
+ languages,
69
+ emailValidation,
70
+ timezoneValidation,
71
+ siteLanguageValidation,
72
+ currencyValidation,
73
+ darkModeValidation,
74
+ directChallengeValidation,
75
+ additionalFields,
76
+ additionalInitialValues = {},
77
+ additionalValidation = {},
78
+ labels = {},
79
+ }) => {
80
+ const { tComponent } = useI18n();
81
+ const [apiErrors, setApiErrors] = useState<Record<string, string>>({});
82
+ const [saving, setSaving] = useState(false);
83
+ const [successMessage, setSuccessMessage] = useState<string | null>(null);
84
+
85
+ const timezones = useMemo(() => moment.tz.names(), []);
86
+ const currencies = useMemo(() => CurrencyCode.getAllData().map(c => ({ code: c.code, label: `${c.code} - ${c.currency}` })), []);
87
+
88
+ const validation = {
89
+ email: emailValidation || Yup.string()
90
+ .email(tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Validation_InvalidEmail))
91
+ .required(tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Validation_Required)),
92
+ timezone: timezoneValidation || Yup.string()
93
+ .required(tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Validation_TimezoneRequired))
94
+ .test('valid-timezone', tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Validation_TimezoneInvalid), (value) => !value || moment.tz.zone(value) !== null),
95
+ siteLanguage: siteLanguageValidation || Yup.string()
96
+ .required(tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Validation_Required)),
97
+ currency: currencyValidation || Yup.string()
98
+ .required(tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Validation_Required))
99
+ .test('valid-currency', tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Validation_Required), (value) => !value || CurrencyCode.isValid(value)),
100
+ darkMode: darkModeValidation || Yup.boolean()
101
+ .required(tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Validation_Required)),
102
+ directChallenge: directChallengeValidation || Yup.boolean()
103
+ .required(tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Validation_Required)),
104
+ };
105
+
106
+ const formik = useFormik<UserSettingsFormValues>({
107
+ initialValues: {
108
+ ...initialValues,
109
+ ...additionalInitialValues,
110
+ },
111
+ enableReinitialize: true,
112
+ validationSchema: Yup.object({
113
+ email: validation.email,
114
+ timezone: validation.timezone,
115
+ siteLanguage: validation.siteLanguage,
116
+ currency: validation.currency,
117
+ darkMode: validation.darkMode,
118
+ directChallenge: validation.directChallenge,
119
+ ...additionalValidation,
120
+ }),
121
+ onSubmit: async (values, { setSubmitting, setFieldError, setTouched }) => {
122
+ setSaving(true);
123
+ setSuccessMessage(null);
124
+ const result = await onSubmit(values);
125
+
126
+ if ('success' in result && result.success) {
127
+ setSuccessMessage(result.message || labels.successMessage || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_SaveSuccess));
128
+ setApiErrors({});
129
+ } else {
130
+ const newApiErrors: Record<string, string> = {};
131
+ const fieldsToTouch: Record<string, boolean> = {};
132
+
133
+ if ('field' in result && result.field) {
134
+ setFieldError(result.field, result.error);
135
+ fieldsToTouch[result.field] = true;
136
+ }
137
+
138
+ if ('errors' in result && result.errors) {
139
+ result.errors.forEach((err) => {
140
+ if (err.path && err.msg) {
141
+ setFieldError(err.path, err.msg);
142
+ fieldsToTouch[err.path] = true;
143
+ }
144
+ });
145
+ }
146
+
147
+ if ('error' in result && result.error && !Object.keys(newApiErrors).length) {
148
+ newApiErrors.general = result.error;
149
+ }
150
+
151
+ setApiErrors(newApiErrors);
152
+ setTouched(fieldsToTouch, false);
153
+ }
154
+ setSubmitting(false);
155
+ setSaving(false);
156
+ },
157
+ });
158
+
159
+ return (
160
+ <Container maxWidth="sm">
161
+ <Box sx={{ mt: 4, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
162
+ <Typography variant="h4" component="h1" gutterBottom>
163
+ {labels.title || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_Title)}
164
+ </Typography>
165
+
166
+ <Box component="form" onSubmit={formik.handleSubmit} sx={{ mt: 1, width: '100%' }}>
167
+ <TextField
168
+ fullWidth
169
+ id="email"
170
+ name="email"
171
+ label={labels.email || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Common_Email)}
172
+ value={formik.values.email}
173
+ onChange={formik.handleChange}
174
+ onBlur={formik.handleBlur}
175
+ error={Boolean(formik.touched.email && (formik.errors.email || apiErrors.email))}
176
+ helperText={
177
+ (formik.touched.email && (formik.errors.email || apiErrors.email)) ||
178
+ labels.emailHelper ||
179
+ tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_EmailHelper)
180
+ }
181
+ margin="normal"
182
+ />
183
+
184
+ <FormControl fullWidth margin="normal">
185
+ <InputLabel id="timezone-label">
186
+ {labels.timezone || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Common_Timezone)}
187
+ </InputLabel>
188
+ <Select
189
+ labelId="timezone-label"
190
+ id="timezone"
191
+ name="timezone"
192
+ value={formik.values.timezone}
193
+ onChange={formik.handleChange}
194
+ onBlur={formik.handleBlur}
195
+ error={formik.touched.timezone && Boolean(formik.errors.timezone)}
196
+ label={labels.timezone || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Common_Timezone)}
197
+ >
198
+ {timezones.map((tz) => (
199
+ <MenuItem key={tz} value={tz}>
200
+ {tz}
201
+ </MenuItem>
202
+ ))}
203
+ </Select>
204
+ {formik.touched.timezone && (formik.errors.timezone || apiErrors.timezone) && (
205
+ <Typography color="error" variant="caption">
206
+ {formik.errors.timezone || apiErrors.timezone}
207
+ </Typography>
208
+ )}
209
+ </FormControl>
210
+
211
+ <FormControl fullWidth margin="normal">
212
+ <InputLabel id="language-label">
213
+ {labels.siteLanguage || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_SiteLanguage)}
214
+ </InputLabel>
215
+ <Select
216
+ labelId="language-label"
217
+ id="siteLanguage"
218
+ name="siteLanguage"
219
+ value={formik.values.siteLanguage}
220
+ onChange={formik.handleChange}
221
+ onBlur={formik.handleBlur}
222
+ error={formik.touched.siteLanguage && Boolean(formik.errors.siteLanguage)}
223
+ label={labels.siteLanguage || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_SiteLanguage)}
224
+ >
225
+ {languages.map((lang) => (
226
+ <MenuItem key={lang.code} value={lang.code}>
227
+ {lang.label}
228
+ </MenuItem>
229
+ ))}
230
+ </Select>
231
+ {formik.touched.siteLanguage && (formik.errors.siteLanguage || apiErrors.siteLanguage) && (
232
+ <Typography color="error" variant="caption">
233
+ {formik.errors.siteLanguage || apiErrors.siteLanguage}
234
+ </Typography>
235
+ )}
236
+ </FormControl>
237
+
238
+ <FormControl fullWidth margin="normal">
239
+ <InputLabel id="currency-label">
240
+ {labels.currency || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_Currency)}
241
+ </InputLabel>
242
+ <Select
243
+ labelId="currency-label"
244
+ id="currency"
245
+ name="currency"
246
+ value={formik.values.currency}
247
+ onChange={formik.handleChange}
248
+ onBlur={formik.handleBlur}
249
+ error={formik.touched.currency && Boolean(formik.errors.currency)}
250
+ label={labels.currency || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_Currency)}
251
+ >
252
+ {currencies.map((curr) => (
253
+ <MenuItem key={curr.code} value={curr.code}>
254
+ {curr.label}
255
+ </MenuItem>
256
+ ))}
257
+ </Select>
258
+ {formik.touched.currency && (formik.errors.currency || apiErrors.currency) && (
259
+ <Typography color="error" variant="caption">
260
+ {formik.errors.currency || apiErrors.currency}
261
+ </Typography>
262
+ )}
263
+ </FormControl>
264
+
265
+ <FormControl fullWidth margin="normal">
266
+ <FormControlLabel
267
+ control={
268
+ <Switch
269
+ id="darkMode"
270
+ name="darkMode"
271
+ checked={formik.values.darkMode}
272
+ onChange={formik.handleChange}
273
+ />
274
+ }
275
+ label={labels.darkMode || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_DarkMode)}
276
+ />
277
+ </FormControl>
278
+
279
+ <FormControl fullWidth margin="normal">
280
+ <FormControlLabel
281
+ control={
282
+ <Switch
283
+ id="directChallenge"
284
+ name="directChallenge"
285
+ checked={formik.values.directChallenge}
286
+ onChange={formik.handleChange}
287
+ />
288
+ }
289
+ label={labels.directChallenge || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Registration_DirectChallengeLabel)}
290
+ />
291
+ <Typography variant="caption" color="text.secondary" sx={{ ml: 4, mt: -1 }}>
292
+ {labels.directChallengeHelper || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Registration_DirectChallengeHelper)}
293
+ </Typography>
294
+ </FormControl>
295
+
296
+ {additionalFields && additionalFields(formik)}
297
+
298
+ {apiErrors.general && (
299
+ <Alert severity="error" sx={{ mt: 2, mb: 2 }}>
300
+ {apiErrors.general}
301
+ </Alert>
302
+ )}
303
+
304
+ {successMessage && (
305
+ <Alert severity="success" sx={{ mt: 2, mb: 2 }}>
306
+ {successMessage}
307
+ </Alert>
308
+ )}
309
+
310
+ <Button
311
+ type="submit"
312
+ fullWidth
313
+ variant="contained"
314
+ color="primary"
315
+ sx={{ mt: 3, mb: 2 }}
316
+ disabled={formik.isSubmitting}
317
+ >
318
+ {saving
319
+ ? labels.saving || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_Saving)
320
+ : labels.save || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Settings_Save)}
321
+ </Button>
322
+ </Box>
323
+ </Box>
324
+ </Container>
325
+ );
326
+ };
327
+
328
+ export default UserSettingsForm;
@@ -0,0 +1,133 @@
1
+ import {
2
+ Alert,
3
+ Box,
4
+ Button,
5
+ CircularProgress,
6
+ Container,
7
+ Link,
8
+ Typography,
9
+ } from '@mui/material';
10
+ import { FC, useEffect, useState } from 'react';
11
+ import { SuiteCoreComponentId, SuiteCoreStringKey } from '@digitaldefiance/suite-core-lib';
12
+ import { useI18n } from '../contexts';
13
+
14
+ export interface VerifyEmailPageProps {
15
+ token: string | null;
16
+ onVerify: (token: string) => Promise<{ success: boolean; message?: string }>;
17
+ labels?: {
18
+ title?: string;
19
+ verifying?: string;
20
+ success?: string;
21
+ failed?: string;
22
+ noToken?: string;
23
+ proceedToLogin?: string;
24
+ contactSupport?: string;
25
+ requestNewEmail?: string;
26
+ };
27
+ loginLink?: string;
28
+ resendLink?: string;
29
+ }
30
+
31
+ export const VerifyEmailPage: FC<VerifyEmailPageProps> = ({
32
+ token,
33
+ onVerify,
34
+ labels = {},
35
+ loginLink = '/login',
36
+ resendLink = '/resend-verification',
37
+ }) => {
38
+ const { t, tComponent } = useI18n();
39
+ const [message, setMessage] = useState('');
40
+ const [loading, setLoading] = useState(true);
41
+ const [verificationStatus, setVerificationStatus] = useState<'pending' | 'success' | 'error'>(
42
+ 'pending'
43
+ );
44
+
45
+ const translatedLabels = {
46
+ title: labels.title || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.Common_EmailVerification),
47
+ success: labels.success || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.EmailVerification_Success),
48
+ failed: labels.failed || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.EmailVerification_Failed),
49
+ noToken: labels.noToken || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.NoVerificationTokenProvided),
50
+ proceedToLogin: labels.proceedToLogin || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.ProceedToLogin),
51
+ contactSupport: labels.contactSupport || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.HavingTroubleContactSupport),
52
+ requestNewEmail: labels.requestNewEmail || tComponent<SuiteCoreStringKey>(SuiteCoreComponentId, SuiteCoreStringKey.RequestNewVerificationEmail),
53
+ };
54
+
55
+ useEffect(() => {
56
+ const verifyEmail = async (verificationToken: string) => {
57
+ try {
58
+ const result = await onVerify(verificationToken);
59
+ if (result.success) {
60
+ setMessage(result.message || translatedLabels.success);
61
+ setVerificationStatus('success');
62
+ } else {
63
+ setMessage(result.message || translatedLabels.failed);
64
+ setVerificationStatus('error');
65
+ }
66
+ } catch {
67
+ setMessage(translatedLabels.failed);
68
+ setVerificationStatus('error');
69
+ } finally {
70
+ setLoading(false);
71
+ }
72
+ };
73
+
74
+ if (token) {
75
+ verifyEmail(token);
76
+ } else {
77
+ setLoading(false);
78
+ setMessage(translatedLabels.noToken);
79
+ setVerificationStatus('error');
80
+ }
81
+ }, [token, onVerify, translatedLabels]);
82
+
83
+ return (
84
+ <Container maxWidth="sm">
85
+ <Box
86
+ sx={{
87
+ mt: 8,
88
+ display: 'flex',
89
+ flexDirection: 'column',
90
+ alignItems: 'center',
91
+ }}
92
+ >
93
+ <Typography variant="h4" component="h1" gutterBottom>
94
+ {translatedLabels.title}
95
+ </Typography>
96
+
97
+ {loading ? (
98
+ <CircularProgress />
99
+ ) : (
100
+ <Box sx={{ width: '100%', mt: 2 }}>
101
+ <Alert severity={verificationStatus === 'success' ? 'success' : 'error'} sx={{ mb: 2 }}>
102
+ {message}
103
+ </Alert>
104
+
105
+ {verificationStatus === 'success' && (
106
+ <Button
107
+ variant="contained"
108
+ color="primary"
109
+ component={Link}
110
+ href={loginLink}
111
+ fullWidth
112
+ >
113
+ {translatedLabels.proceedToLogin}
114
+ </Button>
115
+ )}
116
+
117
+ {verificationStatus === 'error' && (
118
+ <Typography variant="body1">
119
+ {translatedLabels.contactSupport}{' '}
120
+ <Link href={resendLink} color="primary">
121
+ {translatedLabels.requestNewEmail}
122
+ </Link>
123
+ .
124
+ </Typography>
125
+ )}
126
+ </Box>
127
+ )}
128
+ </Box>
129
+ </Container>
130
+ );
131
+ };
132
+
133
+ 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