@@ -54,6 +54,8 @@ import {
5454 MenuSubTrigger ,
5555 MenuTrigger ,
5656} from "../ui/menu" ;
57+ import { readLocalApi } from "~/localApi" ;
58+
5759import { toastManager } from "../ui/toast" ;
5860import {
5961 AlertDialog ,
@@ -117,7 +119,7 @@ const zoomLabel = (zoomFactor: number) => `${Math.round(zoomFactor * 100)}%`;
117119 * it. Anything unrecognised reads as a plain read failure rather than leaking
118120 * the raw message into a toast.
119121 */
120- const importFailureReason = ( cause : unknown ) : BrowserImportFailureReason => {
122+ export const importFailureReason = ( cause : unknown ) : BrowserImportFailureReason => {
121123 const message = String ( ( cause as { message ?: unknown } | undefined ) ?. message ?? "" ) ;
122124 return (
123125 BrowserImportFailureReason . literals . find ( ( reason ) => message . includes ( `failed: ${ reason } .` ) ) ??
@@ -520,13 +522,32 @@ function DesktopOnlyBrowserDefaults({ children }: { readonly children: ReactNode
520522 * files, and the answer changes while the app is running (quitting the browser
521523 * clears `browserRunning`), so a value cached at mount would go stale.
522524 */
525+ /**
526+ * Opens System Settings → Privacy & Security → Full Disk Access. The scheme is
527+ * unchanged from the old System Preferences and still resolves on Ventura and
528+ * later.
529+ */
530+ const FULL_DISK_ACCESS_SETTINGS_URL =
531+ "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFilesAccess" ;
532+
523533function BrowserProfilesSetting ( { disabled } : { readonly disabled : boolean } ) {
524534 const userProfiles = useClientSettings ( ( settings ) => settings . browserProfiles ) ;
525535 const defaultProfileId = useClientSettings ( ( settings ) => settings . browserDefaultProfileId ) ;
526536 const updateSettings = useUpdatePrimarySettings ( ) ;
527537 const environmentId = usePrimaryEnvironment ( ) ?. environmentId ;
528538 const [ sources , setSources ] = useState < ReadonlyArray < BrowserImportSource > | null > ( null ) ;
529539 const [ busy , setBusy ] = useState ( false ) ;
540+ // Reading Safari's cookies needs Full Disk Access, which the user won't have
541+ // granted before their first import — so this is an expected step, not an
542+ // error. The whole request is held so the dialog's confirm button can run
543+ // the import itself once access is on, instead of sending the user back to
544+ // the menu.
545+ const [ pendingFullDiskAccessImport , setPendingFullDiskAccessImport ] = useState < {
546+ readonly source : BrowserImportSource ;
547+ readonly sourceProfileDirectory : string ;
548+ readonly targetProfileId : string ;
549+ readonly targetName : string ;
550+ } | null > ( null ) ;
530551 const [ profilePendingRemoval , setProfilePendingRemoval ] = useState < BrowserProfile | null > ( null ) ;
531552
532553 const profiles = resolveBrowserProfiles ( userProfiles ) ;
@@ -653,10 +674,20 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
653674 } ) ;
654675 } )
655676 . catch ( ( cause : unknown ) => {
677+ const reason = importFailureReason ( cause ) ;
678+ if ( reason === "needsFullDiskAccess" ) {
679+ setPendingFullDiskAccessImport ( {
680+ source,
681+ sourceProfileDirectory,
682+ targetProfileId,
683+ targetName,
684+ } ) ;
685+ return ;
686+ }
656687 toastManager . add ( {
657688 type : "error" ,
658689 title : `Could not import from ${ source . name } ` ,
659- description : BROWSER_IMPORT_FAILURE_COPY [ importFailureReason ( cause ) ] ,
690+ description : BROWSER_IMPORT_FAILURE_COPY [ reason ] ,
660691 } ) ;
661692 } )
662693 . finally ( ( ) => setBusy ( false ) ) ;
@@ -882,6 +913,50 @@ function BrowserProfilesSetting({ disabled }: { readonly disabled: boolean }) {
882913 </ AlertDialogFooter >
883914 </ AlertDialogPopup >
884915 </ AlertDialog >
916+ < AlertDialog
917+ open = { pendingFullDiskAccessImport !== null }
918+ onOpenChange = { ( open ) => {
919+ if ( ! open ) setPendingFullDiskAccessImport ( null ) ;
920+ } }
921+ >
922+ < AlertDialogPopup >
923+ < AlertDialogHeader >
924+ < AlertDialogTitle > Let T3 Code read Safari's cookies</ AlertDialogTitle >
925+ < AlertDialogDescription >
926+ Safari keeps your cookies somewhere only apps with Full Disk Access can reach. Turn
927+ that on for T3 Code in System Settings, then come back and finish the import.
928+ </ AlertDialogDescription >
929+ </ AlertDialogHeader >
930+ < AlertDialogFooter >
931+ < Button
932+ variant = "outline"
933+ onClick = { ( ) =>
934+ void readLocalApi ( )
935+ ?. shell . openExternal ( FULL_DISK_ACCESS_SETTINGS_URL )
936+ . catch ( ( ) => undefined )
937+ }
938+ >
939+ Open System Settings
940+ </ Button >
941+ < Button
942+ onClick = { ( ) => {
943+ const pending = pendingFullDiskAccessImport ;
944+ setPendingFullDiskAccessImport ( null ) ;
945+ if ( pending ) {
946+ runImport (
947+ pending . source ,
948+ pending . sourceProfileDirectory ,
949+ pending . targetProfileId ,
950+ pending . targetName ,
951+ ) ;
952+ }
953+ } }
954+ >
955+ I've turned it on
956+ </ Button >
957+ </ AlertDialogFooter >
958+ </ AlertDialogPopup >
959+ </ AlertDialog >
885960 </ SettingsRow >
886961 ) ;
887962}
0 commit comments