@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,374 @@
1
+ /**
2
+ * FakeEmailAdminPanel — dev-only panel for inspecting captured emails.
3
+ *
4
+ * Renders only in development/test environments where FakeEmailService is
5
+ * active. Calls the AdminEmailRouter endpoints mounted at `baseUrl/api/admin/emails`.
6
+ *
7
+ * Self-contained: wraps itself with AuthenticatedApiProvider internally.
8
+ * Usage (inside SuiteConfigProvider):
9
+ * <FakeEmailAdminPanel />
10
+ */
11
+ import {
12
+ SuiteCoreComponentId,
13
+ SuiteCoreStringKey,
14
+ SuiteCoreStringKeyValue,
15
+ } from '@digitaldefiance/suite-core-lib';
16
+ import DeleteIcon from '@mui/icons-material/Delete';
17
+ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
18
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
19
+ import RefreshIcon from '@mui/icons-material/Refresh';
20
+ import {
21
+ Alert,
22
+ Box,
23
+ Button,
24
+ Chip,
25
+ CircularProgress,
26
+ Collapse,
27
+ Divider,
28
+ IconButton,
29
+ List,
30
+ ListItemButton,
31
+ ListItemText,
32
+ Paper,
33
+ Typography,
34
+ } from '@mui/material';
35
+ import { FC, useCallback, useEffect, useRef, useState } from 'react';
36
+ import { useI18n } from '../contexts';
37
+ import { ConfirmationDialog } from './ConfirmationDialog';
38
+ import {
39
+ AuthenticatedApiProvider,
40
+ useAuthenticatedApi,
41
+ } from '../hooks/useAuthenticatedApi';
42
+
43
+ /** Shape of a single captured email as returned by AdminEmailRouter */
44
+ export interface CapturedEmailInfo {
45
+ to: string;
46
+ subject: string;
47
+ text: string;
48
+ html: string;
49
+ timestamp: string;
50
+ }
51
+
52
+ /** Shape of a recipient summary as returned by GET /api/admin/emails */
53
+ export interface RecipientSummary {
54
+ address: string;
55
+ count: number;
56
+ }
57
+
58
+ /**
59
+ * Renders email HTML content in a sandboxed iframe so that links are
60
+ * clickable and open in a new tab. Falls back to plain text when no
61
+ * HTML body is available.
62
+ */
63
+ const EmailBodyIframe: FC<{ html: string; text: string }> = ({
64
+ html,
65
+ text,
66
+ }) => {
67
+ const iframeRef = useRef<HTMLIFrameElement>(null);
68
+
69
+ useEffect(() => {
70
+ const iframe = iframeRef.current;
71
+ if (!iframe) return;
72
+ const doc = iframe.contentDocument ?? iframe.contentWindow?.document;
73
+ if (!doc) return;
74
+
75
+ if (html) {
76
+ // Inject a <base target="_blank"> so every link opens in a new tab,
77
+ // then write the original HTML body.
78
+ doc.open();
79
+ doc.write(
80
+ `<!DOCTYPE html><html><head><base target="_blank"><style>body{font-family:sans-serif;font-size:14px;margin:8px;}</style></head><body>${html}</body></html>`
81
+ );
82
+ doc.close();
83
+ } else {
84
+ doc.open();
85
+ doc.write(
86
+ `<!DOCTYPE html><html><head><style>body{font-family:sans-serif;font-size:14px;margin:8px;white-space:pre-wrap;}</style></head><body>${text.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</body></html>`
87
+ );
88
+ doc.close();
89
+ }
90
+
91
+ // Auto-resize iframe height to fit content
92
+ const resize = () => {
93
+ if (doc.body) {
94
+ iframe.style.height = `${doc.body.scrollHeight + 16}px`;
95
+ }
96
+ };
97
+ // Resize after content loads (images, etc.)
98
+ iframe.addEventListener('load', resize);
99
+ resize();
100
+ // Small delay to catch late-rendering content
101
+ const timer = setTimeout(resize, 200);
102
+ return () => {
103
+ iframe.removeEventListener('load', resize);
104
+ clearTimeout(timer);
105
+ };
106
+ }, [html, text]);
107
+
108
+ return (
109
+ <iframe
110
+ ref={iframeRef}
111
+ sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin"
112
+ style={{
113
+ width: '100%',
114
+ minHeight: 80,
115
+ border: '1px solid #e0e0e0',
116
+ borderRadius: 4,
117
+ background: '#fff',
118
+ }}
119
+ title="Email body"
120
+ />
121
+ );
122
+ };
123
+
124
+ const FakeEmailAdminPanelContent: FC = () => {
125
+ const api = useAuthenticatedApi();
126
+ const { tComponent } = useI18n();
127
+ const t = (key: SuiteCoreStringKeyValue) =>
128
+ tComponent<SuiteCoreStringKeyValue>(SuiteCoreComponentId, key);
129
+
130
+ const [recipients, setRecipients] = useState<RecipientSummary[]>([]);
131
+ const [expanded, setExpanded] = useState<string | null>(null);
132
+ /** Tracks which individual email is expanded to show its body (address:idx) */
133
+ const [expandedEmail, setExpandedEmail] = useState<string | null>(null);
134
+ const [emailsByAddress, setEmailsByAddress] = useState<
135
+ Record<string, CapturedEmailInfo[]>
136
+ >({});
137
+ const [loading, setLoading] = useState(false);
138
+ const [error, setError] = useState<string | null>(null);
139
+ const [clearDialogOpen, setClearDialogOpen] = useState(false);
140
+
141
+ const loadRecipients = useCallback(async () => {
142
+ setLoading(true);
143
+ setError(null);
144
+ try {
145
+ const res = await api.get<{ recipients: RecipientSummary[] }>(
146
+ '/admin/emails'
147
+ );
148
+ setRecipients(res.data.recipients);
149
+ } catch (err) {
150
+ setError(err instanceof Error ? err.message : String(err));
151
+ } finally {
152
+ setLoading(false);
153
+ }
154
+ }, [api]);
155
+
156
+ useEffect(() => {
157
+ void loadRecipients();
158
+ }, [loadRecipients]);
159
+
160
+ const toggleExpand = useCallback(
161
+ async (address: string) => {
162
+ if (expanded === address) {
163
+ setExpanded(null);
164
+ return;
165
+ }
166
+ setExpanded(address);
167
+ if (!emailsByAddress[address]) {
168
+ try {
169
+ const res = await api.get<CapturedEmailInfo[]>(
170
+ `/admin/emails/${encodeURIComponent(address)}`
171
+ );
172
+ setEmailsByAddress((prev) => ({ ...prev, [address]: res.data }));
173
+ } catch (err) {
174
+ setError(err instanceof Error ? err.message : String(err));
175
+ }
176
+ }
177
+ },
178
+ [api, expanded, emailsByAddress]
179
+ );
180
+
181
+ const handleClearAll = useCallback(async () => {
182
+ setClearDialogOpen(false);
183
+ try {
184
+ await api.delete('/admin/emails');
185
+ setRecipients([]);
186
+ setEmailsByAddress({});
187
+ setExpanded(null);
188
+ } catch (err) {
189
+ setError(err instanceof Error ? err.message : String(err));
190
+ }
191
+ }, [api]);
192
+
193
+ return (
194
+ <Paper variant="outlined" sx={{ p: 2 }}>
195
+ {/* Header */}
196
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
197
+ <Typography variant="subtitle1" fontWeight="bold" sx={{ flexGrow: 1 }}>
198
+ {t(SuiteCoreStringKey.FakeEmail_Admin_Title)}
199
+ </Typography>
200
+ <Chip label="DEV" size="small" color="warning" />
201
+ <IconButton
202
+ size="small"
203
+ onClick={() => void loadRecipients()}
204
+ title={t(SuiteCoreStringKey.FakeEmail_Admin_Refresh)}
205
+ >
206
+ <RefreshIcon fontSize="small" />
207
+ </IconButton>
208
+ <IconButton
209
+ size="small"
210
+ color="error"
211
+ onClick={() => setClearDialogOpen(true)}
212
+ title={t(SuiteCoreStringKey.FakeEmail_Admin_ClearAll)}
213
+ disabled={recipients.length === 0}
214
+ >
215
+ <DeleteIcon fontSize="small" />
216
+ </IconButton>
217
+ </Box>
218
+
219
+ <Divider sx={{ mb: 1 }} />
220
+
221
+ {/* Error */}
222
+ {error && (
223
+ <Alert severity="error" sx={{ mb: 1 }} onClose={() => setError(null)}>
224
+ {error}
225
+ </Alert>
226
+ )}
227
+
228
+ {/* Loading */}
229
+ {loading && (
230
+ <Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
231
+ <CircularProgress size={24} />
232
+ </Box>
233
+ )}
234
+
235
+ {/* Empty state */}
236
+ {!loading && recipients.length === 0 && (
237
+ <Typography variant="body2" color="text.secondary">
238
+ {t(SuiteCoreStringKey.FakeEmail_Admin_NoRecipients)}
239
+ </Typography>
240
+ )}
241
+
242
+ {/* Recipient list */}
243
+ {!loading && recipients.length > 0 && (
244
+ <List dense disablePadding>
245
+ {recipients.map((r) => (
246
+ <Box key={r.address}>
247
+ <ListItemButton
248
+ onClick={() => void toggleExpand(r.address)}
249
+ sx={{ px: 0, py: 0.5 }}
250
+ >
251
+ <ListItemText
252
+ primary={r.address}
253
+ secondary={`${r.count} ${t(SuiteCoreStringKey.FakeEmail_Admin_Recipients)}`}
254
+ />
255
+ {expanded === r.address ? (
256
+ <ExpandLessIcon fontSize="small" />
257
+ ) : (
258
+ <ExpandMoreIcon fontSize="small" />
259
+ )}
260
+ </ListItemButton>
261
+
262
+ <Collapse in={expanded === r.address} unmountOnExit>
263
+ {emailsByAddress[r.address]?.length === 0 ? (
264
+ <Typography
265
+ variant="body2"
266
+ color="text.secondary"
267
+ sx={{ pl: 2, py: 0.5 }}
268
+ >
269
+ {t(SuiteCoreStringKey.FakeEmail_Admin_NoEmails)}
270
+ </Typography>
271
+ ) : (
272
+ <List dense disablePadding sx={{ pl: 2 }}>
273
+ {(emailsByAddress[r.address] ?? []).map((email, idx) => {
274
+ const emailKey = `${r.address}:${idx}`;
275
+ const isEmailExpanded = expandedEmail === emailKey;
276
+ return (
277
+ <Box key={idx} sx={{ py: 0.25 }}>
278
+ <ListItemButton
279
+ disableGutters
280
+ onClick={() =>
281
+ setExpandedEmail(isEmailExpanded ? null : emailKey)
282
+ }
283
+ sx={{ py: 0.25, px: 0.5, borderRadius: 1 }}
284
+ >
285
+ <ListItemText
286
+ primary={email.subject || '(no subject)'}
287
+ secondary={`${t(SuiteCoreStringKey.FakeEmail_Admin_Timestamp)}: ${new Date(email.timestamp).toLocaleString()}`}
288
+ primaryTypographyProps={{ variant: 'body2' }}
289
+ secondaryTypographyProps={{ variant: 'caption' }}
290
+ />
291
+ {isEmailExpanded ? (
292
+ <ExpandLessIcon fontSize="small" />
293
+ ) : (
294
+ <ExpandMoreIcon fontSize="small" />
295
+ )}
296
+ </ListItemButton>
297
+ <Collapse in={isEmailExpanded} unmountOnExit>
298
+ <Box sx={{ pl: 1, pr: 1, pb: 1 }}>
299
+ <EmailBodyIframe
300
+ html={email.html}
301
+ text={email.text}
302
+ />
303
+ </Box>
304
+ </Collapse>
305
+ </Box>
306
+ );
307
+ })}
308
+ </List>
309
+ )}
310
+ </Collapse>
311
+ <Divider />
312
+ </Box>
313
+ ))}
314
+ </List>
315
+ )}
316
+
317
+ {/* Clear confirmation */}
318
+ <ConfirmationDialog
319
+ open={clearDialogOpen}
320
+ title={t(SuiteCoreStringKey.FakeEmail_Admin_ClearAll)}
321
+ message={t(SuiteCoreStringKey.FakeEmail_Admin_ClearConfirm)}
322
+ onConfirm={() => void handleClearAll()}
323
+ onCancel={() => setClearDialogOpen(false)}
324
+ />
325
+ </Paper>
326
+ );
327
+ };
328
+
329
+ export const FakeEmailAdminPanel: FC = () => (
330
+ <AuthenticatedApiProvider>
331
+ <FakeEmailAdminPanelContent />
332
+ </AuthenticatedApiProvider>
333
+ );
334
+
335
+ export default FakeEmailAdminPanel;
336
+
337
+ // Buttons component for use outside the panel (e.g. in a toolbar)
338
+ export interface FakeEmailAdminButtonsProps {
339
+ onRefresh: () => void;
340
+ onClear: () => void;
341
+ disabled?: boolean;
342
+ }
343
+
344
+ export const FakeEmailAdminButtons: FC<FakeEmailAdminButtonsProps> = ({
345
+ onRefresh,
346
+ onClear,
347
+ disabled = false,
348
+ }) => {
349
+ const { tComponent } = useI18n();
350
+ const t = (key: SuiteCoreStringKeyValue) =>
351
+ tComponent<SuiteCoreStringKeyValue>(SuiteCoreComponentId, key);
352
+
353
+ return (
354
+ <Box sx={{ display: 'flex', gap: 1 }}>
355
+ <Button
356
+ size="small"
357
+ startIcon={<RefreshIcon />}
358
+ onClick={onRefresh}
359
+ disabled={disabled}
360
+ >
361
+ {t(SuiteCoreStringKey.FakeEmail_Admin_Refresh)}
362
+ </Button>
363
+ <Button
364
+ size="small"
365
+ color="error"
366
+ startIcon={<DeleteIcon />}
367
+ onClick={onClear}
368
+ disabled={disabled}
369
+ >
370
+ {t(SuiteCoreStringKey.FakeEmail_Admin_ClearAll)}
371
+ </Button>
372
+ </Box>
373
+ );
374
+ };
@@ -0,0 +1,52 @@
1
+ import { getFlagCode } from '@digitaldefiance/suite-core-lib';
2
+ import { Box, SxProps, Theme } from '@mui/material';
3
+ import { FC } from 'react';
4
+
5
+ export interface FlagProps {
6
+ language: string;
7
+ sx?: SxProps<Theme>;
8
+ }
9
+
10
+ /**
11
+ * A simple component to display a flag icon for a given language.
12
+ *
13
+ * Props:
14
+ * language: The language to display a flag for, as a StringLanguages enum value.
15
+ * sx: Optional styles to apply to the component.
16
+ *
17
+ * Returns a Box component with an SVG flag icon from flagcdn.com as a ::before pseudo-element.
18
+ * The flag is sized to 1.5rem by default, but can be overridden by passing a custom sx prop.
19
+ * The component also includes an aria-label for accessibility, set to `Flag for <language>`.
20
+ */
21
+ export const Flag: FC<FlagProps> = ({ language, sx }) => {
22
+ const flagContent = getFlagCode(language);
23
+ if (!flagContent) {
24
+ return null;
25
+ }
26
+ return (
27
+ <Box
28
+ component="span"
29
+ aria-label={`Flag for ${language}`}
30
+ sx={{
31
+ fontSize: '1.5rem',
32
+ lineHeight: 1,
33
+ verticalAlign: 'middle',
34
+ '&::before': {
35
+ content: `" "`,
36
+ display: 'inline-block',
37
+ width: '1em',
38
+ height: '1em',
39
+ backgroundImage: `url(https://flagcdn.com/${flagContent.toLowerCase()}.svg)`,
40
+ backgroundSize: 'contain',
41
+ backgroundRepeat: 'no-repeat',
42
+ backgroundPosition: 'center',
43
+ border: '1px solid rgba(0, 0, 0, 0.1)',
44
+ borderRadius: '2px',
45
+ },
46
+ ...sx,
47
+ }}
48
+ />
49
+ );
50
+ };
51
+
52
+ export default Flag;
@@ -0,0 +1,174 @@
1
+ import {
2
+ SuiteCoreComponentId,
3
+ SuiteCoreStringKey,
4
+ SuiteCoreStringKeyValue,
5
+ } from '@digitaldefiance/suite-core-lib';
6
+ import {
7
+ Alert,
8
+ Box,
9
+ Button,
10
+ Container,
11
+ TextField,
12
+ Typography,
13
+ } from '@mui/material';
14
+ import { useFormik } from 'formik';
15
+ import { FC, useState } from 'react';
16
+ import * as Yup from 'yup';
17
+ import { useI18n } from '../contexts';
18
+
19
+ export interface ForgotPasswordFormValues {
20
+ email: string;
21
+ }
22
+
23
+ export interface ForgotPasswordFormProps {
24
+ onSubmit: (values: ForgotPasswordFormValues) => Promise<void>;
25
+ emailValidation?: Yup.StringSchema;
26
+ labels?: {
27
+ title?: string;
28
+ email?: string;
29
+ sendResetLink?: string;
30
+ successMessage?: string;
31
+ };
32
+ }
33
+
34
+ export const ForgotPasswordForm: FC<ForgotPasswordFormProps> = ({
35
+ onSubmit,
36
+ emailValidation,
37
+ labels = {},
38
+ }) => {
39
+ const { tComponent } = useI18n();
40
+ const [success, setSuccess] = useState(false);
41
+ const [apiError, setApiError] = useState<string>('');
42
+
43
+ const validation = {
44
+ email:
45
+ emailValidation ||
46
+ Yup.string()
47
+ .email(
48
+ tComponent<SuiteCoreStringKeyValue>(
49
+ SuiteCoreComponentId,
50
+ SuiteCoreStringKey.Validation_InvalidEmail
51
+ )
52
+ )
53
+ .required(
54
+ tComponent<SuiteCoreStringKeyValue>(
55
+ SuiteCoreComponentId,
56
+ SuiteCoreStringKey.Validation_Required
57
+ )
58
+ ),
59
+ };
60
+
61
+ const translatedLabels = {
62
+ title:
63
+ labels.title ||
64
+ tComponent<SuiteCoreStringKeyValue>(
65
+ SuiteCoreComponentId,
66
+ SuiteCoreStringKey.ForgotPassword_Title
67
+ ),
68
+ email:
69
+ labels.email ||
70
+ tComponent<SuiteCoreStringKeyValue>(
71
+ SuiteCoreComponentId,
72
+ SuiteCoreStringKey.Common_Email
73
+ ),
74
+ sendResetLink:
75
+ labels.sendResetLink ||
76
+ tComponent<SuiteCoreStringKeyValue>(
77
+ SuiteCoreComponentId,
78
+ SuiteCoreStringKey.ForgotPassword_SendResetLink
79
+ ),
80
+ successMessage:
81
+ labels.successMessage ||
82
+ tComponent<SuiteCoreStringKeyValue>(
83
+ SuiteCoreComponentId,
84
+ SuiteCoreStringKey.PasswordReset_Success
85
+ ),
86
+ };
87
+
88
+ const formik = useFormik<ForgotPasswordFormValues>({
89
+ initialValues: {
90
+ email: '',
91
+ },
92
+ validationSchema: Yup.object({
93
+ email: validation.email,
94
+ }),
95
+ onSubmit: async (values) => {
96
+ try {
97
+ await onSubmit(values);
98
+ setSuccess(true);
99
+ setApiError('');
100
+ } catch (error: unknown) {
101
+ const err = error as { response?: { data?: { message?: string } } };
102
+ setApiError(
103
+ err.response?.data?.message ||
104
+ tComponent<SuiteCoreStringKeyValue>(
105
+ SuiteCoreComponentId,
106
+ SuiteCoreStringKey.ForgotPassword_Error
107
+ )
108
+ );
109
+ setSuccess(false);
110
+ }
111
+ },
112
+ });
113
+
114
+ return (
115
+ <Container maxWidth="sm">
116
+ <Box
117
+ sx={{
118
+ mt: 8,
119
+ display: 'flex',
120
+ flexDirection: 'column',
121
+ alignItems: 'center',
122
+ }}
123
+ >
124
+ <Typography variant="h4" component="h1" gutterBottom>
125
+ {translatedLabels.title}
126
+ </Typography>
127
+
128
+ <Box
129
+ component="form"
130
+ onSubmit={formik.handleSubmit}
131
+ sx={{ mt: 1, width: '100%' }}
132
+ >
133
+ <TextField
134
+ fullWidth
135
+ id="email"
136
+ name="email"
137
+ label={translatedLabels.email}
138
+ value={formik.values.email}
139
+ onChange={formik.handleChange}
140
+ onBlur={formik.handleBlur}
141
+ error={Boolean(formik.touched.email && formik.errors.email)}
142
+ helperText={formik.touched.email && formik.errors.email}
143
+ margin="normal"
144
+ />
145
+
146
+ {apiError && (
147
+ <Alert severity="error" sx={{ mt: 2, mb: 2 }}>
148
+ {apiError}
149
+ </Alert>
150
+ )}
151
+
152
+ {success && (
153
+ <Alert severity="success" sx={{ mt: 2, mb: 2 }}>
154
+ {translatedLabels.successMessage}
155
+ </Alert>
156
+ )}
157
+
158
+ <Button
159
+ type="submit"
160
+ fullWidth
161
+ variant="contained"
162
+ color="primary"
163
+ sx={{ mt: 3, mb: 2 }}
164
+ disabled={formik.isSubmitting}
165
+ >
166
+ {translatedLabels.sendResetLink}
167
+ </Button>
168
+ </Box>
169
+ </Box>
170
+ </Container>
171
+ );
172
+ };
173
+
174
+ export default ForgotPasswordForm;