@digitaldefiance/express-suite-react-components 5.0.1 → 5.0.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 (311) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +3 -4
  3. package/src/auth/AdminRoute.tsx +54 -0
  4. package/src/auth/Private.tsx +17 -0
  5. package/src/auth/PrivateRoute.tsx +28 -0
  6. package/src/auth/UnAuth.tsx +16 -0
  7. package/src/auth/UnAuthRoute.tsx +30 -0
  8. package/src/auth/{index.d.ts → index.ts} +2 -3
  9. package/src/components/ApiAccess.tsx +175 -0
  10. package/src/components/BackupCodeLoginForm.tsx +489 -0
  11. package/src/components/BackupCodesForm.tsx +287 -0
  12. package/src/components/CategoryMenu.tsx +155 -0
  13. package/src/components/ChangePasswordForm.tsx +273 -0
  14. package/src/components/ConfirmationDialog.tsx +48 -0
  15. package/src/components/CurrencyCodeSelector.tsx +60 -0
  16. package/src/components/CurrencyInput.tsx +80 -0
  17. package/src/components/DashboardPage.tsx +24 -0
  18. package/src/components/DropdownMenu.tsx +105 -0
  19. package/src/components/ExpirationSecondsSelector.tsx +60 -0
  20. package/src/components/FakeEmailAdminPanel.tsx +374 -0
  21. package/src/components/Flag.tsx +52 -0
  22. package/src/components/ForgotPasswordForm.tsx +174 -0
  23. package/src/components/LoginForm.tsx +468 -0
  24. package/src/components/LogoutPage.tsx +21 -0
  25. package/src/components/RegisterForm.tsx +1169 -0
  26. package/src/components/ResetPasswordForm.tsx +247 -0
  27. package/src/components/SideMenu.tsx +46 -0
  28. package/src/components/SideMenuListItem.tsx +75 -0
  29. package/src/components/T.tsx +24 -0
  30. package/src/components/TDiv.tsx +26 -0
  31. package/src/components/TImgAlt.tsx +28 -0
  32. package/src/components/TopMenu.tsx +252 -0
  33. package/src/components/TotpSetupForm.tsx +188 -0
  34. package/src/components/TotpVerificationForm.tsx +148 -0
  35. package/src/components/TranslatedTitle.tsx +31 -0
  36. package/src/components/UserLanguageSelector.tsx +45 -0
  37. package/src/components/UserMenu.tsx +15 -0
  38. package/src/components/UserSettingsForm.tsx +904 -0
  39. package/src/components/VerifyEmailPage.tsx +222 -0
  40. package/src/components/__tests__/RegisterForm.totp.spec.tsx +377 -0
  41. package/src/components/__tests__/TotpSetupForm.spec.tsx +258 -0
  42. package/src/components/__tests__/TotpVerificationForm.spec.tsx +259 -0
  43. package/src/components/__tests__/UserSettingsForm.totp.spec.tsx +396 -0
  44. package/src/components/{index.d.ts → index.ts} +1 -1
  45. package/src/contexts/AuthProvider.spec.tsx +1284 -0
  46. package/src/contexts/AuthProvider.tsx +977 -0
  47. package/src/contexts/I18nProvider.tsx +164 -0
  48. package/src/contexts/MenuContext.tsx +426 -0
  49. package/src/contexts/SuiteConfigProvider.tsx +93 -0
  50. package/src/contexts/ThemeProvider.tsx +67 -0
  51. package/src/contexts/{index.d.ts → index.ts} +0 -1
  52. package/src/hooks/{index.d.ts → index.ts} +0 -1
  53. package/src/hooks/useAuthenticatedApi.tsx +67 -0
  54. package/src/hooks/useBackupCodes.ts +105 -0
  55. package/src/hooks/useEmailVerification.ts +54 -0
  56. package/src/hooks/useExpiringValue.ts +78 -0
  57. package/src/hooks/useLocalStorage.ts +18 -0
  58. package/src/hooks/useUserSettings.ts +271 -0
  59. package/src/{index.d.ts → index.ts} +1 -1
  60. package/src/interfaces/IAppConfig.ts +7 -0
  61. package/src/interfaces/ICategoryConfig.ts +41 -0
  62. package/src/interfaces/IMenuConfig.ts +28 -0
  63. package/src/interfaces/IMenuOption.ts +79 -0
  64. package/src/interfaces/{index.d.ts → index.ts} +0 -1
  65. package/src/services/__mocks__/authService.ts +14 -0
  66. package/src/services/__tests__/authService.totp.property.spec.ts +360 -0
  67. package/src/services/__tests__/authService.totp.spec.ts +439 -0
  68. package/src/services/api.ts +13 -0
  69. package/src/services/authService.ts +692 -0
  70. package/src/services/authenticatedApi.ts +17 -0
  71. package/src/services/index.ts +3 -0
  72. package/src/types/MenuType.ts +15 -0
  73. package/src/types/expirationSeconds.ts +18 -0
  74. package/src/types/index.ts +1 -0
  75. package/src/types/translation.ts +20 -0
  76. package/src/wrappers/BackupCodeLoginWrapper.tsx +34 -0
  77. package/src/wrappers/BackupCodesWrapper.tsx +28 -0
  78. package/src/wrappers/ChangePasswordFormWrapper.tsx +34 -0
  79. package/src/wrappers/ForgotPasswordFormWrapper.tsx +45 -0
  80. package/src/wrappers/LoginFormWrapper.tsx +108 -0
  81. package/src/wrappers/LogoutPageWrapper.tsx +30 -0
  82. package/src/wrappers/RegisterFormWrapper.tsx +109 -0
  83. package/src/wrappers/ResetPasswordFormWrapper.tsx +57 -0
  84. package/src/wrappers/UserSettingsFormWrapper.tsx +140 -0
  85. package/src/wrappers/VerifyEmailPageWrapper.tsx +27 -0
  86. package/src/wrappers/__tests__/LoginFormWrapper.totp.spec.tsx +339 -0
  87. package/src/wrappers/{index.d.ts → index.tsx} +10 -1
  88. package/src/auth/AdminRoute.d.ts +0 -8
  89. package/src/auth/AdminRoute.d.ts.map +0 -1
  90. package/src/auth/AdminRoute.js +0 -28
  91. package/src/auth/Private.d.ts +0 -6
  92. package/src/auth/Private.d.ts.map +0 -1
  93. package/src/auth/Private.js +0 -14
  94. package/src/auth/PrivateRoute.d.ts +0 -8
  95. package/src/auth/PrivateRoute.d.ts.map +0 -1
  96. package/src/auth/PrivateRoute.js +0 -23
  97. package/src/auth/UnAuth.d.ts +0 -6
  98. package/src/auth/UnAuth.d.ts.map +0 -1
  99. package/src/auth/UnAuth.js +0 -14
  100. package/src/auth/UnAuthRoute.d.ts +0 -8
  101. package/src/auth/UnAuthRoute.d.ts.map +0 -1
  102. package/src/auth/UnAuthRoute.js +0 -22
  103. package/src/auth/index.d.ts.map +0 -1
  104. package/src/auth/index.js +0 -12
  105. package/src/components/ApiAccess.d.ts +0 -16
  106. package/src/components/ApiAccess.d.ts.map +0 -1
  107. package/src/components/ApiAccess.js +0 -77
  108. package/src/components/BackupCodeLoginForm.d.ts +0 -43
  109. package/src/components/BackupCodeLoginForm.d.ts.map +0 -1
  110. package/src/components/BackupCodeLoginForm.js +0 -139
  111. package/src/components/BackupCodesForm.d.ts +0 -26
  112. package/src/components/BackupCodesForm.d.ts.map +0 -1
  113. package/src/components/BackupCodesForm.js +0 -120
  114. package/src/components/CategoryMenu.d.ts +0 -17
  115. package/src/components/CategoryMenu.d.ts.map +0 -1
  116. package/src/components/CategoryMenu.js +0 -65
  117. package/src/components/ChangePasswordForm.d.ts +0 -26
  118. package/src/components/ChangePasswordForm.d.ts.map +0 -1
  119. package/src/components/ChangePasswordForm.js +0 -78
  120. package/src/components/ConfirmationDialog.d.ts +0 -13
  121. package/src/components/ConfirmationDialog.d.ts.map +0 -1
  122. package/src/components/ConfirmationDialog.js +0 -10
  123. package/src/components/CurrencyCodeSelector.d.ts +0 -9
  124. package/src/components/CurrencyCodeSelector.d.ts.map +0 -1
  125. package/src/components/CurrencyCodeSelector.js +0 -31
  126. package/src/components/CurrencyInput.d.ts +0 -13
  127. package/src/components/CurrencyInput.d.ts.map +0 -1
  128. package/src/components/CurrencyInput.js +0 -22
  129. package/src/components/DashboardPage.d.ts +0 -8
  130. package/src/components/DashboardPage.d.ts.map +0 -1
  131. package/src/components/DashboardPage.js +0 -10
  132. package/src/components/DropdownMenu.d.ts +0 -13
  133. package/src/components/DropdownMenu.d.ts.map +0 -1
  134. package/src/components/DropdownMenu.js +0 -63
  135. package/src/components/ExpirationSecondsSelector.d.ts +0 -13
  136. package/src/components/ExpirationSecondsSelector.d.ts.map +0 -1
  137. package/src/components/ExpirationSecondsSelector.js +0 -32
  138. package/src/components/FakeEmailAdminPanel.d.ts +0 -23
  139. package/src/components/FakeEmailAdminPanel.d.ts.map +0 -1
  140. package/src/components/FakeEmailAdminPanel.js +0 -147
  141. package/src/components/Flag.d.ts +0 -20
  142. package/src/components/Flag.d.ts.map +0 -1
  143. package/src/components/Flag.js +0 -43
  144. package/src/components/ForgotPasswordForm.d.ts +0 -18
  145. package/src/components/ForgotPasswordForm.d.ts.map +0 -1
  146. package/src/components/ForgotPasswordForm.js +0 -61
  147. package/src/components/LoginForm.d.ts +0 -44
  148. package/src/components/LoginForm.d.ts.map +0 -1
  149. package/src/components/LoginForm.js +0 -123
  150. package/src/components/LogoutPage.d.ts +0 -8
  151. package/src/components/LogoutPage.d.ts.map +0 -1
  152. package/src/components/LogoutPage.js +0 -16
  153. package/src/components/RegisterForm.d.ts +0 -100
  154. package/src/components/RegisterForm.d.ts.map +0 -1
  155. package/src/components/RegisterForm.js +0 -306
  156. package/src/components/ResetPasswordForm.d.ts +0 -23
  157. package/src/components/ResetPasswordForm.d.ts.map +0 -1
  158. package/src/components/ResetPasswordForm.js +0 -78
  159. package/src/components/SideMenu.d.ts +0 -8
  160. package/src/components/SideMenu.d.ts.map +0 -1
  161. package/src/components/SideMenu.js +0 -25
  162. package/src/components/SideMenuListItem.d.ts +0 -13
  163. package/src/components/SideMenuListItem.d.ts.map +0 -1
  164. package/src/components/SideMenuListItem.js +0 -44
  165. package/src/components/T.d.ts +0 -7
  166. package/src/components/T.d.ts.map +0 -1
  167. package/src/components/T.js +0 -13
  168. package/src/components/TDiv.d.ts +0 -9
  169. package/src/components/TDiv.d.ts.map +0 -1
  170. package/src/components/TDiv.js +0 -14
  171. package/src/components/TImgAlt.d.ts +0 -10
  172. package/src/components/TImgAlt.d.ts.map +0 -1
  173. package/src/components/TImgAlt.js +0 -14
  174. package/src/components/TopMenu.d.ts +0 -50
  175. package/src/components/TopMenu.d.ts.map +0 -1
  176. package/src/components/TopMenu.js +0 -79
  177. package/src/components/TotpSetupForm.d.ts +0 -24
  178. package/src/components/TotpSetupForm.d.ts.map +0 -1
  179. package/src/components/TotpSetupForm.js +0 -82
  180. package/src/components/TotpVerificationForm.d.ts +0 -23
  181. package/src/components/TotpVerificationForm.d.ts.map +0 -1
  182. package/src/components/TotpVerificationForm.js +0 -66
  183. package/src/components/TranslatedTitle.d.ts +0 -8
  184. package/src/components/TranslatedTitle.d.ts.map +0 -1
  185. package/src/components/TranslatedTitle.js +0 -15
  186. package/src/components/UserLanguageSelector.d.ts +0 -4
  187. package/src/components/UserLanguageSelector.d.ts.map +0 -1
  188. package/src/components/UserLanguageSelector.js +0 -31
  189. package/src/components/UserMenu.d.ts +0 -4
  190. package/src/components/UserMenu.d.ts.map +0 -1
  191. package/src/components/UserMenu.js +0 -12
  192. package/src/components/UserSettingsForm.d.ts +0 -113
  193. package/src/components/UserSettingsForm.d.ts.map +0 -1
  194. package/src/components/UserSettingsForm.js +0 -269
  195. package/src/components/VerifyEmailPage.d.ts +0 -24
  196. package/src/components/VerifyEmailPage.d.ts.map +0 -1
  197. package/src/components/VerifyEmailPage.js +0 -81
  198. package/src/components/index.d.ts.map +0 -1
  199. package/src/components/index.js +0 -35
  200. package/src/contexts/AuthProvider.d.ts +0 -186
  201. package/src/contexts/AuthProvider.d.ts.map +0 -1
  202. package/src/contexts/AuthProvider.js +0 -515
  203. package/src/contexts/I18nProvider.d.ts +0 -27
  204. package/src/contexts/I18nProvider.d.ts.map +0 -1
  205. package/src/contexts/I18nProvider.js +0 -60
  206. package/src/contexts/MenuContext.d.ts +0 -20
  207. package/src/contexts/MenuContext.d.ts.map +0 -1
  208. package/src/contexts/MenuContext.js +0 -275
  209. package/src/contexts/SuiteConfigProvider.d.ts +0 -44
  210. package/src/contexts/SuiteConfigProvider.d.ts.map +0 -1
  211. package/src/contexts/SuiteConfigProvider.js +0 -43
  212. package/src/contexts/ThemeProvider.d.ts +0 -15
  213. package/src/contexts/ThemeProvider.d.ts.map +0 -1
  214. package/src/contexts/ThemeProvider.js +0 -36
  215. package/src/contexts/index.d.ts.map +0 -1
  216. package/src/contexts/index.js +0 -8
  217. package/src/hooks/index.d.ts.map +0 -1
  218. package/src/hooks/index.js +0 -9
  219. package/src/hooks/useAuthenticatedApi.d.ts +0 -34
  220. package/src/hooks/useAuthenticatedApi.d.ts.map +0 -1
  221. package/src/hooks/useAuthenticatedApi.js +0 -46
  222. package/src/hooks/useBackupCodes.d.ts +0 -15
  223. package/src/hooks/useBackupCodes.d.ts.map +0 -1
  224. package/src/hooks/useBackupCodes.js +0 -74
  225. package/src/hooks/useEmailVerification.d.ts +0 -10
  226. package/src/hooks/useEmailVerification.d.ts.map +0 -1
  227. package/src/hooks/useEmailVerification.js +0 -40
  228. package/src/hooks/useExpiringValue.d.ts +0 -14
  229. package/src/hooks/useExpiringValue.d.ts.map +0 -1
  230. package/src/hooks/useExpiringValue.js +0 -53
  231. package/src/hooks/useLocalStorage.d.ts +0 -2
  232. package/src/hooks/useLocalStorage.d.ts.map +0 -1
  233. package/src/hooks/useLocalStorage.js +0 -15
  234. package/src/hooks/useUserSettings.d.ts +0 -48
  235. package/src/hooks/useUserSettings.d.ts.map +0 -1
  236. package/src/hooks/useUserSettings.js +0 -171
  237. package/src/index.d.ts.map +0 -1
  238. package/src/index.js +0 -12
  239. package/src/interfaces/IAppConfig.d.ts +0 -8
  240. package/src/interfaces/IAppConfig.d.ts.map +0 -1
  241. package/src/interfaces/IAppConfig.js +0 -2
  242. package/src/interfaces/ICategoryConfig.d.ts +0 -41
  243. package/src/interfaces/ICategoryConfig.d.ts.map +0 -1
  244. package/src/interfaces/ICategoryConfig.js +0 -2
  245. package/src/interfaces/IMenuConfig.d.ts +0 -28
  246. package/src/interfaces/IMenuConfig.d.ts.map +0 -1
  247. package/src/interfaces/IMenuConfig.js +0 -2
  248. package/src/interfaces/IMenuOption.d.ts +0 -82
  249. package/src/interfaces/IMenuOption.d.ts.map +0 -1
  250. package/src/interfaces/IMenuOption.js +0 -2
  251. package/src/interfaces/index.d.ts.map +0 -1
  252. package/src/interfaces/index.js +0 -7
  253. package/src/services/__mocks__/authService.d.ts +0 -21
  254. package/src/services/__mocks__/authService.d.ts.map +0 -1
  255. package/src/services/__mocks__/authService.js +0 -15
  256. package/src/services/api.d.ts +0 -3
  257. package/src/services/api.d.ts.map +0 -1
  258. package/src/services/api.js +0 -14
  259. package/src/services/authService.d.ts +0 -126
  260. package/src/services/authService.d.ts.map +0 -1
  261. package/src/services/authService.js +0 -471
  262. package/src/services/authenticatedApi.d.ts +0 -3
  263. package/src/services/authenticatedApi.d.ts.map +0 -1
  264. package/src/services/authenticatedApi.js +0 -18
  265. package/src/services/index.d.ts +0 -4
  266. package/src/services/index.d.ts.map +0 -1
  267. package/src/services/index.js +0 -6
  268. package/src/types/MenuType.d.ts +0 -11
  269. package/src/types/MenuType.d.ts.map +0 -1
  270. package/src/types/MenuType.js +0 -12
  271. package/src/types/expirationSeconds.d.ts +0 -3
  272. package/src/types/expirationSeconds.d.ts.map +0 -1
  273. package/src/types/expirationSeconds.js +0 -17
  274. package/src/types/index.d.ts +0 -2
  275. package/src/types/index.d.ts.map +0 -1
  276. package/src/types/index.js +0 -4
  277. package/src/types/translation.d.ts +0 -10
  278. package/src/types/translation.d.ts.map +0 -1
  279. package/src/types/translation.js +0 -9
  280. package/src/wrappers/BackupCodeLoginWrapper.d.ts +0 -8
  281. package/src/wrappers/BackupCodeLoginWrapper.d.ts.map +0 -1
  282. package/src/wrappers/BackupCodeLoginWrapper.js +0 -20
  283. package/src/wrappers/BackupCodesWrapper.d.ts +0 -7
  284. package/src/wrappers/BackupCodesWrapper.d.ts.map +0 -1
  285. package/src/wrappers/BackupCodesWrapper.js +0 -17
  286. package/src/wrappers/ChangePasswordFormWrapper.d.ts +0 -8
  287. package/src/wrappers/ChangePasswordFormWrapper.d.ts.map +0 -1
  288. package/src/wrappers/ChangePasswordFormWrapper.js +0 -21
  289. package/src/wrappers/ForgotPasswordFormWrapper.d.ts +0 -8
  290. package/src/wrappers/ForgotPasswordFormWrapper.d.ts.map +0 -1
  291. package/src/wrappers/ForgotPasswordFormWrapper.js +0 -29
  292. package/src/wrappers/LoginFormWrapper.d.ts +0 -21
  293. package/src/wrappers/LoginFormWrapper.d.ts.map +0 -1
  294. package/src/wrappers/LoginFormWrapper.js +0 -72
  295. package/src/wrappers/LogoutPageWrapper.d.ts +0 -9
  296. package/src/wrappers/LogoutPageWrapper.d.ts.map +0 -1
  297. package/src/wrappers/LogoutPageWrapper.js +0 -21
  298. package/src/wrappers/RegisterFormWrapper.d.ts +0 -20
  299. package/src/wrappers/RegisterFormWrapper.d.ts.map +0 -1
  300. package/src/wrappers/RegisterFormWrapper.js +0 -51
  301. package/src/wrappers/ResetPasswordFormWrapper.d.ts +0 -9
  302. package/src/wrappers/ResetPasswordFormWrapper.d.ts.map +0 -1
  303. package/src/wrappers/ResetPasswordFormWrapper.js +0 -34
  304. package/src/wrappers/UserSettingsFormWrapper.d.ts +0 -29
  305. package/src/wrappers/UserSettingsFormWrapper.d.ts.map +0 -1
  306. package/src/wrappers/UserSettingsFormWrapper.js +0 -95
  307. package/src/wrappers/VerifyEmailPageWrapper.d.ts +0 -8
  308. package/src/wrappers/VerifyEmailPageWrapper.d.ts.map +0 -1
  309. package/src/wrappers/VerifyEmailPageWrapper.js +0 -21
  310. package/src/wrappers/index.d.ts.map +0 -1
  311. package/src/wrappers/index.js +0 -24
@@ -0,0 +1,164 @@
1
+ import {
2
+ GlobalActiveContext,
3
+ IActiveContext,
4
+ LanguageRegistry,
5
+ } from '@digitaldefiance/i18n-lib';
6
+ import {
7
+ createContext,
8
+ FC,
9
+ ReactNode,
10
+ useCallback,
11
+ useContext,
12
+ useState,
13
+ } from 'react';
14
+
15
+ /**
16
+ * Minimal structural interface accepted by I18nProvider.
17
+ * Both I18nEngine and PluginI18nEngine satisfy this interface.
18
+ */
19
+ export interface II18nEngineCompat {
20
+ setLanguage(language: string): void;
21
+ translate(
22
+ componentId: string,
23
+ stringKey: string,
24
+ variables?: Record<string, string | number>,
25
+ language?: string
26
+ ): string;
27
+ translateStringKey?(
28
+ key: string,
29
+ vars?: Record<string, string | number>,
30
+ language?: string
31
+ ): string;
32
+ /** Template processor that handles {{component.key}} and {variable} patterns */
33
+ t?(
34
+ template: string,
35
+ variables?: Record<string, string | number>,
36
+ language?: string
37
+ ): string;
38
+ }
39
+
40
+ export interface I18nProviderProps {
41
+ children: ReactNode;
42
+ i18nEngine: II18nEngineCompat;
43
+ onLanguageChange?: (language: string) => Promise<void>;
44
+ }
45
+
46
+ export interface I18nContextType {
47
+ t: (
48
+ key: string,
49
+ vars?: Record<string, string | number>,
50
+ language?: string
51
+ ) => string;
52
+ tBranded: <TStringKey extends string>(
53
+ stringKey: TStringKey,
54
+ vars?: Record<string, string | number>,
55
+ language?: string
56
+ ) => string;
57
+ tComponent: <TStringKey extends string>(
58
+ componentId: string,
59
+ stringKey: TStringKey,
60
+ vars?: Record<string, string | number>,
61
+ language?: string
62
+ ) => string;
63
+ changeLanguage: (language: string) => void;
64
+ currentLanguage: string;
65
+ }
66
+
67
+ const I18nContext = createContext<I18nContextType | undefined>(undefined);
68
+
69
+ export const I18nProvider: FC<I18nProviderProps> = ({
70
+ children,
71
+ i18nEngine,
72
+ onLanguageChange,
73
+ }) => {
74
+ const context = GlobalActiveContext.getInstance<
75
+ string,
76
+ IActiveContext<string>
77
+ >();
78
+ const [currentLanguage, setCurrentLanguage] = useState<string>(
79
+ context.userLanguage
80
+ );
81
+
82
+ const changeLanguage = useCallback(
83
+ async (language: string) => {
84
+ const languageDetails = LanguageRegistry.getLanguageByCode(language);
85
+ if (language && languageDetails) {
86
+ context.userLanguage = language;
87
+ i18nEngine.setLanguage(language);
88
+ localStorage.setItem('language', languageDetails.name);
89
+ localStorage.setItem('languageCode', language);
90
+ setCurrentLanguage(language);
91
+ if (onLanguageChange) {
92
+ await onLanguageChange(language);
93
+ }
94
+ }
95
+ },
96
+ [onLanguageChange, i18nEngine, context]
97
+ );
98
+
99
+ const tBranded = useCallback(
100
+ (
101
+ key: string,
102
+ vars?: Record<string, string | number>,
103
+ language?: string
104
+ ) => {
105
+ if (!i18nEngine.translateStringKey) return `[${key}]`;
106
+ return i18nEngine.translateStringKey(key, vars, language ?? currentLanguage);
107
+ },
108
+ [i18nEngine, currentLanguage]
109
+ );
110
+
111
+ const t = useCallback(
112
+ (
113
+ key: string,
114
+ vars?: Record<string, string | number>,
115
+ language?: string
116
+ ) => {
117
+ // If the engine has a template processor and the key looks like a template
118
+ // (contains {{...}} or {variable} patterns), use t() for template resolution
119
+ if (i18nEngine.t && (/\{\{/.test(key) || /\{[A-Za-z]/.test(key))) {
120
+ return i18nEngine.t(key, vars, language ?? currentLanguage);
121
+ }
122
+ if (i18nEngine.translateStringKey) {
123
+ return i18nEngine.translateStringKey(key, vars, language ?? currentLanguage);
124
+ }
125
+ return `[${key}]`;
126
+ },
127
+ [i18nEngine, currentLanguage]
128
+ );
129
+
130
+ const tComponent = useCallback(
131
+ <TStringKey extends string>(
132
+ componentId: string,
133
+ stringKey: TStringKey,
134
+ vars?: Record<string, string | number>,
135
+ language?: string
136
+ ): string => {
137
+ return i18nEngine.translate(
138
+ componentId,
139
+ stringKey,
140
+ vars,
141
+ language ?? currentLanguage
142
+ );
143
+ },
144
+ [currentLanguage, i18nEngine]
145
+ );
146
+
147
+ const value = {
148
+ t,
149
+ tComponent,
150
+ tBranded,
151
+ changeLanguage,
152
+ currentLanguage,
153
+ };
154
+
155
+ return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
156
+ };
157
+
158
+ export const useI18n = () => {
159
+ const context = useContext(I18nContext);
160
+ if (context === undefined) {
161
+ throw new Error('useI18n must be used within an I18nProvider');
162
+ }
163
+ return context;
164
+ };
@@ -0,0 +1,426 @@
1
+ // src/app/menuContext.tsx
2
+ import {
3
+ IRoleDTO,
4
+ SuiteCoreComponentId,
5
+ SuiteCoreStringKey,
6
+ SuiteCoreStringKeyValue,
7
+ } from '@digitaldefiance/suite-core-lib';
8
+ import {
9
+ AccountCircle,
10
+ Autorenew as AutorenewIcon,
11
+ Brightness4,
12
+ Brightness7,
13
+ Dashboard as DashboardIcon,
14
+ Key as KeyIcon,
15
+ LockOpen as LockOpenIcon,
16
+ LockReset as LockResetIcon,
17
+ Login as LoginIcon,
18
+ ExitToApp as LogoutIcon,
19
+ PersonAdd as PersonAddIcon,
20
+ Settings,
21
+ } from '@mui/icons-material';
22
+ import {
23
+ FC,
24
+ ReactNode,
25
+ createContext,
26
+ useCallback,
27
+ useContext,
28
+ useEffect,
29
+ useMemo,
30
+ useRef,
31
+ useState,
32
+ } from 'react';
33
+ import { useUserSettings } from '../hooks';
34
+ import { IMenuConfig } from '../interfaces/IMenuConfig';
35
+ import { IMenuOption } from '../interfaces/IMenuOption';
36
+ import { createAuthenticatedApiClient } from '../services';
37
+ import { MenuType, MenuTypes } from '../types/MenuType';
38
+ import { useAuth } from './AuthProvider';
39
+ import { useI18n } from './I18nProvider';
40
+ import { useSuiteConfig } from './SuiteConfigProvider';
41
+ import { useTheme } from './ThemeProvider';
42
+
43
+ interface MenuProviderProps {
44
+ children: ReactNode;
45
+ menuConfigs?: IMenuConfig[];
46
+ enableBackupCodes?: boolean;
47
+ /**
48
+ * IDs of menu options to suppress from every menu. Matches both the built-in
49
+ * options (e.g. 'change-password', 'user-settings', 'logout') and any option
50
+ * registered via `menuConfigs`. Dividers left orphaned by the removal are
51
+ * cleaned up automatically. Defaults to none hidden, so existing consumers are
52
+ * unaffected.
53
+ */
54
+ hiddenMenuIds?: readonly string[];
55
+ }
56
+
57
+ interface MenuContextType {
58
+ menuOptions: IMenuOption[];
59
+ getMenuOptions: (
60
+ menuType: MenuType,
61
+ includeDividers: boolean
62
+ ) => IMenuOption[];
63
+ registerMenuOption: (option: IMenuOption) => () => void;
64
+ registerMenuOptions: (options: IMenuOption[]) => () => void;
65
+ getTopMenus: () => Array<IMenuConfig>;
66
+ }
67
+
68
+ const MenuContext = createContext<MenuContextType | undefined>(undefined);
69
+
70
+ export const MenuProvider: FC<MenuProviderProps> = ({
71
+ children,
72
+ menuConfigs = [],
73
+ enableBackupCodes = true,
74
+ hiddenMenuIds = [],
75
+ }) => {
76
+ const {
77
+ userData: user,
78
+ isAuthenticated,
79
+ admin: isGlobalAdmin,
80
+ mnemonic,
81
+ clearMnemonic,
82
+ wallet,
83
+ clearWallet,
84
+ } = useAuth();
85
+ const { mode: colorMode } = useTheme();
86
+ const registeredMenuOptions = useRef(new Set<() => void>());
87
+ const [registeredOptions, setRegisteredOptions] = useState<
88
+ Map<string, IMenuOption>
89
+ >(new Map<string, IMenuOption>());
90
+ const { tComponent } = useI18n();
91
+ const { baseUrl } = useSuiteConfig();
92
+ const authenticatedApi = useMemo(
93
+ () => createAuthenticatedApiClient(baseUrl),
94
+ [baseUrl]
95
+ );
96
+ const { toggleColorMode } = useUserSettings({
97
+ authenticatedApi,
98
+ isAuthenticated,
99
+ });
100
+
101
+ const registerMenuOption = useCallback((option: IMenuOption) => {
102
+ const unregister = () => {
103
+ setRegisteredOptions((prev) => {
104
+ const newMap = new Map(prev);
105
+ newMap.delete(option.id);
106
+ return newMap;
107
+ });
108
+ registeredMenuOptions.current.delete(unregister);
109
+ };
110
+
111
+ setRegisteredOptions((prev) => {
112
+ const newMap = new Map(prev);
113
+ newMap.set(option.id, option);
114
+ return newMap;
115
+ });
116
+ registeredMenuOptions.current.add(unregister);
117
+
118
+ return unregister;
119
+ }, []);
120
+
121
+ const registerMenuOptions = useCallback(
122
+ (options: IMenuOption[]) => {
123
+ const unregisterFunctions = options.map(registerMenuOption);
124
+ return () => unregisterFunctions.forEach((f) => f());
125
+ },
126
+ [registerMenuOption]
127
+ );
128
+
129
+ const menuOptions = useMemo(() => {
130
+ const isUserRestricted = () => {
131
+ return user?.roles?.some((role: IRoleDTO) => role.child) ?? false;
132
+ };
133
+ let index = 0;
134
+ const baseOptions: IMenuOption[] = [
135
+ {
136
+ id: 'dashboard',
137
+ label: tComponent<SuiteCoreStringKeyValue>(
138
+ SuiteCoreComponentId,
139
+ SuiteCoreStringKey.Common_Dashboard
140
+ ),
141
+ icon: <DashboardIcon />,
142
+ link: '/dashboard',
143
+ requiresAuth: true,
144
+ includeOnMenus: [MenuTypes.SideMenu],
145
+ index: index++,
146
+ },
147
+ {
148
+ id: 'user-divider',
149
+ label: '',
150
+ divider: true,
151
+ includeOnMenus: [MenuTypes.SideMenu],
152
+ index: index++,
153
+ requiresAuth: false,
154
+ },
155
+ {
156
+ id: 'logout',
157
+ label: tComponent<SuiteCoreStringKeyValue>(
158
+ SuiteCoreComponentId,
159
+ SuiteCoreStringKey.LogoutButton
160
+ ),
161
+ icon: <LogoutIcon />,
162
+ link: '/logout',
163
+ requiresAuth: true,
164
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
165
+ index: index++,
166
+ },
167
+ {
168
+ id: 'login',
169
+ label: tComponent<SuiteCoreStringKeyValue>(
170
+ SuiteCoreComponentId,
171
+ SuiteCoreStringKey.Login_LoginButton
172
+ ),
173
+ icon: <LoginIcon />,
174
+ link: '/login',
175
+ requiresAuth: false,
176
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
177
+ index: index++,
178
+ },
179
+ {
180
+ id: 'register',
181
+ label: tComponent<SuiteCoreStringKeyValue>(
182
+ SuiteCoreComponentId,
183
+ SuiteCoreStringKey.RegisterButton
184
+ ),
185
+ icon: <PersonAddIcon />,
186
+ link: '/register',
187
+ requiresAuth: false,
188
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
189
+ index: index++,
190
+ },
191
+ {
192
+ id: 'forgot-password',
193
+ label: tComponent<SuiteCoreStringKeyValue>(
194
+ SuiteCoreComponentId,
195
+ SuiteCoreStringKey.ForgotPassword_Title
196
+ ),
197
+ icon: <LockOpenIcon />,
198
+ link: '/forgot-password',
199
+ requiresAuth: false,
200
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
201
+ index: index++,
202
+ },
203
+ {
204
+ id: 'change-password',
205
+ label: tComponent<SuiteCoreStringKeyValue>(
206
+ SuiteCoreComponentId,
207
+ SuiteCoreStringKey.Common_ChangePassword
208
+ ),
209
+ icon: <LockResetIcon />,
210
+ link: '/change-password',
211
+ requiresAuth: true,
212
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
213
+ index: index++,
214
+ },
215
+ ...(enableBackupCodes
216
+ ? [
217
+ {
218
+ id: 'backup-code',
219
+ label: tComponent<SuiteCoreStringKeyValue>(
220
+ SuiteCoreComponentId,
221
+ SuiteCoreStringKey.BackupCodeRecovery_Title
222
+ ),
223
+ icon: <KeyIcon />,
224
+ link: '/backup-code',
225
+ requiresAuth: false,
226
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
227
+ index: index++,
228
+ },
229
+ {
230
+ id: 'backup-codes',
231
+ label: tComponent<SuiteCoreStringKeyValue>(
232
+ SuiteCoreComponentId,
233
+ SuiteCoreStringKey.BackupCodeRecovery_GenerateNewCodes
234
+ ),
235
+ icon: <AutorenewIcon />,
236
+ link: '/backup-codes',
237
+ requiresAuth: true,
238
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
239
+ index: index++,
240
+ },
241
+ ]
242
+ : []),
243
+ {
244
+ id: 'divider',
245
+ label: '',
246
+ divider: true,
247
+ includeOnMenus: [MenuTypes.SideMenu],
248
+ index: index++,
249
+ requiresAuth: false,
250
+ },
251
+ {
252
+ id: 'clear-mnemonic',
253
+ label: tComponent<SuiteCoreStringKeyValue>(
254
+ SuiteCoreComponentId,
255
+ SuiteCoreStringKey.Common_ClearMnemonic
256
+ ),
257
+ action: clearMnemonic,
258
+ icon: <KeyIcon />,
259
+ requiresAuth: true,
260
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
261
+ index: index++,
262
+ filter: () => !!mnemonic,
263
+ },
264
+ {
265
+ id: 'clear-wallet',
266
+ label: tComponent<SuiteCoreStringKeyValue>(
267
+ SuiteCoreComponentId,
268
+ SuiteCoreStringKey.Common_ClearWallet
269
+ ),
270
+ action: clearWallet,
271
+ icon: <KeyIcon />,
272
+ requiresAuth: true,
273
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
274
+ index: index++,
275
+ filter: () => !!wallet,
276
+ },
277
+ ...(isUserRestricted() ? [] : []),
278
+ {
279
+ id: 'color-divider',
280
+ label: '',
281
+ divider: true,
282
+ includeOnMenus: [MenuTypes.SideMenu],
283
+ index: index++,
284
+ requiresAuth: undefined,
285
+ },
286
+ {
287
+ id: 'theme-toggle',
288
+ label:
289
+ colorMode === 'dark'
290
+ ? tComponent<SuiteCoreStringKeyValue>(
291
+ SuiteCoreComponentId,
292
+ SuiteCoreStringKey.Common_ThemeToggle_Light
293
+ )
294
+ : tComponent<SuiteCoreStringKeyValue>(
295
+ SuiteCoreComponentId,
296
+ SuiteCoreStringKey.Common_ThemeToggle_Dark
297
+ ),
298
+ icon: colorMode === 'dark' ? <Brightness7 /> : <Brightness4 />,
299
+ includeOnMenus: [MenuTypes.SideMenu],
300
+ index: index++,
301
+ requiresAuth: undefined,
302
+ action: toggleColorMode,
303
+ },
304
+ {
305
+ id: 'user-settings',
306
+ label: tComponent<SuiteCoreStringKeyValue>(
307
+ SuiteCoreComponentId,
308
+ SuiteCoreStringKey.Settings_Title
309
+ ),
310
+ icon: <Settings />,
311
+ link: '/user-settings',
312
+ requiresAuth: true,
313
+ includeOnMenus: [MenuTypes.UserMenu, MenuTypes.SideMenu],
314
+ index: index++,
315
+ },
316
+ ];
317
+
318
+ const allOptions = [...baseOptions, ...registeredOptions.values()];
319
+ return allOptions.sort((a, b) => a.index - b.index);
320
+ }, [
321
+ tComponent,
322
+ registeredOptions,
323
+ user?.roles,
324
+ colorMode,
325
+ toggleColorMode,
326
+ clearMnemonic,
327
+ clearWallet,
328
+ mnemonic,
329
+ wallet,
330
+ enableBackupCodes,
331
+ ]);
332
+
333
+ const getMenuOptions = useCallback(
334
+ (menuType: MenuType, includeDividers: boolean) => {
335
+ const hidden = new Set(hiddenMenuIds);
336
+ const MenuFilter = (o: IMenuOption) => {
337
+ // Suppress any option whose id the consumer asked to hide.
338
+ if (hidden.has(o.id)) return false;
339
+
340
+ // Apply the custom filter first
341
+ let customFilterPasses = true;
342
+ if (o.filter !== undefined) {
343
+ customFilterPasses = o.filter(o);
344
+ }
345
+ if (!customFilterPasses) return false;
346
+
347
+ if (o.divider === true && !includeDividers) return false;
348
+
349
+ if (o.requiresAdmin === true && !isGlobalAdmin) return false;
350
+
351
+ return (
352
+ o.includeOnMenus.includes(menuType) &&
353
+ (o.requiresAuth === undefined || o.requiresAuth === isAuthenticated)
354
+ );
355
+ };
356
+
357
+ const filtered = menuOptions.filter(MenuFilter);
358
+
359
+ // Drop dividers left orphaned once hidden items are removed: a divider at
360
+ // the start or end of the list, or immediately following another divider,
361
+ // carries no meaning and would show as a stray separator.
362
+ return filtered.filter((o, i) => {
363
+ if (o.divider !== true) return true;
364
+ const prev = filtered[i - 1];
365
+ if (prev === undefined || prev.divider === true) return false;
366
+ if (i === filtered.length - 1) return false;
367
+ return true;
368
+ });
369
+ },
370
+ [hiddenMenuIds, isAuthenticated, isGlobalAdmin, menuOptions]
371
+ );
372
+
373
+ useEffect(() => {
374
+ if (menuConfigs.length > 0) {
375
+ return registerMenuOptions(
376
+ menuConfigs.flatMap((config) => config.options)
377
+ );
378
+ }
379
+ return undefined;
380
+ }, [menuConfigs, registerMenuOptions]);
381
+
382
+ const getTopMenus = useCallback(() => {
383
+ const menus: Array<IMenuConfig & { isUserMenu?: boolean }> = [
384
+ ...menuConfigs.map((config) => ({ ...config, isUserMenu: false })),
385
+ {
386
+ menuType: MenuTypes.UserMenu,
387
+ menuIcon: <AccountCircle />,
388
+ priority: 0,
389
+ options: [],
390
+ isUserMenu: true,
391
+ },
392
+ ];
393
+ return menus.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
394
+ }, [menuConfigs]);
395
+
396
+ const contextValue = useMemo(() => {
397
+ return {
398
+ menuOptions: menuOptions,
399
+ getMenuOptions: getMenuOptions,
400
+ registerMenuOption: registerMenuOption,
401
+ registerMenuOptions: registerMenuOptions,
402
+ getTopMenus: getTopMenus,
403
+ };
404
+ }, [
405
+ menuOptions,
406
+ getMenuOptions,
407
+ registerMenuOption,
408
+ registerMenuOptions,
409
+ getTopMenus,
410
+ ]);
411
+
412
+ const memoizedChildren = useMemo(() => children, [children]);
413
+ return (
414
+ <MenuContext.Provider value={contextValue}>
415
+ {memoizedChildren}
416
+ </MenuContext.Provider>
417
+ );
418
+ };
419
+
420
+ export const useMenu = (): MenuContextType => {
421
+ const context = useContext(MenuContext);
422
+ if (context === undefined) {
423
+ throw new Error('useMenu must be used within a MenuProvider');
424
+ }
425
+ return context;
426
+ };
@@ -0,0 +1,93 @@
1
+ import { createContext, ReactNode, useContext } from 'react';
2
+
3
+ export interface SuiteConfigRoutes {
4
+ dashboard?: string;
5
+ login?: string;
6
+ register?: string;
7
+ verifyEmail?: string;
8
+ forgotPassword?: string;
9
+ resetPassword?: string;
10
+ settings?: string;
11
+ }
12
+
13
+ export interface SuiteConfigContextData {
14
+ /**
15
+ * Base URL for API calls
16
+ */
17
+ baseUrl: string;
18
+
19
+ /**
20
+ * Application routes for navigation
21
+ */
22
+ routes: SuiteConfigRoutes;
23
+
24
+ /**
25
+ * Available languages for the application
26
+ */
27
+ languages: Array<{ code: string; label: string }>;
28
+
29
+ /**
30
+ * Available timezones
31
+ */
32
+ timezones?: string[];
33
+ }
34
+
35
+ const defaultRoutes: SuiteConfigRoutes = {
36
+ dashboard: '/dashboard',
37
+ login: '/login',
38
+ register: '/register',
39
+ verifyEmail: '/verify-email',
40
+ forgotPassword: '/forgot-password',
41
+ resetPassword: '/reset-password',
42
+ settings: '/settings',
43
+ };
44
+
45
+ const defaultLanguages = [
46
+ { code: 'en-US', label: 'English (US)' },
47
+ { code: 'en-GB', label: 'English (UK)' },
48
+ { code: 'es-ES', label: 'Español' },
49
+ { code: 'fr-FR', label: 'Français' },
50
+ { code: 'de-DE', label: 'Deutsch' },
51
+ { code: 'ja', label: '日本語' },
52
+ { code: 'zh-CN', label: '中文 (简体)' },
53
+ { code: 'uk', label: 'Українська' },
54
+ ];
55
+
56
+ const SuiteConfigContext = createContext<SuiteConfigContextData | undefined>(undefined);
57
+
58
+ export interface SuiteConfigProviderProps {
59
+ children: ReactNode;
60
+ baseUrl: string;
61
+ routes?: Partial<SuiteConfigRoutes>;
62
+ languages?: Array<{ code: string; label: string }>;
63
+ timezones?: string[];
64
+ }
65
+
66
+ export const SuiteConfigProvider = ({
67
+ children,
68
+ baseUrl,
69
+ routes = {},
70
+ languages = defaultLanguages,
71
+ timezones,
72
+ }: SuiteConfigProviderProps) => {
73
+ const value: SuiteConfigContextData = {
74
+ baseUrl,
75
+ routes: { ...defaultRoutes, ...routes },
76
+ languages,
77
+ timezones,
78
+ };
79
+
80
+ return (
81
+ <SuiteConfigContext.Provider value={value}>
82
+ {children}
83
+ </SuiteConfigContext.Provider>
84
+ );
85
+ };
86
+
87
+ export const useSuiteConfig = (): SuiteConfigContextData => {
88
+ const context = useContext(SuiteConfigContext);
89
+ if (!context) {
90
+ throw new Error('useSuiteConfig must be used within a SuiteConfigProvider');
91
+ }
92
+ return context;
93
+ };