@remit/web-client 0.0.292 → 0.0.293
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 +1 -1
- package/src/components/onboarding/OnboardingWizard.flow.test.ts +45 -0
- package/src/components/onboarding/OnboardingWizard.tsx +2 -8
- package/src/components/onboarding/onboarding-servers.stories.tsx +49 -0
- package/src/lib/provider-presets.test.ts +33 -2
- package/src/lib/provider-presets.ts +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.293",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -169,6 +169,51 @@ describe("OnboardingWizard — the first-run path", () => {
|
|
|
169
169
|
assert.equal(imapHostField(dom).value, "imap.fastmail.com");
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
it("picks the Gmail preset from a Gmail address and locks both hosts", async () => {
|
|
173
|
+
const dom = start();
|
|
174
|
+
await walkToServers(dom, "alice@gmail.com");
|
|
175
|
+
|
|
176
|
+
const provider = dom.query<HTMLSelectElement>("#provider-select");
|
|
177
|
+
assert.ok(provider);
|
|
178
|
+
assert.equal(provider.value, "gmail");
|
|
179
|
+
const imap = imapHostField(dom);
|
|
180
|
+
const smtp = dom.query<HTMLInputElement>(
|
|
181
|
+
'input[placeholder="smtp.example.com"]',
|
|
182
|
+
);
|
|
183
|
+
assert.ok(smtp, "expected the SMTP host field");
|
|
184
|
+
assert.equal(imap.value, "imap.gmail.com");
|
|
185
|
+
assert.equal(smtp.value, "smtp.gmail.com");
|
|
186
|
+
assert.equal(imap.readOnly, true);
|
|
187
|
+
assert.equal(smtp.readOnly, true);
|
|
188
|
+
const security = [
|
|
189
|
+
...dom.document.querySelectorAll<HTMLSelectElement>("fieldset select"),
|
|
190
|
+
];
|
|
191
|
+
assert.deepEqual(
|
|
192
|
+
security.map((select) => [select.value, select.disabled]),
|
|
193
|
+
[
|
|
194
|
+
["tls", true],
|
|
195
|
+
["starttls", true],
|
|
196
|
+
],
|
|
197
|
+
);
|
|
198
|
+
assert.match(dom.text(), /2-Step Verification/);
|
|
199
|
+
assert.match(dom.text(), /create an app password/);
|
|
200
|
+
const link = dom.byText("a", "Get an app password");
|
|
201
|
+
assert.equal(
|
|
202
|
+
link.getAttribute("href"),
|
|
203
|
+
"https://support.google.com/accounts/answer/185833",
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("offers no Gmail sign-in on the connector step", () => {
|
|
208
|
+
const dom = start();
|
|
209
|
+
clickText(dom, "Add your first account");
|
|
210
|
+
assert.match(dom.text(), /How does this account connect/);
|
|
211
|
+
assert.doesNotMatch(
|
|
212
|
+
dom.text(),
|
|
213
|
+
/Gmail|Sign in with Google|No app passwords/,
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
172
217
|
it("falls back to a guess for a domain nobody has heard of", async () => {
|
|
173
218
|
const dom = start();
|
|
174
219
|
await walkToServers(dom, "alice@unheard-of.example");
|
|
@@ -257,7 +257,7 @@ function StepConnector({
|
|
|
257
257
|
</>
|
|
258
258
|
}
|
|
259
259
|
>
|
|
260
|
-
<div className="grid grid-cols-1 gap-3 sm:grid-cols-
|
|
260
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
261
261
|
<ConnectorTile
|
|
262
262
|
name="IMAP / SMTP"
|
|
263
263
|
description="Any mail provider — Fastmail, iCloud, your own server."
|
|
@@ -265,12 +265,6 @@ function StepConnector({
|
|
|
265
265
|
selected={selected === "imap"}
|
|
266
266
|
onSelect={() => setSelected("imap")}
|
|
267
267
|
/>
|
|
268
|
-
<ConnectorTile
|
|
269
|
-
name="Gmail"
|
|
270
|
-
description="Sign in with Google. No app passwords."
|
|
271
|
-
icon={<AtSign className="size-5" />}
|
|
272
|
-
comingSoon
|
|
273
|
-
/>
|
|
274
268
|
<ConnectorTile
|
|
275
269
|
name="Outlook / Microsoft 365"
|
|
276
270
|
description="Sign in with Microsoft. Works with Outlook.com and work accounts."
|
|
@@ -588,7 +582,7 @@ function StepAddress({
|
|
|
588
582
|
);
|
|
589
583
|
}
|
|
590
584
|
|
|
591
|
-
function StepServers({
|
|
585
|
+
export function StepServers({
|
|
592
586
|
email,
|
|
593
587
|
imapConfig,
|
|
594
588
|
smtpConfig,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, within } from "storybook/test";
|
|
3
|
+
import { getPresetById } from "@/lib/provider-presets";
|
|
4
|
+
import { StepServers } from "./OnboardingWizard";
|
|
5
|
+
|
|
6
|
+
const gmail = getPresetById("gmail");
|
|
7
|
+
if (!gmail) throw new Error("the gmail preset is missing");
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof StepServers> = {
|
|
10
|
+
title: "Playground/Shipped/Onboarding/Servers",
|
|
11
|
+
component: StepServers,
|
|
12
|
+
parameters: { layout: "fullscreen" },
|
|
13
|
+
args: {
|
|
14
|
+
email: "alice@gmail.com",
|
|
15
|
+
imapConfig: gmail.imap,
|
|
16
|
+
smtpConfig: gmail.smtp,
|
|
17
|
+
discovered: true,
|
|
18
|
+
onContinue: () => {},
|
|
19
|
+
onBack: () => {},
|
|
20
|
+
onChange: () => {},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
type Story = StoryObj<typeof StepServers>;
|
|
26
|
+
|
|
27
|
+
export const GmailPreset: Story = {
|
|
28
|
+
play: async ({ canvasElement }) => {
|
|
29
|
+
const canvas = within(canvasElement);
|
|
30
|
+
await expect(canvas.getByLabelText("Provider")).toHaveValue("gmail");
|
|
31
|
+
await expect(canvas.getByDisplayValue("imap.gmail.com")).toHaveAttribute(
|
|
32
|
+
"readonly",
|
|
33
|
+
);
|
|
34
|
+
await expect(canvas.getByDisplayValue("smtp.gmail.com")).toHaveAttribute(
|
|
35
|
+
"readonly",
|
|
36
|
+
);
|
|
37
|
+
for (const security of canvas.getAllByLabelText("Security")) {
|
|
38
|
+
await expect(security).toBeDisabled();
|
|
39
|
+
}
|
|
40
|
+
await expect(canvas.getByText(/2-Step Verification/)).toBeVisible();
|
|
41
|
+
await expect(
|
|
42
|
+
canvas.getByRole("link", { name: "Get an app password" }),
|
|
43
|
+
).toHaveAttribute("href", gmail.passwordHelp.url);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const GmailPresetPhone: Story = {
|
|
48
|
+
globals: { viewport: { value: "mobile" } },
|
|
49
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
+
import { APP_PASSWORD_URLS, discoverSettings } from "./autodiscovery.js";
|
|
3
4
|
import {
|
|
4
5
|
getPresetById,
|
|
5
6
|
PROVIDER_PRESETS,
|
|
@@ -29,12 +30,16 @@ const EXPECTED: Record<string, ExpectedSettings> = {
|
|
|
29
30
|
imap: { host: "imap.fastmail.com", port: 993, security: "tls" },
|
|
30
31
|
smtp: { host: "smtp.fastmail.com", port: 465, security: "tls" },
|
|
31
32
|
},
|
|
33
|
+
gmail: {
|
|
34
|
+
imap: { host: "imap.gmail.com", port: 993, security: "tls" },
|
|
35
|
+
smtp: { host: "smtp.gmail.com", port: 587, security: "starttls" },
|
|
36
|
+
},
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
describe("provider presets", () => {
|
|
35
|
-
it("exposes exactly the
|
|
40
|
+
it("exposes exactly the five documented providers", () => {
|
|
36
41
|
const ids = PROVIDER_PRESETS.map((p) => p.id).sort();
|
|
37
|
-
assert.deepEqual(ids, ["aol", "fastmail", "icloud", "yahoo"]);
|
|
42
|
+
assert.deepEqual(ids, ["aol", "fastmail", "gmail", "icloud", "yahoo"]);
|
|
38
43
|
});
|
|
39
44
|
|
|
40
45
|
for (const preset of PROVIDER_PRESETS) {
|
|
@@ -71,6 +76,18 @@ describe("provider presets", () => {
|
|
|
71
76
|
assert.equal(preset.label, "iCloud");
|
|
72
77
|
});
|
|
73
78
|
|
|
79
|
+
it("tells a Gmail user to turn on 2-Step Verification before creating an app password", () => {
|
|
80
|
+
const gmail = getPresetById("gmail");
|
|
81
|
+
assert.ok(gmail);
|
|
82
|
+
const { text, url } = gmail.passwordHelp;
|
|
83
|
+
const twoStep = text.search(/2-Step Verification/);
|
|
84
|
+
const appPassword = text.search(/create an app password/i);
|
|
85
|
+
assert.ok(twoStep >= 0, "names 2-Step Verification");
|
|
86
|
+
assert.ok(appPassword > twoStep, "2-Step Verification comes first");
|
|
87
|
+
assert.equal(url, APP_PASSWORD_URLS["gmail.com"]);
|
|
88
|
+
assert.equal(url, APP_PASSWORD_URLS["googlemail.com"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
74
91
|
it("returns undefined for an unknown id", () => {
|
|
75
92
|
assert.equal(getPresetById("custom"), undefined);
|
|
76
93
|
});
|
|
@@ -82,6 +99,20 @@ describe("provider presets", () => {
|
|
|
82
99
|
assert.equal(presetIdForEmail("carol@yahoo.com"), "yahoo");
|
|
83
100
|
assert.equal(presetIdForEmail("dave@aol.com"), "aol");
|
|
84
101
|
assert.equal(presetIdForEmail("erin@fastmail.com"), "fastmail");
|
|
102
|
+
assert.equal(presetIdForEmail("frank@gmail.com"), "gmail");
|
|
103
|
+
assert.equal(presetIdForEmail("grace@googlemail.com"), "gmail");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("resolves a Gmail address to the preset its server lookup agrees with", async () => {
|
|
107
|
+
for (const email of ["user@gmail.com", "User@GoogleMail.com"]) {
|
|
108
|
+
const preset = getPresetById(presetIdForEmail(email));
|
|
109
|
+
assert.ok(preset, email);
|
|
110
|
+
assert.equal(preset.id, "gmail");
|
|
111
|
+
const discovered = await discoverSettings(email);
|
|
112
|
+
assert.ok(discovered, email);
|
|
113
|
+
assert.deepEqual(preset.imap, discovered.imap);
|
|
114
|
+
assert.deepEqual(preset.smtp, discovered.smtp);
|
|
115
|
+
}
|
|
85
116
|
});
|
|
86
117
|
|
|
87
118
|
it("is case-insensitive on the domain", () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { APP_PASSWORD_URLS, type ServerSettings } from "./autodiscovery.js";
|
|
2
2
|
|
|
3
3
|
export interface ProviderPreset {
|
|
4
4
|
id: string;
|
|
@@ -54,6 +54,17 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
|
|
54
54
|
url: "https://www.fastmail.help/hc/en-us/articles/360058752854",
|
|
55
55
|
},
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
id: "gmail",
|
|
59
|
+
label: "Gmail",
|
|
60
|
+
username: "full-email",
|
|
61
|
+
imap: { host: "imap.gmail.com", port: 993, security: "tls" },
|
|
62
|
+
smtp: { host: "smtp.gmail.com", port: 587, security: "starttls" },
|
|
63
|
+
passwordHelp: {
|
|
64
|
+
text: "Gmail requires an app password, not your Google password. First turn on 2-Step Verification for your Google Account, then create an app password under Security → 2-Step Verification → App passwords.",
|
|
65
|
+
url: APP_PASSWORD_URLS["gmail.com"],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
57
68
|
];
|
|
58
69
|
|
|
59
70
|
export function getPresetById(id: string): ProviderPreset | undefined {
|
|
@@ -70,6 +81,8 @@ const DOMAIN_TO_PRESET: Record<string, string> = {
|
|
|
70
81
|
"aol.com": "aol",
|
|
71
82
|
"fastmail.com": "fastmail",
|
|
72
83
|
"fastmail.fm": "fastmail",
|
|
84
|
+
"gmail.com": "gmail",
|
|
85
|
+
"googlemail.com": "gmail",
|
|
73
86
|
};
|
|
74
87
|
|
|
75
88
|
/** Pre-select a provider preset from a known email domain; "" for Custom. */
|