@remit/web-client 0.0.1

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 (298) hide show
  1. package/harness/build.mjs +84 -0
  2. package/harness/index.html +37 -0
  3. package/harness/vite-preset.ts +29 -0
  4. package/index.html +47 -0
  5. package/package.json +147 -0
  6. package/public/config.js +5 -0
  7. package/public/locales/de/common.json +1 -0
  8. package/public/locales/de/errors.json +1 -0
  9. package/public/locales/de/mail.json +13 -0
  10. package/public/locales/de/settings.json +1 -0
  11. package/public/locales/en/common.json +26 -0
  12. package/public/locales/en/errors.json +1 -0
  13. package/public/locales/en/mail.json +69 -0
  14. package/public/locales/en/settings.json +1 -0
  15. package/public/locales/fr/common.json +1 -0
  16. package/public/locales/fr/errors.json +1 -0
  17. package/public/locales/fr/mail.json +13 -0
  18. package/public/locales/fr/settings.json +1 -0
  19. package/public/locales/nl/common.json +1 -0
  20. package/public/locales/nl/errors.json +1 -0
  21. package/public/locales/nl/mail.json +13 -0
  22. package/public/locales/nl/settings.json +1 -0
  23. package/src/auth/AccountMenu.tsx +58 -0
  24. package/src/auth/AccountSession.tsx +19 -0
  25. package/src/auth/BetterAuthShell.tsx +198 -0
  26. package/src/auth/account-menu-mode.test.ts +64 -0
  27. package/src/auth/account-menu-mode.ts +28 -0
  28. package/src/auth/amplify-config.test.ts +158 -0
  29. package/src/auth/amplify-config.ts +60 -0
  30. package/src/auth/auth-error.test.ts +109 -0
  31. package/src/auth/auth-error.ts +107 -0
  32. package/src/auth/auth-interceptor.test.ts +86 -0
  33. package/src/auth/auth-interceptor.ts +23 -0
  34. package/src/auth/auth.css +334 -0
  35. package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
  36. package/src/auth/better-auth-config.ts +85 -0
  37. package/src/auth/better-auth-provider.tsx +20 -0
  38. package/src/auth/cognito/CognitoAccount.tsx +48 -0
  39. package/src/auth/cognito/CognitoShell.tsx +266 -0
  40. package/src/auth/cognito/cognito-token.test.ts +110 -0
  41. package/src/auth/cognito/cognito-token.ts +47 -0
  42. package/src/auth/cognito/cognito.css +21 -0
  43. package/src/auth/cognito-provider.tsx +22 -0
  44. package/src/auth/combined-provider.tsx +42 -0
  45. package/src/auth/composition.test.ts +136 -0
  46. package/src/auth/provider.tsx +63 -0
  47. package/src/auth/sign-out-visibility.test.ts +40 -0
  48. package/src/auth/sign-out-visibility.ts +10 -0
  49. package/src/auth/sign-up-disabled.test.ts +23 -0
  50. package/src/components/compose/AddressField.tsx +237 -0
  51. package/src/components/compose/ComposeBody.tsx +36 -0
  52. package/src/components/compose/ComposeForm.tsx +676 -0
  53. package/src/components/compose/ComposeProvider.tsx +133 -0
  54. package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
  55. package/src/components/compose/FromSelector.tsx +67 -0
  56. package/src/components/compose/FullCompose.tsx +82 -0
  57. package/src/components/compose/InlineCompose.tsx +29 -0
  58. package/src/components/compose/MobileComposeSheet.tsx +152 -0
  59. package/src/components/compose/PlateEditor.tsx +86 -0
  60. package/src/components/compose/PlateToolbar.tsx +110 -0
  61. package/src/components/compose/SubjectField.tsx +19 -0
  62. package/src/components/compose/draft-discard-banner.test.ts +76 -0
  63. package/src/components/compose/index.ts +6 -0
  64. package/src/components/compose/sanitize-quote-html.ts +23 -0
  65. package/src/components/layout/AppShellSkeleton.tsx +53 -0
  66. package/src/components/layout/ComposeFab.tsx +86 -0
  67. package/src/components/layout/Drawer.tsx +105 -0
  68. package/src/components/mail/AutoMovedIndicator.tsx +48 -0
  69. package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
  70. package/src/components/mail/BlockedImagesNotice.tsx +41 -0
  71. package/src/components/mail/BriefPane.tsx +289 -0
  72. package/src/components/mail/ConversationView.tsx +462 -0
  73. package/src/components/mail/DailyBrief.tsx +451 -0
  74. package/src/components/mail/DraftsView.tsx +279 -0
  75. package/src/components/mail/FlaggedList.tsx +170 -0
  76. package/src/components/mail/FlaggedPane.tsx +217 -0
  77. package/src/components/mail/IntelligencePane.test.ts +123 -0
  78. package/src/components/mail/IntelligencePane.tsx +441 -0
  79. package/src/components/mail/MailListHeader.tsx +187 -0
  80. package/src/components/mail/MailNav.tsx +70 -0
  81. package/src/components/mail/MailSidebarAdapter.tsx +351 -0
  82. package/src/components/mail/MailViewChrome.tsx +95 -0
  83. package/src/components/mail/MailboxPane.tsx +1219 -0
  84. package/src/components/mail/MessageActionMenu.tsx +368 -0
  85. package/src/components/mail/MessageBody.tsx +207 -0
  86. package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
  87. package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
  88. package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
  89. package/src/components/mail/MessageCard.tsx +401 -0
  90. package/src/components/mail/MessageDetail.tsx +153 -0
  91. package/src/components/mail/MessageList.tsx +804 -0
  92. package/src/components/mail/MessageListItem.tsx +267 -0
  93. package/src/components/mail/MessageToolbar.render.test.ts +57 -0
  94. package/src/components/mail/MessageToolbar.tsx +166 -0
  95. package/src/components/mail/MobileMessageBar.tsx +142 -0
  96. package/src/components/mail/MoveToTrigger.tsx +250 -0
  97. package/src/components/mail/NavMenuButton.tsx +25 -0
  98. package/src/components/mail/OutboxPane.tsx +531 -0
  99. package/src/components/mail/PullToRefresh.tsx +24 -0
  100. package/src/components/mail/RawMessageView.tsx +84 -0
  101. package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
  102. package/src/components/mail/SelectionToolbar.tsx +139 -0
  103. package/src/components/mail/SpamRescue.render.test.ts +35 -0
  104. package/src/components/mail/SpamRescue.tsx +138 -0
  105. package/src/components/mail/SwipeableMessageRow.tsx +148 -0
  106. package/src/components/mail/message-body-error-banner-content.ts +87 -0
  107. package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
  108. package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
  109. package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
  110. package/src/components/mail/organize/OrganizePanel.tsx +344 -0
  111. package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
  112. package/src/components/settings/AccountFormPanel.test.ts +72 -0
  113. package/src/components/settings/AccountFormPanel.tsx +942 -0
  114. package/src/components/settings/DangerZone.tsx +30 -0
  115. package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
  116. package/src/components/settings/DeleteAccountDialog.tsx +224 -0
  117. package/src/components/settings/FiltersList.render.test.ts +71 -0
  118. package/src/components/settings/FiltersList.tsx +93 -0
  119. package/src/components/settings/account-form-helpers.test.ts +172 -0
  120. package/src/components/settings/account-form-helpers.ts +105 -0
  121. package/src/components/ui/AppVersion.tsx +50 -0
  122. package/src/components/ui/BugReportButton.tsx +45 -0
  123. package/src/components/ui/ConfirmDialog.tsx +125 -0
  124. package/src/components/ui/DropdownMenu.tsx +105 -0
  125. package/src/components/ui/EmptyState.tsx +11 -0
  126. package/src/components/ui/ErrorBanner.tsx +88 -0
  127. package/src/components/ui/ErrorBannerProvider.tsx +77 -0
  128. package/src/components/ui/ErrorBannerStack.tsx +34 -0
  129. package/src/components/ui/ErrorState.tsx +80 -0
  130. package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
  131. package/src/components/ui/FatalErrorOverlay.tsx +151 -0
  132. package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
  133. package/src/components/ui/SlidePanel.tsx +72 -0
  134. package/src/components/ui/error-banners.test.ts +207 -0
  135. package/src/components/ui/error-banners.ts +111 -0
  136. package/src/hooks/queries/keys.ts +38 -0
  137. package/src/hooks/useArchiveMailbox.ts +104 -0
  138. package/src/hooks/useAutoMovedBadge.ts +77 -0
  139. package/src/hooks/useCurrentMailboxName.ts +99 -0
  140. package/src/hooks/useDebouncedValue.ts +12 -0
  141. package/src/hooks/useDeleteMessages.test.ts +103 -0
  142. package/src/hooks/useDeleteMessages.ts +226 -0
  143. package/src/hooks/useFilters.ts +112 -0
  144. package/src/hooks/useIntelligenceData.test.ts +269 -0
  145. package/src/hooks/useIntelligenceData.ts +284 -0
  146. package/src/hooks/useIsDark.ts +27 -0
  147. package/src/hooks/useKeyboardNavigation.ts +192 -0
  148. package/src/hooks/useLayoutTier.test.ts +47 -0
  149. package/src/hooks/useLayoutTier.ts +44 -0
  150. package/src/hooks/useLongPress.test.ts +291 -0
  151. package/src/hooks/useLongPress.ts +87 -0
  152. package/src/hooks/useMailboxAccount.ts +62 -0
  153. package/src/hooks/useMailboxNameIndex.ts +32 -0
  154. package/src/hooks/useMarkAsRead.test.ts +165 -0
  155. package/src/hooks/useMarkAsRead.ts +319 -0
  156. package/src/hooks/useMediaQuery.ts +39 -0
  157. package/src/hooks/useMessageBodyContent.test.ts +370 -0
  158. package/src/hooks/useMessageBodyContent.ts +380 -0
  159. package/src/hooks/useMoveMessages.test.ts +63 -0
  160. package/src/hooks/useMoveMessages.ts +221 -0
  161. package/src/hooks/useOrganizeJob.ts +90 -0
  162. package/src/hooks/useOrganizePreview.ts +40 -0
  163. package/src/hooks/useRescueCandidates.test.ts +217 -0
  164. package/src/hooks/useRescueCandidates.ts +48 -0
  165. package/src/hooks/useSaveDraft.ts +118 -0
  166. package/src/hooks/useSelection.test.ts +75 -0
  167. package/src/hooks/useSelection.ts +211 -0
  168. package/src/hooks/useSemanticSearch.ts +94 -0
  169. package/src/hooks/useSignature.ts +69 -0
  170. package/src/hooks/useStaleAccountSync.test.ts +211 -0
  171. package/src/hooks/useStaleAccountSync.ts +246 -0
  172. package/src/hooks/useSwipeNavigation.test.ts +35 -0
  173. package/src/hooks/useSwipeNavigation.ts +99 -0
  174. package/src/hooks/useToggleStar.ts +187 -0
  175. package/src/hooks/useToggleTrusted.test.ts +102 -0
  176. package/src/hooks/useToggleTrusted.ts +161 -0
  177. package/src/hooks/useTriageKeyboard.ts +103 -0
  178. package/src/hooks/useTriggerSync.test.ts +24 -0
  179. package/src/hooks/useTriggerSync.ts +58 -0
  180. package/src/hooks/useUpdateAddressFlags.ts +125 -0
  181. package/src/hooks/useVisualViewport.test.ts +127 -0
  182. package/src/hooks/useVisualViewport.ts +51 -0
  183. package/src/index.css +19 -0
  184. package/src/lib/account-order.test.ts +84 -0
  185. package/src/lib/account-order.ts +13 -0
  186. package/src/lib/adjacent-message.test.ts +43 -0
  187. package/src/lib/adjacent-message.ts +20 -0
  188. package/src/lib/api.ts +82 -0
  189. package/src/lib/app-info.ts +15 -0
  190. package/src/lib/auto-moved.test.ts +140 -0
  191. package/src/lib/auto-moved.ts +68 -0
  192. package/src/lib/autodiscovery.test.ts +94 -0
  193. package/src/lib/autodiscovery.ts +354 -0
  194. package/src/lib/brief.test.ts +366 -0
  195. package/src/lib/brief.ts +180 -0
  196. package/src/lib/bug-report.test.ts +213 -0
  197. package/src/lib/bug-report.ts +176 -0
  198. package/src/lib/client.ts +36 -0
  199. package/src/lib/console-errors.test.ts +64 -0
  200. package/src/lib/console-errors.ts +92 -0
  201. package/src/lib/conversation-target.test.ts +68 -0
  202. package/src/lib/conversation-target.ts +59 -0
  203. package/src/lib/display-category.ts +20 -0
  204. package/src/lib/drafts.test.ts +241 -0
  205. package/src/lib/drafts.ts +121 -0
  206. package/src/lib/error-classifier.test.ts +133 -0
  207. package/src/lib/error-classifier.ts +96 -0
  208. package/src/lib/fatal-error.test.ts +97 -0
  209. package/src/lib/fatal-error.ts +165 -0
  210. package/src/lib/folder-roles.test.ts +113 -0
  211. package/src/lib/folder-roles.ts +112 -0
  212. package/src/lib/format.test.ts +115 -0
  213. package/src/lib/format.ts +198 -0
  214. package/src/lib/i18n.ts +36 -0
  215. package/src/lib/intelligence-pref.test.ts +57 -0
  216. package/src/lib/intelligence-pref.ts +25 -0
  217. package/src/lib/keymap-dispatch.test.ts +178 -0
  218. package/src/lib/keymap-dispatch.ts +187 -0
  219. package/src/lib/keymap.test.ts +49 -0
  220. package/src/lib/keymap.ts +178 -0
  221. package/src/lib/mail-context.ts +64 -0
  222. package/src/lib/mail-route.test.ts +80 -0
  223. package/src/lib/mail-route.ts +44 -0
  224. package/src/lib/message-body-source.test.ts +98 -0
  225. package/src/lib/message-body-source.ts +48 -0
  226. package/src/lib/move-targets.test.ts +195 -0
  227. package/src/lib/move-targets.ts +74 -0
  228. package/src/lib/onboarding-completion.test.ts +64 -0
  229. package/src/lib/onboarding-completion.ts +23 -0
  230. package/src/lib/organize/filter-status.test.ts +99 -0
  231. package/src/lib/organize/filter-status.ts +86 -0
  232. package/src/lib/organize/organize-copy.test.ts +93 -0
  233. package/src/lib/organize/organize-copy.ts +61 -0
  234. package/src/lib/organize/organize-model.test.ts +104 -0
  235. package/src/lib/organize/organize-model.ts +105 -0
  236. package/src/lib/organize/organize-poll.test.ts +37 -0
  237. package/src/lib/organize/organize-poll.ts +28 -0
  238. package/src/lib/outbox-status.test.ts +64 -0
  239. package/src/lib/outbox-status.ts +75 -0
  240. package/src/lib/plate-serializer.ts +164 -0
  241. package/src/lib/provider-presets.test.ts +96 -0
  242. package/src/lib/provider-presets.ts +81 -0
  243. package/src/lib/query-error-handler.test.ts +144 -0
  244. package/src/lib/query-error-handler.ts +34 -0
  245. package/src/lib/query-escalation.integration.test.ts +138 -0
  246. package/src/lib/recent-searches.test.ts +53 -0
  247. package/src/lib/recent-searches.ts +34 -0
  248. package/src/lib/rescue-candidates.test.ts +94 -0
  249. package/src/lib/rescue-candidates.ts +35 -0
  250. package/src/lib/rescue-telemetry.test.ts +75 -0
  251. package/src/lib/rescue-telemetry.ts +55 -0
  252. package/src/lib/rum-adapter.test.ts +299 -0
  253. package/src/lib/rum-adapter.ts +107 -0
  254. package/src/lib/saved-searches.test.ts +80 -0
  255. package/src/lib/saved-searches.ts +52 -0
  256. package/src/lib/search-pending.test.ts +61 -0
  257. package/src/lib/search-pending.ts +24 -0
  258. package/src/lib/search-query.test.ts +35 -0
  259. package/src/lib/search-query.ts +11 -0
  260. package/src/lib/search-result.test.ts +160 -0
  261. package/src/lib/search-result.ts +102 -0
  262. package/src/lib/search-schemas.ts +34 -0
  263. package/src/lib/search-token-index.test.ts +112 -0
  264. package/src/lib/search-token-index.ts +56 -0
  265. package/src/lib/search-tokens.test.ts +218 -0
  266. package/src/lib/search-tokens.ts +133 -0
  267. package/src/lib/telemetry-context.tsx +9 -0
  268. package/src/lib/telemetry.ts +17 -0
  269. package/src/lib/theme-preference.ts +46 -0
  270. package/src/lib/theme.ts +33 -0
  271. package/src/lib/utils.ts +4 -0
  272. package/src/main.tsx +10 -0
  273. package/src/routeTree.gen.ts +392 -0
  274. package/src/router.test.ts +81 -0
  275. package/src/router.tsx +31 -0
  276. package/src/routes/__root.tsx +69 -0
  277. package/src/routes/index.tsx +7 -0
  278. package/src/routes/mail/$mailboxId.tsx +27 -0
  279. package/src/routes/mail/flagged.tsx +37 -0
  280. package/src/routes/mail/index.tsx +41 -0
  281. package/src/routes/mail/outbox.tsx +13 -0
  282. package/src/routes/mail.tsx +413 -0
  283. package/src/routes/onboarding.tsx +35 -0
  284. package/src/routes/settings/-accounts.test.ts +65 -0
  285. package/src/routes/settings/accounts.tsx +542 -0
  286. package/src/routes/settings/advanced.tsx +55 -0
  287. package/src/routes/settings/appearance.tsx +140 -0
  288. package/src/routes/settings/filters.tsx +141 -0
  289. package/src/routes/settings/folders.tsx +216 -0
  290. package/src/routes/settings/index.tsx +5 -0
  291. package/src/routes/settings/senders.tsx +466 -0
  292. package/src/routes/settings/suggested-vips.tsx +10 -0
  293. package/src/routes/settings.tsx +66 -0
  294. package/src/runtime-config.ts +75 -0
  295. package/src/shell/index.tsx +98 -0
  296. package/src/types/index.ts +73 -0
  297. package/src/vite-env.d.ts +4 -0
  298. package/vite.base.ts +43 -0
@@ -0,0 +1,30 @@
1
+ import { Button, DangerZoneSection } from "@remit/ui";
2
+ import { useState } from "react";
3
+ import { DeleteAccountDialog } from "./DeleteAccountDialog";
4
+
5
+ export function DangerZone() {
6
+ const [dialogOpen, setDialogOpen] = useState(false);
7
+
8
+ return (
9
+ <section className="mt-8 border-t border-line pt-6">
10
+ <DangerZoneSection
11
+ title="Delete your Remit account"
12
+ description="Disconnects every account and erases Remit's copy of your mail, insights and preferences. Your mail at the providers is untouched."
13
+ action={
14
+ <Button
15
+ variant="danger"
16
+ size="sm"
17
+ onClick={() => setDialogOpen(true)}
18
+ >
19
+ Delete your Remit account
20
+ </Button>
21
+ }
22
+ />
23
+
24
+ <DeleteAccountDialog
25
+ open={dialogOpen}
26
+ onClose={() => setDialogOpen(false)}
27
+ />
28
+ </section>
29
+ );
30
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * React-render smoke test for `DeleteAccountDialog`.
3
+ *
4
+ * `DangerZone` mounts this dialog unconditionally on the settings/accounts
5
+ * route. The previous version called `useAuthenticator` at the top of render,
6
+ * which throws `USE_AUTHENTICATOR_ERROR` when no `Authenticator.Provider` is
7
+ * in scope. In local-dev / e2e / the visual harness (`isCognitoConfigured()`
8
+ * === false) `AuthShell` mounts NO provider, so the whole settings/accounts
9
+ * route crashed into the fatal-error escalate page (#745 surfaced this, #741).
10
+ *
11
+ * The fix gates the hook-using inner component behind `isCognitoConfigured()`.
12
+ * This test pins that: rendering the dialog without a provider must not throw.
13
+ *
14
+ * Uses `react-dom/server`'s `renderToString` so the assertion runs without
15
+ * pulling in jsdom/happy-dom (no new deps).
16
+ */
17
+
18
+ import assert from "node:assert/strict";
19
+ import { describe, it } from "node:test";
20
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
21
+ import { createElement } from "react";
22
+ import { renderToString } from "react-dom/server";
23
+ import { DeleteAccountDialog } from "./DeleteAccountDialog";
24
+
25
+ const renderDialog = (open: boolean): string =>
26
+ renderToString(
27
+ createElement(
28
+ QueryClientProvider,
29
+ { client: new QueryClient() },
30
+ createElement(DeleteAccountDialog, {
31
+ open,
32
+ onClose: () => undefined,
33
+ }),
34
+ ) as never,
35
+ );
36
+
37
+ describe("DeleteAccountDialog renders without an Authenticator.Provider in scope (#741)", () => {
38
+ it("mounts WITHOUT calling `useAuthenticator` outside `Authenticator.Provider` — the hook would throw and crash the settings/accounts route into the fatal-error escalate page", () => {
39
+ // Default test env: `isCognitoConfigured()` returns false — same shape as
40
+ // `AuthShell` running without the provider. The dialog's hooks
41
+ // (`useAuthenticator`, `useState`, `useMutation`) all run on mount,
42
+ // before the `open` gate, so rendering it closed still exercises the
43
+ // gated path. Pre-fix, `useAuthenticator` ran unconditionally here and
44
+ // `renderToString` threw `USE_AUTHENTICATOR_ERROR`.
45
+ assert.doesNotThrow(() => renderDialog(false));
46
+ });
47
+ });
@@ -0,0 +1,224 @@
1
+ import {
2
+ meOperationsCreateExportMutation,
3
+ meOperationsDeleteMeMutation,
4
+ } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
5
+ import { Button, Dialog, Input } from "@remit/ui";
6
+ import { useMutation } from "@tanstack/react-query";
7
+ import { AlertTriangle, Download, X } from "lucide-react";
8
+ import { useState } from "react";
9
+ import { useAuthProvider } from "@/auth/provider";
10
+
11
+ interface DeleteAccountDialogProps {
12
+ open: boolean;
13
+ onClose: () => void;
14
+ }
15
+
16
+ interface DeleteAccountDialogViewProps extends DeleteAccountDialogProps {
17
+ accountEmail: string;
18
+ signOut: () => void | Promise<void>;
19
+ }
20
+
21
+ function DeleteAccountDialogView({
22
+ open,
23
+ onClose,
24
+ accountEmail,
25
+ signOut,
26
+ }: DeleteAccountDialogViewProps) {
27
+ const [confirmEmail, setConfirmEmail] = useState("");
28
+ const [mismatch, setMismatch] = useState(false);
29
+ const [deleted, setDeleted] = useState(false);
30
+
31
+ const mutation = useMutation({
32
+ ...meOperationsDeleteMeMutation(),
33
+ onSuccess: () => {
34
+ setDeleted(true);
35
+ setTimeout(() => signOut(), 2000);
36
+ },
37
+ });
38
+
39
+ const exportMutation = useMutation(meOperationsCreateExportMutation());
40
+
41
+ const handleClose = () => {
42
+ if (mutation.isPending) return;
43
+ setConfirmEmail("");
44
+ setMismatch(false);
45
+ setDeleted(false);
46
+ mutation.reset();
47
+ exportMutation.reset();
48
+ onClose();
49
+ };
50
+
51
+ const handleDelete = () => {
52
+ if (confirmEmail.trim().toLowerCase() !== accountEmail.toLowerCase()) {
53
+ setMismatch(true);
54
+ return;
55
+ }
56
+ setMismatch(false);
57
+ mutation.mutate({ body: { confirmEmail: confirmEmail.trim() } });
58
+ };
59
+
60
+ return (
61
+ <Dialog open={open} onClose={handleClose} title="Delete your Remit account">
62
+ <header className="flex items-center gap-2 border-b border-line px-5 py-3">
63
+ <AlertTriangle className="size-4 shrink-0 text-danger" />
64
+ <span className="flex-1 text-sm font-semibold text-fg">
65
+ Delete your Remit account
66
+ </span>
67
+ <Button
68
+ variant="ghost"
69
+ size="sm"
70
+ icon={<X className="size-3.5" />}
71
+ onClick={handleClose}
72
+ aria-label="Cancel"
73
+ />
74
+ </header>
75
+
76
+ {deleted ? (
77
+ <div className="px-5 py-6 text-sm text-fg-muted">
78
+ <p className="font-medium text-fg">Account deletion scheduled.</p>
79
+ <p className="mt-1">
80
+ Your data will be erased shortly. Signing you out…
81
+ </p>
82
+ </div>
83
+ ) : (
84
+ <>
85
+ <div className="space-y-4 px-5 py-4 text-sm text-fg-muted">
86
+ <p>This permanently erases everything Remit holds for you:</p>
87
+ <ul className="space-y-1.5 text-xs">
88
+ <li className="flex gap-2">
89
+ <span className="text-danger">•</span>
90
+ All connected accounts disconnected and access tokens revoked.
91
+ </li>
92
+ <li className="flex gap-2">
93
+ <span className="text-danger">•</span>
94
+ Synced mail cache and search index.
95
+ </li>
96
+ <li className="flex gap-2">
97
+ <span className="text-danger">•</span>
98
+ AI history and insights.
99
+ </li>
100
+ <li className="flex gap-2">
101
+ <span className="text-danger">•</span>
102
+ Preferences and rules.
103
+ </li>
104
+ </ul>
105
+
106
+ <div className="rounded-sm border border-line bg-surface-sunken px-3 py-2 text-xs">
107
+ <strong className="text-fg">
108
+ Your mail at Gmail / IMAP is not deleted.
109
+ </strong>{" "}
110
+ This only removes Remit's copy and its access — the mail stays in
111
+ your provider mailboxes.
112
+ </div>
113
+
114
+ <div className="text-xs">
115
+ <button
116
+ type="button"
117
+ onClick={() => exportMutation.mutate({})}
118
+ aria-busy={exportMutation.isPending}
119
+ className="inline-flex items-center gap-1.5 font-medium text-accent hover:underline"
120
+ >
121
+ <Download className="size-3.5" />
122
+ {exportMutation.isPending
123
+ ? "Requesting export…"
124
+ : "Export my data first"}
125
+ </button>
126
+ {exportMutation.isSuccess && (
127
+ // biome-ignore lint/a11y/useSemanticElements: <p> with role="status" preserves block layout; <output> is inline
128
+ <p className="mt-1.5 text-fg-muted" role="status">
129
+ Export requested. We'll prepare a ZIP of your mail — find it
130
+ under your data exports when it's ready.
131
+ </p>
132
+ )}
133
+ {exportMutation.isError && (
134
+ <p className="mt-1.5 text-danger" role="alert">
135
+ Couldn't start the export. Please try again.
136
+ </p>
137
+ )}
138
+ </div>
139
+
140
+ <div>
141
+ <label
142
+ htmlFor="confirm-remit-email"
143
+ className="mb-1 block text-xs font-medium text-fg"
144
+ >
145
+ Type{" "}
146
+ <span className="font-mono text-fg-muted">{accountEmail}</span>{" "}
147
+ to confirm
148
+ </label>
149
+ <Input
150
+ id="confirm-remit-email"
151
+ type="email"
152
+ placeholder={accountEmail}
153
+ value={confirmEmail}
154
+ onChange={(e) => {
155
+ setConfirmEmail(e.target.value);
156
+ if (mismatch) setMismatch(false);
157
+ if (mutation.isError) mutation.reset();
158
+ }}
159
+ autoComplete="off"
160
+ />
161
+ {mismatch && (
162
+ <p className="mt-1.5 text-xs text-danger" role="alert">
163
+ That doesn't match your account email. Type it exactly to
164
+ confirm.
165
+ </p>
166
+ )}
167
+ {mutation.isError && (
168
+ <p className="mt-1.5 text-xs text-danger" role="alert">
169
+ {mutation.error instanceof Error
170
+ ? mutation.error.message
171
+ : "Something went wrong. Please try again."}
172
+ </p>
173
+ )}
174
+ </div>
175
+ </div>
176
+
177
+ <footer className="flex items-center justify-end gap-2 border-t border-line px-5 py-3">
178
+ <Button variant="secondary" size="sm" onClick={handleClose}>
179
+ Cancel
180
+ </Button>
181
+ <Button
182
+ variant="danger"
183
+ size="sm"
184
+ onClick={handleDelete}
185
+ aria-busy={mutation.isPending}
186
+ >
187
+ {mutation.isPending ? "Deleting…" : "Delete everything"}
188
+ </Button>
189
+ </footer>
190
+ </>
191
+ )}
192
+ </Dialog>
193
+ );
194
+ }
195
+
196
+ /**
197
+ * Delete-account dialog. Safe to mount anywhere — `DangerZone` renders it
198
+ * unconditionally on the settings/accounts route. Identity comes from the
199
+ * composed auth provider's `Account` render-prop; when no session is active
200
+ * (e2e / visual harness / signed out) it falls back to the view with an empty
201
+ * identity and a no-op `signOut`.
202
+ */
203
+ export function DeleteAccountDialog(props: DeleteAccountDialogProps) {
204
+ const { Account } = useAuthProvider();
205
+ return (
206
+ <Account
207
+ fallback={
208
+ <DeleteAccountDialogView
209
+ {...props}
210
+ accountEmail=""
211
+ signOut={() => {}}
212
+ />
213
+ }
214
+ >
215
+ {({ email, signOut }) => (
216
+ <DeleteAccountDialogView
217
+ {...props}
218
+ accountEmail={email ?? ""}
219
+ signOut={signOut}
220
+ />
221
+ )}
222
+ </Account>
223
+ );
224
+ }
@@ -0,0 +1,71 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { RemitImapFilterResponse } from "@remit/api-http-client/types.gen.ts";
4
+ import React, { createElement } from "react";
5
+ import { renderToString } from "react-dom/server";
6
+ import { FiltersList } from "./FiltersList";
7
+
8
+ // The node test loader transpiles remit-ui's `.tsx` with the classic JSX
9
+ // runtime, which references a global `React`. Vite uses the automatic runtime,
10
+ // so this shim only exists for the SSR test harness.
11
+ (globalThis as { React?: typeof React }).React = React;
12
+
13
+ const NOW = Date.parse("2026-07-12T12:00:00Z");
14
+
15
+ const filter = (
16
+ overrides: Partial<RemitImapFilterResponse>,
17
+ ): RemitImapFilterResponse => ({
18
+ filterId: "f-1",
19
+ accountConfigId: "acc-1",
20
+ name: "Travel",
21
+ scope: "Standing",
22
+ state: "Active",
23
+ hasAnchor: true,
24
+ ruleChangedAt: 0,
25
+ matchOperator: "And",
26
+ literalClauses: [],
27
+ actionLabelId: "None",
28
+ actionMailboxId: "mbx-travel",
29
+ createdAt: 0,
30
+ updatedAt: 0,
31
+ ...overrides,
32
+ });
33
+
34
+ const render = (filters: RemitImapFilterResponse[]) =>
35
+ renderToString(
36
+ createElement(FiltersList, {
37
+ filters,
38
+ mailboxName: (id: string) => (id === "mbx-travel" ? "Travel" : undefined),
39
+ onDelete: () => undefined,
40
+ now: NOW,
41
+ }) as never,
42
+ );
43
+
44
+ describe("FiltersList", () => {
45
+ it("renders the empty state with a pointer to Organize", () => {
46
+ const html = render([]);
47
+ assert.match(html, /No filters yet/);
48
+ });
49
+
50
+ it("marks a standing filter Active and shows its move target", () => {
51
+ const html = render([filter({})]);
52
+ assert.match(html, /Active/);
53
+ assert.match(html, /Moves matches to Travel/);
54
+ assert.match(html, /always/);
55
+ });
56
+
57
+ it("keeps an expired temporary filter visible and marks it Expired (RFC 034 Decision 1.2)", () => {
58
+ const html = render([
59
+ filter({
60
+ filterId: "f-2",
61
+ name: "Lisbon trip",
62
+ scope: "Temporary",
63
+ state: "Active",
64
+ expiresAt: "2026-07-10T00:00:00Z",
65
+ }),
66
+ ]);
67
+ assert.match(html, /Lisbon trip/);
68
+ assert.match(html, /Expired/);
69
+ assert.match(html, /expired/);
70
+ });
71
+ });
@@ -0,0 +1,93 @@
1
+ import type { RemitImapFilterResponse } from "@remit/api-http-client/types.gen.ts";
2
+ import { Badge, Button } from "@remit/ui";
3
+ import { Trash2 } from "lucide-react";
4
+ import {
5
+ filterDisplayStatus,
6
+ formatExpiresAt,
7
+ } from "@/lib/organize/filter-status";
8
+ import { NO_ACTION } from "@/lib/organize/organize-model";
9
+
10
+ interface FiltersListProps {
11
+ filters: RemitImapFilterResponse[];
12
+ /** Resolve a destination mailbox id to a folder name for display. */
13
+ mailboxName: (mailboxId: string) => string | undefined;
14
+ onDelete: (filterId: string) => void;
15
+ deletingFilterId?: string;
16
+ /** Injected for deterministic status in tests; defaults to now. */
17
+ now?: number;
18
+ }
19
+
20
+ /**
21
+ * The account's standing filters. Expired temporary filters stay listed and
22
+ * are marked Expired distinctly rather than hidden (RFC 034 Decision 1.2).
23
+ */
24
+ export function FiltersList({
25
+ filters,
26
+ mailboxName,
27
+ onDelete,
28
+ deletingFilterId,
29
+ now = Date.now(),
30
+ }: FiltersListProps) {
31
+ if (filters.length === 0) {
32
+ return (
33
+ <p className="py-6 text-sm text-fg-muted">
34
+ No filters yet. Select a few messages in the inbox and choose Organize
35
+ to make one.
36
+ </p>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <ul className="divide-y divide-line rounded-md border border-line">
42
+ {filters.map((filter) => {
43
+ const status = filterDisplayStatus(filter, now);
44
+ const expired = status === "Expired";
45
+ const folder =
46
+ filter.actionMailboxId !== NO_ACTION
47
+ ? mailboxName(filter.actionMailboxId)
48
+ : undefined;
49
+ const expiresLabel = formatExpiresAt(filter.expiresAt);
50
+
51
+ return (
52
+ <li
53
+ key={filter.filterId}
54
+ className="flex items-start gap-3 px-3 py-2.5"
55
+ >
56
+ <div className="min-w-0 flex-1">
57
+ <div className="flex items-center gap-2">
58
+ <span
59
+ className={`truncate text-sm font-medium ${
60
+ expired ? "text-fg-muted" : "text-fg"
61
+ }`}
62
+ >
63
+ {filter.name}
64
+ </span>
65
+ <Badge tone={expired ? "neutral" : "positive"} dot>
66
+ {status}
67
+ </Badge>
68
+ </div>
69
+ <p className="mt-0.5 text-xs text-fg-subtle">
70
+ {folder ? `Moves matches to ${folder}` : "No move action"}
71
+ {filter.scope === "Temporary" && expiresLabel
72
+ ? expired
73
+ ? ` · expired ${expiresLabel}`
74
+ : ` · until ${expiresLabel}`
75
+ : filter.scope === "Standing"
76
+ ? " · always"
77
+ : ""}
78
+ </p>
79
+ </div>
80
+ <Button
81
+ variant="ghost"
82
+ size="sm"
83
+ icon={<Trash2 className="size-4 text-danger" />}
84
+ onClick={() => onDelete(filter.filterId)}
85
+ disabled={deletingFilterId === filter.filterId}
86
+ aria-label={`Delete filter ${filter.name}`}
87
+ />
88
+ </li>
89
+ );
90
+ })}
91
+ </ul>
92
+ );
93
+ }
@@ -0,0 +1,172 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import {
4
+ accountIsMissingSmtp,
5
+ appendAppPasswordHint,
6
+ computeSmtpAutoFill,
7
+ deriveSmtpHostFromImap,
8
+ isAuthError,
9
+ } from "./account-form-helpers.js";
10
+
11
+ describe("deriveSmtpHostFromImap", () => {
12
+ test("rewrites imap.* to smtp.*", () => {
13
+ assert.equal(
14
+ deriveSmtpHostFromImap("imap.example.com"),
15
+ "smtp.example.com",
16
+ );
17
+ });
18
+
19
+ test("matches the imap. prefix case-insensitively", () => {
20
+ assert.equal(
21
+ deriveSmtpHostFromImap("IMAP.example.com"),
22
+ "smtp.example.com",
23
+ );
24
+ });
25
+
26
+ test("returns the host unchanged when no imap. prefix is present", () => {
27
+ assert.equal(
28
+ deriveSmtpHostFromImap("mail.example.com"),
29
+ "mail.example.com",
30
+ );
31
+ });
32
+
33
+ test("only rewrites the prefix, not other occurrences of imap", () => {
34
+ assert.equal(
35
+ deriveSmtpHostFromImap("imap.imap-server.example.com"),
36
+ "smtp.imap-server.example.com",
37
+ );
38
+ });
39
+ });
40
+
41
+ describe("computeSmtpAutoFill", () => {
42
+ test("fills SMTP fields when SMTP host is empty and IMAP host is set", () => {
43
+ const result = computeSmtpAutoFill({
44
+ imapHost: "imap.example.com",
45
+ smtpHost: "",
46
+ });
47
+ assert.deepEqual(result, {
48
+ smtpHost: "smtp.example.com",
49
+ smtpPort: 587,
50
+ smtpTls: false,
51
+ smtpStartTls: true,
52
+ });
53
+ });
54
+
55
+ test("fills SMTP fields when SMTP host is undefined and IMAP host is set", () => {
56
+ const result = computeSmtpAutoFill({
57
+ imapHost: "imap.example.com",
58
+ });
59
+ assert.deepEqual(result, {
60
+ smtpHost: "smtp.example.com",
61
+ smtpPort: 587,
62
+ smtpTls: false,
63
+ smtpStartTls: true,
64
+ });
65
+ });
66
+
67
+ test("fills SMTP fields when SMTP host is whitespace", () => {
68
+ const result = computeSmtpAutoFill({
69
+ imapHost: "imap.example.com",
70
+ smtpHost: " ",
71
+ });
72
+ assert.notEqual(result, null);
73
+ assert.equal(result?.smtpHost, "smtp.example.com");
74
+ });
75
+
76
+ test("returns null when SMTP host is already set — never overrides user input", () => {
77
+ const result = computeSmtpAutoFill({
78
+ imapHost: "imap.example.com",
79
+ smtpHost: "smtp.different.com",
80
+ smtpPort: 465,
81
+ smtpTls: true,
82
+ smtpStartTls: false,
83
+ });
84
+ assert.equal(result, null);
85
+ });
86
+
87
+ test("returns null when both IMAP host and SMTP host are empty", () => {
88
+ const result = computeSmtpAutoFill({
89
+ imapHost: "",
90
+ smtpHost: "",
91
+ });
92
+ assert.equal(result, null);
93
+ });
94
+
95
+ test("returns null when IMAP host is whitespace", () => {
96
+ const result = computeSmtpAutoFill({
97
+ imapHost: " ",
98
+ smtpHost: "",
99
+ });
100
+ assert.equal(result, null);
101
+ });
102
+
103
+ test("trims IMAP host before deriving SMTP host", () => {
104
+ const result = computeSmtpAutoFill({
105
+ imapHost: " imap.example.com ",
106
+ smtpHost: "",
107
+ });
108
+ assert.equal(result?.smtpHost, "smtp.example.com");
109
+ });
110
+ });
111
+
112
+ describe("accountIsMissingSmtp", () => {
113
+ test("true when smtpEnabled is undefined (legacy row)", () => {
114
+ assert.equal(accountIsMissingSmtp({}), true);
115
+ });
116
+
117
+ test("true when smtpEnabled is false", () => {
118
+ assert.equal(accountIsMissingSmtp({ smtpEnabled: false }), true);
119
+ });
120
+
121
+ test("false when smtpEnabled is true", () => {
122
+ assert.equal(accountIsMissingSmtp({ smtpEnabled: true }), false);
123
+ });
124
+ });
125
+
126
+ describe("isAuthError", () => {
127
+ for (const msg of [
128
+ "AUTHENTICATIONFAILED",
129
+ "535 5.7.8 Bad credentials",
130
+ "Invalid login",
131
+ "bad credential",
132
+ ]) {
133
+ test(`true for "${msg}"`, () => {
134
+ assert.equal(isAuthError(msg), true);
135
+ });
136
+ }
137
+
138
+ test("false for a network error", () => {
139
+ assert.equal(isAuthError("connect ETIMEDOUT 1.2.3.4:993"), false);
140
+ });
141
+
142
+ test("false for undefined", () => {
143
+ assert.equal(isAuthError(undefined), false);
144
+ });
145
+ });
146
+
147
+ describe("appendAppPasswordHint", () => {
148
+ const hint = "iCloud requires an app-specific password.";
149
+
150
+ test("appends the hint to an auth error", () => {
151
+ const result = appendAppPasswordHint("Invalid login", hint);
152
+ assert.equal(result, `Invalid login — ${hint}`);
153
+ });
154
+
155
+ test("leaves a non-auth error untouched", () => {
156
+ assert.equal(
157
+ appendAppPasswordHint("connect ETIMEDOUT", hint),
158
+ "connect ETIMEDOUT",
159
+ );
160
+ });
161
+
162
+ test("returns the message unchanged when there is no hint", () => {
163
+ assert.equal(
164
+ appendAppPasswordHint("Invalid login", undefined),
165
+ "Invalid login",
166
+ );
167
+ });
168
+
169
+ test("returns undefined when there is no message", () => {
170
+ assert.equal(appendAppPasswordHint(undefined, hint), undefined);
171
+ });
172
+ });