@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,105 @@
1
+ import {
2
+ SuiteCoreStringKey,
3
+ TranslatableSuiteError,
4
+ } from '@digitaldefiance/suite-core-lib';
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { useSuiteConfig } from '../contexts';
7
+ import { createAuthenticatedApiClient } from '../services';
8
+
9
+ export interface UseBackupCodesOptions {
10
+ initialCodeCount?: number | null;
11
+ }
12
+
13
+ export interface UseBackupCodesResult {
14
+ backupCodesRemaining: number | null;
15
+ isLoading: boolean;
16
+ error: Error | null;
17
+ generateBackupCodes: (
18
+ password?: string,
19
+ mnemonic?: string
20
+ ) => Promise<{
21
+ message: string;
22
+ backupCodes: string[];
23
+ }>;
24
+ refreshCodeCount: () => Promise<void>;
25
+ }
26
+
27
+ export const useBackupCodes = (
28
+ options: UseBackupCodesOptions = {}
29
+ ): UseBackupCodesResult => {
30
+ const { baseUrl } = useSuiteConfig();
31
+ const [backupCodesRemaining, setBackupCodesRemaining] = useState<
32
+ number | null
33
+ >(options.initialCodeCount ?? null);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [error, setError] = useState<Error | null>(null);
36
+ const requestedOnMountRef = useRef(false);
37
+ const api = useMemo(() => createAuthenticatedApiClient(baseUrl), [baseUrl]);
38
+
39
+ const refreshCodeCount = useCallback(async () => {
40
+ setIsLoading(true);
41
+ setError(null);
42
+ try {
43
+ const result = await api.get<{ codeCount: number }>('/user/backup-codes');
44
+ if (result?.data?.codeCount !== undefined) {
45
+ setBackupCodesRemaining(result.data.codeCount);
46
+ }
47
+ } catch (err) {
48
+ setError(
49
+ err instanceof Error
50
+ ? err
51
+ : new TranslatableSuiteError(
52
+ SuiteCoreStringKey.BackupCodes_FailedToFetch
53
+ )
54
+ );
55
+ setBackupCodesRemaining(0);
56
+ } finally {
57
+ setIsLoading(false);
58
+ }
59
+ }, [api]);
60
+
61
+ useEffect(() => {
62
+ if (requestedOnMountRef.current) return;
63
+ if (backupCodesRemaining !== null) return;
64
+
65
+ requestedOnMountRef.current = true;
66
+ refreshCodeCount();
67
+ }, [backupCodesRemaining, refreshCodeCount]);
68
+
69
+ const generateBackupCodes = async (password?: string, mnemonic?: string) => {
70
+ setIsLoading(true);
71
+ setError(null);
72
+ try {
73
+ const result = await api.post<{ message: string; backupCodes: string[] }>(
74
+ '/user/backup-codes',
75
+ {
76
+ ...(password ? { password } : {}),
77
+ ...(mnemonic ? { mnemonic } : {}),
78
+ }
79
+ );
80
+ return {
81
+ message: result.data.message,
82
+ backupCodes: result.data.backupCodes,
83
+ };
84
+ } catch (err) {
85
+ const error =
86
+ err instanceof Error
87
+ ? err
88
+ : new TranslatableSuiteError(
89
+ SuiteCoreStringKey.BackupCodes_FailedToGenerate
90
+ );
91
+ setError(error);
92
+ throw error;
93
+ } finally {
94
+ setIsLoading(false);
95
+ }
96
+ };
97
+
98
+ return {
99
+ backupCodesRemaining,
100
+ isLoading,
101
+ error,
102
+ generateBackupCodes,
103
+ refreshCodeCount,
104
+ };
105
+ };
@@ -0,0 +1,49 @@
1
+ import {
2
+ getSuiteCoreTranslation,
3
+ SuiteCoreStringKey,
4
+ } from '@digitaldefiance/suite-core-lib';
5
+ import { useMemo, useState } from 'react';
6
+ import { useSuiteConfig } from '../contexts';
7
+ import { createAuthenticatedApiClient } from '../services';
8
+
9
+ export interface UseEmailVerificationResult {
10
+ isVerifying: boolean;
11
+ error: Error | null;
12
+ verifyEmail: (
13
+ token: string
14
+ ) => Promise<{ success: boolean; message?: string }>;
15
+ }
16
+
17
+ export const useEmailVerification = (): UseEmailVerificationResult => {
18
+ const { baseUrl } = useSuiteConfig();
19
+ const api = useMemo(() => createAuthenticatedApiClient(baseUrl), [baseUrl]);
20
+ const [isVerifying, setIsVerifying] = useState(false);
21
+ const [error, setError] = useState<Error | null>(null);
22
+
23
+ const verifyEmail = async (verificationToken: string) => {
24
+ setIsVerifying(true);
25
+ setError(null);
26
+ try {
27
+ const result = await api.post<{ message: string }>('/verify-email', {
28
+ token: verificationToken,
29
+ });
30
+ return { success: true, message: result.data.message };
31
+ } catch (err: unknown) {
32
+ const axiosError = err as { response?: { data?: { message?: string } } };
33
+ const errorMessage =
34
+ axiosError.response?.data?.message ||
35
+ getSuiteCoreTranslation(SuiteCoreStringKey.Error_VerificationFailed);
36
+ const error = new Error(errorMessage);
37
+ setError(error);
38
+ return { success: false, message: errorMessage };
39
+ } finally {
40
+ setIsVerifying(false);
41
+ }
42
+ };
43
+
44
+ return {
45
+ isVerifying,
46
+ error,
47
+ verifyEmail,
48
+ };
49
+ };
@@ -0,0 +1,78 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ export interface TimerInfo {
4
+ timeout: NodeJS.Timeout;
5
+ startTime: number;
6
+ durationMs: number;
7
+ }
8
+
9
+ export interface ExpiringValueReturn<T> {
10
+ value: T | undefined;
11
+ setValue: (newValue: T, durationSeconds?: number, saveToStorage?: boolean) => () => void;
12
+ clearValue: () => void;
13
+ getRemainingTime: () => number;
14
+ isActive: boolean;
15
+ }
16
+
17
+ export function useExpiringValue<T>(
18
+ defaultDurationSeconds: number,
19
+ localStorageKey?: string
20
+ ): ExpiringValueReturn<T> {
21
+ const [value, _setValue] = useState<T | undefined>(undefined);
22
+ const [timer, setTimer] = useState<TimerInfo | undefined>(undefined);
23
+ const [durationSeconds, _setDurationSeconds] = useState<number>(() => {
24
+ if (localStorageKey) {
25
+ const stored = localStorage.getItem(localStorageKey);
26
+ return stored ? parseInt(stored, 10) : defaultDurationSeconds;
27
+ }
28
+ return defaultDurationSeconds;
29
+ });
30
+
31
+ const getRemainingTime = useCallback((): number => {
32
+ if (!timer) return 0;
33
+ const elapsed = Date.now() - timer.startTime;
34
+ const remaining = Math.max(0, timer.durationMs - elapsed);
35
+ return Math.ceil(remaining / 1000);
36
+ }, [timer]);
37
+
38
+ const setValue = useCallback((newValue: T, customDurationSeconds?: number, saveToStorage?: boolean) => {
39
+ const finalDurationSeconds = customDurationSeconds ?? durationSeconds;
40
+
41
+ if (saveToStorage && customDurationSeconds !== undefined && localStorageKey) {
42
+ _setDurationSeconds(customDurationSeconds);
43
+ localStorage.setItem(localStorageKey, customDurationSeconds.toString());
44
+ }
45
+
46
+ _setValue(newValue);
47
+
48
+ if (timer) {
49
+ clearTimeout(timer.timeout);
50
+ }
51
+
52
+ const startTime = Date.now();
53
+ const durationMs = finalDurationSeconds * 1000;
54
+ const timeout = setTimeout(() => {
55
+ _setValue(undefined);
56
+ setTimer(undefined);
57
+ }, durationMs);
58
+
59
+ setTimer({ timeout, startTime, durationMs });
60
+
61
+ return () => {
62
+ clearTimeout(timeout);
63
+ setTimer(undefined);
64
+ };
65
+ }, [durationSeconds, timer, localStorageKey]);
66
+
67
+ const clearValue = useCallback(() => {
68
+ if (timer) {
69
+ clearTimeout(timer.timeout);
70
+ setTimer(undefined);
71
+ }
72
+ _setValue(undefined);
73
+ }, [timer]);
74
+
75
+ const isActive = Boolean(timer && value !== undefined);
76
+
77
+ return { value, setValue, clearValue, getRemainingTime, isActive };
78
+ }
@@ -0,0 +1,18 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { LocalStorageManager } from '@digitaldefiance/suite-core-lib';
3
+
4
+ export function useLocalStorage<T>(
5
+ key: string,
6
+ defaultValue: T
7
+ ): [T, (value: T) => void] {
8
+ const [storedValue, setStoredValue] = useState<T>(() => {
9
+ return LocalStorageManager.getValue(key, defaultValue);
10
+ });
11
+
12
+ const setValue = useCallback((value: T) => {
13
+ setStoredValue(value);
14
+ LocalStorageManager.setValue(key, value);
15
+ }, [key]);
16
+
17
+ return [storedValue, setValue];
18
+ }
@@ -0,0 +1,269 @@
1
+ import { EmailString } from '@digitaldefiance/ecies-lib';
2
+ import {
3
+ CurrencyCode,
4
+ DefaultCurrencyCode,
5
+ DefaultLanguageCode,
6
+ DefaultTimezone,
7
+ Timezone,
8
+ } from '@digitaldefiance/i18n-lib';
9
+ import {
10
+ dehydrateUserSettings,
11
+ getSuiteCoreTranslation,
12
+ IUserSettings,
13
+ IUserSettingsDTO,
14
+ SuiteCoreStringKey,
15
+ TranslatableSuiteError,
16
+ } from '@digitaldefiance/suite-core-lib';
17
+ import { useCallback, useEffect, useState } from 'react';
18
+ import { useAuth, useI18n, useTheme } from '../contexts';
19
+
20
+ const defaultUserSettings: Partial<IUserSettings> = {
21
+ darkMode: false,
22
+ currency: new CurrencyCode(DefaultCurrencyCode),
23
+ timezone: new Timezone(DefaultTimezone),
24
+ siteLanguage: DefaultLanguageCode,
25
+ directChallenge: false,
26
+ } as const;
27
+
28
+ export interface UseUserSettingsOptions {
29
+ authenticatedApi: {
30
+ post: <T = unknown>(
31
+ url: string,
32
+ data: Record<string, unknown>
33
+ ) => Promise<{ data: T }>;
34
+ };
35
+ isAuthenticated: boolean;
36
+ }
37
+
38
+ export interface UseUserSettingsResult {
39
+ currentLanguage: string;
40
+ changeLanguage: (languageCode: string) => Promise<void>;
41
+ userSettings: IUserSettings | undefined;
42
+ setUserSettingAndUpdateSettings: (
43
+ setting?: Partial<IUserSettings>
44
+ ) => Promise<void>;
45
+ toggleColorMode: () => Promise<void>;
46
+ }
47
+
48
+ /**
49
+ * Hook for managing user settings state and synchronization.
50
+ * Used by AuthProvider to handle user settings logic.
51
+ */
52
+ export const useUserSettings = ({
53
+ authenticatedApi,
54
+ isAuthenticated,
55
+ }: UseUserSettingsOptions): UseUserSettingsResult => {
56
+ const { setColorMode: themeSetPaletteMode, mode: currentThemeMode } =
57
+ useTheme();
58
+ const { currentLanguage, changeLanguage } = useI18n();
59
+ const [userSettings, setUserSettings] = useState<IUserSettings | undefined>(
60
+ undefined
61
+ );
62
+
63
+ const setUserSettingAndUpdateSettings = useCallback(
64
+ async (setting?: Partial<IUserSettings>) => {
65
+ if (setting) {
66
+ // Merge settings with defaults and existing settings
67
+ const merged = {
68
+ ...defaultUserSettings,
69
+ ...(userSettings ? userSettings : {}),
70
+ ...setting,
71
+ };
72
+
73
+ // TypeScript requires all IUserSettings fields, but during initialization
74
+ // some fields like email may not be set yet. We'll use Partial until
75
+ // all required fields are available.
76
+ setUserSettings(merged as IUserSettings);
77
+
78
+ if (setting.darkMode !== undefined) {
79
+ themeSetPaletteMode(setting.darkMode ? 'dark' : 'light');
80
+ }
81
+ if (
82
+ setting.siteLanguage !== undefined &&
83
+ setting.siteLanguage !== currentLanguage
84
+ ) {
85
+ changeLanguage(setting.siteLanguage);
86
+ }
87
+
88
+ // Send to server if authenticated and we have complete settings (including email)
89
+ if (isAuthenticated && merged.email) {
90
+ const dehydratedSettings = dehydrateUserSettings(
91
+ merged as IUserSettings
92
+ );
93
+ await authenticatedApi.post(
94
+ '/user/settings',
95
+ dehydratedSettings as unknown as Record<string, unknown>
96
+ );
97
+ }
98
+ } else {
99
+ setUserSettings(undefined);
100
+ themeSetPaletteMode('light');
101
+ }
102
+ },
103
+ [
104
+ isAuthenticated,
105
+ userSettings,
106
+ currentLanguage,
107
+ changeLanguage,
108
+ themeSetPaletteMode,
109
+ authenticatedApi,
110
+ ]
111
+ );
112
+
113
+ const changeLanguageSetting = useCallback(
114
+ async (languageCode: string) => {
115
+ await setUserSettingAndUpdateSettings({ siteLanguage: languageCode });
116
+ },
117
+ [setUserSettingAndUpdateSettings]
118
+ );
119
+
120
+ const toggleColorMode = useCallback(async () => {
121
+ // When not authenticated, read from the current theme mode
122
+ // When authenticated, read from userSettings (synced with server)
123
+ const currentDarkMode = isAuthenticated
124
+ ? userSettings?.darkMode ?? false
125
+ : currentThemeMode === 'dark';
126
+ await setUserSettingAndUpdateSettings({ darkMode: !currentDarkMode });
127
+ }, [
128
+ isAuthenticated,
129
+ userSettings?.darkMode,
130
+ currentThemeMode,
131
+ setUserSettingAndUpdateSettings,
132
+ ]);
133
+
134
+ return {
135
+ changeLanguage: changeLanguageSetting,
136
+ currentLanguage,
137
+ userSettings,
138
+ setUserSettingAndUpdateSettings,
139
+ toggleColorMode,
140
+ };
141
+ };
142
+
143
+ // Public-facing hook for components
144
+ export interface UserSettingsValues extends IUserSettingsDTO {
145
+ [key: string]: string | boolean;
146
+ }
147
+
148
+ export interface UseUserSettingsPublicResult {
149
+ settings: UserSettingsValues | null;
150
+ isLoading: boolean;
151
+ error: Error | null;
152
+ updateSettings: (values: UserSettingsValues) => Promise<
153
+ | {
154
+ success: boolean;
155
+ message: string;
156
+ }
157
+ | {
158
+ error: string;
159
+ errorType?: string;
160
+ field?: string;
161
+ errors?: Array<{ path: string; msg: string }>;
162
+ }
163
+ >;
164
+ refreshSettings: () => Promise<void>;
165
+ }
166
+
167
+ /**
168
+ * Public hook for components to manage user settings.
169
+ * Provides a simpler API for fetching and updating settings.
170
+ */
171
+ export const useUserSettingsPublic = (): UseUserSettingsPublicResult => {
172
+ const { userData, setUserSetting, userSettings } = useAuth();
173
+ const [settings, setSettings] = useState<UserSettingsValues | null>(null);
174
+ const [isLoading, setIsLoading] = useState(true);
175
+ const [error, setError] = useState<Error | null>(null);
176
+
177
+ const refreshSettings = useCallback(async () => {
178
+ setIsLoading(true);
179
+ setError(null);
180
+ try {
181
+ if (userSettings) {
182
+ const dehydratedSettings = dehydrateUserSettings(userSettings);
183
+ setSettings(dehydratedSettings as UserSettingsValues);
184
+ } else if (userData) {
185
+ const fallback = {
186
+ email: userData.email || '',
187
+ timezone: userData.timezone || 'UTC',
188
+ siteLanguage: userData.siteLanguage || 'en-US',
189
+ currency: userData.currency || 'USD',
190
+ darkMode: userData.darkMode || false,
191
+ directChallenge: userData.directChallenge || false,
192
+ };
193
+ setSettings(fallback);
194
+ }
195
+ } catch (err) {
196
+ setError(
197
+ err instanceof Error
198
+ ? err
199
+ : new TranslatableSuiteError(
200
+ SuiteCoreStringKey.Settings_RetrieveFailure
201
+ )
202
+ );
203
+ } finally {
204
+ setIsLoading(false);
205
+ }
206
+ }, [userSettings, userData]);
207
+
208
+ useEffect(() => {
209
+ refreshSettings();
210
+ }, [refreshSettings]);
211
+
212
+ const updateSettings = useCallback(
213
+ async (values: UserSettingsValues) => {
214
+ setIsLoading(true);
215
+ setError(null);
216
+ try {
217
+ await setUserSetting({
218
+ darkMode: values.darkMode,
219
+ currency: new CurrencyCode(values.currency),
220
+ timezone: new Timezone(values.timezone),
221
+ siteLanguage: values.siteLanguage,
222
+ email: new EmailString(values.email),
223
+ directChallenge: values.directChallenge,
224
+ });
225
+
226
+ setSettings(values);
227
+
228
+ return {
229
+ success: true,
230
+ message: getSuiteCoreTranslation(
231
+ SuiteCoreStringKey.Settings_SaveSuccess
232
+ ),
233
+ };
234
+ } catch (err: unknown) {
235
+ const axiosError = err as {
236
+ response?: {
237
+ data?: {
238
+ message?: string;
239
+ errorType?: string;
240
+ field?: string;
241
+ errors?: Array<{ path: string; msg: string }>;
242
+ };
243
+ };
244
+ };
245
+ const errorMessage =
246
+ axiosError.response?.data?.message ||
247
+ getSuiteCoreTranslation(SuiteCoreStringKey.Settings_UpdateFailed);
248
+ setError(err instanceof Error ? err : new Error(errorMessage));
249
+ return {
250
+ error: errorMessage,
251
+ errorType: axiosError.response?.data?.errorType,
252
+ field: axiosError.response?.data?.field,
253
+ errors: axiosError.response?.data?.errors,
254
+ };
255
+ } finally {
256
+ setIsLoading(false);
257
+ }
258
+ },
259
+ [setUserSetting]
260
+ );
261
+
262
+ return {
263
+ settings,
264
+ isLoading,
265
+ error,
266
+ updateSettings,
267
+ refreshSettings,
268
+ };
269
+ };
@@ -1,3 +1,4 @@
1
+ // Export all components
1
2
  export * from './auth';
2
3
  export * from './components';
3
4
  export * from './contexts';
@@ -6,4 +7,3 @@ export * from './interfaces';
6
7
  export * from './services';
7
8
  export * from './types';
8
9
  export * from './wrappers';
9
- //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ export interface IAppConfig {
2
+ hostname: string;
3
+ siteTitle: string;
4
+ server: string;
5
+ }
@@ -0,0 +1,11 @@
1
+ import { ReactElement } from 'react';
2
+ import { IMenuOption } from './IMenuOption';
3
+ import { createMenuType } from '../types';
4
+
5
+ export interface IMenuConfig {
6
+ menuType: ReturnType<typeof createMenuType>;
7
+ menuIcon: ReactElement;
8
+ priority?: number;
9
+ options: IMenuOption[];
10
+ isUserMenu?: boolean;
11
+ }
@@ -0,0 +1,55 @@
1
+ import { ReactNode } from 'react';
2
+ import { To } from 'react-router-dom';
3
+ import { MenuType } from '../types/MenuType';
4
+
5
+ export interface IMenuOption {
6
+ /**
7
+ * Unique identifier for the menu option
8
+ */
9
+ id: string;
10
+ /**
11
+ * Text to display for the menu option
12
+ */
13
+ label: string;
14
+ /**
15
+ * Where the menu option should be displayed
16
+ */
17
+ includeOnMenus: MenuType[];
18
+ /**
19
+ * Display order for the menu option. Lower numbers are displayed first.
20
+ */
21
+ index: number;
22
+ /**
23
+ * Whether the menu option is a divider
24
+ */
25
+ divider?: boolean;
26
+ /**
27
+ * Icon to display for the menu option
28
+ */
29
+ icon?: ReactNode;
30
+ /**
31
+ * Link to navigate to when the menu option is clicked
32
+ * Mutually exclusive with `action`
33
+ * Can be a string, a react-router To object, or an object with pathname and optional state
34
+ */
35
+ link?: To | { pathname: string; state?: unknown };
36
+ /**
37
+ * Function to execute when the menu option is clicked
38
+ * Mutually exclusive with `link`
39
+ * @returns void
40
+ */
41
+ action?: () => void;
42
+ /**
43
+ * Whether the menu option requires authentication
44
+ * true = requires authentication
45
+ * false = requires unauthenticated
46
+ * undefined = always show
47
+ */
48
+ requiresAuth: boolean | undefined;
49
+ /**
50
+ * Custom filter function to determine if the menu option should be displayed
51
+ * @param option - The menu option to filter
52
+ * @returns boolean
53
+ */
54
+ filter?: (option: IMenuOption) => boolean;
55
+ }
@@ -0,0 +1,3 @@
1
+ export * from './IAppConfig';
2
+ export * from './IMenuOption';
3
+ export * from './IMenuConfig';
@@ -0,0 +1,14 @@
1
+ import { jest } from '@jest/globals';
2
+
3
+ export const mockAuthService = {
4
+ verifyToken: jest.fn(),
5
+ directLogin: jest.fn(),
6
+ emailChallengeLogin: jest.fn(),
7
+ refreshToken: jest.fn(),
8
+ register: jest.fn(),
9
+ requestEmailLogin: jest.fn(),
10
+ backupCodeLogin: jest.fn(),
11
+ changePassword: jest.fn(),
12
+ };
13
+
14
+ export const createAuthService = jest.fn(() => mockAuthService);
@@ -0,0 +1,13 @@
1
+ import axios from 'axios';
2
+
3
+ export function createApiClient(baseURL: string) {
4
+ const api = axios.create({ baseURL });
5
+
6
+ api.interceptors.request.use((config) => {
7
+ const languageCode = localStorage.getItem('languageCode') ?? 'en-US';
8
+ config.headers['Accept-Language'] = languageCode;
9
+ return config;
10
+ });
11
+
12
+ return api;
13
+ }