@remit/web-client 0.0.299 → 0.0.301

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.
@@ -0,0 +1,181 @@
1
+ import {
2
+ accountDetailOperationsUpdateAccountMutation,
3
+ configOperationsGetConfigQueryKey,
4
+ microsoftOAuthOperationsMicrosoftOAuthStartMutation,
5
+ } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
6
+ import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
7
+ import { microsoftServicesGranted } from "@remit/mail-oauth-service";
8
+ import {
9
+ type AccountService,
10
+ type AccountServiceChange,
11
+ AccountServiceChangeDialog,
12
+ type AccountServiceIntent,
13
+ } from "@remit/ui";
14
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
15
+ import { useState } from "react";
16
+ import { formatErrorMessage } from "@/components/ui/ErrorState";
17
+ import {
18
+ REDIRECT_STALL_MESSAGE,
19
+ useRedirectEnded,
20
+ } from "@/hooks/useRedirectEnded";
21
+ import { softErrorMeta } from "@/lib/error-classifier";
22
+ import {
23
+ type AccountServiceRefusal,
24
+ AccountServicesCard,
25
+ } from "./AccountServicesCard";
26
+
27
+ const SERVICES: AccountService[] = ["Mail", "Calendar"];
28
+ const PROVIDER_NAME = "Microsoft";
29
+
30
+ const isMicrosoft = (account: RemitImapAccountResponse): boolean =>
31
+ account.authType === "oauthMicrosoft";
32
+
33
+ const withService = (
34
+ services: AccountService[],
35
+ service: AccountService,
36
+ ): AccountService[] =>
37
+ SERVICES.filter(
38
+ (candidate) => candidate === service || services.includes(candidate),
39
+ );
40
+
41
+ export interface AccountServicesProps {
42
+ account: RemitImapAccountResponse;
43
+ onRemoveAccount: () => void;
44
+ }
45
+
46
+ export function AccountServices({
47
+ account,
48
+ onRemoveAccount,
49
+ }: AccountServicesProps) {
50
+ const queryClient = useQueryClient();
51
+ const [change, setChange] = useState<AccountServiceChange | null>(null);
52
+ const [refusal, setRefusal] = useState<AccountServiceRefusal | null>(null);
53
+ const [redirecting, setRedirecting] = useState<AccountService | null>(null);
54
+
55
+ const save = useMutation({
56
+ ...accountDetailOperationsUpdateAccountMutation(),
57
+ meta: softErrorMeta,
58
+ onSuccess: () =>
59
+ queryClient.invalidateQueries({
60
+ queryKey: configOperationsGetConfigQueryKey(),
61
+ }),
62
+ });
63
+
64
+ const consent = useMutation({
65
+ ...microsoftOAuthOperationsMicrosoftOAuthStartMutation(),
66
+ meta: softErrorMeta,
67
+ });
68
+
69
+ const markRedirectStarted = useRedirectEnded((end) => {
70
+ const service = redirecting;
71
+ setRedirecting(null);
72
+ if (end !== "stalled" || service === null) return;
73
+ setRefusal({ service, intent: "on", message: REDIRECT_STALL_MESSAGE });
74
+ });
75
+
76
+ const enabled =
77
+ save.isPending && save.variables?.body.syncedServices
78
+ ? save.variables.body.syncedServices
79
+ : account.syncedServices;
80
+ const offered = isMicrosoft(account) ? SERVICES : ["Mail" as const];
81
+ const consented = isMicrosoft(account)
82
+ ? microsoftServicesGranted(account.grantedScopes)
83
+ : offered;
84
+
85
+ const commit = (
86
+ services: AccountService[],
87
+ service: AccountService,
88
+ intent: AccountServiceIntent,
89
+ ) => {
90
+ setRefusal(null);
91
+ save.mutate(
92
+ {
93
+ path: { accountId: account.accountId },
94
+ body: { syncedServices: services },
95
+ },
96
+ {
97
+ onError: (error) =>
98
+ setRefusal({ service, intent, message: formatErrorMessage(error) }),
99
+ },
100
+ );
101
+ };
102
+
103
+ const startConsent = (service: AccountService) => {
104
+ setRefusal(null);
105
+ consent.mutate(
106
+ {
107
+ body: {
108
+ email: account.email,
109
+ services: withService(enabled, service),
110
+ },
111
+ },
112
+ {
113
+ onSuccess: (data) => {
114
+ setRedirecting(service);
115
+ markRedirectStarted();
116
+ window.location.assign(data.authorizationUrl);
117
+ },
118
+ onError: (error) =>
119
+ setRefusal({
120
+ service,
121
+ intent: "on",
122
+ message: formatErrorMessage(error),
123
+ }),
124
+ },
125
+ );
126
+ };
127
+
128
+ const requestChange = (
129
+ service: AccountService,
130
+ intent: AccountServiceIntent,
131
+ ) => {
132
+ if (intent === "off") {
133
+ setChange({ kind: enabled.length === 1 ? "last" : "disable", service });
134
+ return;
135
+ }
136
+ if (!consented.includes(service)) {
137
+ setChange({ kind: "enable", service });
138
+ return;
139
+ }
140
+ commit(withService(enabled, service), service, "on");
141
+ };
142
+
143
+ const confirm = () => {
144
+ const confirmed = change;
145
+ setChange(null);
146
+ if (confirmed === null) return;
147
+ if (confirmed.kind === "last") {
148
+ onRemoveAccount();
149
+ return;
150
+ }
151
+ if (confirmed.kind === "enable") {
152
+ startConsent(confirmed.service);
153
+ return;
154
+ }
155
+ commit(
156
+ enabled.filter((service) => service !== confirmed.service),
157
+ confirmed.service,
158
+ "off",
159
+ );
160
+ };
161
+
162
+ return (
163
+ <>
164
+ <AccountServicesCard
165
+ providerName={PROVIDER_NAME}
166
+ offered={offered}
167
+ enabled={enabled}
168
+ consented={consented}
169
+ disabled={save.isPending || consent.isPending || redirecting !== null}
170
+ refusal={refusal}
171
+ onRequestChange={requestChange}
172
+ />
173
+ <AccountServiceChangeDialog
174
+ change={change}
175
+ providerName={PROVIDER_NAME}
176
+ onConfirm={confirm}
177
+ onCancel={() => setChange(null)}
178
+ />
179
+ </>
180
+ );
181
+ }
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, within } from "storybook/test";
3
+ import { AccountServicesCard } from "./AccountServicesCard";
4
+
5
+ const meta: Meta<typeof AccountServicesCard> = {
6
+ title: "Playground/Shipped/Settings/Accounts/What this account syncs",
7
+ component: AccountServicesCard,
8
+ parameters: { layout: "padded" },
9
+ args: {
10
+ providerName: "Microsoft",
11
+ offered: ["Mail", "Calendar"],
12
+ enabled: ["Mail"],
13
+ consented: ["Mail", "Calendar"],
14
+ disabled: false,
15
+ refusal: null,
16
+ onRequestChange: () => {},
17
+ },
18
+ };
19
+ export default meta;
20
+
21
+ type Story = StoryObj<typeof AccountServicesCard>;
22
+
23
+ export const MailOnly: Story = {};
24
+
25
+ export const BothOn: Story = { args: { enabled: ["Mail", "Calendar"] } };
26
+
27
+ export const CalendarNeedsSignIn: Story = {
28
+ args: { consented: ["Mail"] },
29
+ play: async ({ canvasElement }) => {
30
+ const canvas = within(canvasElement);
31
+ await expect(
32
+ canvas.getByText(/signing in with Microsoft again/),
33
+ ).toBeVisible();
34
+ },
35
+ };
36
+
37
+ export const Saving: Story = {
38
+ args: { enabled: ["Mail", "Calendar"], disabled: true },
39
+ play: async ({ canvasElement }) => {
40
+ const canvas = within(canvasElement);
41
+ await expect(
42
+ canvas.getByRole("switch", { name: "Sync calendar" }),
43
+ ).toBeDisabled();
44
+ },
45
+ };
46
+
47
+ export const CalendarRefused: Story = {
48
+ args: {
49
+ refusal: {
50
+ service: "Calendar",
51
+ intent: "on",
52
+ message:
53
+ "Couldn't open Microsoft's sign-in page. Check your connection, and anything blocking redirects, then try again.",
54
+ },
55
+ },
56
+ play: async ({ canvasElement }) => {
57
+ const canvas = within(canvasElement);
58
+ await expect(
59
+ canvas.getByRole("switch", { name: "Sync calendar" }),
60
+ ).not.toBeChecked();
61
+ await expect(canvas.getByText("Couldn't turn calendar on")).toBeVisible();
62
+ await expect(
63
+ canvas.getByRole("link", { name: "Report an issue" }),
64
+ ).toBeVisible();
65
+ },
66
+ };
67
+
68
+ export const Dark: Story = {
69
+ ...CalendarRefused,
70
+ parameters: { theme: "dark" },
71
+ };
@@ -0,0 +1,76 @@
1
+ import {
2
+ type AccountService,
3
+ type AccountServiceIntent,
4
+ AccountServiceToggles,
5
+ Banner,
6
+ } from "@remit/ui";
7
+ import { buildBugReportContext, buildGitHubIssueUrl } from "@/lib/bug-report";
8
+
9
+ export interface AccountServiceRefusal {
10
+ service: AccountService;
11
+ intent: AccountServiceIntent;
12
+ message: string;
13
+ }
14
+
15
+ export interface AccountServicesCardProps {
16
+ providerName: string;
17
+ offered: AccountService[];
18
+ enabled: AccountService[];
19
+ consented: AccountService[];
20
+ disabled: boolean;
21
+ refusal: AccountServiceRefusal | null;
22
+ onRequestChange: (
23
+ service: AccountService,
24
+ intent: AccountServiceIntent,
25
+ ) => void;
26
+ }
27
+
28
+ const refusalTitle = (refusal: AccountServiceRefusal): string =>
29
+ `Couldn't turn ${refusal.service.toLowerCase()} ${refusal.intent}`;
30
+
31
+ const reportHref = (refusal: AccountServiceRefusal): string =>
32
+ buildGitHubIssueUrl(
33
+ buildBugReportContext({
34
+ title: `Bug: ${refusalTitle(refusal)}`,
35
+ errorMessage: `${refusalTitle(refusal)} — ${refusal.message}`,
36
+ }),
37
+ );
38
+
39
+ export function AccountServicesCard({
40
+ providerName,
41
+ offered,
42
+ enabled,
43
+ consented,
44
+ disabled,
45
+ refusal,
46
+ onRequestChange,
47
+ }: AccountServicesCardProps) {
48
+ if (offered.length < 2) return null;
49
+
50
+ return (
51
+ <section className="space-y-2 rounded-sm border border-line bg-surface px-row-inset py-3">
52
+ <AccountServiceToggles
53
+ providerName={providerName}
54
+ offered={offered}
55
+ enabled={enabled}
56
+ consented={consented}
57
+ disabled={disabled}
58
+ onRequestChange={onRequestChange}
59
+ />
60
+ {refusal && (
61
+ <Banner tone="danger" variant="soft">
62
+ <p className="font-medium">{refusalTitle(refusal)}</p>
63
+ <p className="mt-0.5 break-words text-fg-muted">{refusal.message}</p>
64
+ <a
65
+ href={reportHref(refusal)}
66
+ target="_blank"
67
+ rel="noopener noreferrer"
68
+ className="mt-1 inline-block font-medium text-accent hover:underline"
69
+ >
70
+ Report an issue
71
+ </a>
72
+ </Banner>
73
+ )}
74
+ </section>
75
+ );
76
+ }
@@ -0,0 +1,363 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { HttpResponse, http } from "msw";
3
+ import { expect, userEvent, waitFor, within } from "storybook/test";
4
+ import { AppStory } from "@/mocks/story-frame/AppStory";
5
+ import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
6
+ import { mailWorld, NOW } from "@/mocks/story-frame/mail-world";
7
+ import { signedInAs } from "@/mocks/story-frame/session";
8
+ import { makeAccount } from "@/test-support/fixtures";
9
+
10
+ const accounts = [
11
+ makeAccount({
12
+ accountId: "acc-personal",
13
+ email: "alice.tan@gmail.example",
14
+ displayName: "Personal",
15
+ lastSyncAt: NOW,
16
+ }),
17
+ makeAccount({
18
+ accountId: "acc-work",
19
+ email: "alice@northwind.example",
20
+ displayName: "Work",
21
+ lastSyncAt: NOW,
22
+ lastError:
23
+ "AUTHENTICATIONFAILED: [ALERT] Application-specific password required",
24
+ }),
25
+ makeAccount({
26
+ accountId: "acc-forum",
27
+ email: "alice@synthcollective.example",
28
+ displayName: "Synthwave Forum",
29
+ lastSyncAt: NOW,
30
+ muted: { value: true, setAt: NOW },
31
+ }),
32
+ ];
33
+
34
+ const world = mailWorld({ accounts });
35
+
36
+ const meta = {
37
+ title: "Playground/Shipped/Settings/Accounts",
38
+ component: AppStory,
39
+ args: { url: "/settings/accounts" },
40
+ parameters: {
41
+ layout: "fullscreen",
42
+ msw: { handlers: mailHandlers(world) },
43
+ },
44
+ } satisfies Meta<typeof AppStory>;
45
+ export default meta;
46
+
47
+ type Story = StoryObj<typeof meta>;
48
+
49
+ const desktop = { viewport: { value: "desktop", isRotated: false } };
50
+ const tablet = { viewport: { value: "tablet", isRotated: false } };
51
+ const phone = { viewport: { value: "mobile", isRotated: false } };
52
+
53
+ const page = within(document.body);
54
+
55
+ const openEditPanel = async (canvasElement: HTMLElement, index: number) => {
56
+ const canvas = within(canvasElement);
57
+ await canvas.findAllByRole("button", { name: "Manage" });
58
+ const manage = canvas.getAllByRole("button", { name: "Manage" })[index];
59
+ if (manage) await userEvent.click(manage);
60
+ return page.findByRole("dialog", { name: "Edit Account" });
61
+ };
62
+
63
+ export const Accounts: Story = {
64
+ globals: desktop,
65
+ play: async ({ canvasElement }) => {
66
+ const canvas = within(canvasElement);
67
+ await expect(await canvas.findByText("Personal")).toBeVisible();
68
+ await expect(canvas.getByText("3 accounts")).toBeVisible();
69
+ await expect(
70
+ canvas.getByText(/Application-specific password required/),
71
+ ).toBeVisible();
72
+ await expect(
73
+ canvas.getByRole("button", { name: "Reconnect" }),
74
+ ).toBeVisible();
75
+ await expect(
76
+ canvas.getAllByRole("button", { name: "Manage" }),
77
+ ).toHaveLength(2);
78
+ },
79
+ };
80
+
81
+ export const AccountsScrolling: Story = {
82
+ name: "Accounts — desktop, beyond the fold",
83
+ globals: desktop,
84
+ parameters: {
85
+ msw: {
86
+ handlers: mailHandlers(
87
+ mailWorld({
88
+ accounts: Array.from({ length: 9 }, (_, index) =>
89
+ makeAccount({
90
+ accountId: `acc-${index + 1}`,
91
+ email: `alice+${index + 1}@northwind.example`,
92
+ displayName: `Account ${index + 1}`,
93
+ lastSyncAt: NOW,
94
+ }),
95
+ ),
96
+ }),
97
+ ),
98
+ },
99
+ },
100
+ play: async ({ canvasElement }) => {
101
+ const canvas = within(canvasElement);
102
+ await canvas.findByText("Account 9");
103
+ const leave = canvas.getByRole("button", {
104
+ name: "Delete your Remit account",
105
+ });
106
+ leave.scrollIntoView();
107
+ await expect(leave).toBeVisible();
108
+ await expect(
109
+ canvas.getByRole("heading", { name: "Accounts" }),
110
+ ).toBeVisible();
111
+ },
112
+ };
113
+
114
+ export const AccountsMissingSyncDate: Story = {
115
+ parameters: {
116
+ msw: {
117
+ handlers: mailHandlers(
118
+ mailWorld({
119
+ accounts: [
120
+ makeAccount({
121
+ accountId: "acc-personal",
122
+ email: "alice.tan@gmail.example",
123
+ displayName: "Personal",
124
+ }),
125
+ ],
126
+ }),
127
+ ),
128
+ },
129
+ },
130
+ play: async ({ canvasElement }) => {
131
+ const canvas = within(canvasElement);
132
+ await expect(await canvas.findByText(/never synced/)).toBeVisible();
133
+ },
134
+ };
135
+
136
+ export const ShellPhone: Story = {
137
+ name: "Shell — phone",
138
+ globals: phone,
139
+ play: async ({ canvasElement }) => {
140
+ const canvas = within(canvasElement);
141
+ await canvas.findByText("Personal");
142
+ await expect(
143
+ canvas.getByRole("button", { name: "Open settings menu" }),
144
+ ).toBeVisible();
145
+ await expect(
146
+ canvas.getByRole("button", { name: "Back to mail" }),
147
+ ).toBeVisible();
148
+ },
149
+ };
150
+
151
+ export const ShellTablet: Story = {
152
+ name: "Shell — tablet",
153
+ globals: tablet,
154
+ play: async ({ canvasElement }) => {
155
+ const canvas = within(canvasElement);
156
+ await canvas.findByText("Personal");
157
+ await expect(
158
+ canvas.getByRole("button", { name: "Open settings menu" }),
159
+ ).toBeVisible();
160
+ },
161
+ };
162
+
163
+ export const AccountsEmpty: Story = {
164
+ name: "Accounts — empty",
165
+ parameters: {
166
+ msw: { handlers: mailHandlers(mailWorld({ accounts: [] })) },
167
+ },
168
+ play: async ({ canvasElement }) => {
169
+ const canvas = within(canvasElement);
170
+ await expect(
171
+ await canvas.findByText("No accounts configured."),
172
+ ).toBeVisible();
173
+ await expect(
174
+ canvas.getByRole("button", { name: "Add your first account" }),
175
+ ).toBeVisible();
176
+ },
177
+ };
178
+
179
+ export const AccountsLoading: Story = {
180
+ name: "Accounts — loading",
181
+ parameters: {
182
+ msw: {
183
+ handlers: [
184
+ http.get("/api/config", () => new Promise<never>(() => undefined)),
185
+ ...mailHandlers(world),
186
+ ],
187
+ },
188
+ },
189
+ play: async ({ canvasElement }) => {
190
+ const canvas = within(canvasElement);
191
+ await expect(
192
+ await canvas.findByRole("status", { name: "Loading accounts" }),
193
+ ).toBeVisible();
194
+ },
195
+ };
196
+
197
+ export const AccountsOauthSuccess: Story = {
198
+ name: "Accounts — OAuth success",
199
+ args: { url: "/settings/accounts?connected=acc-personal" },
200
+ play: async ({ canvasElement }) => {
201
+ const canvas = within(canvasElement);
202
+ await expect(
203
+ await canvas.findByText("Account connected successfully."),
204
+ ).toBeVisible();
205
+ },
206
+ };
207
+
208
+ export const AccountsOauthError: Story = {
209
+ name: "Accounts — OAuth error",
210
+ args: { url: "/settings/accounts?oauthError=consent_required" },
211
+ play: async ({ canvasElement }) => {
212
+ const canvas = within(canvasElement);
213
+ await expect(
214
+ await canvas.findByText(/admin needs to approve Remit/),
215
+ ).toBeVisible();
216
+ },
217
+ };
218
+
219
+ export const AccountsDeleteConfirm: Story = {
220
+ name: "Accounts — delete confirm",
221
+ play: async ({ canvasElement }) => {
222
+ const canvas = within(canvasElement);
223
+ await canvas.findByText("Work");
224
+ const remove = canvas.getAllByRole("button", { name: "Delete account" })[1];
225
+ if (remove) await userEvent.click(remove);
226
+ const dialog = within(
227
+ await page.findByRole("dialog", { name: "Delete account" }),
228
+ );
229
+ await expect(dialog.getByText("Are you sure?")).toBeVisible();
230
+ await expect(dialog.getByText("alice@northwind.example")).toBeVisible();
231
+ await expect(
232
+ dialog.getByRole("button", { name: "Delete account" }),
233
+ ).toBeEnabled();
234
+ },
235
+ };
236
+
237
+ export const AccountEdit: Story = {
238
+ name: "Accounts — edit",
239
+ globals: desktop,
240
+ play: async ({ canvasElement }) => {
241
+ const panel = within(await openEditPanel(canvasElement, 0));
242
+ await expect(panel.getByLabelText("Display name (optional)")).toHaveValue(
243
+ "Personal",
244
+ );
245
+ await expect(panel.getByLabelText("Email Address")).toHaveValue(
246
+ "alice.tan@gmail.example",
247
+ );
248
+ },
249
+ };
250
+
251
+ export const AccountEditClosed: Story = {
252
+ name: "Accounts — edit panel closed",
253
+ globals: desktop,
254
+ play: async ({ canvasElement }) => {
255
+ const canvas = within(canvasElement);
256
+ await expect(await canvas.findByText("Personal")).toBeVisible();
257
+ await expect(
258
+ page.queryByRole("dialog", { name: "Edit Account" }),
259
+ ).toBeNull();
260
+ await expect(
261
+ canvas.getByRole("button", { name: "Add account" }),
262
+ ).toBeVisible();
263
+ },
264
+ };
265
+
266
+ export const AccountEditPhone: Story = {
267
+ name: "Accounts — edit, phone",
268
+ globals: phone,
269
+ play: async ({ canvasElement }) => {
270
+ const panel = await openEditPanel(canvasElement, 0);
271
+ await expect(panel).toBeVisible();
272
+ await expect(
273
+ within(panel).getByLabelText("Display name (optional)"),
274
+ ).toBeVisible();
275
+ },
276
+ };
277
+
278
+ export const AccountEditNoDisplayName: Story = {
279
+ name: "Accounts — edit, no display name",
280
+ parameters: {
281
+ msw: {
282
+ handlers: mailHandlers(
283
+ mailWorld({
284
+ accounts: [
285
+ makeAccount({
286
+ accountId: "acc-personal",
287
+ email: "alice.tan@gmail.example",
288
+ lastSyncAt: NOW,
289
+ }),
290
+ ],
291
+ }),
292
+ ),
293
+ },
294
+ },
295
+ play: async ({ canvasElement }) => {
296
+ const canvas = within(canvasElement);
297
+ await expect(await canvas.findByText("Alice Tan")).toBeVisible();
298
+ const panel = within(await openEditPanel(canvasElement, 0));
299
+ const field = panel.getByLabelText("Display name (optional)");
300
+ await expect(field).toHaveValue("");
301
+ await expect(field).toHaveAttribute("placeholder", "Alice");
302
+ },
303
+ };
304
+
305
+ export const DangerZone: Story = {
306
+ name: "Danger zone",
307
+ play: async ({ canvasElement }) => {
308
+ const canvas = within(canvasElement);
309
+ await canvas.findByText("Personal");
310
+ await expect(
311
+ canvas.getByText(/erases Remit's copy of your mail/),
312
+ ).toBeInTheDocument();
313
+ await expect(
314
+ canvas.getByRole("button", { name: "Delete your Remit account" }),
315
+ ).toBeInTheDocument();
316
+ },
317
+ };
318
+
319
+ export const DangerZoneConfirm: Story = {
320
+ name: "Danger zone — confirm dialog",
321
+ args: { authProvider: signedInAs("alice.tan@gmail.example") },
322
+ play: async ({ canvasElement }) => {
323
+ const canvas = within(canvasElement);
324
+ await canvas.findByText("Personal");
325
+ await userEvent.click(
326
+ canvas.getByRole("button", { name: "Delete your Remit account" }),
327
+ );
328
+ const dialog = within(
329
+ await page.findByRole("dialog", { name: "Delete your Remit account" }),
330
+ );
331
+ await expect(dialog.getByText("alice.tan@gmail.example")).toBeVisible();
332
+ await userEvent.type(
333
+ dialog.getByLabelText(/to confirm/),
334
+ "someone@else.example",
335
+ );
336
+ await userEvent.click(
337
+ dialog.getByRole("button", { name: "Delete everything" }),
338
+ );
339
+ await waitFor(() =>
340
+ expect(dialog.getByRole("alert")).toHaveTextContent(
341
+ "That doesn't match your account email",
342
+ ),
343
+ );
344
+ },
345
+ };
346
+
347
+ export const AccountsUnreadable: Story = {
348
+ name: "Accounts — unreadable",
349
+ parameters: {
350
+ msw: {
351
+ handlers: [
352
+ http.get("/api/config", () => HttpResponse.error()),
353
+ ...mailHandlers(world),
354
+ ],
355
+ },
356
+ },
357
+ play: async ({ canvasElement }) => {
358
+ const canvas = within(canvasElement);
359
+ await expect(
360
+ await canvas.findByText("Couldn't load accounts"),
361
+ ).toBeVisible();
362
+ },
363
+ };