Skip to content
Source
Sign inSign up
Open History to see the last commit

browserImportWizard.logic.ts

150 lines4.9 KB

1import type { BrowserImportFailureReason, BrowserImportSource } from "@t3tools/contracts";
2​
3export interface WizardTargetProfile {
4 readonly id: string;
5 readonly name: string;
6}
7​
8export type WizardTarget =
9 | { readonly kind: "new"; readonly profileId: string }
10 | { readonly kind: "existing"; readonly profileId: string; readonly name: string };
11​
12export type WizardTargetSelection =
13 | { readonly kind: "new" }
14 | { readonly kind: "existing"; readonly profileId: string };
15​
16export function initialTargetSelection(
17 canCreateProfile: boolean,
18 targetProfiles: ReadonlyArray<WizardTargetProfile>,
19): WizardTargetSelection {
20 if (canCreateProfile) return { kind: "new" };
21 const first = targetProfiles[0];
22 return first ? { kind: "existing", profileId: first.id } : { kind: "new" };
24​
25export function resolveWizardTarget(
26 selection: WizardTargetSelection,
27 newProfileId: string,
28 targetProfiles: ReadonlyArray<WizardTargetProfile>,
29): WizardTarget | undefined {
30 if (selection.kind === "new") return { kind: "new", profileId: newProfileId };
31 const profile = targetProfiles.find((candidate) => candidate.id === selection.profileId);
32 if (profile === undefined) return undefined;
33 return {
34 kind: "existing",
35 profileId: selection.profileId,
36 name: profile.name,
37 };
39​
40export type ImportOutcome =
41 | {
42 readonly kind: "imported";
43 readonly imported: number;
44 readonly skipped: number;
45 readonly skippedDomains: ReadonlyArray<string>;
46 readonly targetName: string;
47 }
48 | {
49 readonly kind: "blocked";
50 readonly reason: BrowserImportFailureReason;
51 /** Shown instead of the reason's copy, such as a managed policy's refusal. */
52 readonly message?: string;
53 };
54​
55export type WizardStep =
56 | { readonly step: "quit" }
57 | {
58 readonly step: "fullDiskAccess";
59 readonly resume: "configure" | "import";
60 readonly checked?: boolean;
61 }
62 | { readonly step: "configure" }
63 | { readonly step: "checking"; readonly check: "browser" | "fullDiskAccess" }
64 | { readonly step: "importing" }
65 | {
66 readonly step: "done";
67 readonly imported: number;
68 readonly skipped: number;
69 readonly skippedDomains: ReadonlyArray<string>;
70 readonly targetName: string;
71 }
72 | {
73 readonly step: "blocked";
74 readonly reason: BrowserImportFailureReason;
75 readonly message?: string;
76 };
77​
78export function canCloseWizard(step: WizardStep): boolean {
79 return step.step !== "importing";
81​
82export function initialWizardStep(source: BrowserImportSource): WizardStep {
83 if (source.unavailable === "browserRunning") return { step: "quit" };
84 if (source.unavailable === "needsFullDiskAccess") {
85 return { step: "fullDiskAccess", resume: "configure" };
86 }
87 if (source.unavailable !== undefined) return { step: "blocked", reason: source.unavailable };
88 if (source.profiles.length === 0) return { step: "blocked", reason: "unknownSourceProfile" };
89 return { step: "configure" };
91​
92export function outcomeToStep(outcome: ImportOutcome): WizardStep {
93 if (outcome.kind === "imported") {
94 return {
95 step: "done",
96 imported: outcome.imported,
97 skipped: outcome.skipped,
98 skippedDomains: outcome.skippedDomains,
99 targetName: outcome.targetName,
100 };
101 }
102 if (outcome.reason === "browserRunning") return { step: "quit" };
103 if (outcome.reason === "needsFullDiskAccess") {
104 return { step: "fullDiskAccess", resume: "import", checked: true };
105 }
106 return outcome.message === undefined
107 ? { step: "blocked", reason: outcome.reason }
108 : { step: "blocked", reason: outcome.reason, message: outcome.message };
110​
111export function refreshedSourceStep(source: BrowserImportSource | undefined): WizardStep {
112 if (source === undefined) return { step: "blocked", reason: "unknownSource" };
113 return initialWizardStep(source);
115​
116export function fullDiskAccessRecheckStep(source: BrowserImportSource | undefined): WizardStep {
117 const next = refreshedSourceStep(source);
118 return next.step === "fullDiskAccess" ? { ...next, checked: true } : next;
120​
121export function refreshedSourceProfileDirectory(
122 currentDirectory: string,
123 source: BrowserImportSource,
124): string {
125 if (source.profiles.some((profile) => profile.directory === currentDirectory)) {
126 return currentDirectory;
127 }
128 return source.profiles[0]?.directory ?? "";
130​
131export function isRetryableReason(reason: BrowserImportFailureReason): boolean {
132 switch (reason) {
133 case "needsKeychainApproval":
134 case "keychainItemMissing":
135 case "keychainUnavailable":
136 case "readFailed":
137 case "sessionUnavailable":
138 case "profileNotSaved":
139 return true;
140 default:
141 return false;
142 }
144​
145export function formatSkippedDomains(domains: ReadonlyArray<string>): string {
146 if (domains.length === 0) return "";
147 if (domains.length === 1) return domains[0]!;
148 if (domains.length <= 3) return `${domains.slice(0, -1).join(", ")} and ${domains.at(-1)}`;
149 return `${domains.slice(0, 3).join(", ")} and ${domains.length - 3} more`;