@remit/web-client 0.0.300 → 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.
- 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/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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RemitImapSystemUpdateCheck,
|
|
3
|
+
RemitImapSystemUpdateResponse,
|
|
4
|
+
RemitImapSystemUpdateRun,
|
|
5
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
6
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
7
|
+
import { HttpResponse, http } from "msw";
|
|
8
|
+
import { expect, userEvent, waitFor, within } from "storybook/test";
|
|
9
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
10
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
11
|
+
import { mailWorld } from "@/mocks/story-frame/mail-world";
|
|
12
|
+
|
|
13
|
+
const CURRENT = "0.9.3";
|
|
14
|
+
const NEXT = "1.0.0";
|
|
15
|
+
const LOGS = "remit logs updater --since 2026-09-24T12:40";
|
|
16
|
+
|
|
17
|
+
const checked: RemitImapSystemUpdateCheck = {
|
|
18
|
+
status: "ok",
|
|
19
|
+
lastCheckedAt: "2026-09-24T12:39:00Z",
|
|
20
|
+
latestVersion: CURRENT,
|
|
21
|
+
updateAvailable: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const released: RemitImapSystemUpdateCheck = {
|
|
25
|
+
status: "ok",
|
|
26
|
+
lastCheckedAt: "2026-09-24T12:39:00Z",
|
|
27
|
+
latestVersion: NEXT,
|
|
28
|
+
publishedAt: "2026-09-22T09:00:00Z",
|
|
29
|
+
summary: "Faster search and a calmer daily brief.",
|
|
30
|
+
releaseNotesUrl: `https://github.com/remit-mail/reader/releases/tag/${NEXT}`,
|
|
31
|
+
updateAvailable: true,
|
|
32
|
+
schemaVersion: 42,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const run = (
|
|
36
|
+
outcome: RemitImapSystemUpdateRun["outcome"],
|
|
37
|
+
message: string,
|
|
38
|
+
startedAt = "2026-09-24T12:40:00Z",
|
|
39
|
+
): RemitImapSystemUpdateRun => ({
|
|
40
|
+
runId: "run-1",
|
|
41
|
+
fromVersion: CURRENT,
|
|
42
|
+
targetVersion: NEXT,
|
|
43
|
+
phase: outcome === null ? "verifying" : "committing",
|
|
44
|
+
outcome,
|
|
45
|
+
startedAt,
|
|
46
|
+
updatedAt: startedAt,
|
|
47
|
+
message,
|
|
48
|
+
logCommand: LOGS,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const update = (
|
|
52
|
+
overrides: Partial<RemitImapSystemUpdateResponse>,
|
|
53
|
+
): RemitImapSystemUpdateResponse => ({
|
|
54
|
+
currentVersion: CURRENT,
|
|
55
|
+
currentSchemaVersion: 42,
|
|
56
|
+
check: checked,
|
|
57
|
+
run: null,
|
|
58
|
+
...overrides,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const answering = (systemUpdate: RemitImapSystemUpdateResponse) => ({
|
|
62
|
+
msw: { handlers: mailHandlers(mailWorld({ systemUpdate })) },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const meta = {
|
|
66
|
+
title: "Playground/Shipped/Settings/Advanced/Self-update",
|
|
67
|
+
component: AppStory,
|
|
68
|
+
args: { url: "/settings/advanced" },
|
|
69
|
+
parameters: { layout: "fullscreen", ...answering(update({})) },
|
|
70
|
+
} satisfies Meta<typeof AppStory>;
|
|
71
|
+
export default meta;
|
|
72
|
+
|
|
73
|
+
type Story = StoryObj<typeof meta>;
|
|
74
|
+
|
|
75
|
+
const page = within(document.body);
|
|
76
|
+
|
|
77
|
+
export const AdvancedUpToDate: Story = {
|
|
78
|
+
play: async ({ canvasElement }) => {
|
|
79
|
+
const canvas = within(canvasElement);
|
|
80
|
+
await expect(
|
|
81
|
+
await canvas.findByText(`Remit ${CURRENT} is the latest version.`),
|
|
82
|
+
).toBeVisible();
|
|
83
|
+
await expect(
|
|
84
|
+
canvas.getByRole("button", { name: "Check again" }),
|
|
85
|
+
).toBeVisible();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const AdvancedNeverChecked: Story = {
|
|
90
|
+
parameters: answering(update({ check: { status: "disabled" } })),
|
|
91
|
+
play: async ({ canvasElement }) => {
|
|
92
|
+
const canvas = within(canvasElement);
|
|
93
|
+
await expect(
|
|
94
|
+
await canvas.findByText(/No update check has run yet/),
|
|
95
|
+
).toBeVisible();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const AdvancedUpdateAvailable: Story = {
|
|
100
|
+
parameters: answering(update({ check: released })),
|
|
101
|
+
play: async ({ canvasElement }) => {
|
|
102
|
+
const canvas = within(canvasElement);
|
|
103
|
+
await expect(
|
|
104
|
+
await canvas.findByRole("button", { name: `Install ${NEXT}` }),
|
|
105
|
+
).toBeVisible();
|
|
106
|
+
await expect(canvas.getByText("update available")).toBeVisible();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const AdvancedCheckFailed: Story = {
|
|
111
|
+
parameters: answering(
|
|
112
|
+
update({
|
|
113
|
+
check: {
|
|
114
|
+
status: "failed",
|
|
115
|
+
lastCheckedAt: "2026-09-21T12:00:00Z",
|
|
116
|
+
error: "No route to github.com — the server has no outbound network.",
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
),
|
|
120
|
+
play: async ({ canvasElement }) => {
|
|
121
|
+
const canvas = within(canvasElement);
|
|
122
|
+
await expect(
|
|
123
|
+
await canvas.findByText("Could not reach the update source."),
|
|
124
|
+
).toBeVisible();
|
|
125
|
+
await expect(canvas.getByText(/No route to github.com/)).toBeVisible();
|
|
126
|
+
await expect(
|
|
127
|
+
canvas.getByRole("button", { name: "Try again" }),
|
|
128
|
+
).toBeVisible();
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const AdvancedSucceeded: Story = {
|
|
133
|
+
parameters: answering(
|
|
134
|
+
update({ currentVersion: NEXT, run: run("succeeded", "Updated") }),
|
|
135
|
+
),
|
|
136
|
+
play: async ({ canvasElement }) => {
|
|
137
|
+
const canvas = within(canvasElement);
|
|
138
|
+
await expect(
|
|
139
|
+
await canvas.findByText(`Updated to Remit ${NEXT}.`),
|
|
140
|
+
).toBeVisible();
|
|
141
|
+
await expect(canvas.getByText(`Remit ${NEXT} is running.`)).toBeVisible();
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const AdvancedRolledBack: Story = {
|
|
146
|
+
parameters: answering(
|
|
147
|
+
update({
|
|
148
|
+
run: run(
|
|
149
|
+
"rolledBack",
|
|
150
|
+
'migration 0042_add_thread_index failed: relation "threads" does not exist',
|
|
151
|
+
),
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
play: async ({ canvasElement }) => {
|
|
155
|
+
const canvas = within(canvasElement);
|
|
156
|
+
await expect(
|
|
157
|
+
await canvas.findByText(/relation "threads" does not exist/),
|
|
158
|
+
).toBeVisible();
|
|
159
|
+
await expect(canvas.getByText(LOGS)).toBeVisible();
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const AdvancedRollbackFailed: Story = {
|
|
164
|
+
parameters: answering(
|
|
165
|
+
update({
|
|
166
|
+
run: run(
|
|
167
|
+
"rollbackFailed",
|
|
168
|
+
"migration 0042_add_thread_index failed and the snapshot restore errored: database is locked",
|
|
169
|
+
),
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
172
|
+
play: async ({ canvasElement }) => {
|
|
173
|
+
const canvas = within(canvasElement);
|
|
174
|
+
await expect(
|
|
175
|
+
await canvas.findByText(/the snapshot restore errored/),
|
|
176
|
+
).toBeVisible();
|
|
177
|
+
await expect(canvas.getByText(LOGS)).toBeVisible();
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const AdvancedAbandoned: Story = {
|
|
182
|
+
parameters: answering(
|
|
183
|
+
update({
|
|
184
|
+
run: run(
|
|
185
|
+
"abandoned",
|
|
186
|
+
"manifest fetch timed out before anything was pulled",
|
|
187
|
+
),
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
play: async ({ canvasElement }) => {
|
|
191
|
+
const canvas = within(canvasElement);
|
|
192
|
+
await expect(
|
|
193
|
+
await canvas.findByText(/manifest fetch timed out/),
|
|
194
|
+
).toBeVisible();
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const ConsentWithSchemaMigration: Story = {
|
|
199
|
+
parameters: answering(update({ currentSchemaVersion: 41, check: released })),
|
|
200
|
+
play: async ({ canvasElement }) => {
|
|
201
|
+
const canvas = within(canvasElement);
|
|
202
|
+
await userEvent.click(
|
|
203
|
+
await canvas.findByRole("button", { name: `Install ${NEXT}` }),
|
|
204
|
+
);
|
|
205
|
+
const dialog = within(
|
|
206
|
+
await page.findByRole("dialog", { name: `Install Remit ${NEXT}` }),
|
|
207
|
+
);
|
|
208
|
+
await expect(
|
|
209
|
+
dialog.getByText(/updates the database while Remit is offline/),
|
|
210
|
+
).toBeVisible();
|
|
211
|
+
await expect(
|
|
212
|
+
dialog.getByRole("button", { name: "Install and restart" }),
|
|
213
|
+
).toBeVisible();
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const RestartInProgress: Story = {
|
|
218
|
+
parameters: answering(
|
|
219
|
+
update({
|
|
220
|
+
run: run(null, "", new Date(Date.now() - 52_000).toISOString()),
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
play: async () => {
|
|
224
|
+
await expect(
|
|
225
|
+
await page.findByRole("heading", { name: `Installing Remit ${NEXT}` }),
|
|
226
|
+
).toBeVisible();
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
let answered = false;
|
|
231
|
+
|
|
232
|
+
const realNow = Date.now;
|
|
233
|
+
|
|
234
|
+
const clockAhead = (ms: number) => {
|
|
235
|
+
Date.now = () => realNow.call(Date) + ms;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const ServerNeverCameBack: Story = {
|
|
239
|
+
parameters: {
|
|
240
|
+
msw: {
|
|
241
|
+
handlers: [
|
|
242
|
+
http.get("/api/system/update", () => {
|
|
243
|
+
if (answered) return HttpResponse.error();
|
|
244
|
+
answered = true;
|
|
245
|
+
return HttpResponse.json<RemitImapSystemUpdateResponse>(
|
|
246
|
+
update({ run: run(null, "", new Date().toISOString()) }),
|
|
247
|
+
);
|
|
248
|
+
}),
|
|
249
|
+
...mailHandlers(mailWorld()),
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
beforeEach: () => {
|
|
254
|
+
answered = false;
|
|
255
|
+
return () => {
|
|
256
|
+
Date.now = realNow;
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
play: async () => {
|
|
260
|
+
await page.findByRole("heading", { name: `Installing Remit ${NEXT}` });
|
|
261
|
+
clockAhead(20 * 60_000);
|
|
262
|
+
await waitFor(
|
|
263
|
+
() =>
|
|
264
|
+
expect(page.getByRole("alertdialog")).toHaveTextContent(
|
|
265
|
+
"Your mail is safe at your provider",
|
|
266
|
+
),
|
|
267
|
+
{ timeout: 10_000 },
|
|
268
|
+
);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RemitImapAddressResponse,
|
|
3
|
+
RemitImapVipSuggestionEntry,
|
|
4
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
5
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
6
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
7
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
8
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
9
|
+
import { mailWorld, NOW } from "@/mocks/story-frame/mail-world";
|
|
10
|
+
|
|
11
|
+
const suggestion = (
|
|
12
|
+
addressId: string,
|
|
13
|
+
displayName: string,
|
|
14
|
+
normalizedEmail: string,
|
|
15
|
+
inboundCount: number,
|
|
16
|
+
replyCount: number,
|
|
17
|
+
): RemitImapVipSuggestionEntry => ({
|
|
18
|
+
addressId,
|
|
19
|
+
displayName,
|
|
20
|
+
normalizedEmail,
|
|
21
|
+
inboundCount,
|
|
22
|
+
outboundCount: replyCount,
|
|
23
|
+
replyCount,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const flagged = (
|
|
27
|
+
addressId: string,
|
|
28
|
+
displayName: string,
|
|
29
|
+
normalizedEmail: string,
|
|
30
|
+
flags: RemitImapAddressResponse["flags"],
|
|
31
|
+
): RemitImapAddressResponse => {
|
|
32
|
+
const [localPart = "", domain = ""] = normalizedEmail.split("@");
|
|
33
|
+
return {
|
|
34
|
+
addressId,
|
|
35
|
+
accountConfigId: "cfg-1",
|
|
36
|
+
displayName,
|
|
37
|
+
localPart,
|
|
38
|
+
domain,
|
|
39
|
+
normalizedEmail,
|
|
40
|
+
flags,
|
|
41
|
+
inboundCount: 0,
|
|
42
|
+
outboundCount: 0,
|
|
43
|
+
replyCount: 0,
|
|
44
|
+
lastInboundAt: NOW,
|
|
45
|
+
lastReplyAt: 0,
|
|
46
|
+
createdAt: NOW,
|
|
47
|
+
updatedAt: NOW,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const world = mailWorld({
|
|
52
|
+
vipSuggestions: [
|
|
53
|
+
suggestion("addr-ada", "Ada Lovelace", "ada@acme.example", 48, 21),
|
|
54
|
+
suggestion("addr-grace", "Grace Hopper", "grace@example.org", 12, 4),
|
|
55
|
+
suggestion("addr-linus", "Linus", "linus@example.org", 7, 0),
|
|
56
|
+
],
|
|
57
|
+
addresses: [
|
|
58
|
+
flagged("addr-weekly", "The Weekly", "digest@weekly.example", {
|
|
59
|
+
muted: { value: true, setAt: NOW, reason: "too frequent" },
|
|
60
|
+
}),
|
|
61
|
+
flagged("addr-outdoor", "Outdoor Store", "news@outdoor.example", {
|
|
62
|
+
muted: { value: true, setAt: NOW },
|
|
63
|
+
}),
|
|
64
|
+
flagged("addr-spam", "Prize Desk", "win@prize.example", {
|
|
65
|
+
blocked: { value: true, setAt: NOW },
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const meta = {
|
|
71
|
+
title: "Playground/Shipped/Settings/Senders",
|
|
72
|
+
component: AppStory,
|
|
73
|
+
args: { url: "/settings/senders" },
|
|
74
|
+
parameters: {
|
|
75
|
+
layout: "fullscreen",
|
|
76
|
+
msw: { handlers: mailHandlers(world) },
|
|
77
|
+
},
|
|
78
|
+
} satisfies Meta<typeof AppStory>;
|
|
79
|
+
export default meta;
|
|
80
|
+
|
|
81
|
+
type Story = StoryObj<typeof meta>;
|
|
82
|
+
|
|
83
|
+
export const SendersAndRules: Story = {
|
|
84
|
+
play: async ({ canvasElement }) => {
|
|
85
|
+
const canvas = within(canvasElement);
|
|
86
|
+
await expect(await canvas.findByText("Ada Lovelace")).toBeVisible();
|
|
87
|
+
await expect(
|
|
88
|
+
canvas.getByText("48 received · you replied 21×"),
|
|
89
|
+
).toBeVisible();
|
|
90
|
+
await expect(
|
|
91
|
+
canvas.getByText("7 received · you've never replied"),
|
|
92
|
+
).toBeVisible();
|
|
93
|
+
await expect(
|
|
94
|
+
canvas.getAllByRole("button", { name: "Add VIP" }),
|
|
95
|
+
).toHaveLength(3);
|
|
96
|
+
await expect(canvas.getByText("3 of 3 flagged senders")).toBeVisible();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const FilteredVips: Story = {
|
|
101
|
+
play: async ({ canvasElement }) => {
|
|
102
|
+
const canvas = within(canvasElement);
|
|
103
|
+
await canvas.findByText("Ada Lovelace");
|
|
104
|
+
await userEvent.type(
|
|
105
|
+
canvas.getByPlaceholderText("Filter vips by name or address"),
|
|
106
|
+
"grace",
|
|
107
|
+
);
|
|
108
|
+
await expect(
|
|
109
|
+
await canvas.findByText("1 of 3 flagged senders"),
|
|
110
|
+
).toBeVisible();
|
|
111
|
+
await expect(canvas.queryByText("Ada Lovelace")).toBeNull();
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const MutedSearch: Story = {
|
|
116
|
+
play: async ({ canvasElement }) => {
|
|
117
|
+
const canvas = within(canvasElement);
|
|
118
|
+
await canvas.findByText("Ada Lovelace");
|
|
119
|
+
await userEvent.click(canvas.getByRole("tab", { name: /^Muted/ }));
|
|
120
|
+
await expect(
|
|
121
|
+
canvas.getByText(/Type 2\+ characters to search muted senders/),
|
|
122
|
+
).toBeVisible();
|
|
123
|
+
await userEvent.type(
|
|
124
|
+
canvas.getByPlaceholderText("Filter muted by name or address"),
|
|
125
|
+
"example",
|
|
126
|
+
);
|
|
127
|
+
await expect(await canvas.findByText("The Weekly")).toBeVisible();
|
|
128
|
+
await expect(canvas.getByText("Outdoor Store")).toBeVisible();
|
|
129
|
+
await expect(canvas.queryByText("Prize Desk")).toBeNull();
|
|
130
|
+
await expect(
|
|
131
|
+
canvas.getAllByRole("button", { name: "Remove muted flag" }),
|
|
132
|
+
).toHaveLength(2);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const Empty: Story = {
|
|
137
|
+
parameters: {
|
|
138
|
+
msw: { handlers: mailHandlers(mailWorld()) },
|
|
139
|
+
},
|
|
140
|
+
play: async ({ canvasElement }) => {
|
|
141
|
+
const canvas = within(canvasElement);
|
|
142
|
+
await expect(
|
|
143
|
+
await canvas.findByText(/No VIP suggestions yet/),
|
|
144
|
+
).toBeVisible();
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const Phone: Story = {
|
|
149
|
+
globals: { viewport: { value: "mobile", isRotated: false } },
|
|
150
|
+
play: async ({ canvasElement }) => {
|
|
151
|
+
const canvas = within(canvasElement);
|
|
152
|
+
await expect(await canvas.findByText("Ada Lovelace")).toBeVisible();
|
|
153
|
+
await expect(canvas.getByRole("tab", { name: /^VIPs/ })).toBeVisible();
|
|
154
|
+
},
|
|
155
|
+
};
|
|
@@ -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
|
});
|