@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,48 @@
1
+ import { useAuthenticator } from "@aws-amplify/ui-react";
2
+ import { isCognitoConfigured } from "../amplify-config";
3
+ import type { AuthAccountProps } from "../provider";
4
+ import { isSignOutVisible } from "../sign-out-visibility";
5
+
6
+ const cognitoUserEmail = (
7
+ user: { signInDetails?: { loginId?: string }; username?: string } | undefined,
8
+ ): string | null => {
9
+ if (!user) return null;
10
+ return user.signInDetails?.loginId ?? user.username ?? null;
11
+ };
12
+
13
+ const CognitoAccountInner = ({
14
+ children,
15
+ fallback = null,
16
+ }: AuthAccountProps) => {
17
+ const { authStatus, signOut, user } = useAuthenticator((ctx) => [
18
+ ctx.authStatus,
19
+ ctx.signOut,
20
+ ctx.user,
21
+ ]);
22
+
23
+ if (!isSignOutVisible({ configured: true, authStatus })) {
24
+ return <>{fallback}</>;
25
+ }
26
+
27
+ return (
28
+ <>{children({ email: cognitoUserEmail(user), signOut: () => signOut() })}</>
29
+ );
30
+ };
31
+
32
+ /**
33
+ * Cognito identity for the account affordances. `useAuthenticator` throws when
34
+ * called outside `Authenticator.Provider`, which `CognitoShell` only mounts
35
+ * once Cognito is configured — so the hook-using inner is gated on that,
36
+ * constant for the app's lifetime, keeping the Rules of Hooks intact.
37
+ */
38
+ export const CognitoAccount = ({
39
+ children,
40
+ fallback = null,
41
+ }: AuthAccountProps) => {
42
+ if (!isCognitoConfigured()) {
43
+ return <>{fallback}</>;
44
+ }
45
+ return (
46
+ <CognitoAccountInner fallback={fallback}>{children}</CognitoAccountInner>
47
+ );
48
+ };
@@ -0,0 +1,266 @@
1
+ import {
2
+ Authenticator,
3
+ createTheme,
4
+ ThemeProvider,
5
+ useAuthenticator,
6
+ } from "@aws-amplify/ui-react";
7
+ import { AuthCard, AuthFooter, AuthHero, Banner } from "@remit/ui";
8
+ import type { ReactNode } from "react";
9
+ import { AppShellSkeleton } from "@/components/layout/AppShellSkeleton";
10
+ import { authFooterNote } from "../account-menu-mode";
11
+ import { isCognitoConfigured } from "../amplify-config";
12
+
13
+ interface CognitoShellProps {
14
+ children: ReactNode;
15
+ }
16
+
17
+ const REMIT_FONT =
18
+ 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
19
+
20
+ const cssVar = (name: string): string => `var(${name})`;
21
+
22
+ /* remit-ui semantic vars (tokens.css declares these on :root / .dark).
23
+ We reference the raw vars — not the Tailwind `--color-*` aliases —
24
+ because the `@theme inline` block inlines those into utilities rather
25
+ than guaranteeing them as custom properties. */
26
+ const BRAND = cssVar("--accent");
27
+ const BRAND_HOVER = cssVar("--accent-hover");
28
+ const BRAND_SOFT = cssVar("--accent-soft");
29
+ const BRAND_FG = cssVar("--accent-fg");
30
+ const CARD_BG = cssVar("--surface");
31
+ const CARD_FG = cssVar("--fg");
32
+ const PAGE_BG = cssVar("--surface-sunken");
33
+ const MUTED_FG = cssVar("--fg-muted");
34
+ const BORDER = cssVar("--line");
35
+
36
+ const remitTheme = createTheme({
37
+ name: "remit",
38
+ tokens: {
39
+ colors: {
40
+ brand: {
41
+ primary: {
42
+ 10: { value: BRAND_SOFT },
43
+ 20: { value: BRAND_SOFT },
44
+ 40: { value: BRAND },
45
+ 60: { value: BRAND },
46
+ 80: { value: BRAND },
47
+ 90: { value: BRAND },
48
+ 100: { value: BRAND },
49
+ },
50
+ },
51
+ background: {
52
+ primary: { value: CARD_BG },
53
+ secondary: { value: PAGE_BG },
54
+ },
55
+ font: {
56
+ primary: { value: CARD_FG },
57
+ secondary: { value: MUTED_FG },
58
+ tertiary: { value: MUTED_FG },
59
+ interactive: { value: BRAND },
60
+ inverse: { value: BRAND_FG },
61
+ hover: { value: BRAND },
62
+ focus: { value: BRAND },
63
+ active: { value: BRAND },
64
+ },
65
+ border: {
66
+ primary: { value: BORDER },
67
+ secondary: { value: BORDER },
68
+ focus: { value: BRAND },
69
+ },
70
+ },
71
+ fonts: {
72
+ default: {
73
+ variable: { value: REMIT_FONT },
74
+ static: { value: REMIT_FONT },
75
+ },
76
+ },
77
+ radii: {
78
+ /* Literal values from the remit-ui radius scale (2/3/5/7/10px).
79
+ Tailwind's `@theme inline` only emits theme variables that are
80
+ referenced from compiled CSS — var(--radius-*) lookups from
81
+ this JS theme object are invisible to its scanner, so they
82
+ would resolve to nothing at runtime. */
83
+ small: { value: "3px" },
84
+ medium: { value: "5px" },
85
+ large: { value: "7px" },
86
+ },
87
+ components: {
88
+ authenticator: {
89
+ router: {
90
+ borderColor: { value: BORDER },
91
+ borderWidth: { value: "1px" },
92
+ boxShadow: {
93
+ value:
94
+ "0 10px 30px -10px rgb(15 23 42 / 0.18), 0 4px 12px -6px rgb(15 23 42 / 0.1)",
95
+ },
96
+ backgroundColor: { value: CARD_BG },
97
+ },
98
+ form: {
99
+ padding: { value: "1.75rem" },
100
+ },
101
+ orContainer: {
102
+ color: { value: MUTED_FG },
103
+ },
104
+ },
105
+ button: {
106
+ color: { value: CARD_FG },
107
+ primary: {
108
+ backgroundColor: { value: BRAND },
109
+ color: { value: BRAND_FG },
110
+ _hover: {
111
+ backgroundColor: { value: BRAND_HOVER },
112
+ color: { value: BRAND_FG },
113
+ },
114
+ _focus: {
115
+ backgroundColor: { value: BRAND },
116
+ color: { value: BRAND_FG },
117
+ },
118
+ _active: {
119
+ backgroundColor: { value: BRAND },
120
+ color: { value: BRAND_FG },
121
+ },
122
+ },
123
+ link: {
124
+ color: { value: BRAND },
125
+ _hover: {
126
+ color: { value: BRAND },
127
+ backgroundColor: { value: "transparent" },
128
+ },
129
+ _focus: {
130
+ color: { value: BRAND },
131
+ backgroundColor: { value: "transparent" },
132
+ },
133
+ _active: {
134
+ color: { value: BRAND },
135
+ backgroundColor: { value: "transparent" },
136
+ },
137
+ },
138
+ },
139
+ fieldcontrol: {
140
+ color: { value: CARD_FG },
141
+ borderColor: { value: BORDER },
142
+ _focus: {
143
+ borderColor: { value: BRAND },
144
+ boxShadow: { value: `0 0 0 2px ${BRAND}` },
145
+ },
146
+ },
147
+ field: {
148
+ label: {
149
+ color: { value: CARD_FG },
150
+ },
151
+ },
152
+ tabs: {
153
+ borderColor: { value: BORDER },
154
+ item: {
155
+ color: { value: MUTED_FG },
156
+ backgroundColor: { value: "transparent" },
157
+ _active: {
158
+ color: { value: BRAND },
159
+ borderColor: { value: BRAND },
160
+ backgroundColor: { value: "transparent" },
161
+ },
162
+ _hover: {
163
+ color: { value: BRAND },
164
+ },
165
+ _focus: {
166
+ color: { value: BRAND },
167
+ },
168
+ },
169
+ },
170
+ heading: {
171
+ color: { value: CARD_FG },
172
+ },
173
+ text: {
174
+ color: { value: CARD_FG },
175
+ },
176
+ },
177
+ },
178
+ });
179
+
180
+ // Amplify's default placeholders are title-case ("Enter your Username"); the
181
+ // design + app copy convention is lowercase (#883).
182
+ const authFormFields = {
183
+ signIn: {
184
+ username: { placeholder: "Enter your username" },
185
+ password: { placeholder: "Enter your password" },
186
+ },
187
+ signUp: {
188
+ username: { placeholder: "Enter your username" },
189
+ password: { placeholder: "Enter your password" },
190
+ confirm_password: { placeholder: "Confirm your password" },
191
+ },
192
+ };
193
+
194
+ const LocalDevBanner = () => (
195
+ <Banner
196
+ tone="warning"
197
+ className="fixed top-0 left-0 right-0 z-50 h-7 sm:h-10 overflow-hidden border-x-0 border-t-0"
198
+ >
199
+ <span className="flex items-center gap-2">
200
+ <strong className="font-semibold shrink-0">Local dev</strong>
201
+ <span className="truncate hidden sm:inline">
202
+ — Cognito not configured. Set VITE_COGNITO_USER_POOL_ID and
203
+ VITE_COGNITO_CLIENT_ID in .env.local to enable sign-in.
204
+ </span>
205
+ <span className="truncate sm:hidden">— no Cognito; signed out</span>
206
+ </span>
207
+ </Banner>
208
+ );
209
+
210
+ const CognitoAuthFooter = () => (
211
+ <AuthFooter
212
+ note={authFooterNote({
213
+ betterAuthEnabled: false,
214
+ cognitoConfigured: isCognitoConfigured(),
215
+ })}
216
+ />
217
+ );
218
+
219
+ const SignInGate = ({ children }: { children: ReactNode }) => {
220
+ const { authStatus } = useAuthenticator((ctx) => [ctx.authStatus]);
221
+
222
+ if (authStatus === "authenticated") {
223
+ return <>{children}</>;
224
+ }
225
+
226
+ // While Amplify hydrates a stored session, render the app-shell skeleton
227
+ // instead of flashing the sign-in form (or a blank screen) — most returning
228
+ // users resolve to `authenticated` and never see the form.
229
+ if (authStatus === "configuring") {
230
+ return <AppShellSkeleton />;
231
+ }
232
+
233
+ return (
234
+ <ThemeProvider theme={remitTheme}>
235
+ <AuthCard>
236
+ <AuthHero />
237
+ <Authenticator
238
+ hideSignUp={false}
239
+ formFields={authFormFields}
240
+ components={{ Footer: CognitoAuthFooter }}
241
+ />
242
+ </AuthCard>
243
+ </ThemeProvider>
244
+ );
245
+ };
246
+
247
+ export const CognitoShell = ({ children }: CognitoShellProps) => {
248
+ if (!isCognitoConfigured()) {
249
+ // The dev banner is fixed at the top. Pad content below it AND shrink the
250
+ // inner `main` (hardcoded `h-dvh`) by the banner height — otherwise main's
251
+ // full 100dvh starts below the 40px padding and overflows, pushing pinned
252
+ // footers like the compose Send bar 40px below the fold (#788).
253
+ return (
254
+ <div className="pt-7 sm:pt-10 [&_#main-content]:h-[calc(100dvh-1.75rem)] sm:[&_#main-content]:h-[calc(100dvh-2.5rem)]">
255
+ <LocalDevBanner />
256
+ {children}
257
+ </div>
258
+ );
259
+ }
260
+
261
+ return (
262
+ <Authenticator.Provider>
263
+ <SignInGate>{children}</SignInGate>
264
+ </Authenticator.Provider>
265
+ );
266
+ };
@@ -0,0 +1,110 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+
4
+ type AmplifyAuthMocks = {
5
+ session: unknown;
6
+ fetchCalls: number;
7
+ fetchImpl: null | (() => unknown);
8
+ };
9
+
10
+ declare global {
11
+ // eslint-disable-next-line no-var
12
+ var __AMPLIFY_AUTH_MOCKS__: AmplifyAuthMocks | undefined;
13
+ // eslint-disable-next-line no-var
14
+ var __AMPLIFY_CONFIG_MOCKS__:
15
+ | { configured: boolean; configureCalls: number }
16
+ | undefined;
17
+ }
18
+
19
+ let cacheBust = 2000;
20
+
21
+ const FAR_FUTURE_EXP = Math.floor(Date.now() / 1000) + 3600;
22
+
23
+ const idToken = (value: string, exp: number = FAR_FUTURE_EXP) => ({
24
+ toString: () => value,
25
+ payload: { exp },
26
+ });
27
+
28
+ // `configured` drives `isCognitoConfigured` (the amplify-config stub reads it
29
+ // fresh); the session itself comes from the `aws-amplify/auth` stub via
30
+ // `__AMPLIFY_AUTH_MOCKS__`.
31
+ const loadCognitoToken = async (
32
+ configured: boolean,
33
+ authMocks: Partial<AmplifyAuthMocks>,
34
+ ) => {
35
+ cacheBust += 1;
36
+ globalThis.__REMIT_CONFIG__ = {};
37
+ globalThis.__AMPLIFY_CONFIG_MOCKS__ = { configured, configureCalls: 0 };
38
+ globalThis.__AMPLIFY_AUTH_MOCKS__ = {
39
+ session: { tokens: {} },
40
+ fetchCalls: 0,
41
+ fetchImpl: null,
42
+ ...authMocks,
43
+ };
44
+ return import(`./cognito-token.ts?v=${cacheBust}`);
45
+ };
46
+
47
+ describe("getCognitoToken", () => {
48
+ test("returns the Cognito id token when configured", async () => {
49
+ const mod = await loadCognitoToken(true, {
50
+ session: { tokens: { idToken: idToken("ID-TOKEN-123") } },
51
+ });
52
+ assert.equal(await mod.getCognitoToken(), "ID-TOKEN-123");
53
+ });
54
+
55
+ test("returns null when the session has no idToken", async () => {
56
+ const mod = await loadCognitoToken(true, { session: { tokens: {} } });
57
+ assert.equal(await mod.getCognitoToken(), null);
58
+ });
59
+
60
+ test("returns null when the tokens field is absent", async () => {
61
+ const mod = await loadCognitoToken(true, { session: {} });
62
+ assert.equal(await mod.getCognitoToken(), null);
63
+ });
64
+
65
+ test("returns null when cognito is not configured", async () => {
66
+ const mod = await loadCognitoToken(false, {
67
+ session: { tokens: { idToken: idToken("UNUSED") } },
68
+ });
69
+ assert.equal(await mod.getCognitoToken(), null);
70
+ assert.equal(globalThis.__AMPLIFY_AUTH_MOCKS__?.fetchCalls, 0);
71
+ });
72
+
73
+ test("propagates errors when fetchAuthSession rejects (let it crash)", async () => {
74
+ const mod = await loadCognitoToken(true, {
75
+ fetchImpl: () => {
76
+ throw new Error("session fetch failed");
77
+ },
78
+ });
79
+ await assert.rejects(() => mod.getCognitoToken(), /session fetch failed/);
80
+ });
81
+
82
+ test("reuses the cached token across calls without re-fetching the session", async () => {
83
+ const mod = await loadCognitoToken(true, {
84
+ session: { tokens: { idToken: idToken("CACHED") } },
85
+ });
86
+ assert.equal(await mod.getCognitoToken(), "CACHED");
87
+ assert.equal(await mod.getCognitoToken(), "CACHED");
88
+ assert.equal(globalThis.__AMPLIFY_AUTH_MOCKS__?.fetchCalls, 1);
89
+ });
90
+
91
+ test("re-fetches the session when the cached token is near expiry", async () => {
92
+ const almostExpired = Math.floor(Date.now() / 1000) + 5;
93
+ const mod = await loadCognitoToken(true, {
94
+ session: { tokens: { idToken: idToken("STALE", almostExpired) } },
95
+ });
96
+ await mod.getCognitoToken();
97
+ await mod.getCognitoToken();
98
+ assert.equal(globalThis.__AMPLIFY_AUTH_MOCKS__?.fetchCalls, 2);
99
+ });
100
+
101
+ test("resetCognitoTokenCache forces the next call to re-fetch the session", async () => {
102
+ const mod = await loadCognitoToken(true, {
103
+ session: { tokens: { idToken: idToken("CACHED") } },
104
+ });
105
+ await mod.getCognitoToken();
106
+ mod.resetCognitoTokenCache();
107
+ await mod.getCognitoToken();
108
+ assert.equal(globalThis.__AMPLIFY_AUTH_MOCKS__?.fetchCalls, 2);
109
+ });
110
+ });
@@ -0,0 +1,47 @@
1
+ import { fetchAuthSession } from "aws-amplify/auth";
2
+ import { isCognitoConfigured } from "../amplify-config";
3
+
4
+ const EXPIRY_SKEW_SECONDS = 60;
5
+
6
+ interface CachedToken {
7
+ value: string;
8
+ expiresAt: number;
9
+ }
10
+
11
+ let cached: CachedToken | null = null;
12
+
13
+ const nowSeconds = (): number => Math.floor(Date.now() / 1000);
14
+
15
+ const isUsable = (token: CachedToken | null): token is CachedToken =>
16
+ token !== null && token.expiresAt - EXPIRY_SKEW_SECONDS > nowSeconds();
17
+
18
+ const refreshCognitoToken = async (): Promise<string | null> => {
19
+ const session = await fetchAuthSession();
20
+ const idToken = session.tokens?.idToken;
21
+ if (!idToken) {
22
+ cached = null;
23
+ return null;
24
+ }
25
+ cached = {
26
+ value: idToken.toString(),
27
+ expiresAt: idToken.payload.exp ?? nowSeconds() + EXPIRY_SKEW_SECONDS,
28
+ };
29
+ return cached.value;
30
+ };
31
+
32
+ /**
33
+ * The Cognito bearer token: the Amplify session's ID token, cached until it
34
+ * nears expiry. Returns `null` when Cognito is not configured (local dev), so
35
+ * the request simply carries no token. This is the `getToken` behind the
36
+ * Cognito auth provider — the `aws-amplify/auth` dependency lives here, never in
37
+ * the shell or a screen.
38
+ */
39
+ export const getCognitoToken = async (): Promise<string | null> => {
40
+ if (!isCognitoConfigured()) return null;
41
+ if (isUsable(cached)) return cached.value;
42
+ return refreshCognitoToken();
43
+ };
44
+
45
+ export const resetCognitoTokenCache = (): void => {
46
+ cached = null;
47
+ };
@@ -0,0 +1,21 @@
1
+ /* Amplify ships an unlayered reset (`button, input { font-size: inherit }`)
2
+ that, being unlayered, outranks every Tailwind utility on form elements —
3
+ so text-sm/text-2xs on sidebar header buttons were ignored and inherited
4
+ the 16px body size. Importing it into the `amplify` cascade layer, declared
5
+ here before Tailwind, lets Tailwind's utilities win. Must be imported as
6
+ CSS (not via a JS `import "...styles.css"`, which lands unlayered).
7
+
8
+ This sheet ships ONLY in a build that composes the Cognito shell. A build
9
+ that omits Cognito never imports it, so the Amplify reset and the
10
+ Authenticator overrides below stay out of that bundle entirely. */
11
+ @layer amplify;
12
+ @import "@aws-amplify/ui-react/styles.css" layer(amplify);
13
+
14
+ /* ------------------------------------------------------------------ */
15
+ /* Auth page */
16
+ /* The page frame (radial-gradient bg, centring, card max-width) lives */
17
+ /* in the remit-ui <AuthCard> component. Amplify structural overrides */
18
+ /* below are unlayered so they beat both @layer amplify and Tailwind */
19
+ /* preflight; scoped to [data-auth-page] so nothing else is affected. */
20
+ /* ------------------------------------------------------------------ */
21
+ @import "../auth.css";
@@ -0,0 +1,22 @@
1
+ import { configureAmplify } from "./amplify-config";
2
+ import { CognitoAccount } from "./cognito/CognitoAccount";
3
+ import { CognitoShell } from "./cognito/CognitoShell";
4
+ import {
5
+ getCognitoToken,
6
+ resetCognitoTokenCache,
7
+ } from "./cognito/cognito-token";
8
+ import type { AuthProvider } from "./provider";
9
+
10
+ /**
11
+ * Cognito (AWS Amplify) identity. Importing this module is what pulls Amplify
12
+ * into a build — a distributor that composes a different provider never
13
+ * references it, so `aws-amplify` and `@aws-amplify/ui-react` stay out of that
14
+ * bundle.
15
+ */
16
+ export const cognitoAuthProvider: AuthProvider = {
17
+ configure: configureAmplify,
18
+ getToken: getCognitoToken,
19
+ resetToken: resetCognitoTokenCache,
20
+ Shell: CognitoShell,
21
+ Account: CognitoAccount,
22
+ };
@@ -0,0 +1,42 @@
1
+ import type { ReactNode } from "react";
2
+ import { configureAmplify } from "./amplify-config";
3
+ import { isBetterAuthEnabled } from "./better-auth-config";
4
+ import { betterAuthProvider } from "./better-auth-provider";
5
+ import { cognitoAuthProvider } from "./cognito-provider";
6
+ import type { AuthAccountProps, AuthProvider } from "./provider";
7
+
8
+ /**
9
+ * Both identity systems in one build, selected at runtime by
10
+ * `betterAuthEnabled`. This is the default composition and what
11
+ * `@remit/web-client-dist` ships — one artifact that serves a Cognito or a
12
+ * better-auth deployment depending on `config.js`. A distributor that wants a
13
+ * single provider composes `cognitoAuthProvider` or `betterAuthProvider`
14
+ * directly and omits the other.
15
+ */
16
+ const CombinedShell = ({ children }: { children: ReactNode }) =>
17
+ isBetterAuthEnabled() ? (
18
+ <betterAuthProvider.Shell>{children}</betterAuthProvider.Shell>
19
+ ) : (
20
+ <cognitoAuthProvider.Shell>{children}</cognitoAuthProvider.Shell>
21
+ );
22
+
23
+ const CombinedAccount = (props: AuthAccountProps) =>
24
+ isBetterAuthEnabled() ? (
25
+ <betterAuthProvider.Account {...props} />
26
+ ) : (
27
+ <cognitoAuthProvider.Account {...props} />
28
+ );
29
+
30
+ export const combinedAuthProvider: AuthProvider = {
31
+ configure: configureAmplify,
32
+ getToken: () =>
33
+ isBetterAuthEnabled()
34
+ ? betterAuthProvider.getToken()
35
+ : cognitoAuthProvider.getToken(),
36
+ resetToken: () => {
37
+ betterAuthProvider.resetToken();
38
+ cognitoAuthProvider.resetToken();
39
+ },
40
+ Shell: CombinedShell,
41
+ Account: CombinedAccount,
42
+ };
@@ -0,0 +1,136 @@
1
+ import assert from "node:assert/strict";
2
+ import { dirname, join } from "node:path";
3
+ import { describe, it } from "node:test";
4
+ import { fileURLToPath } from "node:url";
5
+ import { build } from "esbuild";
6
+
7
+ /**
8
+ * Proves the auth shell is a composition choice, not a runtime toggle inside
9
+ * one bundle: a build that composes the better-auth provider contains no
10
+ * Amplify/Cognito code, and one that composes the Cognito provider does. The
11
+ * app shell and screens are walked from the real entry each time, so this fails
12
+ * the moment any surface reaches back to a specific identity SDK.
13
+ */
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ const srcDir = join(here, "..");
16
+ const packageDir = join(srcDir, "..");
17
+
18
+ interface Variant {
19
+ specifier: string;
20
+ name: string;
21
+ }
22
+
23
+ const VARIANTS: Record<"betterAuth" | "cognito", Variant> = {
24
+ betterAuth: {
25
+ specifier: "@/auth/better-auth-provider",
26
+ name: "betterAuthProvider",
27
+ },
28
+ cognito: {
29
+ specifier: "@/auth/cognito-provider",
30
+ name: "cognitoAuthProvider",
31
+ },
32
+ };
33
+
34
+ interface Graph {
35
+ inputs: string[];
36
+ importPaths: string[];
37
+ }
38
+
39
+ // Externalize third-party packages, but bundle in-repo `@remit/*` workspace
40
+ // source so the walk matches what the real vite build includes. A shared
41
+ // primitive (e.g. `@remit/ui`) that pulled in Amplify would then surface in this
42
+ // graph and fail the test, rather than hiding behind an external edge.
43
+ const externalizeThirdParty = {
44
+ name: "externalize-third-party",
45
+ setup(build: import("esbuild").PluginBuild) {
46
+ build.onResolve({ filter: /.*/ }, (args) => {
47
+ const path = args.path;
48
+ if (path.startsWith(".") || path.startsWith("/")) return null;
49
+ if (path === "@" || path.startsWith("@/")) return null;
50
+ if (path.startsWith("@remit/")) return null;
51
+ return { path, external: true };
52
+ });
53
+ },
54
+ };
55
+
56
+ const bundleGraph = async (variant: Variant): Promise<Graph> => {
57
+ const entry = [
58
+ 'import { mountApp } from "@/shell";',
59
+ `import { ${variant.name} } from "${variant.specifier}";`,
60
+ `mountApp({ authProvider: ${variant.name} });`,
61
+ "",
62
+ ].join("\n");
63
+
64
+ const result = await build({
65
+ stdin: {
66
+ contents: entry,
67
+ resolveDir: packageDir,
68
+ loader: "tsx",
69
+ sourcefile: "compose-entry.tsx",
70
+ },
71
+ bundle: true,
72
+ write: false,
73
+ metafile: true,
74
+ logLevel: "silent",
75
+ format: "esm",
76
+ platform: "browser",
77
+ jsx: "automatic",
78
+ jsxImportSource: "react",
79
+ plugins: [externalizeThirdParty],
80
+ loader: { ".css": "empty", ".png": "empty", ".svg": "empty" },
81
+ absWorkingDir: packageDir,
82
+ alias: { "@": srcDir },
83
+ });
84
+
85
+ const inputs = Object.keys(result.metafile.inputs);
86
+ const importPaths = new Set<string>();
87
+ for (const input of Object.values(result.metafile.inputs)) {
88
+ for (const imported of input.imports) importPaths.add(imported.path);
89
+ }
90
+ return { inputs, importPaths: [...importPaths] };
91
+ };
92
+
93
+ const mentionsAmplify = ({ inputs, importPaths }: Graph): boolean =>
94
+ [...inputs, ...importPaths].some((path) => /aws-amplify/.test(path));
95
+
96
+ const composesCognitoShell = ({ inputs }: Graph): boolean =>
97
+ inputs.some(
98
+ (path) =>
99
+ path.includes("auth/cognito/") || path.includes("cognito-provider"),
100
+ );
101
+
102
+ describe("web-client composition", () => {
103
+ it("omits every Amplify/Cognito module when composing the better-auth provider", async () => {
104
+ const graph = await bundleGraph(VARIANTS.betterAuth);
105
+
106
+ assert.ok(
107
+ graph.inputs.some((path) => path.includes("shell/index")),
108
+ "the app shell should be walked from the entry",
109
+ );
110
+ assert.equal(
111
+ mentionsAmplify(graph),
112
+ false,
113
+ "a better-auth build must not reference aws-amplify anywhere in its graph",
114
+ );
115
+ assert.equal(
116
+ composesCognitoShell(graph),
117
+ false,
118
+ "a better-auth build must not pull the Cognito shell",
119
+ );
120
+ });
121
+
122
+ it("includes the Amplify/Cognito modules when composing the cognito provider", async () => {
123
+ const graph = await bundleGraph(VARIANTS.cognito);
124
+
125
+ assert.equal(
126
+ mentionsAmplify(graph),
127
+ true,
128
+ "a cognito build must reference aws-amplify",
129
+ );
130
+ assert.equal(
131
+ composesCognitoShell(graph),
132
+ true,
133
+ "a cognito build must pull the Cognito shell",
134
+ );
135
+ });
136
+ });