@remit/web-client 0.0.229 → 0.0.230
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/config-import/ConfigImportWizard.flow.test.ts +338 -0
- package/src/components/config-import/ConfigImportWizard.tsx +720 -0
- package/src/components/config-import/config-import.fixtures.ts +229 -0
- package/src/components/config-import/config-import.stories.tsx +449 -0
- package/src/components/config-import/steps.tsx +772 -0
- package/src/components/onboarding/OnboardingWizard.tsx +64 -390
- package/src/components/onboarding/credential-steps.tsx +441 -0
- package/src/components/settings/ConfigExportCard.tsx +114 -0
- package/src/components/settings/ConfigTransferPanel.tsx +72 -0
- package/src/lib/config-import.test.ts +306 -0
- package/src/lib/config-import.ts +492 -0
- package/src/lib/json.ts +11 -0
- package/src/routeTree.gen.ts +21 -0
- package/src/routes/import.tsx +51 -0
- package/src/routes/onboarding.tsx +9 -2
- package/src/routes/settings/accounts.tsx +30 -8
- package/src/routes/settings/advanced.tsx +5 -1
- package/src/routing/sections.ts +3 -1
|
@@ -19,14 +19,12 @@
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
accountOperationsCreateAccountMutation,
|
|
22
|
-
accountOperationsTestConnectionMutation,
|
|
23
22
|
configOperationsGetConfigOptions,
|
|
24
23
|
configOperationsGetConfigQueryKey,
|
|
25
24
|
microsoftOAuthOperationsMicrosoftOAuthStartMutation,
|
|
26
25
|
syncOperationsGetSyncStatusOptions,
|
|
27
26
|
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
28
27
|
import {
|
|
29
|
-
AppPasswordHint,
|
|
30
28
|
Banner,
|
|
31
29
|
Button,
|
|
32
30
|
CheckRow,
|
|
@@ -34,14 +32,13 @@ import {
|
|
|
34
32
|
FieldLabel,
|
|
35
33
|
Input,
|
|
36
34
|
Kbd,
|
|
37
|
-
PasswordInput,
|
|
38
35
|
Select,
|
|
39
36
|
ServerFields,
|
|
40
37
|
securityToApi,
|
|
41
38
|
WizardShell,
|
|
42
39
|
} from "@remit/ui";
|
|
43
40
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
44
|
-
import { AtSign, Inbox, Loader2, Mail, Server } from "lucide-react";
|
|
41
|
+
import { AtSign, FileJson, Inbox, Loader2, Mail, Server } from "lucide-react";
|
|
45
42
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
46
43
|
import {
|
|
47
44
|
REDIRECT_STALL_MESSAGE,
|
|
@@ -53,7 +50,6 @@ import {
|
|
|
53
50
|
discoverSettings,
|
|
54
51
|
getAppPasswordUrl,
|
|
55
52
|
getDiscoveryStatusMessage,
|
|
56
|
-
type SecurityMode,
|
|
57
53
|
} from "../../lib/autodiscovery.js";
|
|
58
54
|
import {
|
|
59
55
|
getPresetById,
|
|
@@ -61,6 +57,11 @@ import {
|
|
|
61
57
|
presetIdForEmail,
|
|
62
58
|
} from "../../lib/provider-presets.js";
|
|
63
59
|
import { computeSmtpAutoFill } from "../settings/account-form-helpers.js";
|
|
60
|
+
import {
|
|
61
|
+
ConnectionTestStep,
|
|
62
|
+
CredentialsStep,
|
|
63
|
+
type ServerConfig,
|
|
64
|
+
} from "./credential-steps.js";
|
|
64
65
|
|
|
65
66
|
/* ------------------------------------------------------------------ */
|
|
66
67
|
/* Wizard step definitions */
|
|
@@ -101,12 +102,6 @@ const STEP_INDEX: Record<WizardStep, number> = {
|
|
|
101
102
|
/* State shapes */
|
|
102
103
|
/* ------------------------------------------------------------------ */
|
|
103
104
|
|
|
104
|
-
interface ServerConfig {
|
|
105
|
-
host: string;
|
|
106
|
-
port: number;
|
|
107
|
-
security: SecurityMode;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
105
|
interface WizardState {
|
|
111
106
|
email: string;
|
|
112
107
|
displayName: string;
|
|
@@ -128,31 +123,22 @@ const DEFAULT_SMTP: ServerConfig = {
|
|
|
128
123
|
security: "starttls",
|
|
129
124
|
};
|
|
130
125
|
|
|
131
|
-
type TestPhase =
|
|
132
|
-
| "idle"
|
|
133
|
-
| "running"
|
|
134
|
-
| "success"
|
|
135
|
-
| "auth-failure"
|
|
136
|
-
| "network-failure";
|
|
137
|
-
|
|
138
|
-
interface TestResult {
|
|
139
|
-
imapState: "pending" | "running" | "ok" | "failed";
|
|
140
|
-
smtpState: "pending" | "running" | "ok" | "failed";
|
|
141
|
-
imapDetail?: string;
|
|
142
|
-
smtpDetail?: string;
|
|
143
|
-
rawError?: string;
|
|
144
|
-
phase: TestPhase;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/* ------------------------------------------------------------------ */
|
|
148
|
-
/* Helpers */
|
|
149
|
-
/* ------------------------------------------------------------------ */
|
|
150
|
-
|
|
151
126
|
/* ------------------------------------------------------------------ */
|
|
152
127
|
/* Individual step components */
|
|
153
128
|
/* ------------------------------------------------------------------ */
|
|
154
129
|
|
|
155
|
-
|
|
130
|
+
/**
|
|
131
|
+
* First run. Importing a config file sits beside adding the first account, not
|
|
132
|
+
* under it: someone arriving from another Reader has already made every choice
|
|
133
|
+
* the wizard is about to ask for, and the file is the shorter road.
|
|
134
|
+
*/
|
|
135
|
+
export function StepWelcome({
|
|
136
|
+
onStart,
|
|
137
|
+
onImportConfig,
|
|
138
|
+
}: {
|
|
139
|
+
onStart: () => void;
|
|
140
|
+
onImportConfig?: () => void;
|
|
141
|
+
}) {
|
|
156
142
|
// Keyboard: Enter advances
|
|
157
143
|
useEffect(() => {
|
|
158
144
|
const handler = (e: KeyboardEvent) => {
|
|
@@ -168,12 +154,26 @@ function StepWelcome({ onStart }: { onStart: () => void }) {
|
|
|
168
154
|
activeStep={0}
|
|
169
155
|
hideSteps
|
|
170
156
|
title="Welcome to Remit"
|
|
171
|
-
subtitle=
|
|
157
|
+
subtitle={
|
|
158
|
+
onImportConfig
|
|
159
|
+
? "Set up your first account, or bring the settings from another Reader."
|
|
160
|
+
: "One inbox for all your mail, with context about every message."
|
|
161
|
+
}
|
|
172
162
|
footer={
|
|
173
163
|
<>
|
|
174
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
164
|
+
{onImportConfig ? (
|
|
165
|
+
<Button
|
|
166
|
+
variant="secondary"
|
|
167
|
+
icon={<FileJson className="size-4" />}
|
|
168
|
+
onClick={onImportConfig}
|
|
169
|
+
>
|
|
170
|
+
Import a config file
|
|
171
|
+
</Button>
|
|
172
|
+
) : (
|
|
173
|
+
<span className="text-2xs text-fg-subtle">
|
|
174
|
+
<Kbd>Enter</Kbd> to continue
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
177
|
<Button variant="primary" onClick={onStart}>
|
|
178
178
|
Add your first account
|
|
179
179
|
</Button>
|
|
@@ -189,6 +189,13 @@ function StepWelcome({ onStart }: { onStart: () => void }) {
|
|
|
189
189
|
writing to you — sender history, authenticity checks, and similar
|
|
190
190
|
messages, right next to every email.
|
|
191
191
|
</p>
|
|
192
|
+
{onImportConfig && (
|
|
193
|
+
<p className="mt-3 max-w-sm text-sm leading-relaxed text-fg-muted">
|
|
194
|
+
A config file carries your accounts, folder roles, rules, flagged
|
|
195
|
+
senders and appearance from another Reader. It never carries
|
|
196
|
+
passwords, so you sign in again here.
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
192
199
|
</div>
|
|
193
200
|
</WizardShell>
|
|
194
201
|
);
|
|
@@ -778,357 +785,6 @@ function StepServers({
|
|
|
778
785
|
);
|
|
779
786
|
}
|
|
780
787
|
|
|
781
|
-
function StepCredentials({
|
|
782
|
-
email,
|
|
783
|
-
username,
|
|
784
|
-
password,
|
|
785
|
-
onContinue,
|
|
786
|
-
onBack,
|
|
787
|
-
onChange,
|
|
788
|
-
}: {
|
|
789
|
-
email: string;
|
|
790
|
-
username: string;
|
|
791
|
-
password: string;
|
|
792
|
-
onContinue: () => void;
|
|
793
|
-
onBack: () => void;
|
|
794
|
-
onChange: (username: string, password: string) => void;
|
|
795
|
-
}) {
|
|
796
|
-
const [localUsername, setLocalUsername] = useState(username || email);
|
|
797
|
-
const [localPassword, setLocalPassword] = useState(password);
|
|
798
|
-
const [error, setError] = useState<string | null>(null);
|
|
799
|
-
|
|
800
|
-
const domain = email.split("@")[1] ?? "";
|
|
801
|
-
const appPasswordUrl = getAppPasswordUrl(email);
|
|
802
|
-
const isValid = localUsername.trim() !== "" && localPassword.trim() !== "";
|
|
803
|
-
|
|
804
|
-
useEffect(() => {
|
|
805
|
-
onChange(localUsername, localPassword);
|
|
806
|
-
}, [localUsername, localPassword, onChange]);
|
|
807
|
-
|
|
808
|
-
const handleContinue = useCallback(() => {
|
|
809
|
-
if (!isValid) {
|
|
810
|
-
setError("Enter your username and password.");
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
setError(null);
|
|
814
|
-
onContinue();
|
|
815
|
-
}, [isValid, onContinue]);
|
|
816
|
-
|
|
817
|
-
// Keyboard: Enter advances, Esc goes back
|
|
818
|
-
useEffect(() => {
|
|
819
|
-
const handler = (e: KeyboardEvent) => {
|
|
820
|
-
if (e.key === "Enter") handleContinue();
|
|
821
|
-
if (e.key === "Escape") onBack();
|
|
822
|
-
};
|
|
823
|
-
window.addEventListener("keydown", handler);
|
|
824
|
-
return () => window.removeEventListener("keydown", handler);
|
|
825
|
-
}, [handleContinue, onBack]);
|
|
826
|
-
|
|
827
|
-
return (
|
|
828
|
-
<WizardShell
|
|
829
|
-
steps={STEP_LABELS}
|
|
830
|
-
activeStep={STEP_INDEX.credentials}
|
|
831
|
-
title={`Sign in to ${domain || "your mail server"}`}
|
|
832
|
-
subtitle="Many providers require an app password instead of your normal one."
|
|
833
|
-
footer={
|
|
834
|
-
<>
|
|
835
|
-
<Button variant="ghost" onClick={onBack}>
|
|
836
|
-
Back
|
|
837
|
-
</Button>
|
|
838
|
-
<Button variant="primary" onClick={handleContinue}>
|
|
839
|
-
Test connection
|
|
840
|
-
</Button>
|
|
841
|
-
</>
|
|
842
|
-
}
|
|
843
|
-
>
|
|
844
|
-
<div className="space-y-3">
|
|
845
|
-
{error && (
|
|
846
|
-
<div className="rounded-md bg-danger-soft px-3 py-2 text-sm text-danger">
|
|
847
|
-
{error}
|
|
848
|
-
</div>
|
|
849
|
-
)}
|
|
850
|
-
<div>
|
|
851
|
-
<FieldLabel htmlFor="credentials-username">Username</FieldLabel>
|
|
852
|
-
<Input
|
|
853
|
-
id="credentials-username"
|
|
854
|
-
value={localUsername}
|
|
855
|
-
onChange={(e) => {
|
|
856
|
-
setLocalUsername(e.target.value);
|
|
857
|
-
setError(null);
|
|
858
|
-
}}
|
|
859
|
-
autoComplete="username"
|
|
860
|
-
/>
|
|
861
|
-
</div>
|
|
862
|
-
<div>
|
|
863
|
-
<FieldLabel htmlFor="credentials-password">
|
|
864
|
-
Password or app password
|
|
865
|
-
</FieldLabel>
|
|
866
|
-
<PasswordInput
|
|
867
|
-
id="credentials-password"
|
|
868
|
-
value={localPassword}
|
|
869
|
-
onChange={(e) => {
|
|
870
|
-
setLocalPassword(e.target.value);
|
|
871
|
-
setError(null);
|
|
872
|
-
}}
|
|
873
|
-
autoComplete="current-password"
|
|
874
|
-
/>
|
|
875
|
-
</div>
|
|
876
|
-
<AppPasswordHint url={appPasswordUrl} />
|
|
877
|
-
</div>
|
|
878
|
-
</WizardShell>
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
function StepTest({
|
|
883
|
-
email,
|
|
884
|
-
imapConfig,
|
|
885
|
-
smtpConfig,
|
|
886
|
-
username,
|
|
887
|
-
password,
|
|
888
|
-
onSuccess,
|
|
889
|
-
onBackToCredentials,
|
|
890
|
-
onBackToServers,
|
|
891
|
-
}: {
|
|
892
|
-
email: string;
|
|
893
|
-
imapConfig: ServerConfig;
|
|
894
|
-
smtpConfig: ServerConfig;
|
|
895
|
-
username: string;
|
|
896
|
-
password: string;
|
|
897
|
-
onSuccess: () => void;
|
|
898
|
-
onBackToCredentials: () => void;
|
|
899
|
-
onBackToServers: () => void;
|
|
900
|
-
}) {
|
|
901
|
-
const [testResult, setTestResult] = useState<TestResult>({
|
|
902
|
-
imapState: "running",
|
|
903
|
-
smtpState: "pending",
|
|
904
|
-
phase: "running",
|
|
905
|
-
});
|
|
906
|
-
const [attempt, setAttempt] = useState(0);
|
|
907
|
-
const smtpConfigured = smtpConfig.host.trim() !== "";
|
|
908
|
-
const imapTls = securityToApi(imapConfig.security);
|
|
909
|
-
const smtpTls = securityToApi(smtpConfig.security);
|
|
910
|
-
|
|
911
|
-
const testMutation = useMutation({
|
|
912
|
-
...accountOperationsTestConnectionMutation(),
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: runs on mount and each Retry via `attempt`; the connection inputs are read intentionally and must not re-trigger the test on every render
|
|
916
|
-
useEffect(() => {
|
|
917
|
-
let cancelled = false;
|
|
918
|
-
const timers: number[] = [];
|
|
919
|
-
|
|
920
|
-
setTestResult({
|
|
921
|
-
imapState: "running",
|
|
922
|
-
smtpState: "pending",
|
|
923
|
-
phase: "running",
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
testMutation.mutate(
|
|
927
|
-
{
|
|
928
|
-
body: {
|
|
929
|
-
username,
|
|
930
|
-
password,
|
|
931
|
-
imapHost: imapConfig.host,
|
|
932
|
-
imapPort: imapConfig.port,
|
|
933
|
-
imapTls: imapTls.tls,
|
|
934
|
-
imapStartTls: imapTls.startTls,
|
|
935
|
-
smtpHost: smtpConfig.host,
|
|
936
|
-
smtpPort: smtpConfig.port,
|
|
937
|
-
smtpTls: smtpTls.tls,
|
|
938
|
-
smtpStartTls: smtpTls.startTls,
|
|
939
|
-
},
|
|
940
|
-
},
|
|
941
|
-
{
|
|
942
|
-
onSuccess: (data) => {
|
|
943
|
-
if (cancelled) return;
|
|
944
|
-
// Show IMAP result first, then SMTP. SMTP is optional: when no
|
|
945
|
-
// host was given the account is receive-only, so IMAP alone
|
|
946
|
-
// decides success.
|
|
947
|
-
const imapOk = data.imapSuccess;
|
|
948
|
-
const smtpOk = smtpConfigured ? data.smtpSuccess : true;
|
|
949
|
-
|
|
950
|
-
setTestResult({
|
|
951
|
-
imapState: imapOk ? "ok" : "failed",
|
|
952
|
-
smtpState: smtpConfigured ? "running" : "ok",
|
|
953
|
-
imapDetail: imapOk
|
|
954
|
-
? `Connected — ${imapConfig.host}:${imapConfig.port}`
|
|
955
|
-
: data.imapError,
|
|
956
|
-
phase: "running",
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
timers.push(
|
|
960
|
-
window.setTimeout(() => {
|
|
961
|
-
if (cancelled) return;
|
|
962
|
-
// An auth error can come from either protocol; route on the
|
|
963
|
-
// one that actually failed. IMAP failures take precedence in
|
|
964
|
-
// the raw-error display since IMAP is tested first.
|
|
965
|
-
const isAuthError = (msg: string | undefined): boolean => {
|
|
966
|
-
const m = msg?.toLowerCase() ?? "";
|
|
967
|
-
return (
|
|
968
|
-
m.includes("auth") ||
|
|
969
|
-
m.includes("login") ||
|
|
970
|
-
m.includes("535") ||
|
|
971
|
-
m.includes("credential")
|
|
972
|
-
);
|
|
973
|
-
};
|
|
974
|
-
const isAuthFailure =
|
|
975
|
-
(!imapOk && isAuthError(data.imapError)) ||
|
|
976
|
-
(imapOk &&
|
|
977
|
-
smtpConfigured &&
|
|
978
|
-
!data.smtpSuccess &&
|
|
979
|
-
isAuthError(data.smtpError));
|
|
980
|
-
|
|
981
|
-
setTestResult({
|
|
982
|
-
imapState: imapOk ? "ok" : "failed",
|
|
983
|
-
smtpState: !smtpConfigured
|
|
984
|
-
? "ok"
|
|
985
|
-
: data.smtpSuccess
|
|
986
|
-
? "ok"
|
|
987
|
-
: "failed",
|
|
988
|
-
imapDetail: imapOk
|
|
989
|
-
? `Connected — ${imapConfig.host}:${imapConfig.port}`
|
|
990
|
-
: data.imapError,
|
|
991
|
-
smtpDetail: !smtpConfigured
|
|
992
|
-
? "Not set up — receive-only"
|
|
993
|
-
: data.smtpSuccess
|
|
994
|
-
? `Connected — ${smtpConfig.host}:${smtpConfig.port}`
|
|
995
|
-
: data.smtpError,
|
|
996
|
-
rawError: !imapOk
|
|
997
|
-
? data.imapError
|
|
998
|
-
: smtpConfigured && !data.smtpSuccess
|
|
999
|
-
? data.smtpError
|
|
1000
|
-
: undefined,
|
|
1001
|
-
phase:
|
|
1002
|
-
imapOk && smtpOk
|
|
1003
|
-
? "success"
|
|
1004
|
-
: isAuthFailure
|
|
1005
|
-
? "auth-failure"
|
|
1006
|
-
: "network-failure",
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
if (imapOk && smtpOk) {
|
|
1010
|
-
timers.push(
|
|
1011
|
-
window.setTimeout(() => {
|
|
1012
|
-
if (!cancelled) onSuccess();
|
|
1013
|
-
}, 800),
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1016
|
-
}, 400),
|
|
1017
|
-
);
|
|
1018
|
-
},
|
|
1019
|
-
onError: (err) => {
|
|
1020
|
-
if (cancelled) return;
|
|
1021
|
-
const msg = err instanceof Error ? err.message : "Connection failed";
|
|
1022
|
-
setTestResult({
|
|
1023
|
-
imapState: "failed",
|
|
1024
|
-
smtpState: "failed",
|
|
1025
|
-
imapDetail: msg,
|
|
1026
|
-
smtpDetail: msg,
|
|
1027
|
-
rawError: msg,
|
|
1028
|
-
phase: "network-failure",
|
|
1029
|
-
});
|
|
1030
|
-
},
|
|
1031
|
-
},
|
|
1032
|
-
);
|
|
1033
|
-
|
|
1034
|
-
return () => {
|
|
1035
|
-
cancelled = true;
|
|
1036
|
-
for (const t of timers) window.clearTimeout(t);
|
|
1037
|
-
};
|
|
1038
|
-
}, [attempt]);
|
|
1039
|
-
|
|
1040
|
-
const { phase } = testResult;
|
|
1041
|
-
|
|
1042
|
-
// Keyboard: Esc goes back based on failure type
|
|
1043
|
-
useEffect(() => {
|
|
1044
|
-
const handler = (e: KeyboardEvent) => {
|
|
1045
|
-
if (e.key === "Escape") {
|
|
1046
|
-
if (phase === "auth-failure") onBackToCredentials();
|
|
1047
|
-
else if (phase === "network-failure") onBackToServers();
|
|
1048
|
-
}
|
|
1049
|
-
};
|
|
1050
|
-
window.addEventListener("keydown", handler);
|
|
1051
|
-
return () => window.removeEventListener("keydown", handler);
|
|
1052
|
-
}, [phase, onBackToCredentials, onBackToServers]);
|
|
1053
|
-
|
|
1054
|
-
const footer =
|
|
1055
|
-
phase === "auth-failure" ? (
|
|
1056
|
-
<>
|
|
1057
|
-
<Button variant="ghost" onClick={onBackToCredentials}>
|
|
1058
|
-
Back to credentials
|
|
1059
|
-
</Button>
|
|
1060
|
-
<Button variant="primary" onClick={() => setAttempt((a) => a + 1)}>
|
|
1061
|
-
Retry
|
|
1062
|
-
</Button>
|
|
1063
|
-
</>
|
|
1064
|
-
) : phase === "network-failure" ? (
|
|
1065
|
-
<>
|
|
1066
|
-
<Button variant="ghost" onClick={onBackToServers}>
|
|
1067
|
-
Back to servers
|
|
1068
|
-
</Button>
|
|
1069
|
-
<Button variant="primary" onClick={() => setAttempt((a) => a + 1)}>
|
|
1070
|
-
Retry
|
|
1071
|
-
</Button>
|
|
1072
|
-
</>
|
|
1073
|
-
) : phase === "success" ? (
|
|
1074
|
-
<>
|
|
1075
|
-
<span className="text-2xs text-fg-subtle">Connection verified</span>
|
|
1076
|
-
<Button variant="primary" onClick={onSuccess}>
|
|
1077
|
-
Start syncing
|
|
1078
|
-
</Button>
|
|
1079
|
-
</>
|
|
1080
|
-
) : (
|
|
1081
|
-
<>
|
|
1082
|
-
<span className="text-2xs text-fg-subtle">Testing…</span>
|
|
1083
|
-
<span />
|
|
1084
|
-
</>
|
|
1085
|
-
);
|
|
1086
|
-
|
|
1087
|
-
return (
|
|
1088
|
-
<WizardShell
|
|
1089
|
-
steps={STEP_LABELS}
|
|
1090
|
-
activeStep={STEP_INDEX.test}
|
|
1091
|
-
title="Testing the connection"
|
|
1092
|
-
subtitle={`Checking ${email}`}
|
|
1093
|
-
footer={footer}
|
|
1094
|
-
>
|
|
1095
|
-
<div className="divide-y divide-line">
|
|
1096
|
-
<CheckRow
|
|
1097
|
-
label={`IMAP — ${imapConfig.host}:${imapConfig.port}`}
|
|
1098
|
-
detail={testResult.imapDetail}
|
|
1099
|
-
state={testResult.imapState}
|
|
1100
|
-
/>
|
|
1101
|
-
<CheckRow
|
|
1102
|
-
label={
|
|
1103
|
-
smtpConfigured
|
|
1104
|
-
? `SMTP — ${smtpConfig.host}:${smtpConfig.port}`
|
|
1105
|
-
: "SMTP — sending"
|
|
1106
|
-
}
|
|
1107
|
-
detail={
|
|
1108
|
-
smtpConfigured
|
|
1109
|
-
? testResult.smtpDetail
|
|
1110
|
-
: "Not set up — you can add sending later in Settings"
|
|
1111
|
-
}
|
|
1112
|
-
state={testResult.smtpState}
|
|
1113
|
-
/>
|
|
1114
|
-
{(phase === "auth-failure" || phase === "network-failure") &&
|
|
1115
|
-
testResult.rawError && (
|
|
1116
|
-
<div className="py-3">
|
|
1117
|
-
{phase === "auth-failure" && (
|
|
1118
|
-
<p className="mb-2 text-xs text-fg-muted">
|
|
1119
|
-
Check your password — many providers require an app password.
|
|
1120
|
-
</p>
|
|
1121
|
-
)}
|
|
1122
|
-
<code className="block rounded bg-surface-sunken px-2.5 py-2 text-2xs text-fg-muted">
|
|
1123
|
-
{testResult.rawError}
|
|
1124
|
-
</code>
|
|
1125
|
-
</div>
|
|
1126
|
-
)}
|
|
1127
|
-
</div>
|
|
1128
|
-
</WizardShell>
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
788
|
/**
|
|
1133
789
|
* How long the sync step holds "Go to inbox" shut while it waits for the first
|
|
1134
790
|
* messages. Long enough for a normal IMAP handshake and first batch, short
|
|
@@ -1494,12 +1150,19 @@ export interface OnboardingWizardProps {
|
|
|
1494
1150
|
onComplete: (accountId: string) => void;
|
|
1495
1151
|
/** Called when the user cancels (Esc from connector picker) */
|
|
1496
1152
|
onCancel?: () => void;
|
|
1153
|
+
/**
|
|
1154
|
+
* Offered on the welcome step only, beside adding the first account. Absent
|
|
1155
|
+
* where the wizard is embedded to add an account to an instance that already
|
|
1156
|
+
* has one — importing a configuration is a first-run move, not an add.
|
|
1157
|
+
*/
|
|
1158
|
+
onImportConfig?: () => void;
|
|
1497
1159
|
}
|
|
1498
1160
|
|
|
1499
1161
|
export function OnboardingWizard({
|
|
1500
1162
|
skipWelcome = false,
|
|
1501
1163
|
onComplete,
|
|
1502
1164
|
onCancel,
|
|
1165
|
+
onImportConfig,
|
|
1503
1166
|
}: OnboardingWizardProps) {
|
|
1504
1167
|
const [step, setStep] = useState<WizardStep>(
|
|
1505
1168
|
skipWelcome ? "connector" : "welcome",
|
|
@@ -1599,7 +1262,12 @@ export function OnboardingWizard({
|
|
|
1599
1262
|
|
|
1600
1263
|
switch (step) {
|
|
1601
1264
|
case "welcome":
|
|
1602
|
-
return
|
|
1265
|
+
return (
|
|
1266
|
+
<StepWelcome
|
|
1267
|
+
onStart={handleWelcomeStart}
|
|
1268
|
+
onImportConfig={onImportConfig}
|
|
1269
|
+
/>
|
|
1270
|
+
);
|
|
1603
1271
|
|
|
1604
1272
|
case "connector":
|
|
1605
1273
|
return (
|
|
@@ -1644,7 +1312,11 @@ export function OnboardingWizard({
|
|
|
1644
1312
|
|
|
1645
1313
|
case "credentials":
|
|
1646
1314
|
return (
|
|
1647
|
-
<
|
|
1315
|
+
<CredentialsStep
|
|
1316
|
+
steps={STEP_LABELS}
|
|
1317
|
+
activeStep={STEP_INDEX.credentials}
|
|
1318
|
+
title={`Sign in to ${state.email.split("@")[1] || "your mail server"}`}
|
|
1319
|
+
subtitle="Many providers require an app password instead of your normal one."
|
|
1648
1320
|
email={state.email}
|
|
1649
1321
|
username={state.username}
|
|
1650
1322
|
password={state.password}
|
|
@@ -1656,7 +1328,9 @@ export function OnboardingWizard({
|
|
|
1656
1328
|
|
|
1657
1329
|
case "test":
|
|
1658
1330
|
return (
|
|
1659
|
-
<
|
|
1331
|
+
<ConnectionTestStep
|
|
1332
|
+
steps={STEP_LABELS}
|
|
1333
|
+
activeStep={STEP_INDEX.test}
|
|
1660
1334
|
email={state.email}
|
|
1661
1335
|
imapConfig={state.imapConfig}
|
|
1662
1336
|
smtpConfig={state.smtpConfig}
|