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; /** 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; /** Re-checks the source's availability after the user quits the browser. */ readonly onRefreshSource: () => Promise; /** 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(() => initialWizardStep(initialSource)); const [sourceProfileDirectory, setSourceProfileDirectory] = useState( () => initialSource.profiles[0]?.directory ?? "", ); const [target, setTarget] = useState(() => initialTargetSelection(canCreateProfile, targetProfiles), ); const [targetError, setTargetError] = useState(); // 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 ( (open || !canCloseWizard(step) ? undefined : onClose())}> {step.step === "quit" ? ( ) : step.step === "fullDiskAccess" ? ( ) : step.step === "importing" ? ( ) : step.step === "checking" ? ( ) : step.step === "done" ? ( ) : step.step === "blocked" ? ( ) : ( { setTarget(selection); setTargetError(undefined); }} targetError={targetError} onCancel={onClose} onImport={runImport} /> )} ); } function QuitStep({ source, onCancel, onRechecked, }: { readonly source: BrowserImportSource; readonly onCancel: () => void; readonly onRechecked: () => void; }) { return ( <> Quit {source.name} to import {source.name} is open, so its cookies can’t be read yet. Quit it, then continue. ); } /** "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; 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 ( <> Let T3 Code read {source.name}’s cookies 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. {stillRequired ? (

Full Disk Access is still required. If you just turned it on, quit and reopen T3 Code, then try again.

) : null} ); } 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 ( <> Import from {source.name} Choose which cookies to import for {destinationEnvironmentName}. {/* Side by side when the dialog has room, stacked when it doesn't. */}

From

{source.profiles.map((profile) => ( onSourceProfileChange(profile.directory)} /> ))}

Into

{canCreateProfile ? ( onTargetChange({ kind: "new" })} /> ) : null} {targetProfiles.map((profile) => ( onTargetChange({ kind: "existing", profileId: profile.id })} /> ))}
{targetFeedback ? (

{targetFeedback}

) : null}
); } /** 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 ( ); } function ImportingStep() { return ( <> Importing cookies This may take a moment. Importing… ); } function CheckingStep({ sourceName, check, }: { readonly sourceName: string; readonly check: "browser" | "fullDiskAccess"; }) { return ( <> Checking {sourceName} {check === "fullDiskAccess" ? "Checking Full Disk Access." : "Checking whether the browser has closed."} {check === "fullDiskAccess" ? "Checking access…" : "Checking…"} ); } function DoneStep({ imported, skipped, skippedDomains, targetName, destinationEnvironmentName, onClose, }: { readonly imported: number; readonly skipped: number; readonly skippedDomains: ReadonlyArray; readonly targetName: string; readonly destinationEnvironmentName: string; readonly onClose: () => void; }) { return ( <> {imported > 0 ? `Imported ${cookieResultCount(imported)}` : skipped > 0 ? `Skipped ${cookieResultCount(skipped)}` : "No cookies found"} {imported > 0 ? `Added to ${targetName} for ${destinationEnvironmentName}.${skipped > 0 ? ` ${cookieResultCount(skipped)} skipped.` : ""}` : skipped > 0 ? `No cookies were imported for ${destinationEnvironmentName}.` : `There were no cookies to import for ${destinationEnvironmentName}.`} {skippedDomains.length > 0 ? (

Skipped

{formatSkippedDomains(skippedDomains)}

) : null} } onClick={onClose}> Done ); } function BlockedStep({ source, reason, onClose, onRetry, }: { readonly source: BrowserImportSource; readonly reason: keyof typeof BROWSER_IMPORT_FAILURE_COPY; readonly onClose: () => void; readonly onRetry: (() => void) | undefined; }) { return ( <> Couldn’t import from {source.name} {BROWSER_IMPORT_FAILURE_COPY[reason]} {onRetry ? : null} ); }