549 lines · 18.2 KB · TypeScript
RawDownload
import type { BrowserImportSource } from "@t3tools/contracts";import { BROWSER_IMPORT_FAILURE_COPY } from "@t3tools/contracts";import { ArrowDownIcon, ArrowRightIcon, CheckIcon } from "lucide-react";import { useRef, useState } from "react"; import { cn, randomUUID } from "~/lib/utils"; import { Button } from "../ui/button";import {  Dialog,  DialogClose,  DialogDescription,  DialogFooter,  DialogHeader,  DialogPanel,  DialogPopup,  DialogTitle,} from "../ui/dialog";import { Spinner } from "../ui/spinner";import {  initialWizardStep,  initialTargetSelection,  canCloseWizard,  isRetryableReason,  formatSkippedDomains,  fullDiskAccessRecheckStep,  outcomeToStep,  refreshedSourceProfileDirectory,  refreshedSourceStep,  resolveWizardTarget,  type ImportOutcome,  type WizardTarget,  type WizardTargetProfile,  type WizardTargetSelection,  type WizardStep,} from "./browserImportWizard.logic"; export type { WizardTarget } from "./browserImportWizard.logic"; interface BrowserImportWizardProps {  readonly source: BrowserImportSource;  /** Captured when the wizard opens so destination copy and writes stay stable. */  readonly destinationEnvironmentName: string;  /** Existing profiles the import can go into. Incognito is excluded upstream. */  readonly targetProfiles: ReadonlyArray<WizardTargetProfile>;  /** Whether a new profile can still be created (profile cap). */  readonly canCreateProfile: boolean;  /**   * Runs the import and returns how it went. For a new target the caller only   * registers the profile once the import succeeds, so a blocked attempt never   * leaves an empty profile behind.   */  readonly onImport: (input: {    readonly sourceProfileDirectory: string;    readonly target: WizardTarget;  }) => Promise<ImportOutcome>;  /** Re-checks the source's availability after the user quits the browser. */  readonly onRefreshSource: () => Promise<BrowserImportSource | undefined>;  /** Opens the OS setting that grants access to a protected cookie store. */  readonly onOpenFullDiskAccessSettings: () => void;  readonly onClose: () => void;} /** * Guides one browser's cookies into a profile. * * Every state the import can be in — the browser is open, a profile has to be * chosen, the read failed — is a screen the user can move forward from, rather * than a disabled row that only says no. */export function BrowserImportWizard({  source: initialSource,  destinationEnvironmentName,  targetProfiles,  canCreateProfile,  onImport,  onRefreshSource,  onOpenFullDiskAccessSettings,  onClose,}: BrowserImportWizardProps) {  const [source, setSource] = useState(initialSource);  const [step, setStep] = useState<WizardStep>(() => initialWizardStep(initialSource));  const [sourceProfileDirectory, setSourceProfileDirectory] = useState(    () => initialSource.profiles[0]?.directory ?? "",  );  const [target, setTarget] = useState<WizardTargetSelection>(() =>    initialTargetSelection(canCreateProfile, targetProfiles),  );  const [targetError, setTargetError] = useState<string>();  // Stable across retries so a keychain re-approval lands in one profile, not  // a new one each time.  const newProfileId = useRef(`profile-${randomUUID()}`);  // A second Import click before React has left the configure screen would  // start a second run; the parent refuses it, and applying that refusal here  // would drop the wizard out of the importing step while the first write is  // still going. The ref settles synchronously where state does not.  const importInFlight = useRef(false);   const runImport = () => {    if (importInFlight.current) return;    const chosen = resolveWizardTarget(target, newProfileId.current, targetProfiles);    if (chosen === undefined) {      setTargetError("That profile is no longer available. Choose where to import these cookies.");      setStep({ step: "configure" });      return;    }    setTargetError(undefined);    importInFlight.current = true;    setStep({ step: "importing" });    void onImport({ sourceProfileDirectory, target: chosen })      .then((outcome) => setStep(outcomeToStep(outcome)))      .catch(() => setStep({ step: "blocked", reason: "readFailed" }))      .finally(() => {        importInFlight.current = false;      });  };   // Re-lists the source after the user did something outside the app (quit the  // browser, granted access) and routes to wherever the refreshed source says.  const recheckSource = (    check: "browser" | "fullDiskAccess",    nextStep: (refreshed: BrowserImportSource | undefined) => WizardStep,  ) => {    setStep({ step: "checking", check });    void onRefreshSource()      .then((refreshed) => {        if (refreshed) {          setSource(refreshed);          setSourceProfileDirectory((current) =>            refreshedSourceProfileDirectory(current, refreshed),          );        }        setStep(nextStep(refreshed));      })      .catch(() => setStep({ step: "blocked", reason: "readFailed" }));  };  const recheckAfterQuit = () => recheckSource("browser", refreshedSourceStep);  const recheckFullDiskAccess = () => recheckSource("fullDiskAccess", fullDiskAccessRecheckStep);   return (    <Dialog open onOpenChange={(open) => (open || !canCloseWizard(step) ? undefined : onClose())}>      <DialogPopup className="max-w-lg" showCloseButton={canCloseWizard(step)}>        {step.step === "quit" ? (          <QuitStep source={source} onCancel={onClose} onRechecked={recheckAfterQuit} />        ) : step.step === "fullDiskAccess" ? (          <FullDiskAccessStep            source={source}            onCancel={onClose}            onOpenSettings={onOpenFullDiskAccessSettings}            onGranted={step.resume === "import" ? runImport : recheckFullDiskAccess}            stillRequired={step.checked === true}          />        ) : step.step === "importing" ? (          <ImportingStep />        ) : step.step === "checking" ? (          <CheckingStep sourceName={source.name} check={step.check} />        ) : step.step === "done" ? (          <DoneStep            {...step}            destinationEnvironmentName={destinationEnvironmentName}            onClose={onClose}          />        ) : step.step === "blocked" ? (          <BlockedStep            source={source}            reason={step.reason}            onClose={onClose}            onRetry={isRetryableReason(step.reason) ? runImport : undefined}          />        ) : (          <ConfigureStep            source={source}            destinationEnvironmentName={destinationEnvironmentName}            targetProfiles={targetProfiles}            canCreateProfile={canCreateProfile}            sourceProfileDirectory={sourceProfileDirectory}            onSourceProfileChange={setSourceProfileDirectory}            target={target}            onTargetChange={(selection) => {              setTarget(selection);              setTargetError(undefined);            }}            targetError={targetError}            onCancel={onClose}            onImport={runImport}          />        )}      </DialogPopup>    </Dialog>  );} function QuitStep({  source,  onCancel,  onRechecked,}: {  readonly source: BrowserImportSource;  readonly onCancel: () => void;  readonly onRechecked: () => void;}) {  return (    <>      <DialogHeader>        <DialogTitle>Quit {source.name} to import</DialogTitle>        <DialogDescription>          {source.name} is open, so its cookies can&rsquo;t be read yet. Quit it, then continue.        </DialogDescription>      </DialogHeader>      <DialogFooter>        <Button variant="outline" onClick={onCancel}>          Cancel        </Button>        <Button onClick={onRechecked}>I&rsquo;ve quit it</Button>      </DialogFooter>    </>  );} /** "5,065 cookies", or "no cookies", or nothing when the store is unreadable. */function cookieCountLabel(count: number | undefined): string | undefined {  if (count === undefined) return undefined;  if (count === 0) return "no cookies";  return `${count.toLocaleString()} ${count === 1 ? "cookie" : "cookies"}`;} function cookieResultCount(count: number): string {  return `${count.toLocaleString()} ${count === 1 ? "cookie" : "cookies"}`;} type ConfigureStepProps = {  readonly source: BrowserImportSource;  readonly destinationEnvironmentName: string;  readonly targetProfiles: ReadonlyArray<WizardTargetProfile>;  readonly canCreateProfile: boolean;  readonly sourceProfileDirectory: string;  readonly onSourceProfileChange: (directory: string) => void;  readonly target: WizardTargetSelection;  readonly targetError: string | undefined;  readonly onTargetChange: (target: WizardTargetSelection) => void;  readonly onCancel: () => void;  readonly onImport: () => void;}; function FullDiskAccessStep({  source,  onCancel,  onOpenSettings,  onGranted,  stillRequired,}: {  readonly source: BrowserImportSource;  readonly onCancel: () => void;  readonly onOpenSettings: () => void;  readonly onGranted: () => void;  readonly stillRequired: boolean;}) {  return (    <>      <DialogHeader>        <DialogTitle>Let T3 Code read {source.name}&rsquo;s cookies</DialogTitle>        <DialogDescription>          To import cookies from {source.name}, T3 Code needs Full Disk Access. Turn it on in System          Settings, then come back to finish the import — you can revoke it again once the import is          done.        </DialogDescription>      </DialogHeader>      {stillRequired ? (        <DialogPanel>          <p role="status" className="text-sm text-muted-foreground">            Full Disk Access is still required. If you just turned it on, quit and reopen T3 Code,            then try again.          </p>        </DialogPanel>      ) : null}      <DialogFooter>        <Button variant="outline" onClick={onCancel}>          Cancel        </Button>        <Button variant="outline" onClick={onOpenSettings}>          Open System Settings        </Button>        <Button onClick={onGranted}>I&rsquo;ve turned it on</Button>      </DialogFooter>    </>  );}function ConfigureStep({  source,  destinationEnvironmentName,  targetProfiles,  canCreateProfile,  sourceProfileDirectory,  onSourceProfileChange,  target,  targetError,  onTargetChange,  onCancel,  onImport,}: ConfigureStepProps) {  const targetMissing =    target.kind === "existing" &&    !targetProfiles.some((profile) => profile.id === target.profileId);  // The "New profile" tile is unrendered once the cap is reached, so a target  // chosen before that leaves nothing selected in "Into" — say so, the same  // way a vanished existing target is explained.  const targetUncreatable = target.kind === "new" && !canCreateProfile;  const targetFeedback =    targetError ??    (targetMissing      ? "That profile is no longer available. Choose where to import these cookies."      : targetUncreatable        ? "You've reached the profile limit. Choose an existing profile to import into."        : undefined);  return (    <>      <DialogHeader>        <DialogTitle>Import from {source.name}</DialogTitle>        <DialogDescription>          Choose which cookies to import for {destinationEnvironmentName}.        </DialogDescription>      </DialogHeader>      <DialogPanel>        {/* Side by side when the dialog has room, stacked when it doesn't. */}        <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">          <section className="flex-1 space-y-2">            <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">              From            </p>            {source.profiles.map((profile) => (              <SelectableTile                key={profile.directory}                selected={sourceProfileDirectory === profile.directory}                title={profile.name}                subtitle={cookieCountLabel(profile.cookieCount)}                onSelect={() => onSourceProfileChange(profile.directory)}              />            ))}          </section>          <div className="flex shrink-0 items-center justify-center text-muted-foreground">            <ArrowDownIcon className="size-4 sm:hidden" />            <ArrowRightIcon className="hidden size-4 sm:block" />          </div>          <section className="flex-1 space-y-2">            <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">              Into            </p>            {canCreateProfile ? (              <SelectableTile                selected={target.kind === "new"}                title="New profile"                subtitle="Created for these cookies"                onSelect={() => onTargetChange({ kind: "new" })}              />            ) : null}            {targetProfiles.map((profile) => (              <SelectableTile                key={profile.id}                selected={target.kind === "existing" && target.profileId === profile.id}                title={profile.name}                subtitle="Existing profile"                onSelect={() => onTargetChange({ kind: "existing", profileId: profile.id })}              />            ))}          </section>        </div>        {targetFeedback ? (          <p role="alert" className="mt-3 text-sm text-destructive">            {targetFeedback}          </p>        ) : null}      </DialogPanel>      <DialogFooter>        <Button variant="outline" onClick={onCancel}>          Cancel        </Button>        <Button          disabled={sourceProfileDirectory === "" || targetMissing || targetUncreatable}          onClick={onImport}        >          Import        </Button>      </DialogFooter>    </>  );} /** One selectable option: a name, an optional detail line, and a check. */function SelectableTile({  selected,  title,  subtitle,  onSelect,}: {  readonly selected: boolean;  readonly title: string;  readonly subtitle?: string | undefined;  readonly onSelect: () => void;}) {  return (    <button      type="button"      aria-pressed={selected}      onClick={onSelect}      className={cn(        "flex w-full cursor-pointer items-center justify-between gap-3 rounded-lg border px-3 py-2 text-left outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",        selected          ? "border-primary bg-primary/8"          : "border-border/60 hover:border-border hover:bg-muted/40",      )}    >      <span className="min-w-0">        <span className="block truncate text-sm font-medium text-foreground">{title}</span>        {subtitle ? (          <span className="block truncate text-xs tabular-nums text-muted-foreground">            {subtitle}          </span>        ) : null}      </span>      <span        className={cn(          "grid size-4 shrink-0 place-items-center rounded-full border",          selected ? "border-primary bg-primary text-primary-foreground" : "border-input",        )}      >        {selected ? <CheckIcon className="size-2.5" /> : null}      </span>    </button>  );} function ImportingStep() {  return (    <>      <DialogHeader>        <DialogTitle>Importing cookies</DialogTitle>        <DialogDescription>This may take a moment.</DialogDescription>      </DialogHeader>      <DialogPanel className="flex items-center gap-3 py-6">        <Spinner className="size-4 text-muted-foreground" />        <span className="text-sm text-muted-foreground">Importing…</span>      </DialogPanel>    </>  );} function CheckingStep({  sourceName,  check,}: {  readonly sourceName: string;  readonly check: "browser" | "fullDiskAccess";}) {  return (    <>      <DialogHeader>        <DialogTitle>Checking {sourceName}</DialogTitle>        <DialogDescription>          {check === "fullDiskAccess"            ? "Checking Full Disk Access."            : "Checking whether the browser has closed."}        </DialogDescription>      </DialogHeader>      <DialogPanel className="flex items-center gap-3 py-6">        <Spinner className="size-4 text-muted-foreground" />        <span className="text-sm text-muted-foreground">          {check === "fullDiskAccess" ? "Checking access…" : "Checking…"}        </span>      </DialogPanel>    </>  );} function DoneStep({  imported,  skipped,  skippedDomains,  targetName,  destinationEnvironmentName,  onClose,}: {  readonly imported: number;  readonly skipped: number;  readonly skippedDomains: ReadonlyArray<string>;  readonly targetName: string;  readonly destinationEnvironmentName: string;  readonly onClose: () => void;}) {  return (    <>      <DialogHeader>        <DialogTitle>          {imported > 0            ? `Imported ${cookieResultCount(imported)}`            : skipped > 0              ? `Skipped ${cookieResultCount(skipped)}`              : "No cookies found"}        </DialogTitle>        <DialogDescription>          {imported > 0

Lines 1–500 of 549.