174 lines · 6.4 KB · TypeScript
RawDownload
import type { BrowserImportFailureReason, BrowserImportSource } from "@t3tools/contracts"; export interface WizardTargetProfile {  readonly id: string;  readonly name: string;} export type WizardTarget =  | { readonly kind: "new"; readonly profileId: string }  | { readonly kind: "existing"; readonly profileId: string; readonly name: string }; export type WizardTargetSelection =  | { readonly kind: "new" }  | { readonly kind: "existing"; readonly profileId: string }; export function initialTargetSelection(  canCreateProfile: boolean,  targetProfiles: ReadonlyArray<WizardTargetProfile>,): WizardTargetSelection {  if (canCreateProfile) return { kind: "new" };  const first = targetProfiles[0];  return first ? { kind: "existing", profileId: first.id } : { kind: "new" };} export function resolveWizardTarget(  selection: WizardTargetSelection,  newProfileId: string,  targetProfiles: ReadonlyArray<WizardTargetProfile>,): WizardTarget | undefined {  if (selection.kind === "new") return { kind: "new", profileId: newProfileId };  const profile = targetProfiles.find((candidate) => candidate.id === selection.profileId);  if (profile === undefined) return undefined;  return {    kind: "existing",    profileId: selection.profileId,    name: profile.name,  };} /** * What the import wizard produces once it has actually tried to import. The * parent runs the import and classifies the result; the wizard only reacts to * it, which keeps the step transitions pure and testable. */export type ImportOutcome =  | {      readonly kind: "imported";      readonly imported: number;      readonly skipped: number;      readonly skippedDomains: ReadonlyArray<string>;      readonly targetName: string;    }  | { readonly kind: "blocked"; readonly reason: BrowserImportFailureReason }; /** * The wizard's screens. Every one is a place the user can act from — there are * no dead ends. `blocked` covers the reasons no local step recovers. */export type WizardStep =  | { readonly step: "quit" }  | {      readonly step: "fullDiskAccess";      readonly resume: "configure" | "import";      readonly checked?: boolean;    }  | { readonly step: "configure" }  | { readonly step: "checking"; readonly check: "browser" | "fullDiskAccess" }  | { readonly step: "importing" }  | {      readonly step: "done";      readonly imported: number;      readonly skipped: number;      readonly skippedDomains: ReadonlyArray<string>;      readonly targetName: string;    }  | { readonly step: "blocked"; readonly reason: BrowserImportFailureReason }; /** The import owns its target partition until the write finishes. */export function canCloseWizard(step: WizardStep): boolean {  return step.step !== "importing";} /** * Where the wizard opens for a source. A running browser is the one thing we * know up front, from the source listing; everything else is discovered by * trying, so the wizard starts by letting the user choose what to import. */export function initialWizardStep(source: BrowserImportSource): WizardStep {  if (source.unavailable === "browserRunning") return { step: "quit" };  if (source.unavailable === "needsFullDiskAccess") {    return { step: "fullDiskAccess", resume: "configure" };  }  if (source.unavailable !== undefined) return { step: "blocked", reason: source.unavailable };  if (source.profiles.length === 0) return { step: "blocked", reason: "unknownSourceProfile" };  return { step: "configure" };} /** Where an attempted import lands the wizard, by how it turned out. */export function outcomeToStep(outcome: ImportOutcome): WizardStep {  if (outcome.kind === "imported") {    return {      step: "done",      imported: outcome.imported,      skipped: outcome.skipped,      skippedDomains: outcome.skippedDomains,      targetName: outcome.targetName,    };  }  // A browser that reopened mid-import routes back to the quit screen; every  // other failure surfaces on the blocked screen, which offers a retry when  // one could help.  if (outcome.reason === "browserRunning") return { step: "quit" };  if (outcome.reason === "needsFullDiskAccess") {    // The failed import already checked access, so explain that it is still    // denied instead of returning to an indistinguishable permission screen.    return { step: "fullDiskAccess", resume: "import", checked: true };  }  return { step: "blocked", reason: outcome.reason };} /** Where a fresh availability check lands the wizard after the user quits. */export function refreshedSourceStep(source: BrowserImportSource | undefined): WizardStep {  if (source === undefined) return { step: "blocked", reason: "unknownSource" };  return initialWizardStep(source);} /** A denied FDA recheck returns to the permission step with visible feedback. */export function fullDiskAccessRecheckStep(source: BrowserImportSource | undefined): WizardStep {  const next = refreshedSourceStep(source);  return next.step === "fullDiskAccess" ? { ...next, checked: true } : next;} /** Preserve the chosen source profile when a post-quit refresh still lists it. */export function refreshedSourceProfileDirectory(  currentDirectory: string,  source: BrowserImportSource,): string {  if (source.profiles.some((profile) => profile.directory === currentDirectory)) {    return currentDirectory;  }  return source.profiles[0]?.directory ?? "";} /** * Whether retrying could clear a failure. The keychain prompt can be approved * on a second try, a missing key appears once the user signs in to the * browser (which is what its copy asks for), and a read or session error may * be transient; an unsupported browser will not change, so it gets no retry * button. */export function isRetryableReason(reason: BrowserImportFailureReason): boolean {  switch (reason) {    case "needsKeychainApproval":    case "keychainItemMissing":    case "keychainUnavailable":    case "readFailed":    case "sessionUnavailable":    case "profileNotSaved":      return true;    default:      return false;  }} /** * Names the sites whose cookies were skipped: "example.com and google.com", * or "a, b, c and 4 more" past a few, so the line stays short. */export function formatSkippedDomains(domains: ReadonlyArray<string>): string {  if (domains.length === 0) return "";  if (domains.length === 1) return domains[0]!;  if (domains.length <= 3) return `${domains.slice(0, -1).join(", ")} and ${domains.at(-1)}`;  return `${domains.slice(0, 3).join(", ")} and ${domains.length - 3} more`;}