@remit/web-client 0.0.300 → 0.0.302
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/package.json +2 -1
- package/src/auth/amplify-config.ts +2 -3
- package/src/auth/cognito-sign-in.stories.tsx +90 -0
- package/src/auth/email-sign-in.stories.tsx +128 -0
- package/src/components/compose/ComposeForm.tsx +14 -0
- package/src/components/compose/mail-sync-off-refusal.ts +7 -0
- package/src/components/compose/send-from-mail-off-account.render.test.ts +173 -0
- package/src/components/onboarding/onboarding.stories.tsx +460 -0
- package/src/components/settings/AccountServices.render.test.ts +254 -0
- package/src/components/settings/AccountServices.tsx +181 -0
- package/src/components/settings/AccountServicesCard.stories.tsx +71 -0
- package/src/components/settings/AccountServicesCard.tsx +76 -0
- package/src/components/settings/accounts.stories.tsx +363 -0
- package/src/components/settings/advanced.stories.tsx +162 -0
- package/src/components/settings/appearance.stories.tsx +33 -0
- package/src/components/settings/folders.stories.tsx +89 -0
- package/src/components/settings/self-update.stories.tsx +270 -0
- package/src/components/settings/senders.stories.tsx +155 -0
- package/src/mocks/story-frame/AppStory.tsx +21 -6
- package/src/mocks/story-frame/mail-handlers.ts +69 -7
- package/src/mocks/story-frame/mail-world.ts +22 -4
- package/src/mocks/story-frame/onboarding-handlers.ts +97 -0
- package/src/mocks/story-frame/session.tsx +19 -0
- package/src/routes/settings/-accounts.test.ts +1 -0
- package/src/routes/settings/accounts.tsx +28 -20
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
} from "@tanstack/react-query";
|
|
7
7
|
import { createMemoryHistory, RouterProvider } from "@tanstack/react-router";
|
|
8
8
|
import { useState } from "react";
|
|
9
|
+
import {
|
|
10
|
+
type AuthProvider,
|
|
11
|
+
AuthProviderProvider,
|
|
12
|
+
noneAuthProvider,
|
|
13
|
+
} from "@/auth/provider";
|
|
9
14
|
import {
|
|
10
15
|
handleMutationCacheError,
|
|
11
16
|
handleQueryCacheError,
|
|
@@ -17,6 +22,7 @@ import "@/lib/client";
|
|
|
17
22
|
|
|
18
23
|
export interface AppStoryProps {
|
|
19
24
|
url: string;
|
|
25
|
+
authProvider?: AuthProvider;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
const createStoryQueryClient = (): QueryClient =>
|
|
@@ -29,19 +35,28 @@ const createStoryQueryClient = (): QueryClient =>
|
|
|
29
35
|
},
|
|
30
36
|
});
|
|
31
37
|
|
|
32
|
-
export function AppStory({
|
|
38
|
+
export function AppStory({
|
|
39
|
+
url,
|
|
40
|
+
authProvider = noneAuthProvider,
|
|
41
|
+
}: AppStoryProps) {
|
|
33
42
|
const [queryClient] = useState(createStoryQueryClient);
|
|
34
|
-
const [router] = useState(() =>
|
|
35
|
-
|
|
43
|
+
const [router] = useState(() => {
|
|
44
|
+
authProvider.configure();
|
|
45
|
+
return createAppRouter(
|
|
36
46
|
queryClient,
|
|
37
47
|
noopTelemetry,
|
|
38
48
|
createMemoryHistory({ initialEntries: [url] }),
|
|
39
|
-
)
|
|
40
|
-
);
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
const { Shell } = authProvider;
|
|
41
52
|
|
|
42
53
|
return (
|
|
43
54
|
<QueryClientProvider client={queryClient}>
|
|
44
|
-
<
|
|
55
|
+
<AuthProviderProvider value={authProvider}>
|
|
56
|
+
<Shell>
|
|
57
|
+
<RouterProvider router={router} />
|
|
58
|
+
</Shell>
|
|
59
|
+
</AuthProviderProvider>
|
|
45
60
|
</QueryClientProvider>
|
|
46
61
|
);
|
|
47
62
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AccountDetailOperationsUpdateAccountResponse,
|
|
3
|
+
AddressDetailOperationsUpdateAddressResponse,
|
|
4
|
+
AddressOperationsSearchAddressesResponse,
|
|
2
5
|
FilterOperationsListFiltersResponse,
|
|
3
6
|
LabelOperationsListLabelsResponse,
|
|
7
|
+
MailboxDetailOperationsRenameMailboxResponse,
|
|
4
8
|
MailboxOperationsListMailboxesResponse,
|
|
5
9
|
MeOperationsListQuarantineResponse,
|
|
6
10
|
MeOperationsListVipSuggestionsResponse,
|
|
@@ -169,6 +173,22 @@ export const mailHandlers = (
|
|
|
169
173
|
makeConfig(world.accounts),
|
|
170
174
|
),
|
|
171
175
|
),
|
|
176
|
+
http.patch(
|
|
177
|
+
`${API}/accounts/:accountId/mailboxes/:mailboxId`,
|
|
178
|
+
({ params }) => {
|
|
179
|
+
const mailbox = world.mailboxes.find(
|
|
180
|
+
(candidate) => candidate.mailboxId === params.mailboxId,
|
|
181
|
+
);
|
|
182
|
+
if (!mailbox)
|
|
183
|
+
return HttpResponse.json(
|
|
184
|
+
{ status: 404, message: "Mailbox not found" },
|
|
185
|
+
{ status: 404 },
|
|
186
|
+
);
|
|
187
|
+
return HttpResponse.json<MailboxDetailOperationsRenameMailboxResponse>(
|
|
188
|
+
mailbox,
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
),
|
|
172
192
|
http.get(`${API}/accounts/:accountId/mailboxes`, ({ params }) =>
|
|
173
193
|
HttpResponse.json<MailboxOperationsListMailboxesResponse>({
|
|
174
194
|
items: world.mailboxes.filter(
|
|
@@ -176,6 +196,48 @@ export const mailHandlers = (
|
|
|
176
196
|
),
|
|
177
197
|
}),
|
|
178
198
|
),
|
|
199
|
+
http.patch(`${API}/accounts/:accountId`, ({ params }) => {
|
|
200
|
+
const account = world.accounts.find(
|
|
201
|
+
(candidate) => candidate.accountId === params.accountId,
|
|
202
|
+
);
|
|
203
|
+
if (!account)
|
|
204
|
+
return HttpResponse.json(
|
|
205
|
+
{ status: 404, message: "Account not found" },
|
|
206
|
+
{ status: 404 },
|
|
207
|
+
);
|
|
208
|
+
return HttpResponse.json<AccountDetailOperationsUpdateAccountResponse>(
|
|
209
|
+
account,
|
|
210
|
+
);
|
|
211
|
+
}),
|
|
212
|
+
http.delete(
|
|
213
|
+
`${API}/accounts/:accountId`,
|
|
214
|
+
() => new HttpResponse(null, { status: 204 }),
|
|
215
|
+
),
|
|
216
|
+
http.get(`${API}/addresses/search`, ({ request }) => {
|
|
217
|
+
const needle = (
|
|
218
|
+
new URL(request.url).searchParams.get("q") ?? ""
|
|
219
|
+
).toLowerCase();
|
|
220
|
+
return HttpResponse.json<AddressOperationsSearchAddressesResponse>({
|
|
221
|
+
items: world.addresses.filter(
|
|
222
|
+
(address) =>
|
|
223
|
+
address.normalizedEmail.includes(needle) ||
|
|
224
|
+
(address.displayName ?? "").toLowerCase().includes(needle),
|
|
225
|
+
),
|
|
226
|
+
});
|
|
227
|
+
}),
|
|
228
|
+
http.patch(`${API}/addresses/:addressId`, ({ params }) => {
|
|
229
|
+
const address = world.addresses.find(
|
|
230
|
+
(candidate) => candidate.addressId === params.addressId,
|
|
231
|
+
);
|
|
232
|
+
if (!address)
|
|
233
|
+
return HttpResponse.json(
|
|
234
|
+
{ status: 404, message: "Address not found" },
|
|
235
|
+
{ status: 404 },
|
|
236
|
+
);
|
|
237
|
+
return HttpResponse.json<AddressDetailOperationsUpdateAddressResponse>(
|
|
238
|
+
address,
|
|
239
|
+
);
|
|
240
|
+
}),
|
|
179
241
|
http.get(`${API}/accounts/:accountId/sync/status`, ({ params }) =>
|
|
180
242
|
HttpResponse.json<SyncOperationsGetSyncStatusResponse>({
|
|
181
243
|
accountId: String(params.accountId),
|
|
@@ -272,19 +334,19 @@ export const mailHandlers = (
|
|
|
272
334
|
}),
|
|
273
335
|
),
|
|
274
336
|
http.get(`${API}/me/quarantine`, () =>
|
|
275
|
-
HttpResponse.json<MeOperationsListQuarantineResponse>({
|
|
337
|
+
HttpResponse.json<MeOperationsListQuarantineResponse>({
|
|
338
|
+
entries: world.quarantine,
|
|
339
|
+
}),
|
|
276
340
|
),
|
|
277
341
|
http.get(`${API}/me/vip-suggestions`, () =>
|
|
278
342
|
HttpResponse.json<MeOperationsListVipSuggestionsResponse>({
|
|
279
|
-
suggestions:
|
|
343
|
+
suggestions: world.vipSuggestions,
|
|
280
344
|
}),
|
|
281
345
|
),
|
|
282
346
|
http.get(`${API}/system/update`, () =>
|
|
283
|
-
HttpResponse.json<SystemOperationsGetSystemUpdateResponse>(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
run: null,
|
|
287
|
-
}),
|
|
347
|
+
HttpResponse.json<SystemOperationsGetSystemUpdateResponse>(
|
|
348
|
+
world.systemUpdate,
|
|
349
|
+
),
|
|
288
350
|
),
|
|
289
351
|
http.all(`${API}/*`, ({ request }) =>
|
|
290
352
|
HttpResponse.json(
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
RemitImapAccountResponse,
|
|
3
|
+
RemitImapAddressResponse,
|
|
3
4
|
RemitImapCanonicalMailboxRole,
|
|
4
5
|
RemitImapMailboxResponse,
|
|
5
6
|
RemitImapMessageCategory,
|
|
6
7
|
RemitImapOutboxMessageResponse,
|
|
7
8
|
RemitImapOutboxMessageStatus,
|
|
9
|
+
RemitImapQuarantineResponse,
|
|
10
|
+
RemitImapSystemUpdateResponse,
|
|
8
11
|
RemitImapThreadMessageResponse,
|
|
12
|
+
RemitImapVipSuggestionEntry,
|
|
9
13
|
} from "@remit/api-http-client/types.gen.ts";
|
|
10
14
|
import {
|
|
11
15
|
makeAccount,
|
|
@@ -18,12 +22,16 @@ export interface MailWorld {
|
|
|
18
22
|
mailboxes: RemitImapMailboxResponse[];
|
|
19
23
|
threads: RemitImapThreadMessageResponse[];
|
|
20
24
|
outbox: RemitImapOutboxMessageResponse[];
|
|
25
|
+
quarantine: RemitImapQuarantineResponse[];
|
|
26
|
+
vipSuggestions: RemitImapVipSuggestionEntry[];
|
|
27
|
+
addresses: RemitImapAddressResponse[];
|
|
28
|
+
systemUpdate: RemitImapSystemUpdateResponse;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
const MINUTE = 60_000;
|
|
24
32
|
const HOUR = 60 * MINUTE;
|
|
25
33
|
const DAY = 24 * HOUR;
|
|
26
|
-
const NOW = Date.UTC(2026, 8, 24, 13, 0);
|
|
34
|
+
export const NOW = Date.UTC(2026, 8, 24, 13, 0);
|
|
27
35
|
|
|
28
36
|
const ROLES = [
|
|
29
37
|
"Inbox",
|
|
@@ -48,7 +56,7 @@ export const mailboxIdFor = (
|
|
|
48
56
|
role: RemitImapCanonicalMailboxRole,
|
|
49
57
|
): string => `mbx-${accountId}-${role.toLowerCase()}`;
|
|
50
58
|
|
|
51
|
-
const accountWithFolders = (
|
|
59
|
+
export const accountWithFolders = (
|
|
52
60
|
accountId: string,
|
|
53
61
|
email: string,
|
|
54
62
|
displayName: string,
|
|
@@ -67,7 +75,7 @@ const accountWithFolders = (
|
|
|
67
75
|
})),
|
|
68
76
|
});
|
|
69
77
|
|
|
70
|
-
const foldersOf = (accountId: string): RemitImapMailboxResponse[] =>
|
|
78
|
+
export const foldersOf = (accountId: string): RemitImapMailboxResponse[] =>
|
|
71
79
|
ROLES.map((role) =>
|
|
72
80
|
makeMailbox({
|
|
73
81
|
accountId,
|
|
@@ -382,8 +390,18 @@ export const newsletterBacklog = (
|
|
|
382
390
|
|
|
383
391
|
export const mailWorld = (overrides: Partial<MailWorld> = {}): MailWorld => ({
|
|
384
392
|
accounts,
|
|
385
|
-
mailboxes: accounts.flatMap((account) =>
|
|
393
|
+
mailboxes: (overrides.accounts ?? accounts).flatMap((account) =>
|
|
394
|
+
foldersOf(account.accountId),
|
|
395
|
+
),
|
|
386
396
|
threads: [...inboxThreads, ...serverDrafts],
|
|
387
397
|
outbox: [...remitDrafts, ...outboxInEveryStatus],
|
|
398
|
+
quarantine: [],
|
|
399
|
+
vipSuggestions: [],
|
|
400
|
+
addresses: [],
|
|
401
|
+
systemUpdate: {
|
|
402
|
+
currentVersion: "0.0.0-storybook",
|
|
403
|
+
check: { status: "disabled" },
|
|
404
|
+
run: null,
|
|
405
|
+
},
|
|
388
406
|
...overrides,
|
|
389
407
|
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountOperationsCreateAccountResponse,
|
|
3
|
+
AccountOperationsTestConnectionResponse,
|
|
4
|
+
MicrosoftOAuthOperationsMicrosoftOAuthStartResponse,
|
|
5
|
+
RemitImapAccountSyncStatusResponse,
|
|
6
|
+
SyncOperationsGetSyncStatusResponse,
|
|
7
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
8
|
+
import { type HttpHandler, HttpResponse, http } from "msw";
|
|
9
|
+
import { makeAccount } from "@/test-support/fixtures";
|
|
10
|
+
|
|
11
|
+
const API = "/api";
|
|
12
|
+
|
|
13
|
+
export const NEW_ACCOUNT_ID = "acc-new";
|
|
14
|
+
|
|
15
|
+
export type Autoconfig = "found" | "missing" | "slow";
|
|
16
|
+
|
|
17
|
+
export interface OnboardingHandlerOptions {
|
|
18
|
+
autoconfig?: Autoconfig;
|
|
19
|
+
connection?: AccountOperationsTestConnectionResponse;
|
|
20
|
+
createFails?: boolean;
|
|
21
|
+
sync?: Omit<RemitImapAccountSyncStatusResponse, "accountId">;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const autoconfigXml = (domain: string): string => `<?xml version="1.0"?>
|
|
25
|
+
<clientConfig version="1.1">
|
|
26
|
+
<emailProvider id="${domain}">
|
|
27
|
+
<incomingServer type="imap">
|
|
28
|
+
<hostname>mail.${domain}</hostname>
|
|
29
|
+
<port>993</port>
|
|
30
|
+
<socketType>SSL</socketType>
|
|
31
|
+
</incomingServer>
|
|
32
|
+
<outgoingServer type="smtp">
|
|
33
|
+
<hostname>mail.${domain}</hostname>
|
|
34
|
+
<port>465</port>
|
|
35
|
+
<socketType>SSL</socketType>
|
|
36
|
+
</outgoingServer>
|
|
37
|
+
</emailProvider>
|
|
38
|
+
</clientConfig>`;
|
|
39
|
+
|
|
40
|
+
const pending = (): Promise<never> => new Promise<never>(() => undefined);
|
|
41
|
+
|
|
42
|
+
const autoconfigHandlers = (autoconfig: Autoconfig): HttpHandler[] => [
|
|
43
|
+
http.get("https://autoconfig.thunderbird.net/v1.1/:domain", ({ params }) => {
|
|
44
|
+
if (autoconfig === "slow") return pending();
|
|
45
|
+
if (autoconfig === "missing")
|
|
46
|
+
return new HttpResponse(null, { status: 404 });
|
|
47
|
+
return HttpResponse.text(autoconfigXml(String(params.domain)), {
|
|
48
|
+
headers: { "Content-Type": "application/xml" },
|
|
49
|
+
});
|
|
50
|
+
}),
|
|
51
|
+
http.get(
|
|
52
|
+
/^https:\/\/autoconfig\.[^/]+\/mail\/config-v1\.1\.xml$/,
|
|
53
|
+
() => new HttpResponse(null, { status: 404 }),
|
|
54
|
+
),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export const onboardingHandlers = ({
|
|
58
|
+
autoconfig = "found",
|
|
59
|
+
connection = { imapSuccess: true, smtpSuccess: true },
|
|
60
|
+
createFails = false,
|
|
61
|
+
sync = {
|
|
62
|
+
syncPhase: "complete",
|
|
63
|
+
mailboxCountTotal: 6,
|
|
64
|
+
mailboxCountSynced: 6,
|
|
65
|
+
mailboxes: [],
|
|
66
|
+
},
|
|
67
|
+
}: OnboardingHandlerOptions = {}): HttpHandler[] => [
|
|
68
|
+
...autoconfigHandlers(autoconfig),
|
|
69
|
+
http.post(`${API}/accounts/test-connection`, () =>
|
|
70
|
+
HttpResponse.json<AccountOperationsTestConnectionResponse>(connection),
|
|
71
|
+
),
|
|
72
|
+
http.post(`${API}/accounts`, async ({ request }) => {
|
|
73
|
+
if (createFails) return HttpResponse.error();
|
|
74
|
+
const body: unknown = await request.json();
|
|
75
|
+
const email =
|
|
76
|
+
typeof body === "object" &&
|
|
77
|
+
body !== null &&
|
|
78
|
+
"email" in body &&
|
|
79
|
+
typeof body.email === "string"
|
|
80
|
+
? body.email
|
|
81
|
+
: "alice@northwind.example";
|
|
82
|
+
return HttpResponse.json<AccountOperationsCreateAccountResponse>(
|
|
83
|
+
makeAccount({ accountId: NEW_ACCOUNT_ID, email }),
|
|
84
|
+
);
|
|
85
|
+
}),
|
|
86
|
+
http.get(`${API}/accounts/${NEW_ACCOUNT_ID}/sync/status`, () =>
|
|
87
|
+
HttpResponse.json<SyncOperationsGetSyncStatusResponse>({
|
|
88
|
+
accountId: NEW_ACCOUNT_ID,
|
|
89
|
+
...sync,
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
http.post(`${API}/accounts/oauth/microsoft/start`, () =>
|
|
93
|
+
HttpResponse.json<MicrosoftOAuthOperationsMicrosoftOAuthStartResponse>({
|
|
94
|
+
authorizationUrl: "#microsoft-sign-in",
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AuthProvider, noneAuthProvider } from "@/auth/provider";
|
|
2
|
+
|
|
3
|
+
type RuntimeConfig = NonNullable<typeof globalThis.__REMIT_CONFIG__>;
|
|
4
|
+
|
|
5
|
+
export const signedInAs = (email: string): AuthProvider => ({
|
|
6
|
+
...noneAuthProvider,
|
|
7
|
+
Account: ({ children }) => (
|
|
8
|
+
<>{children({ email, signOut: () => undefined })}</>
|
|
9
|
+
),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const withRuntimeConfig =
|
|
13
|
+
(config: RuntimeConfig) => (): (() => void) => {
|
|
14
|
+
const previous = globalThis.__REMIT_CONFIG__;
|
|
15
|
+
globalThis.__REMIT_CONFIG__ = config;
|
|
16
|
+
return () => {
|
|
17
|
+
globalThis.__REMIT_CONFIG__ = previous;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -22,6 +22,7 @@ import { useEffect, useState } from "react";
|
|
|
22
22
|
import { z } from "zod";
|
|
23
23
|
import { OnboardingWizard } from "@/components/onboarding/OnboardingWizard";
|
|
24
24
|
import { AccountFormPanel } from "@/components/settings/AccountFormPanel";
|
|
25
|
+
import { AccountServices } from "@/components/settings/AccountServices";
|
|
25
26
|
import { DangerZone } from "@/components/settings/DangerZone";
|
|
26
27
|
import { ErrorState } from "@/components/ui/ErrorState";
|
|
27
28
|
import {
|
|
@@ -482,26 +483,33 @@ function AccountsSettings() {
|
|
|
482
483
|
);
|
|
483
484
|
|
|
484
485
|
return (
|
|
485
|
-
<
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
486
|
+
<div key={account.accountId} className="space-y-2">
|
|
487
|
+
<AccountHealthCard
|
|
488
|
+
label={accountLabel(account)}
|
|
489
|
+
email={account.email}
|
|
490
|
+
connector={isOAuthAccount ? "Microsoft 365" : "IMAP"}
|
|
491
|
+
syncLabel={deriveSyncLabel(account)}
|
|
492
|
+
state={deriveState(account)}
|
|
493
|
+
// What the mail server said, when it said anything. Not every
|
|
494
|
+
// refusal that fences an account is one the Reconnect button
|
|
495
|
+
// clears — "SmtpClientAuthentication is disabled" is a tenant
|
|
496
|
+
// setting — so the stored reason outranks the generic prompt.
|
|
497
|
+
errorDetail={
|
|
498
|
+
isReauth
|
|
499
|
+
? (account.lastError ?? "Re-authentication required")
|
|
500
|
+
: isMissingPassword
|
|
501
|
+
? "Imported from a config file, which carries no password. Enter one to start syncing."
|
|
502
|
+
: account.lastError
|
|
503
|
+
}
|
|
504
|
+
trailing={trailingButton}
|
|
505
|
+
/>
|
|
506
|
+
<AccountServices
|
|
507
|
+
account={account}
|
|
508
|
+
onRemoveAccount={() =>
|
|
509
|
+
setDeletingAccountId(account.accountId)
|
|
510
|
+
}
|
|
511
|
+
/>
|
|
512
|
+
</div>
|
|
505
513
|
);
|
|
506
514
|
})}
|
|
507
515
|
</div>
|