@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,30 @@
|
|
|
1
|
+
import { Button, DangerZoneSection } from "@remit/ui";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { DeleteAccountDialog } from "./DeleteAccountDialog";
|
|
4
|
+
|
|
5
|
+
export function DangerZone() {
|
|
6
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<section className="mt-8 border-t border-line pt-6">
|
|
10
|
+
<DangerZoneSection
|
|
11
|
+
title="Delete your Remit account"
|
|
12
|
+
description="Disconnects every account and erases Remit's copy of your mail, insights and preferences. Your mail at the providers is untouched."
|
|
13
|
+
action={
|
|
14
|
+
<Button
|
|
15
|
+
variant="danger"
|
|
16
|
+
size="sm"
|
|
17
|
+
onClick={() => setDialogOpen(true)}
|
|
18
|
+
>
|
|
19
|
+
Delete your Remit account
|
|
20
|
+
</Button>
|
|
21
|
+
}
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<DeleteAccountDialog
|
|
25
|
+
open={dialogOpen}
|
|
26
|
+
onClose={() => setDialogOpen(false)}
|
|
27
|
+
/>
|
|
28
|
+
</section>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React-render smoke test for `DeleteAccountDialog`.
|
|
3
|
+
*
|
|
4
|
+
* `DangerZone` mounts this dialog unconditionally on the settings/accounts
|
|
5
|
+
* route. The previous version called `useAuthenticator` at the top of render,
|
|
6
|
+
* which throws `USE_AUTHENTICATOR_ERROR` when no `Authenticator.Provider` is
|
|
7
|
+
* in scope. In local-dev / e2e / the visual harness (`isCognitoConfigured()`
|
|
8
|
+
* === false) `AuthShell` mounts NO provider, so the whole settings/accounts
|
|
9
|
+
* route crashed into the fatal-error escalate page (#745 surfaced this, #741).
|
|
10
|
+
*
|
|
11
|
+
* The fix gates the hook-using inner component behind `isCognitoConfigured()`.
|
|
12
|
+
* This test pins that: rendering the dialog without a provider must not throw.
|
|
13
|
+
*
|
|
14
|
+
* Uses `react-dom/server`'s `renderToString` so the assertion runs without
|
|
15
|
+
* pulling in jsdom/happy-dom (no new deps).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import { describe, it } from "node:test";
|
|
20
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
21
|
+
import { createElement } from "react";
|
|
22
|
+
import { renderToString } from "react-dom/server";
|
|
23
|
+
import { DeleteAccountDialog } from "./DeleteAccountDialog";
|
|
24
|
+
|
|
25
|
+
const renderDialog = (open: boolean): string =>
|
|
26
|
+
renderToString(
|
|
27
|
+
createElement(
|
|
28
|
+
QueryClientProvider,
|
|
29
|
+
{ client: new QueryClient() },
|
|
30
|
+
createElement(DeleteAccountDialog, {
|
|
31
|
+
open,
|
|
32
|
+
onClose: () => undefined,
|
|
33
|
+
}),
|
|
34
|
+
) as never,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
describe("DeleteAccountDialog renders without an Authenticator.Provider in scope (#741)", () => {
|
|
38
|
+
it("mounts WITHOUT calling `useAuthenticator` outside `Authenticator.Provider` — the hook would throw and crash the settings/accounts route into the fatal-error escalate page", () => {
|
|
39
|
+
// Default test env: `isCognitoConfigured()` returns false — same shape as
|
|
40
|
+
// `AuthShell` running without the provider. The dialog's hooks
|
|
41
|
+
// (`useAuthenticator`, `useState`, `useMutation`) all run on mount,
|
|
42
|
+
// before the `open` gate, so rendering it closed still exercises the
|
|
43
|
+
// gated path. Pre-fix, `useAuthenticator` ran unconditionally here and
|
|
44
|
+
// `renderToString` threw `USE_AUTHENTICATOR_ERROR`.
|
|
45
|
+
assert.doesNotThrow(() => renderDialog(false));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
meOperationsCreateExportMutation,
|
|
3
|
+
meOperationsDeleteMeMutation,
|
|
4
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
5
|
+
import { Button, Dialog, Input } from "@remit/ui";
|
|
6
|
+
import { useMutation } from "@tanstack/react-query";
|
|
7
|
+
import { AlertTriangle, Download, X } from "lucide-react";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { useAuthProvider } from "@/auth/provider";
|
|
10
|
+
|
|
11
|
+
interface DeleteAccountDialogProps {
|
|
12
|
+
open: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DeleteAccountDialogViewProps extends DeleteAccountDialogProps {
|
|
17
|
+
accountEmail: string;
|
|
18
|
+
signOut: () => void | Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function DeleteAccountDialogView({
|
|
22
|
+
open,
|
|
23
|
+
onClose,
|
|
24
|
+
accountEmail,
|
|
25
|
+
signOut,
|
|
26
|
+
}: DeleteAccountDialogViewProps) {
|
|
27
|
+
const [confirmEmail, setConfirmEmail] = useState("");
|
|
28
|
+
const [mismatch, setMismatch] = useState(false);
|
|
29
|
+
const [deleted, setDeleted] = useState(false);
|
|
30
|
+
|
|
31
|
+
const mutation = useMutation({
|
|
32
|
+
...meOperationsDeleteMeMutation(),
|
|
33
|
+
onSuccess: () => {
|
|
34
|
+
setDeleted(true);
|
|
35
|
+
setTimeout(() => signOut(), 2000);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const exportMutation = useMutation(meOperationsCreateExportMutation());
|
|
40
|
+
|
|
41
|
+
const handleClose = () => {
|
|
42
|
+
if (mutation.isPending) return;
|
|
43
|
+
setConfirmEmail("");
|
|
44
|
+
setMismatch(false);
|
|
45
|
+
setDeleted(false);
|
|
46
|
+
mutation.reset();
|
|
47
|
+
exportMutation.reset();
|
|
48
|
+
onClose();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleDelete = () => {
|
|
52
|
+
if (confirmEmail.trim().toLowerCase() !== accountEmail.toLowerCase()) {
|
|
53
|
+
setMismatch(true);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setMismatch(false);
|
|
57
|
+
mutation.mutate({ body: { confirmEmail: confirmEmail.trim() } });
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Dialog open={open} onClose={handleClose} title="Delete your Remit account">
|
|
62
|
+
<header className="flex items-center gap-2 border-b border-line px-5 py-3">
|
|
63
|
+
<AlertTriangle className="size-4 shrink-0 text-danger" />
|
|
64
|
+
<span className="flex-1 text-sm font-semibold text-fg">
|
|
65
|
+
Delete your Remit account
|
|
66
|
+
</span>
|
|
67
|
+
<Button
|
|
68
|
+
variant="ghost"
|
|
69
|
+
size="sm"
|
|
70
|
+
icon={<X className="size-3.5" />}
|
|
71
|
+
onClick={handleClose}
|
|
72
|
+
aria-label="Cancel"
|
|
73
|
+
/>
|
|
74
|
+
</header>
|
|
75
|
+
|
|
76
|
+
{deleted ? (
|
|
77
|
+
<div className="px-5 py-6 text-sm text-fg-muted">
|
|
78
|
+
<p className="font-medium text-fg">Account deletion scheduled.</p>
|
|
79
|
+
<p className="mt-1">
|
|
80
|
+
Your data will be erased shortly. Signing you out…
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
<>
|
|
85
|
+
<div className="space-y-4 px-5 py-4 text-sm text-fg-muted">
|
|
86
|
+
<p>This permanently erases everything Remit holds for you:</p>
|
|
87
|
+
<ul className="space-y-1.5 text-xs">
|
|
88
|
+
<li className="flex gap-2">
|
|
89
|
+
<span className="text-danger">•</span>
|
|
90
|
+
All connected accounts disconnected and access tokens revoked.
|
|
91
|
+
</li>
|
|
92
|
+
<li className="flex gap-2">
|
|
93
|
+
<span className="text-danger">•</span>
|
|
94
|
+
Synced mail cache and search index.
|
|
95
|
+
</li>
|
|
96
|
+
<li className="flex gap-2">
|
|
97
|
+
<span className="text-danger">•</span>
|
|
98
|
+
AI history and insights.
|
|
99
|
+
</li>
|
|
100
|
+
<li className="flex gap-2">
|
|
101
|
+
<span className="text-danger">•</span>
|
|
102
|
+
Preferences and rules.
|
|
103
|
+
</li>
|
|
104
|
+
</ul>
|
|
105
|
+
|
|
106
|
+
<div className="rounded-sm border border-line bg-surface-sunken px-3 py-2 text-xs">
|
|
107
|
+
<strong className="text-fg">
|
|
108
|
+
Your mail at Gmail / IMAP is not deleted.
|
|
109
|
+
</strong>{" "}
|
|
110
|
+
This only removes Remit's copy and its access — the mail stays in
|
|
111
|
+
your provider mailboxes.
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="text-xs">
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={() => exportMutation.mutate({})}
|
|
118
|
+
aria-busy={exportMutation.isPending}
|
|
119
|
+
className="inline-flex items-center gap-1.5 font-medium text-accent hover:underline"
|
|
120
|
+
>
|
|
121
|
+
<Download className="size-3.5" />
|
|
122
|
+
{exportMutation.isPending
|
|
123
|
+
? "Requesting export…"
|
|
124
|
+
: "Export my data first"}
|
|
125
|
+
</button>
|
|
126
|
+
{exportMutation.isSuccess && (
|
|
127
|
+
// biome-ignore lint/a11y/useSemanticElements: <p> with role="status" preserves block layout; <output> is inline
|
|
128
|
+
<p className="mt-1.5 text-fg-muted" role="status">
|
|
129
|
+
Export requested. We'll prepare a ZIP of your mail — find it
|
|
130
|
+
under your data exports when it's ready.
|
|
131
|
+
</p>
|
|
132
|
+
)}
|
|
133
|
+
{exportMutation.isError && (
|
|
134
|
+
<p className="mt-1.5 text-danger" role="alert">
|
|
135
|
+
Couldn't start the export. Please try again.
|
|
136
|
+
</p>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div>
|
|
141
|
+
<label
|
|
142
|
+
htmlFor="confirm-remit-email"
|
|
143
|
+
className="mb-1 block text-xs font-medium text-fg"
|
|
144
|
+
>
|
|
145
|
+
Type{" "}
|
|
146
|
+
<span className="font-mono text-fg-muted">{accountEmail}</span>{" "}
|
|
147
|
+
to confirm
|
|
148
|
+
</label>
|
|
149
|
+
<Input
|
|
150
|
+
id="confirm-remit-email"
|
|
151
|
+
type="email"
|
|
152
|
+
placeholder={accountEmail}
|
|
153
|
+
value={confirmEmail}
|
|
154
|
+
onChange={(e) => {
|
|
155
|
+
setConfirmEmail(e.target.value);
|
|
156
|
+
if (mismatch) setMismatch(false);
|
|
157
|
+
if (mutation.isError) mutation.reset();
|
|
158
|
+
}}
|
|
159
|
+
autoComplete="off"
|
|
160
|
+
/>
|
|
161
|
+
{mismatch && (
|
|
162
|
+
<p className="mt-1.5 text-xs text-danger" role="alert">
|
|
163
|
+
That doesn't match your account email. Type it exactly to
|
|
164
|
+
confirm.
|
|
165
|
+
</p>
|
|
166
|
+
)}
|
|
167
|
+
{mutation.isError && (
|
|
168
|
+
<p className="mt-1.5 text-xs text-danger" role="alert">
|
|
169
|
+
{mutation.error instanceof Error
|
|
170
|
+
? mutation.error.message
|
|
171
|
+
: "Something went wrong. Please try again."}
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<footer className="flex items-center justify-end gap-2 border-t border-line px-5 py-3">
|
|
178
|
+
<Button variant="secondary" size="sm" onClick={handleClose}>
|
|
179
|
+
Cancel
|
|
180
|
+
</Button>
|
|
181
|
+
<Button
|
|
182
|
+
variant="danger"
|
|
183
|
+
size="sm"
|
|
184
|
+
onClick={handleDelete}
|
|
185
|
+
aria-busy={mutation.isPending}
|
|
186
|
+
>
|
|
187
|
+
{mutation.isPending ? "Deleting…" : "Delete everything"}
|
|
188
|
+
</Button>
|
|
189
|
+
</footer>
|
|
190
|
+
</>
|
|
191
|
+
)}
|
|
192
|
+
</Dialog>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Delete-account dialog. Safe to mount anywhere — `DangerZone` renders it
|
|
198
|
+
* unconditionally on the settings/accounts route. Identity comes from the
|
|
199
|
+
* composed auth provider's `Account` render-prop; when no session is active
|
|
200
|
+
* (e2e / visual harness / signed out) it falls back to the view with an empty
|
|
201
|
+
* identity and a no-op `signOut`.
|
|
202
|
+
*/
|
|
203
|
+
export function DeleteAccountDialog(props: DeleteAccountDialogProps) {
|
|
204
|
+
const { Account } = useAuthProvider();
|
|
205
|
+
return (
|
|
206
|
+
<Account
|
|
207
|
+
fallback={
|
|
208
|
+
<DeleteAccountDialogView
|
|
209
|
+
{...props}
|
|
210
|
+
accountEmail=""
|
|
211
|
+
signOut={() => {}}
|
|
212
|
+
/>
|
|
213
|
+
}
|
|
214
|
+
>
|
|
215
|
+
{({ email, signOut }) => (
|
|
216
|
+
<DeleteAccountDialogView
|
|
217
|
+
{...props}
|
|
218
|
+
accountEmail={email ?? ""}
|
|
219
|
+
signOut={signOut}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</Account>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { RemitImapFilterResponse } from "@remit/api-http-client/types.gen.ts";
|
|
4
|
+
import React, { createElement } from "react";
|
|
5
|
+
import { renderToString } from "react-dom/server";
|
|
6
|
+
import { FiltersList } from "./FiltersList";
|
|
7
|
+
|
|
8
|
+
// The node test loader transpiles remit-ui's `.tsx` with the classic JSX
|
|
9
|
+
// runtime, which references a global `React`. Vite uses the automatic runtime,
|
|
10
|
+
// so this shim only exists for the SSR test harness.
|
|
11
|
+
(globalThis as { React?: typeof React }).React = React;
|
|
12
|
+
|
|
13
|
+
const NOW = Date.parse("2026-07-12T12:00:00Z");
|
|
14
|
+
|
|
15
|
+
const filter = (
|
|
16
|
+
overrides: Partial<RemitImapFilterResponse>,
|
|
17
|
+
): RemitImapFilterResponse => ({
|
|
18
|
+
filterId: "f-1",
|
|
19
|
+
accountConfigId: "acc-1",
|
|
20
|
+
name: "Travel",
|
|
21
|
+
scope: "Standing",
|
|
22
|
+
state: "Active",
|
|
23
|
+
hasAnchor: true,
|
|
24
|
+
ruleChangedAt: 0,
|
|
25
|
+
matchOperator: "And",
|
|
26
|
+
literalClauses: [],
|
|
27
|
+
actionLabelId: "None",
|
|
28
|
+
actionMailboxId: "mbx-travel",
|
|
29
|
+
createdAt: 0,
|
|
30
|
+
updatedAt: 0,
|
|
31
|
+
...overrides,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const render = (filters: RemitImapFilterResponse[]) =>
|
|
35
|
+
renderToString(
|
|
36
|
+
createElement(FiltersList, {
|
|
37
|
+
filters,
|
|
38
|
+
mailboxName: (id: string) => (id === "mbx-travel" ? "Travel" : undefined),
|
|
39
|
+
onDelete: () => undefined,
|
|
40
|
+
now: NOW,
|
|
41
|
+
}) as never,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
describe("FiltersList", () => {
|
|
45
|
+
it("renders the empty state with a pointer to Organize", () => {
|
|
46
|
+
const html = render([]);
|
|
47
|
+
assert.match(html, /No filters yet/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("marks a standing filter Active and shows its move target", () => {
|
|
51
|
+
const html = render([filter({})]);
|
|
52
|
+
assert.match(html, /Active/);
|
|
53
|
+
assert.match(html, /Moves matches to Travel/);
|
|
54
|
+
assert.match(html, /always/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("keeps an expired temporary filter visible and marks it Expired (RFC 034 Decision 1.2)", () => {
|
|
58
|
+
const html = render([
|
|
59
|
+
filter({
|
|
60
|
+
filterId: "f-2",
|
|
61
|
+
name: "Lisbon trip",
|
|
62
|
+
scope: "Temporary",
|
|
63
|
+
state: "Active",
|
|
64
|
+
expiresAt: "2026-07-10T00:00:00Z",
|
|
65
|
+
}),
|
|
66
|
+
]);
|
|
67
|
+
assert.match(html, /Lisbon trip/);
|
|
68
|
+
assert.match(html, /Expired/);
|
|
69
|
+
assert.match(html, /expired/);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { RemitImapFilterResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
|
+
import { Badge, Button } from "@remit/ui";
|
|
3
|
+
import { Trash2 } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
filterDisplayStatus,
|
|
6
|
+
formatExpiresAt,
|
|
7
|
+
} from "@/lib/organize/filter-status";
|
|
8
|
+
import { NO_ACTION } from "@/lib/organize/organize-model";
|
|
9
|
+
|
|
10
|
+
interface FiltersListProps {
|
|
11
|
+
filters: RemitImapFilterResponse[];
|
|
12
|
+
/** Resolve a destination mailbox id to a folder name for display. */
|
|
13
|
+
mailboxName: (mailboxId: string) => string | undefined;
|
|
14
|
+
onDelete: (filterId: string) => void;
|
|
15
|
+
deletingFilterId?: string;
|
|
16
|
+
/** Injected for deterministic status in tests; defaults to now. */
|
|
17
|
+
now?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The account's standing filters. Expired temporary filters stay listed and
|
|
22
|
+
* are marked Expired distinctly rather than hidden (RFC 034 Decision 1.2).
|
|
23
|
+
*/
|
|
24
|
+
export function FiltersList({
|
|
25
|
+
filters,
|
|
26
|
+
mailboxName,
|
|
27
|
+
onDelete,
|
|
28
|
+
deletingFilterId,
|
|
29
|
+
now = Date.now(),
|
|
30
|
+
}: FiltersListProps) {
|
|
31
|
+
if (filters.length === 0) {
|
|
32
|
+
return (
|
|
33
|
+
<p className="py-6 text-sm text-fg-muted">
|
|
34
|
+
No filters yet. Select a few messages in the inbox and choose Organize
|
|
35
|
+
to make one.
|
|
36
|
+
</p>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<ul className="divide-y divide-line rounded-md border border-line">
|
|
42
|
+
{filters.map((filter) => {
|
|
43
|
+
const status = filterDisplayStatus(filter, now);
|
|
44
|
+
const expired = status === "Expired";
|
|
45
|
+
const folder =
|
|
46
|
+
filter.actionMailboxId !== NO_ACTION
|
|
47
|
+
? mailboxName(filter.actionMailboxId)
|
|
48
|
+
: undefined;
|
|
49
|
+
const expiresLabel = formatExpiresAt(filter.expiresAt);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<li
|
|
53
|
+
key={filter.filterId}
|
|
54
|
+
className="flex items-start gap-3 px-3 py-2.5"
|
|
55
|
+
>
|
|
56
|
+
<div className="min-w-0 flex-1">
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
<span
|
|
59
|
+
className={`truncate text-sm font-medium ${
|
|
60
|
+
expired ? "text-fg-muted" : "text-fg"
|
|
61
|
+
}`}
|
|
62
|
+
>
|
|
63
|
+
{filter.name}
|
|
64
|
+
</span>
|
|
65
|
+
<Badge tone={expired ? "neutral" : "positive"} dot>
|
|
66
|
+
{status}
|
|
67
|
+
</Badge>
|
|
68
|
+
</div>
|
|
69
|
+
<p className="mt-0.5 text-xs text-fg-subtle">
|
|
70
|
+
{folder ? `Moves matches to ${folder}` : "No move action"}
|
|
71
|
+
{filter.scope === "Temporary" && expiresLabel
|
|
72
|
+
? expired
|
|
73
|
+
? ` · expired ${expiresLabel}`
|
|
74
|
+
: ` · until ${expiresLabel}`
|
|
75
|
+
: filter.scope === "Standing"
|
|
76
|
+
? " · always"
|
|
77
|
+
: ""}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
<Button
|
|
81
|
+
variant="ghost"
|
|
82
|
+
size="sm"
|
|
83
|
+
icon={<Trash2 className="size-4 text-danger" />}
|
|
84
|
+
onClick={() => onDelete(filter.filterId)}
|
|
85
|
+
disabled={deletingFilterId === filter.filterId}
|
|
86
|
+
aria-label={`Delete filter ${filter.name}`}
|
|
87
|
+
/>
|
|
88
|
+
</li>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</ul>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
accountIsMissingSmtp,
|
|
5
|
+
appendAppPasswordHint,
|
|
6
|
+
computeSmtpAutoFill,
|
|
7
|
+
deriveSmtpHostFromImap,
|
|
8
|
+
isAuthError,
|
|
9
|
+
} from "./account-form-helpers.js";
|
|
10
|
+
|
|
11
|
+
describe("deriveSmtpHostFromImap", () => {
|
|
12
|
+
test("rewrites imap.* to smtp.*", () => {
|
|
13
|
+
assert.equal(
|
|
14
|
+
deriveSmtpHostFromImap("imap.example.com"),
|
|
15
|
+
"smtp.example.com",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("matches the imap. prefix case-insensitively", () => {
|
|
20
|
+
assert.equal(
|
|
21
|
+
deriveSmtpHostFromImap("IMAP.example.com"),
|
|
22
|
+
"smtp.example.com",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns the host unchanged when no imap. prefix is present", () => {
|
|
27
|
+
assert.equal(
|
|
28
|
+
deriveSmtpHostFromImap("mail.example.com"),
|
|
29
|
+
"mail.example.com",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("only rewrites the prefix, not other occurrences of imap", () => {
|
|
34
|
+
assert.equal(
|
|
35
|
+
deriveSmtpHostFromImap("imap.imap-server.example.com"),
|
|
36
|
+
"smtp.imap-server.example.com",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("computeSmtpAutoFill", () => {
|
|
42
|
+
test("fills SMTP fields when SMTP host is empty and IMAP host is set", () => {
|
|
43
|
+
const result = computeSmtpAutoFill({
|
|
44
|
+
imapHost: "imap.example.com",
|
|
45
|
+
smtpHost: "",
|
|
46
|
+
});
|
|
47
|
+
assert.deepEqual(result, {
|
|
48
|
+
smtpHost: "smtp.example.com",
|
|
49
|
+
smtpPort: 587,
|
|
50
|
+
smtpTls: false,
|
|
51
|
+
smtpStartTls: true,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("fills SMTP fields when SMTP host is undefined and IMAP host is set", () => {
|
|
56
|
+
const result = computeSmtpAutoFill({
|
|
57
|
+
imapHost: "imap.example.com",
|
|
58
|
+
});
|
|
59
|
+
assert.deepEqual(result, {
|
|
60
|
+
smtpHost: "smtp.example.com",
|
|
61
|
+
smtpPort: 587,
|
|
62
|
+
smtpTls: false,
|
|
63
|
+
smtpStartTls: true,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("fills SMTP fields when SMTP host is whitespace", () => {
|
|
68
|
+
const result = computeSmtpAutoFill({
|
|
69
|
+
imapHost: "imap.example.com",
|
|
70
|
+
smtpHost: " ",
|
|
71
|
+
});
|
|
72
|
+
assert.notEqual(result, null);
|
|
73
|
+
assert.equal(result?.smtpHost, "smtp.example.com");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("returns null when SMTP host is already set — never overrides user input", () => {
|
|
77
|
+
const result = computeSmtpAutoFill({
|
|
78
|
+
imapHost: "imap.example.com",
|
|
79
|
+
smtpHost: "smtp.different.com",
|
|
80
|
+
smtpPort: 465,
|
|
81
|
+
smtpTls: true,
|
|
82
|
+
smtpStartTls: false,
|
|
83
|
+
});
|
|
84
|
+
assert.equal(result, null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("returns null when both IMAP host and SMTP host are empty", () => {
|
|
88
|
+
const result = computeSmtpAutoFill({
|
|
89
|
+
imapHost: "",
|
|
90
|
+
smtpHost: "",
|
|
91
|
+
});
|
|
92
|
+
assert.equal(result, null);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("returns null when IMAP host is whitespace", () => {
|
|
96
|
+
const result = computeSmtpAutoFill({
|
|
97
|
+
imapHost: " ",
|
|
98
|
+
smtpHost: "",
|
|
99
|
+
});
|
|
100
|
+
assert.equal(result, null);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("trims IMAP host before deriving SMTP host", () => {
|
|
104
|
+
const result = computeSmtpAutoFill({
|
|
105
|
+
imapHost: " imap.example.com ",
|
|
106
|
+
smtpHost: "",
|
|
107
|
+
});
|
|
108
|
+
assert.equal(result?.smtpHost, "smtp.example.com");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("accountIsMissingSmtp", () => {
|
|
113
|
+
test("true when smtpEnabled is undefined (legacy row)", () => {
|
|
114
|
+
assert.equal(accountIsMissingSmtp({}), true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("true when smtpEnabled is false", () => {
|
|
118
|
+
assert.equal(accountIsMissingSmtp({ smtpEnabled: false }), true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("false when smtpEnabled is true", () => {
|
|
122
|
+
assert.equal(accountIsMissingSmtp({ smtpEnabled: true }), false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("isAuthError", () => {
|
|
127
|
+
for (const msg of [
|
|
128
|
+
"AUTHENTICATIONFAILED",
|
|
129
|
+
"535 5.7.8 Bad credentials",
|
|
130
|
+
"Invalid login",
|
|
131
|
+
"bad credential",
|
|
132
|
+
]) {
|
|
133
|
+
test(`true for "${msg}"`, () => {
|
|
134
|
+
assert.equal(isAuthError(msg), true);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test("false for a network error", () => {
|
|
139
|
+
assert.equal(isAuthError("connect ETIMEDOUT 1.2.3.4:993"), false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("false for undefined", () => {
|
|
143
|
+
assert.equal(isAuthError(undefined), false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("appendAppPasswordHint", () => {
|
|
148
|
+
const hint = "iCloud requires an app-specific password.";
|
|
149
|
+
|
|
150
|
+
test("appends the hint to an auth error", () => {
|
|
151
|
+
const result = appendAppPasswordHint("Invalid login", hint);
|
|
152
|
+
assert.equal(result, `Invalid login — ${hint}`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("leaves a non-auth error untouched", () => {
|
|
156
|
+
assert.equal(
|
|
157
|
+
appendAppPasswordHint("connect ETIMEDOUT", hint),
|
|
158
|
+
"connect ETIMEDOUT",
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("returns the message unchanged when there is no hint", () => {
|
|
163
|
+
assert.equal(
|
|
164
|
+
appendAppPasswordHint("Invalid login", undefined),
|
|
165
|
+
"Invalid login",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("returns undefined when there is no message", () => {
|
|
170
|
+
assert.equal(appendAppPasswordHint(undefined, hint), undefined);
|
|
171
|
+
});
|
|
172
|
+
});
|