@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.
- package/harness/build.mjs +84 -0
- package/harness/index.html +37 -0
- package/harness/vite-preset.ts +29 -0
- package/index.html +47 -0
- package/package.json +147 -0
- package/public/config.js +5 -0
- package/public/locales/de/common.json +1 -0
- package/public/locales/de/errors.json +1 -0
- package/public/locales/de/mail.json +13 -0
- package/public/locales/de/settings.json +1 -0
- package/public/locales/en/common.json +26 -0
- package/public/locales/en/errors.json +1 -0
- package/public/locales/en/mail.json +69 -0
- package/public/locales/en/settings.json +1 -0
- package/public/locales/fr/common.json +1 -0
- package/public/locales/fr/errors.json +1 -0
- package/public/locales/fr/mail.json +13 -0
- package/public/locales/fr/settings.json +1 -0
- package/public/locales/nl/common.json +1 -0
- package/public/locales/nl/errors.json +1 -0
- package/public/locales/nl/mail.json +13 -0
- package/public/locales/nl/settings.json +1 -0
- package/src/auth/AccountMenu.tsx +58 -0
- package/src/auth/AccountSession.tsx +19 -0
- package/src/auth/BetterAuthShell.tsx +198 -0
- package/src/auth/account-menu-mode.test.ts +64 -0
- package/src/auth/account-menu-mode.ts +28 -0
- package/src/auth/amplify-config.test.ts +158 -0
- package/src/auth/amplify-config.ts +60 -0
- package/src/auth/auth-error.test.ts +109 -0
- package/src/auth/auth-error.ts +107 -0
- package/src/auth/auth-interceptor.test.ts +86 -0
- package/src/auth/auth-interceptor.ts +23 -0
- package/src/auth/auth.css +334 -0
- package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
- package/src/auth/better-auth-config.ts +85 -0
- package/src/auth/better-auth-provider.tsx +20 -0
- package/src/auth/cognito/CognitoAccount.tsx +48 -0
- package/src/auth/cognito/CognitoShell.tsx +266 -0
- package/src/auth/cognito/cognito-token.test.ts +110 -0
- package/src/auth/cognito/cognito-token.ts +47 -0
- package/src/auth/cognito/cognito.css +21 -0
- package/src/auth/cognito-provider.tsx +22 -0
- package/src/auth/combined-provider.tsx +42 -0
- package/src/auth/composition.test.ts +136 -0
- package/src/auth/provider.tsx +63 -0
- package/src/auth/sign-out-visibility.test.ts +40 -0
- package/src/auth/sign-out-visibility.ts +10 -0
- package/src/auth/sign-up-disabled.test.ts +23 -0
- package/src/components/compose/AddressField.tsx +237 -0
- package/src/components/compose/ComposeBody.tsx +36 -0
- package/src/components/compose/ComposeForm.tsx +676 -0
- package/src/components/compose/ComposeProvider.tsx +133 -0
- package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
- package/src/components/compose/FromSelector.tsx +67 -0
- package/src/components/compose/FullCompose.tsx +82 -0
- package/src/components/compose/InlineCompose.tsx +29 -0
- package/src/components/compose/MobileComposeSheet.tsx +152 -0
- package/src/components/compose/PlateEditor.tsx +86 -0
- package/src/components/compose/PlateToolbar.tsx +110 -0
- package/src/components/compose/SubjectField.tsx +19 -0
- package/src/components/compose/draft-discard-banner.test.ts +76 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/sanitize-quote-html.ts +23 -0
- package/src/components/layout/AppShellSkeleton.tsx +53 -0
- package/src/components/layout/ComposeFab.tsx +86 -0
- package/src/components/layout/Drawer.tsx +105 -0
- package/src/components/mail/AutoMovedIndicator.tsx +48 -0
- package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
- package/src/components/mail/BlockedImagesNotice.tsx +41 -0
- package/src/components/mail/BriefPane.tsx +289 -0
- package/src/components/mail/ConversationView.tsx +462 -0
- package/src/components/mail/DailyBrief.tsx +451 -0
- package/src/components/mail/DraftsView.tsx +279 -0
- package/src/components/mail/FlaggedList.tsx +170 -0
- package/src/components/mail/FlaggedPane.tsx +217 -0
- package/src/components/mail/IntelligencePane.test.ts +123 -0
- package/src/components/mail/IntelligencePane.tsx +441 -0
- package/src/components/mail/MailListHeader.tsx +187 -0
- package/src/components/mail/MailNav.tsx +70 -0
- package/src/components/mail/MailSidebarAdapter.tsx +351 -0
- package/src/components/mail/MailViewChrome.tsx +95 -0
- package/src/components/mail/MailboxPane.tsx +1219 -0
- package/src/components/mail/MessageActionMenu.tsx +368 -0
- package/src/components/mail/MessageBody.tsx +207 -0
- package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
- package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
- package/src/components/mail/MessageCard.tsx +401 -0
- package/src/components/mail/MessageDetail.tsx +153 -0
- package/src/components/mail/MessageList.tsx +804 -0
- package/src/components/mail/MessageListItem.tsx +267 -0
- package/src/components/mail/MessageToolbar.render.test.ts +57 -0
- package/src/components/mail/MessageToolbar.tsx +166 -0
- package/src/components/mail/MobileMessageBar.tsx +142 -0
- package/src/components/mail/MoveToTrigger.tsx +250 -0
- package/src/components/mail/NavMenuButton.tsx +25 -0
- package/src/components/mail/OutboxPane.tsx +531 -0
- package/src/components/mail/PullToRefresh.tsx +24 -0
- package/src/components/mail/RawMessageView.tsx +84 -0
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
- package/src/components/mail/SelectionToolbar.tsx +139 -0
- package/src/components/mail/SpamRescue.render.test.ts +35 -0
- package/src/components/mail/SpamRescue.tsx +138 -0
- package/src/components/mail/SwipeableMessageRow.tsx +148 -0
- package/src/components/mail/message-body-error-banner-content.ts +87 -0
- package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
- package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
- package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
- package/src/components/mail/organize/OrganizePanel.tsx +344 -0
- package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
- package/src/components/settings/AccountFormPanel.test.ts +72 -0
- package/src/components/settings/AccountFormPanel.tsx +942 -0
- package/src/components/settings/DangerZone.tsx +30 -0
- package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
- package/src/components/settings/DeleteAccountDialog.tsx +224 -0
- package/src/components/settings/FiltersList.render.test.ts +71 -0
- package/src/components/settings/FiltersList.tsx +93 -0
- package/src/components/settings/account-form-helpers.test.ts +172 -0
- package/src/components/settings/account-form-helpers.ts +105 -0
- package/src/components/ui/AppVersion.tsx +50 -0
- package/src/components/ui/BugReportButton.tsx +45 -0
- package/src/components/ui/ConfirmDialog.tsx +125 -0
- package/src/components/ui/DropdownMenu.tsx +105 -0
- package/src/components/ui/EmptyState.tsx +11 -0
- package/src/components/ui/ErrorBanner.tsx +88 -0
- package/src/components/ui/ErrorBannerProvider.tsx +77 -0
- package/src/components/ui/ErrorBannerStack.tsx +34 -0
- package/src/components/ui/ErrorState.tsx +80 -0
- package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
- package/src/components/ui/FatalErrorOverlay.tsx +151 -0
- package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
- package/src/components/ui/SlidePanel.tsx +72 -0
- package/src/components/ui/error-banners.test.ts +207 -0
- package/src/components/ui/error-banners.ts +111 -0
- package/src/hooks/queries/keys.ts +38 -0
- package/src/hooks/useArchiveMailbox.ts +104 -0
- package/src/hooks/useAutoMovedBadge.ts +77 -0
- package/src/hooks/useCurrentMailboxName.ts +99 -0
- package/src/hooks/useDebouncedValue.ts +12 -0
- package/src/hooks/useDeleteMessages.test.ts +103 -0
- package/src/hooks/useDeleteMessages.ts +226 -0
- package/src/hooks/useFilters.ts +112 -0
- package/src/hooks/useIntelligenceData.test.ts +269 -0
- package/src/hooks/useIntelligenceData.ts +284 -0
- package/src/hooks/useIsDark.ts +27 -0
- package/src/hooks/useKeyboardNavigation.ts +192 -0
- package/src/hooks/useLayoutTier.test.ts +47 -0
- package/src/hooks/useLayoutTier.ts +44 -0
- package/src/hooks/useLongPress.test.ts +291 -0
- package/src/hooks/useLongPress.ts +87 -0
- package/src/hooks/useMailboxAccount.ts +62 -0
- package/src/hooks/useMailboxNameIndex.ts +32 -0
- package/src/hooks/useMarkAsRead.test.ts +165 -0
- package/src/hooks/useMarkAsRead.ts +319 -0
- package/src/hooks/useMediaQuery.ts +39 -0
- package/src/hooks/useMessageBodyContent.test.ts +370 -0
- package/src/hooks/useMessageBodyContent.ts +380 -0
- package/src/hooks/useMoveMessages.test.ts +63 -0
- package/src/hooks/useMoveMessages.ts +221 -0
- package/src/hooks/useOrganizeJob.ts +90 -0
- package/src/hooks/useOrganizePreview.ts +40 -0
- package/src/hooks/useRescueCandidates.test.ts +217 -0
- package/src/hooks/useRescueCandidates.ts +48 -0
- package/src/hooks/useSaveDraft.ts +118 -0
- package/src/hooks/useSelection.test.ts +75 -0
- package/src/hooks/useSelection.ts +211 -0
- package/src/hooks/useSemanticSearch.ts +94 -0
- package/src/hooks/useSignature.ts +69 -0
- package/src/hooks/useStaleAccountSync.test.ts +211 -0
- package/src/hooks/useStaleAccountSync.ts +246 -0
- package/src/hooks/useSwipeNavigation.test.ts +35 -0
- package/src/hooks/useSwipeNavigation.ts +99 -0
- package/src/hooks/useToggleStar.ts +187 -0
- package/src/hooks/useToggleTrusted.test.ts +102 -0
- package/src/hooks/useToggleTrusted.ts +161 -0
- package/src/hooks/useTriageKeyboard.ts +103 -0
- package/src/hooks/useTriggerSync.test.ts +24 -0
- package/src/hooks/useTriggerSync.ts +58 -0
- package/src/hooks/useUpdateAddressFlags.ts +125 -0
- package/src/hooks/useVisualViewport.test.ts +127 -0
- package/src/hooks/useVisualViewport.ts +51 -0
- package/src/index.css +19 -0
- package/src/lib/account-order.test.ts +84 -0
- package/src/lib/account-order.ts +13 -0
- package/src/lib/adjacent-message.test.ts +43 -0
- package/src/lib/adjacent-message.ts +20 -0
- package/src/lib/api.ts +82 -0
- package/src/lib/app-info.ts +15 -0
- package/src/lib/auto-moved.test.ts +140 -0
- package/src/lib/auto-moved.ts +68 -0
- package/src/lib/autodiscovery.test.ts +94 -0
- package/src/lib/autodiscovery.ts +354 -0
- package/src/lib/brief.test.ts +366 -0
- package/src/lib/brief.ts +180 -0
- package/src/lib/bug-report.test.ts +213 -0
- package/src/lib/bug-report.ts +176 -0
- package/src/lib/client.ts +36 -0
- package/src/lib/console-errors.test.ts +64 -0
- package/src/lib/console-errors.ts +92 -0
- package/src/lib/conversation-target.test.ts +68 -0
- package/src/lib/conversation-target.ts +59 -0
- package/src/lib/display-category.ts +20 -0
- package/src/lib/drafts.test.ts +241 -0
- package/src/lib/drafts.ts +121 -0
- package/src/lib/error-classifier.test.ts +133 -0
- package/src/lib/error-classifier.ts +96 -0
- package/src/lib/fatal-error.test.ts +97 -0
- package/src/lib/fatal-error.ts +165 -0
- package/src/lib/folder-roles.test.ts +113 -0
- package/src/lib/folder-roles.ts +112 -0
- package/src/lib/format.test.ts +115 -0
- package/src/lib/format.ts +198 -0
- package/src/lib/i18n.ts +36 -0
- package/src/lib/intelligence-pref.test.ts +57 -0
- package/src/lib/intelligence-pref.ts +25 -0
- package/src/lib/keymap-dispatch.test.ts +178 -0
- package/src/lib/keymap-dispatch.ts +187 -0
- package/src/lib/keymap.test.ts +49 -0
- package/src/lib/keymap.ts +178 -0
- package/src/lib/mail-context.ts +64 -0
- package/src/lib/mail-route.test.ts +80 -0
- package/src/lib/mail-route.ts +44 -0
- package/src/lib/message-body-source.test.ts +98 -0
- package/src/lib/message-body-source.ts +48 -0
- package/src/lib/move-targets.test.ts +195 -0
- package/src/lib/move-targets.ts +74 -0
- package/src/lib/onboarding-completion.test.ts +64 -0
- package/src/lib/onboarding-completion.ts +23 -0
- package/src/lib/organize/filter-status.test.ts +99 -0
- package/src/lib/organize/filter-status.ts +86 -0
- package/src/lib/organize/organize-copy.test.ts +93 -0
- package/src/lib/organize/organize-copy.ts +61 -0
- package/src/lib/organize/organize-model.test.ts +104 -0
- package/src/lib/organize/organize-model.ts +105 -0
- package/src/lib/organize/organize-poll.test.ts +37 -0
- package/src/lib/organize/organize-poll.ts +28 -0
- package/src/lib/outbox-status.test.ts +64 -0
- package/src/lib/outbox-status.ts +75 -0
- package/src/lib/plate-serializer.ts +164 -0
- package/src/lib/provider-presets.test.ts +96 -0
- package/src/lib/provider-presets.ts +81 -0
- package/src/lib/query-error-handler.test.ts +144 -0
- package/src/lib/query-error-handler.ts +34 -0
- package/src/lib/query-escalation.integration.test.ts +138 -0
- package/src/lib/recent-searches.test.ts +53 -0
- package/src/lib/recent-searches.ts +34 -0
- package/src/lib/rescue-candidates.test.ts +94 -0
- package/src/lib/rescue-candidates.ts +35 -0
- package/src/lib/rescue-telemetry.test.ts +75 -0
- package/src/lib/rescue-telemetry.ts +55 -0
- package/src/lib/rum-adapter.test.ts +299 -0
- package/src/lib/rum-adapter.ts +107 -0
- package/src/lib/saved-searches.test.ts +80 -0
- package/src/lib/saved-searches.ts +52 -0
- package/src/lib/search-pending.test.ts +61 -0
- package/src/lib/search-pending.ts +24 -0
- package/src/lib/search-query.test.ts +35 -0
- package/src/lib/search-query.ts +11 -0
- package/src/lib/search-result.test.ts +160 -0
- package/src/lib/search-result.ts +102 -0
- package/src/lib/search-schemas.ts +34 -0
- package/src/lib/search-token-index.test.ts +112 -0
- package/src/lib/search-token-index.ts +56 -0
- package/src/lib/search-tokens.test.ts +218 -0
- package/src/lib/search-tokens.ts +133 -0
- package/src/lib/telemetry-context.tsx +9 -0
- package/src/lib/telemetry.ts +17 -0
- package/src/lib/theme-preference.ts +46 -0
- package/src/lib/theme.ts +33 -0
- package/src/lib/utils.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/routeTree.gen.ts +392 -0
- package/src/router.test.ts +81 -0
- package/src/router.tsx +31 -0
- package/src/routes/__root.tsx +69 -0
- package/src/routes/index.tsx +7 -0
- package/src/routes/mail/$mailboxId.tsx +27 -0
- package/src/routes/mail/flagged.tsx +37 -0
- package/src/routes/mail/index.tsx +41 -0
- package/src/routes/mail/outbox.tsx +13 -0
- package/src/routes/mail.tsx +413 -0
- package/src/routes/onboarding.tsx +35 -0
- package/src/routes/settings/-accounts.test.ts +65 -0
- package/src/routes/settings/accounts.tsx +542 -0
- package/src/routes/settings/advanced.tsx +55 -0
- package/src/routes/settings/appearance.tsx +140 -0
- package/src/routes/settings/filters.tsx +141 -0
- package/src/routes/settings/folders.tsx +216 -0
- package/src/routes/settings/index.tsx +5 -0
- package/src/routes/settings/senders.tsx +466 -0
- package/src/routes/settings/suggested-vips.tsx +10 -0
- package/src/routes/settings.tsx +66 -0
- package/src/runtime-config.ts +75 -0
- package/src/shell/index.tsx +98 -0
- package/src/types/index.ts +73 -0
- package/src/vite-env.d.ts +4 -0
- package/vite.base.ts +43 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accountDetailOperationsDeleteAccountMutation,
|
|
3
|
+
configOperationsGetConfigOptions,
|
|
4
|
+
configOperationsGetConfigQueryKey,
|
|
5
|
+
microsoftOAuthOperationsMicrosoftOAuthStartMutation,
|
|
6
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
7
|
+
import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
|
|
8
|
+
import {
|
|
9
|
+
AccountHealthCard,
|
|
10
|
+
Badge,
|
|
11
|
+
Banner,
|
|
12
|
+
Button,
|
|
13
|
+
Dialog,
|
|
14
|
+
type RowAction,
|
|
15
|
+
RowActions,
|
|
16
|
+
SettingsShell,
|
|
17
|
+
} from "@remit/ui";
|
|
18
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
19
|
+
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
20
|
+
import { AlertTriangle, Loader2, Plus, Trash2, X } from "lucide-react";
|
|
21
|
+
import { useEffect, useState } from "react";
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
import { OnboardingWizard } from "@/components/onboarding/OnboardingWizard";
|
|
24
|
+
import { AccountFormPanel } from "@/components/settings/AccountFormPanel";
|
|
25
|
+
import { DangerZone } from "@/components/settings/DangerZone";
|
|
26
|
+
import { ErrorState } from "@/components/ui/ErrorState";
|
|
27
|
+
import { formatRelativeTime } from "@/lib/format";
|
|
28
|
+
import { SETTINGS_ID_TO_PATH, SETTINGS_NAV_ITEMS } from "@/routes/settings";
|
|
29
|
+
|
|
30
|
+
const accountsSearchSchema = z.object({
|
|
31
|
+
editAccountId: z.string().optional(),
|
|
32
|
+
focusSmtp: z.boolean().optional(),
|
|
33
|
+
/** Set by the backend redirect after a successful OAuth callback */
|
|
34
|
+
connected: z.string().optional(),
|
|
35
|
+
/** Set by the backend redirect when the OAuth flow fails */
|
|
36
|
+
oauthError: z.string().optional(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const Route = createFileRoute("/settings/accounts")({
|
|
40
|
+
component: AccountsSettings,
|
|
41
|
+
validateSearch: accountsSearchSchema,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/* ------------------------------------------------------------------ */
|
|
45
|
+
/* Helpers */
|
|
46
|
+
/* ------------------------------------------------------------------ */
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Map an OAuth error code from the `?oauthError` query param to a
|
|
50
|
+
* human-readable message shown to the user.
|
|
51
|
+
*
|
|
52
|
+
* Exported for unit tests.
|
|
53
|
+
*/
|
|
54
|
+
export function mapOauthError(code: string): string {
|
|
55
|
+
const lower = code.toLowerCase();
|
|
56
|
+
|
|
57
|
+
if (lower === "access_denied") {
|
|
58
|
+
return "You cancelled the sign-in.";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
lower === "consent_required" ||
|
|
63
|
+
lower.includes("admin_consent") ||
|
|
64
|
+
lower.includes("interaction_required")
|
|
65
|
+
) {
|
|
66
|
+
return "Your organisation's admin needs to approve Remit. Ask your IT admin to grant the required permissions.";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// IMAP disabled hint — the backend surfaces this when XOAUTH2 login
|
|
70
|
+
// fails because IMAP is not enabled in the Microsoft 365 tenant settings.
|
|
71
|
+
if (lower.includes("imap_disabled") || lower.includes("imap disabled")) {
|
|
72
|
+
return "IMAP is disabled for this account. Ask your admin to enable IMAP access in the Microsoft 365 admin centre.";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `Sign-in failed: ${code}. Please try again.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Derive a friendly primary label from the email's local part so
|
|
80
|
+
* AccountHealthCard doesn't print the same address twice (label primary, email
|
|
81
|
+
* secondary). Falls back to the full email if the local part is empty.
|
|
82
|
+
*/
|
|
83
|
+
function deriveLabel(email: string): string {
|
|
84
|
+
const local = email.split("@")[0] ?? "";
|
|
85
|
+
if (!local) return email;
|
|
86
|
+
return local
|
|
87
|
+
.split(/[._-]+/)
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
90
|
+
.join(" ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The user-chosen display name when set, otherwise a label derived from the
|
|
95
|
+
* email address.
|
|
96
|
+
*/
|
|
97
|
+
function accountLabel(account: RemitImapAccountResponse): string {
|
|
98
|
+
const name = account.displayName?.trim();
|
|
99
|
+
if (name) return name;
|
|
100
|
+
return deriveLabel(account.email);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function deriveSyncLabel(account: RemitImapAccountResponse): string {
|
|
104
|
+
if (!account.lastSyncAt) return "never synced";
|
|
105
|
+
const relative = formatRelativeTime(account.lastSyncAt);
|
|
106
|
+
return relative ? `synced ${relative}` : "never synced";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function deriveState(
|
|
110
|
+
account: RemitImapAccountResponse,
|
|
111
|
+
): "healthy" | "error" | "muted" {
|
|
112
|
+
if (account.muted?.value) return "muted";
|
|
113
|
+
if (account.lastError) return "error";
|
|
114
|
+
if (account.connectionState === "authenticated") return "healthy";
|
|
115
|
+
return "error";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** True when the account needs re-authentication via the OAuth flow. */
|
|
119
|
+
function needsReauth(account: RemitImapAccountResponse): boolean {
|
|
120
|
+
return account.connectionState === "reauth_required";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ------------------------------------------------------------------ */
|
|
124
|
+
/* Help rail copy — matches Storybook accountsHelp exactly */
|
|
125
|
+
/* ------------------------------------------------------------------ */
|
|
126
|
+
|
|
127
|
+
const accountsHelp = (
|
|
128
|
+
<div className="space-y-3">
|
|
129
|
+
<p>
|
|
130
|
+
<strong className="text-fg">Sync health</strong> shows the IMAP connection
|
|
131
|
+
state and the last successful sync per account.
|
|
132
|
+
</p>
|
|
133
|
+
<p>
|
|
134
|
+
<strong className="text-fg">Error</strong> means the last connection
|
|
135
|
+
attempt failed — the raw server response is shown on the card. Reconnect
|
|
136
|
+
re-runs the connection test from the add-account wizard.
|
|
137
|
+
</p>
|
|
138
|
+
<p>
|
|
139
|
+
<strong className="text-fg">Muted</strong> accounts keep syncing but stay
|
|
140
|
+
out of the daily brief and unified counts.
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/* ------------------------------------------------------------------ */
|
|
146
|
+
/* Loading skeleton */
|
|
147
|
+
/* ------------------------------------------------------------------ */
|
|
148
|
+
|
|
149
|
+
const LoadingSkeleton = () => (
|
|
150
|
+
// biome-ignore lint/a11y/useSemanticElements: <div> with role="status" preserves block layout; <output> is inline
|
|
151
|
+
<div
|
|
152
|
+
className="space-y-3"
|
|
153
|
+
role="status"
|
|
154
|
+
aria-busy="true"
|
|
155
|
+
aria-label="Loading accounts"
|
|
156
|
+
>
|
|
157
|
+
{(["skeleton-0", "skeleton-1"] as const).map((key) => (
|
|
158
|
+
<div
|
|
159
|
+
key={key}
|
|
160
|
+
className="rounded-sm border border-line bg-surface animate-pulse p-4"
|
|
161
|
+
>
|
|
162
|
+
<div className="flex items-center gap-3">
|
|
163
|
+
<div className="size-10 rounded-full bg-surface-sunken" />
|
|
164
|
+
<div className="flex-1 space-y-2">
|
|
165
|
+
<div className="h-4 bg-surface-sunken rounded w-1/3" />
|
|
166
|
+
<div className="h-3 bg-surface-sunken rounded w-2/3" />
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
/* ------------------------------------------------------------------ */
|
|
175
|
+
/* Page component */
|
|
176
|
+
/* ------------------------------------------------------------------ */
|
|
177
|
+
|
|
178
|
+
function AccountsSettings() {
|
|
179
|
+
const search = Route.useSearch();
|
|
180
|
+
const navigate = useNavigate({ from: Route.fullPath });
|
|
181
|
+
const [helpOpen, setHelpOpen] = useState(true);
|
|
182
|
+
|
|
183
|
+
const [showForm, setShowForm] = useState(false);
|
|
184
|
+
const [showAddWizard, setShowAddWizard] = useState(false);
|
|
185
|
+
const [editingAccountId, setEditingAccountId] = useState<string | null>(null);
|
|
186
|
+
const [focusSmtp, setFocusSmtp] = useState(false);
|
|
187
|
+
const [deletingAccountId, setDeletingAccountId] = useState<string | null>(
|
|
188
|
+
null,
|
|
189
|
+
);
|
|
190
|
+
/** Success message shown after a successful OAuth callback */
|
|
191
|
+
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
192
|
+
/** Error message shown when the OAuth flow returned an error */
|
|
193
|
+
const [oauthErrorMessage, setOauthErrorMessage] = useState<string | null>(
|
|
194
|
+
null,
|
|
195
|
+
);
|
|
196
|
+
/** Which account is currently being reconnected via OAuth */
|
|
197
|
+
const [reconnectingAccountId, setReconnectingAccountId] = useState<
|
|
198
|
+
string | null
|
|
199
|
+
>(null);
|
|
200
|
+
|
|
201
|
+
const queryClient = useQueryClient();
|
|
202
|
+
|
|
203
|
+
const {
|
|
204
|
+
data: config,
|
|
205
|
+
isPending,
|
|
206
|
+
isError,
|
|
207
|
+
error,
|
|
208
|
+
refetch,
|
|
209
|
+
} = useQuery(configOperationsGetConfigOptions());
|
|
210
|
+
|
|
211
|
+
// Handle ?editAccountId deep-link
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (!search.editAccountId) return;
|
|
214
|
+
setEditingAccountId(search.editAccountId);
|
|
215
|
+
setFocusSmtp(!!search.focusSmtp);
|
|
216
|
+
navigate({
|
|
217
|
+
search: {
|
|
218
|
+
editAccountId: undefined,
|
|
219
|
+
focusSmtp: undefined,
|
|
220
|
+
connected: search.connected,
|
|
221
|
+
oauthError: search.oauthError,
|
|
222
|
+
},
|
|
223
|
+
replace: true,
|
|
224
|
+
});
|
|
225
|
+
}, [
|
|
226
|
+
search.editAccountId,
|
|
227
|
+
search.focusSmtp,
|
|
228
|
+
navigate,
|
|
229
|
+
search.connected,
|
|
230
|
+
search.oauthError,
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
// Handle ?connected — show success, select the account, clear param
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!search.connected) return;
|
|
236
|
+
setSuccessMessage("Account connected successfully.");
|
|
237
|
+
setEditingAccountId(search.connected);
|
|
238
|
+
queryClient.invalidateQueries({
|
|
239
|
+
queryKey: configOperationsGetConfigQueryKey(),
|
|
240
|
+
});
|
|
241
|
+
navigate({
|
|
242
|
+
search: {
|
|
243
|
+
connected: undefined,
|
|
244
|
+
oauthError: undefined,
|
|
245
|
+
editAccountId: undefined,
|
|
246
|
+
focusSmtp: undefined,
|
|
247
|
+
},
|
|
248
|
+
replace: true,
|
|
249
|
+
});
|
|
250
|
+
}, [search.connected, navigate, queryClient]);
|
|
251
|
+
|
|
252
|
+
// Handle ?oauthError — show human-readable error, clear param
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (!search.oauthError) return;
|
|
255
|
+
setOauthErrorMessage(mapOauthError(search.oauthError));
|
|
256
|
+
navigate({
|
|
257
|
+
search: {
|
|
258
|
+
oauthError: undefined,
|
|
259
|
+
connected: undefined,
|
|
260
|
+
editAccountId: undefined,
|
|
261
|
+
focusSmtp: undefined,
|
|
262
|
+
},
|
|
263
|
+
replace: true,
|
|
264
|
+
});
|
|
265
|
+
}, [search.oauthError, navigate]);
|
|
266
|
+
|
|
267
|
+
const reconnectMutation = useMutation({
|
|
268
|
+
...microsoftOAuthOperationsMicrosoftOAuthStartMutation(),
|
|
269
|
+
onSuccess: (data) => {
|
|
270
|
+
window.location.assign(data.authorizationUrl);
|
|
271
|
+
},
|
|
272
|
+
onError: (err) => {
|
|
273
|
+
setOauthErrorMessage(
|
|
274
|
+
err instanceof Error ? err.message : "Failed to start reconnect",
|
|
275
|
+
);
|
|
276
|
+
setReconnectingAccountId(null);
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const deleteMutation = useMutation({
|
|
281
|
+
...accountDetailOperationsDeleteAccountMutation(),
|
|
282
|
+
onSuccess: () => {
|
|
283
|
+
queryClient.invalidateQueries({
|
|
284
|
+
queryKey: configOperationsGetConfigQueryKey(),
|
|
285
|
+
});
|
|
286
|
+
setDeletingAccountId(null);
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const accountToDelete = config?.accounts.find(
|
|
291
|
+
(a) => a.accountId === deletingAccountId,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const accountToEdit = config?.accounts.find(
|
|
295
|
+
(a) => a.accountId === editingAccountId,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const handleClosePanel = () => {
|
|
299
|
+
setShowForm(false);
|
|
300
|
+
setShowAddWizard(false);
|
|
301
|
+
setEditingAccountId(null);
|
|
302
|
+
setFocusSmtp(false);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const handleSelectNav = (id: string) => {
|
|
306
|
+
const path = SETTINGS_ID_TO_PATH[id];
|
|
307
|
+
if (path) void navigate({ to: path });
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<SettingsShell
|
|
312
|
+
items={SETTINGS_NAV_ITEMS}
|
|
313
|
+
activeId="accounts"
|
|
314
|
+
title="Accounts"
|
|
315
|
+
description="Every account keeps syncing — muted ones just stay out of unified views."
|
|
316
|
+
help={accountsHelp}
|
|
317
|
+
helpOpen={helpOpen}
|
|
318
|
+
onToggleHelp={() => setHelpOpen((v) => !v)}
|
|
319
|
+
onSelect={handleSelectNav}
|
|
320
|
+
onBackToMail={() => void navigate({ to: "/mail" })}
|
|
321
|
+
>
|
|
322
|
+
{successMessage && (
|
|
323
|
+
<Banner
|
|
324
|
+
tone="success"
|
|
325
|
+
variant="soft"
|
|
326
|
+
data-testid="oauth-success-banner"
|
|
327
|
+
onDismiss={() => setSuccessMessage(null)}
|
|
328
|
+
>
|
|
329
|
+
{successMessage}
|
|
330
|
+
</Banner>
|
|
331
|
+
)}
|
|
332
|
+
|
|
333
|
+
{oauthErrorMessage && (
|
|
334
|
+
<Banner
|
|
335
|
+
tone="danger"
|
|
336
|
+
variant="soft"
|
|
337
|
+
data-testid="oauth-error-banner"
|
|
338
|
+
onDismiss={() => setOauthErrorMessage(null)}
|
|
339
|
+
>
|
|
340
|
+
{oauthErrorMessage}
|
|
341
|
+
</Banner>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
<div className="flex items-center justify-between">
|
|
345
|
+
<Badge tone="neutral">
|
|
346
|
+
{isPending || isError
|
|
347
|
+
? "accounts"
|
|
348
|
+
: `${config.accounts.length} ${config.accounts.length === 1 ? "account" : "accounts"}`}
|
|
349
|
+
</Badge>
|
|
350
|
+
<Button
|
|
351
|
+
variant="primary"
|
|
352
|
+
size="sm"
|
|
353
|
+
icon={<Plus className="size-3.5" />}
|
|
354
|
+
onClick={() => setShowAddWizard(true)}
|
|
355
|
+
>
|
|
356
|
+
Add account
|
|
357
|
+
</Button>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
{isPending ? (
|
|
361
|
+
<LoadingSkeleton />
|
|
362
|
+
) : isError ? (
|
|
363
|
+
<ErrorState
|
|
364
|
+
title="Couldn't load accounts"
|
|
365
|
+
error={error}
|
|
366
|
+
onRetry={() => {
|
|
367
|
+
refetch();
|
|
368
|
+
}}
|
|
369
|
+
/>
|
|
370
|
+
) : config.accounts.length === 0 ? (
|
|
371
|
+
<div
|
|
372
|
+
className="py-12 text-sm text-fg-muted"
|
|
373
|
+
data-testid="accounts-empty"
|
|
374
|
+
>
|
|
375
|
+
<p className="mb-3">No accounts configured.</p>
|
|
376
|
+
<Button
|
|
377
|
+
variant="primary"
|
|
378
|
+
size="sm"
|
|
379
|
+
icon={<Plus className="size-3.5" />}
|
|
380
|
+
onClick={() => setShowAddWizard(true)}
|
|
381
|
+
>
|
|
382
|
+
Add your first account
|
|
383
|
+
</Button>
|
|
384
|
+
</div>
|
|
385
|
+
) : (
|
|
386
|
+
<div className="space-y-3">
|
|
387
|
+
{config.accounts.map((account) => {
|
|
388
|
+
const isReauth = needsReauth(account);
|
|
389
|
+
const isOAuthAccount = account.authType === "oauthMicrosoft";
|
|
390
|
+
const isReconnecting =
|
|
391
|
+
reconnectingAccountId === account.accountId &&
|
|
392
|
+
reconnectMutation.isPending;
|
|
393
|
+
|
|
394
|
+
const primaryAction: RowAction =
|
|
395
|
+
isReauth && isOAuthAccount
|
|
396
|
+
? {
|
|
397
|
+
label: "Reconnect",
|
|
398
|
+
variant: "secondary",
|
|
399
|
+
busy: isReconnecting,
|
|
400
|
+
busyLabel: "Redirecting…",
|
|
401
|
+
onClick: () => {
|
|
402
|
+
setReconnectingAccountId(account.accountId);
|
|
403
|
+
reconnectMutation.mutate({
|
|
404
|
+
body: { email: account.email },
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
}
|
|
408
|
+
: deriveState(account) === "error"
|
|
409
|
+
? {
|
|
410
|
+
label: "Reconnect",
|
|
411
|
+
variant: "secondary",
|
|
412
|
+
onClick: () => setEditingAccountId(account.accountId),
|
|
413
|
+
}
|
|
414
|
+
: {
|
|
415
|
+
label: "Manage",
|
|
416
|
+
variant: "ghost",
|
|
417
|
+
onClick: () => setEditingAccountId(account.accountId),
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const trailingButton = (
|
|
421
|
+
<RowActions
|
|
422
|
+
actions={[primaryAction]}
|
|
423
|
+
destructive={{
|
|
424
|
+
label: "Delete account",
|
|
425
|
+
iconOnly: true,
|
|
426
|
+
icon: <Trash2 className="size-3.5" />,
|
|
427
|
+
onClick: () => setDeletingAccountId(account.accountId),
|
|
428
|
+
}}
|
|
429
|
+
/>
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<AccountHealthCard
|
|
434
|
+
key={account.accountId}
|
|
435
|
+
label={accountLabel(account)}
|
|
436
|
+
email={account.email}
|
|
437
|
+
connector={isOAuthAccount ? "Microsoft 365" : "IMAP"}
|
|
438
|
+
syncLabel={deriveSyncLabel(account)}
|
|
439
|
+
state={deriveState(account)}
|
|
440
|
+
errorDetail={
|
|
441
|
+
isReauth ? "Re-authentication required" : account.lastError
|
|
442
|
+
}
|
|
443
|
+
trailing={trailingButton}
|
|
444
|
+
/>
|
|
445
|
+
);
|
|
446
|
+
})}
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
{/* Add account wizard — steps 2–7 in a full-screen overlay */}
|
|
451
|
+
{showAddWizard && (
|
|
452
|
+
<div className="fixed inset-0 z-40 overflow-auto bg-canvas">
|
|
453
|
+
<OnboardingWizard
|
|
454
|
+
skipWelcome
|
|
455
|
+
onComplete={() => {
|
|
456
|
+
setShowAddWizard(false);
|
|
457
|
+
queryClient.invalidateQueries({
|
|
458
|
+
queryKey: configOperationsGetConfigQueryKey(),
|
|
459
|
+
});
|
|
460
|
+
}}
|
|
461
|
+
onCancel={() => setShowAddWizard(false)}
|
|
462
|
+
/>
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
|
|
466
|
+
{/* Add/Edit Form Panel */}
|
|
467
|
+
<AccountFormPanel
|
|
468
|
+
isOpen={showForm || !!editingAccountId}
|
|
469
|
+
account={accountToEdit}
|
|
470
|
+
focusSmtp={focusSmtp}
|
|
471
|
+
onClose={handleClosePanel}
|
|
472
|
+
/>
|
|
473
|
+
|
|
474
|
+
{/* Delete Confirmation Dialog */}
|
|
475
|
+
<Dialog
|
|
476
|
+
open={!!deletingAccountId}
|
|
477
|
+
onClose={() => setDeletingAccountId(null)}
|
|
478
|
+
title="Delete account"
|
|
479
|
+
>
|
|
480
|
+
<header className="flex items-center gap-2 border-b border-line px-5 py-3">
|
|
481
|
+
<AlertTriangle className="size-4 shrink-0 text-danger" />
|
|
482
|
+
<span className="flex-1 text-sm font-semibold text-fg">
|
|
483
|
+
Delete account
|
|
484
|
+
</span>
|
|
485
|
+
<Button
|
|
486
|
+
variant="ghost"
|
|
487
|
+
size="sm"
|
|
488
|
+
icon={<X className="size-3.5" />}
|
|
489
|
+
onClick={() => setDeletingAccountId(null)}
|
|
490
|
+
aria-label="Cancel"
|
|
491
|
+
/>
|
|
492
|
+
</header>
|
|
493
|
+
|
|
494
|
+
<div className="space-y-3 px-5 py-4 text-sm text-fg-muted">
|
|
495
|
+
<p className="text-center text-lg font-medium text-fg">
|
|
496
|
+
Are you sure?
|
|
497
|
+
</p>
|
|
498
|
+
{accountToDelete && (
|
|
499
|
+
<p className="text-center">{accountToDelete.email}</p>
|
|
500
|
+
)}
|
|
501
|
+
<ul className="list-inside list-disc space-y-1">
|
|
502
|
+
<li>Remove the account from your settings</li>
|
|
503
|
+
<li>Stop syncing mail for this account</li>
|
|
504
|
+
<li>Delete all associated data (within 24 hours)</li>
|
|
505
|
+
</ul>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<footer className="flex items-center justify-end gap-2 border-t border-line px-5 py-3">
|
|
509
|
+
<Button
|
|
510
|
+
variant="secondary"
|
|
511
|
+
size="sm"
|
|
512
|
+
onClick={() => setDeletingAccountId(null)}
|
|
513
|
+
>
|
|
514
|
+
Cancel
|
|
515
|
+
</Button>
|
|
516
|
+
<Button
|
|
517
|
+
variant="danger"
|
|
518
|
+
size="sm"
|
|
519
|
+
aria-busy={deleteMutation.isPending}
|
|
520
|
+
icon={
|
|
521
|
+
deleteMutation.isPending ? (
|
|
522
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
523
|
+
) : undefined
|
|
524
|
+
}
|
|
525
|
+
onClick={() => {
|
|
526
|
+
if (deleteMutation.isPending) return;
|
|
527
|
+
if (deletingAccountId) {
|
|
528
|
+
deleteMutation.mutate({
|
|
529
|
+
path: { accountId: deletingAccountId },
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}}
|
|
533
|
+
>
|
|
534
|
+
{deleteMutation.isPending ? "Deleting…" : "Delete account"}
|
|
535
|
+
</Button>
|
|
536
|
+
</footer>
|
|
537
|
+
</Dialog>
|
|
538
|
+
|
|
539
|
+
<DangerZone />
|
|
540
|
+
</SettingsShell>
|
|
541
|
+
);
|
|
542
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced settings — stub page. Notification rules, export, and
|
|
3
|
+
* diagnostics are future scope; this keeps the nav item from 404-ing.
|
|
4
|
+
*/
|
|
5
|
+
import { SettingsShell } from "@remit/ui";
|
|
6
|
+
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { AppVersion } from "@/components/ui/AppVersion";
|
|
9
|
+
import { SETTINGS_ID_TO_PATH, SETTINGS_NAV_ITEMS } from "@/routes/settings";
|
|
10
|
+
|
|
11
|
+
export const Route = createFileRoute("/settings/advanced")({
|
|
12
|
+
component: AdvancedSettings,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const advancedHelp = (
|
|
16
|
+
<div className="space-y-3">
|
|
17
|
+
<p>
|
|
18
|
+
<strong className="text-fg">Notification rules</strong>, data export, and
|
|
19
|
+
per-account diagnostics are coming in a future release.
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
function AdvancedSettings() {
|
|
25
|
+
const navigate = useNavigate();
|
|
26
|
+
const [helpOpen, setHelpOpen] = useState(true);
|
|
27
|
+
|
|
28
|
+
const handleSelectNav = (id: string) => {
|
|
29
|
+
const path = SETTINGS_ID_TO_PATH[id];
|
|
30
|
+
if (path) void navigate({ to: path });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<SettingsShell
|
|
35
|
+
items={SETTINGS_NAV_ITEMS}
|
|
36
|
+
activeId="advanced"
|
|
37
|
+
title="Advanced"
|
|
38
|
+
description="Notification rules, export, and diagnostics."
|
|
39
|
+
help={advancedHelp}
|
|
40
|
+
helpOpen={helpOpen}
|
|
41
|
+
onToggleHelp={() => setHelpOpen((v) => !v)}
|
|
42
|
+
onSelect={handleSelectNav}
|
|
43
|
+
onBackToMail={() => void navigate({ to: "/mail" })}
|
|
44
|
+
>
|
|
45
|
+
<p className="text-sm text-fg-muted">
|
|
46
|
+
Advanced options — notification rules, data export, and raw sync
|
|
47
|
+
diagnostics — are coming in a future release.
|
|
48
|
+
</p>
|
|
49
|
+
<div className="border-t border-line pt-4 mt-4">
|
|
50
|
+
<p className="text-sm font-medium text-fg mb-1">About</p>
|
|
51
|
+
<AppVersion />
|
|
52
|
+
</div>
|
|
53
|
+
</SettingsShell>
|
|
54
|
+
);
|
|
55
|
+
}
|