@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,198 @@
1
+ import {
2
+ AuthCard,
3
+ AuthFooter,
4
+ AuthHero,
5
+ Banner,
6
+ Button,
7
+ Input,
8
+ } from "@remit/ui";
9
+ import { type FormEvent, type ReactNode, useState } from "react";
10
+ import { AppShellSkeleton } from "@/components/layout/AppShellSkeleton";
11
+ import { FatalErrorOverlay } from "@/components/ui/FatalErrorOverlay";
12
+ import { authFooterNote } from "./account-menu-mode";
13
+ import {
14
+ type AuthRequest,
15
+ authInlineMessage,
16
+ classifyAuthError,
17
+ reportFatalAuthError,
18
+ } from "./auth-error";
19
+ import {
20
+ authClient,
21
+ isBetterAuthEnabled,
22
+ isSignUpDisabledError,
23
+ } from "./better-auth-config";
24
+
25
+ type Mode = "signIn" | "signUp";
26
+
27
+ interface BetterAuthShellProps {
28
+ children: ReactNode;
29
+ }
30
+
31
+ const SIGN_UP_REQUEST: AuthRequest = {
32
+ method: "POST",
33
+ path: "/api/auth/sign-up/email",
34
+ };
35
+ const SIGN_IN_REQUEST: AuthRequest = {
36
+ method: "POST",
37
+ path: "/api/auth/sign-in/email",
38
+ };
39
+
40
+ export const SignInForm = () => {
41
+ const [mode, setMode] = useState<Mode>("signIn");
42
+ const [name, setName] = useState("");
43
+ const [email, setEmail] = useState("");
44
+ const [password, setPassword] = useState("");
45
+ const [error, setError] = useState<string | null>(null);
46
+ const [submitting, setSubmitting] = useState(false);
47
+ // Learned from the server's rejection, never a client-side flag — the UI
48
+ // cannot disagree with better-auth's `disableSignUp`.
49
+ const [signUpClosed, setSignUpClosed] = useState(false);
50
+
51
+ const activeMode: Mode = signUpClosed ? "signIn" : mode;
52
+
53
+ const handleFailure = (failure: unknown, request: AuthRequest) => {
54
+ if (isSignUpDisabledError(failure)) {
55
+ setSignUpClosed(true);
56
+ setMode("signIn");
57
+ return;
58
+ }
59
+ const kind = classifyAuthError(failure);
60
+ if (kind === "fatal") {
61
+ reportFatalAuthError(failure, request);
62
+ return;
63
+ }
64
+ setError(authInlineMessage(failure, kind));
65
+ };
66
+
67
+ const submit = async (event: FormEvent) => {
68
+ event.preventDefault();
69
+ if (submitting) return;
70
+ setError(null);
71
+ setSubmitting(true);
72
+
73
+ const request = activeMode === "signUp" ? SIGN_UP_REQUEST : SIGN_IN_REQUEST;
74
+ // better-auth returns `{ error }` for a server rejection, but a network
75
+ // failure rejects the promise — fold both into one failure value so a
76
+ // broken route or contract never vanishes as an unhandled rejection.
77
+ const failure: unknown = await (activeMode === "signUp"
78
+ ? authClient.signUp.email({ email, password, name })
79
+ : authClient.signIn.email({ email, password })
80
+ ).then(
81
+ (result) => result.error ?? null,
82
+ (thrown: unknown) => thrown,
83
+ );
84
+
85
+ setSubmitting(false);
86
+ // On success `failure` is null; better-auth's session store updates and
87
+ // useSession re-renders the gate into the app.
88
+ if (failure) handleFailure(failure, request);
89
+ };
90
+
91
+ return (
92
+ <>
93
+ <FatalErrorOverlay />
94
+ <AuthCard>
95
+ <AuthHero />
96
+ <form
97
+ onSubmit={submit}
98
+ className="mt-6 flex flex-col gap-4 rounded-lg border border-line bg-surface p-7 shadow-lg"
99
+ >
100
+ {signUpClosed ? (
101
+ <Banner tone="info">
102
+ New signups are currently closed. Sign in with an existing
103
+ account, or contact your administrator for access.
104
+ </Banner>
105
+ ) : (
106
+ <div className="flex gap-1">
107
+ <Button
108
+ size="sm"
109
+ variant={activeMode === "signIn" ? "secondary" : "ghost"}
110
+ aria-pressed={activeMode === "signIn"}
111
+ onClick={() => setMode("signIn")}
112
+ >
113
+ Sign in
114
+ </Button>
115
+ <Button
116
+ size="sm"
117
+ variant={activeMode === "signUp" ? "secondary" : "ghost"}
118
+ aria-pressed={activeMode === "signUp"}
119
+ onClick={() => setMode("signUp")}
120
+ >
121
+ Create account
122
+ </Button>
123
+ </div>
124
+ )}
125
+
126
+ {error && <Banner tone="danger">{error}</Banner>}
127
+
128
+ {activeMode === "signUp" && (
129
+ <div className="flex flex-col gap-1 text-sm">
130
+ <label htmlFor="ba-name" className="text-fg-muted">
131
+ Name
132
+ </label>
133
+ <Input
134
+ id="ba-name"
135
+ type="text"
136
+ autoComplete="name"
137
+ value={name}
138
+ onChange={(e) => setName(e.target.value)}
139
+ placeholder="Your name"
140
+ required
141
+ />
142
+ </div>
143
+ )}
144
+
145
+ <div className="flex flex-col gap-1 text-sm">
146
+ <label htmlFor="ba-email" className="text-fg-muted">
147
+ Email
148
+ </label>
149
+ <Input
150
+ id="ba-email"
151
+ type="email"
152
+ autoComplete="email"
153
+ value={email}
154
+ onChange={(e) => setEmail(e.target.value)}
155
+ placeholder="you@example.com"
156
+ required
157
+ />
158
+ </div>
159
+
160
+ <div className="flex flex-col gap-1 text-sm">
161
+ <label htmlFor="ba-password" className="text-fg-muted">
162
+ Password
163
+ </label>
164
+ <Input
165
+ id="ba-password"
166
+ type="password"
167
+ autoComplete={
168
+ activeMode === "signUp" ? "new-password" : "current-password"
169
+ }
170
+ value={password}
171
+ onChange={(e) => setPassword(e.target.value)}
172
+ placeholder="Your password"
173
+ required
174
+ />
175
+ </div>
176
+
177
+ <Button type="submit">
178
+ {activeMode === "signUp" ? "Create account" : "Sign in"}
179
+ </Button>
180
+ </form>
181
+ <AuthFooter
182
+ note={authFooterNote({
183
+ betterAuthEnabled: isBetterAuthEnabled(),
184
+ cognitoConfigured: false,
185
+ })}
186
+ />
187
+ </AuthCard>
188
+ </>
189
+ );
190
+ };
191
+
192
+ export const BetterAuthShell = ({ children }: BetterAuthShellProps) => {
193
+ const { data: session, isPending } = authClient.useSession();
194
+
195
+ if (isPending) return <AppShellSkeleton />;
196
+ if (session) return <>{children}</>;
197
+ return <SignInForm />;
198
+ };
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import { COGNITO_FOOTER_NOTE } from "@remit/ui";
4
+ import { accountMenuMode, authFooterNote } from "./account-menu-mode";
5
+
6
+ describe("accountMenuMode", () => {
7
+ test("prefers better-auth when enabled", () => {
8
+ assert.equal(
9
+ accountMenuMode({ betterAuthEnabled: true, cognitoConfigured: false }),
10
+ "betterAuth",
11
+ );
12
+ });
13
+
14
+ test("better-auth wins even when cognito is also configured", () => {
15
+ assert.equal(
16
+ accountMenuMode({ betterAuthEnabled: true, cognitoConfigured: true }),
17
+ "betterAuth",
18
+ );
19
+ });
20
+
21
+ test("falls back to cognito when only cognito is configured", () => {
22
+ assert.equal(
23
+ accountMenuMode({ betterAuthEnabled: false, cognitoConfigured: true }),
24
+ "cognito",
25
+ );
26
+ });
27
+
28
+ test("renders nothing when neither identity provider is active", () => {
29
+ assert.equal(
30
+ accountMenuMode({ betterAuthEnabled: false, cognitoConfigured: false }),
31
+ "none",
32
+ );
33
+ });
34
+ });
35
+
36
+ describe("authFooterNote", () => {
37
+ test("names Cognito only when Cognito is the active provider", () => {
38
+ assert.equal(
39
+ authFooterNote({ betterAuthEnabled: false, cognitoConfigured: true }),
40
+ COGNITO_FOOTER_NOTE,
41
+ );
42
+ });
43
+
44
+ test("stays provider-neutral in better-auth mode", () => {
45
+ assert.equal(
46
+ authFooterNote({ betterAuthEnabled: true, cognitoConfigured: false }),
47
+ undefined,
48
+ );
49
+ });
50
+
51
+ test("stays provider-neutral when better-auth wins over present cognito env", () => {
52
+ assert.equal(
53
+ authFooterNote({ betterAuthEnabled: true, cognitoConfigured: true }),
54
+ undefined,
55
+ );
56
+ });
57
+
58
+ test("stays provider-neutral when no provider is configured", () => {
59
+ assert.equal(
60
+ authFooterNote({ betterAuthEnabled: false, cognitoConfigured: false }),
61
+ undefined,
62
+ );
63
+ });
64
+ });
@@ -0,0 +1,28 @@
1
+ import { COGNITO_FOOTER_NOTE } from "@remit/ui";
2
+
3
+ export type AccountMenuMode = "betterAuth" | "cognito" | "none";
4
+
5
+ export interface AccountMenuModeInput {
6
+ betterAuthEnabled: boolean;
7
+ cognitoConfigured: boolean;
8
+ }
9
+
10
+ export const accountMenuMode = ({
11
+ betterAuthEnabled,
12
+ cognitoConfigured,
13
+ }: AccountMenuModeInput): AccountMenuMode => {
14
+ if (betterAuthEnabled) return "betterAuth";
15
+ if (cognitoConfigured) return "cognito";
16
+ return "none";
17
+ };
18
+
19
+ /**
20
+ * The sign-in footer caption names Cognito only when Cognito is the active
21
+ * provider. Every other mode falls through to the kit's provider-neutral
22
+ * default, so the caption can never claim an identity provider the build does
23
+ * not use.
24
+ */
25
+ export const authFooterNote = (
26
+ input: AccountMenuModeInput,
27
+ ): string | undefined =>
28
+ accountMenuMode(input) === "cognito" ? COGNITO_FOOTER_NOTE : undefined;
@@ -0,0 +1,158 @@
1
+ import assert from "node:assert";
2
+ import { beforeEach, describe, test } from "node:test";
3
+
4
+ type TestConfig = {
5
+ cognito?: {
6
+ userPoolId?: string;
7
+ clientId?: string;
8
+ domain?: string;
9
+ region?: string;
10
+ };
11
+ appOrigin?: string;
12
+ };
13
+ type AmplifyCall = {
14
+ Auth?: {
15
+ Cognito?: {
16
+ userPoolId?: string;
17
+ userPoolClientId?: string;
18
+ loginWith?: {
19
+ oauth?: {
20
+ domain?: string;
21
+ redirectSignIn?: string[];
22
+ redirectSignOut?: string[];
23
+ responseType?: string;
24
+ scopes?: string[];
25
+ };
26
+ };
27
+ };
28
+ };
29
+ };
30
+
31
+ declare global {
32
+ // eslint-disable-next-line no-var
33
+ var __AMPLIFY_MOCKS__: { configureCalls: AmplifyCall[] } | undefined;
34
+ // eslint-disable-next-line no-var
35
+ var __CACHE_BUST__: number | undefined;
36
+ }
37
+
38
+ let cacheBust = 0;
39
+
40
+ const loadModule = async (config: TestConfig) => {
41
+ cacheBust += 1;
42
+ globalThis.__CACHE_BUST__ = cacheBust;
43
+ globalThis.__REMIT_CONFIG__ = config;
44
+ globalThis.__AMPLIFY_MOCKS__ = { configureCalls: [] };
45
+ return import(`./amplify-config.ts?v=${cacheBust}`);
46
+ };
47
+
48
+ const fullConfig: TestConfig = {
49
+ cognito: {
50
+ userPoolId: "us-east-1_pool",
51
+ clientId: "client-id",
52
+ domain: "example.auth.us-east-1.amazoncognito.com",
53
+ region: "us-east-1",
54
+ },
55
+ appOrigin: "https://app.example.com",
56
+ };
57
+
58
+ describe("isCognitoConfigured", () => {
59
+ test("returns true when userPoolId and clientId are present", async () => {
60
+ const mod = await loadModule(fullConfig);
61
+ assert.equal(mod.isCognitoConfigured(), true);
62
+ });
63
+
64
+ test("returns false when both values are missing", async () => {
65
+ const mod = await loadModule({});
66
+ assert.equal(mod.isCognitoConfigured(), false);
67
+ });
68
+
69
+ test("returns false when only userPoolId is set", async () => {
70
+ const mod = await loadModule({ cognito: { userPoolId: "pool" } });
71
+ assert.equal(mod.isCognitoConfigured(), false);
72
+ });
73
+
74
+ test("returns false when only clientId is set", async () => {
75
+ const mod = await loadModule({ cognito: { clientId: "client" } });
76
+ assert.equal(mod.isCognitoConfigured(), false);
77
+ });
78
+ });
79
+
80
+ describe("configureAmplify", () => {
81
+ let originalWarn: typeof console.warn;
82
+ let warnings: unknown[][];
83
+
84
+ beforeEach(() => {
85
+ originalWarn = console.warn;
86
+ warnings = [];
87
+ console.warn = (...args: unknown[]) => {
88
+ warnings.push(args);
89
+ };
90
+ });
91
+
92
+ test("warns and does not call Amplify.configure when unconfigured", async () => {
93
+ const mod = await loadModule({});
94
+ mod.configureAmplify();
95
+ console.warn = originalWarn;
96
+ assert.equal(globalThis.__AMPLIFY_MOCKS__?.configureCalls.length, 0);
97
+ assert.equal(warnings.length, 1);
98
+ const message = String(warnings[0][0]);
99
+ assert.ok(
100
+ message.includes("Cognito"),
101
+ `expected warning to mention Cognito, got: ${message}`,
102
+ );
103
+ });
104
+
105
+ test("passes userPoolId and clientId to Amplify.configure", async () => {
106
+ const mod = await loadModule(fullConfig);
107
+ mod.configureAmplify();
108
+ console.warn = originalWarn;
109
+ const calls = globalThis.__AMPLIFY_MOCKS__?.configureCalls ?? [];
110
+ assert.equal(calls.length, 1);
111
+ const cognito = calls[0]?.Auth?.Cognito;
112
+ assert.equal(cognito?.userPoolId, "us-east-1_pool");
113
+ assert.equal(cognito?.userPoolClientId, "client-id");
114
+ });
115
+
116
+ test("wires OAuth block with appOrigin in redirect arrays when domain set", async () => {
117
+ const mod = await loadModule(fullConfig);
118
+ mod.configureAmplify();
119
+ console.warn = originalWarn;
120
+ const oauth =
121
+ globalThis.__AMPLIFY_MOCKS__?.configureCalls[0]?.Auth?.Cognito?.loginWith
122
+ ?.oauth;
123
+ assert.ok(oauth, "expected oauth block to be present");
124
+ assert.equal(oauth?.domain, "example.auth.us-east-1.amazoncognito.com");
125
+ assert.deepEqual(oauth?.redirectSignIn, ["https://app.example.com"]);
126
+ assert.deepEqual(oauth?.redirectSignOut, ["https://app.example.com"]);
127
+ assert.equal(oauth?.responseType, "code");
128
+ });
129
+
130
+ test("omits loginWith block when no domain configured", async () => {
131
+ const mod = await loadModule({
132
+ cognito: { userPoolId: "pool", clientId: "client" },
133
+ });
134
+ mod.configureAmplify();
135
+ console.warn = originalWarn;
136
+ const cognito =
137
+ globalThis.__AMPLIFY_MOCKS__?.configureCalls[0]?.Auth?.Cognito;
138
+ assert.equal(cognito?.loginWith, undefined);
139
+ });
140
+
141
+ test("uses empty redirect arrays when domain present but appOrigin missing", async () => {
142
+ const mod = await loadModule({
143
+ cognito: {
144
+ userPoolId: "pool",
145
+ clientId: "client",
146
+ domain: "example.auth.us-east-1.amazoncognito.com",
147
+ },
148
+ });
149
+ mod.configureAmplify();
150
+ console.warn = originalWarn;
151
+ const oauth =
152
+ globalThis.__AMPLIFY_MOCKS__?.configureCalls[0]?.Auth?.Cognito?.loginWith
153
+ ?.oauth;
154
+ assert.ok(oauth);
155
+ assert.deepEqual(oauth?.redirectSignIn, []);
156
+ assert.deepEqual(oauth?.redirectSignOut, []);
157
+ });
158
+ });
@@ -0,0 +1,60 @@
1
+ import { Amplify } from "aws-amplify";
2
+ import { getRuntimeConfig } from "../runtime-config";
3
+
4
+ export interface CognitoEnv {
5
+ userPoolId: string;
6
+ userPoolClientId: string;
7
+ domain: string;
8
+ region: string;
9
+ appOrigin: string;
10
+ }
11
+
12
+ const readCognitoEnv = (): CognitoEnv | null => {
13
+ const config = getRuntimeConfig();
14
+ const { userPoolId, clientId, domain, region } = config.cognito;
15
+
16
+ if (!userPoolId || !clientId) return null;
17
+
18
+ return {
19
+ userPoolId,
20
+ userPoolClientId: clientId,
21
+ domain,
22
+ region,
23
+ appOrigin: config.appOrigin,
24
+ };
25
+ };
26
+
27
+ const cognitoEnv = readCognitoEnv();
28
+
29
+ export const isCognitoConfigured = (): boolean => cognitoEnv !== null;
30
+
31
+ export const configureAmplify = (): void => {
32
+ if (!cognitoEnv) {
33
+ console.warn(
34
+ "[auth] Cognito user pool is not configured. Running in local dev mode without Cognito auth.",
35
+ );
36
+ return;
37
+ }
38
+
39
+ const { userPoolId, userPoolClientId, domain, appOrigin } = cognitoEnv;
40
+
41
+ Amplify.configure({
42
+ Auth: {
43
+ Cognito: {
44
+ userPoolId,
45
+ userPoolClientId,
46
+ loginWith: domain
47
+ ? {
48
+ oauth: {
49
+ domain,
50
+ scopes: ["openid", "email", "profile"],
51
+ redirectSignIn: appOrigin ? [appOrigin] : [],
52
+ redirectSignOut: appOrigin ? [appOrigin] : [],
53
+ responseType: "code",
54
+ },
55
+ }
56
+ : undefined,
57
+ },
58
+ },
59
+ });
60
+ };
@@ -0,0 +1,109 @@
1
+ import assert from "node:assert/strict";
2
+ import { afterEach, describe, it } from "node:test";
3
+ import { __resetFatalError, getCurrentFatalError } from "@/lib/fatal-error";
4
+ import {
5
+ type AuthRequest,
6
+ authInlineMessage,
7
+ buildFatalAuthMessage,
8
+ classifyAuthError,
9
+ reportFatalAuthError,
10
+ } from "./auth-error";
11
+
12
+ const SIGN_UP: AuthRequest = {
13
+ method: "POST",
14
+ path: "/api/auth/sign-up/email",
15
+ };
16
+
17
+ describe("classifyAuthError", () => {
18
+ it("treats a 404 on our own route as fatal", () => {
19
+ assert.equal(
20
+ classifyAuthError({ status: 404, statusText: "Not Found" }),
21
+ "fatal",
22
+ );
23
+ });
24
+
25
+ it("treats a 5xx as fatal", () => {
26
+ assert.equal(
27
+ classifyAuthError({ status: 503, statusText: "Service Unavailable" }),
28
+ "fatal",
29
+ );
30
+ });
31
+
32
+ it("treats an unexpected 4xx with no message as fatal", () => {
33
+ assert.equal(classifyAuthError({ status: 418 }), "fatal");
34
+ });
35
+
36
+ it("treats an expected, server-described 4xx as validation", () => {
37
+ assert.equal(
38
+ classifyAuthError({
39
+ status: 422,
40
+ code: "USER_ALREADY_EXISTS",
41
+ message: "User already exists.",
42
+ }),
43
+ "validation",
44
+ );
45
+ assert.equal(
46
+ classifyAuthError({
47
+ status: 401,
48
+ code: "INVALID_EMAIL_OR_PASSWORD",
49
+ message: "Invalid email or password.",
50
+ }),
51
+ "validation",
52
+ );
53
+ });
54
+
55
+ it("treats a statusless failure as network", () => {
56
+ assert.equal(classifyAuthError(new Error("Failed to fetch")), "network");
57
+ });
58
+
59
+ it("treats better-auth's Fetch Error sentinel as network", () => {
60
+ assert.equal(
61
+ classifyAuthError({
62
+ status: 500,
63
+ statusText: "Fetch Error",
64
+ message: "Fetch related error.",
65
+ }),
66
+ "network",
67
+ );
68
+ });
69
+ });
70
+
71
+ describe("authInlineMessage", () => {
72
+ it("surfaces the server's message for a validation error", () => {
73
+ assert.equal(
74
+ authInlineMessage(
75
+ { status: 422, message: "User already exists." },
76
+ "validation",
77
+ ),
78
+ "User already exists.",
79
+ );
80
+ });
81
+
82
+ it("gives a connection hint for a network error", () => {
83
+ assert.match(authInlineMessage(new Error("boom"), "network"), /offline/i);
84
+ });
85
+ });
86
+
87
+ describe("buildFatalAuthMessage", () => {
88
+ it("names the method, path, and status, with 404 troubleshooting guidance", () => {
89
+ const message = buildFatalAuthMessage(
90
+ { status: 404, statusText: "Not Found" },
91
+ SIGN_UP,
92
+ );
93
+ assert.match(message, /POST \/api\/auth\/sign-up\/email/);
94
+ assert.match(message, /404 Not Found/);
95
+ assert.match(message, /backend is running and up to date/);
96
+ });
97
+ });
98
+
99
+ describe("reportFatalAuthError", () => {
100
+ afterEach(() => __resetFatalError());
101
+
102
+ it("escalates to a non-recoverable fatal carrying the request detail", () => {
103
+ reportFatalAuthError({ status: 404, statusText: "Not Found" }, SIGN_UP);
104
+ const fatal = getCurrentFatalError();
105
+ assert.ok(fatal);
106
+ assert.equal(fatal.recoverable, false);
107
+ assert.match(fatal.message, /POST \/api\/auth\/sign-up\/email failed: 404/);
108
+ });
109
+ });