diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index 6f3edee..b40e997 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -278,6 +278,8 @@ pub struct PersistedSettings { // frontend. The field is kept on legacy persisted JSON only; serde ignores // unknown fields when decoding, so existing installs migrate cleanly. pub auto_check_updates: bool, + #[serde(default)] + pub keychain_access_granted: bool, } #[derive(Clone, Debug, Serialize, TS)] diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 6e51be5..a80f3aa 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -289,6 +289,7 @@ fn default_settings() -> PersistedSettings { media_cookie_source: MediaCookieSource::None, site_logins: Vec::new(), auto_check_updates: true, + keychain_access_granted: false, } } diff --git a/src/bindings/PersistedSettings.ts b/src/bindings/PersistedSettings.ts index c2c90ad..91c90bc 100644 --- a/src/bindings/PersistedSettings.ts +++ b/src/bindings/PersistedSettings.ts @@ -8,4 +8,4 @@ import type { SettingsTab } from "./SettingsTab"; import type { SiteLogin } from "./SiteLogin"; import type { Theme } from "./Theme"; -export type PersistedSettings = { theme: Theme, baseDownloadFolder: string, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array, maxConcurrentDownloads: number, globalSpeedLimit: string, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, autoCheckUpdates: boolean, }; +export type PersistedSettings = { theme: Theme, baseDownloadFolder: string, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array, maxConcurrentDownloads: number, globalSpeedLimit: string, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, autoCheckUpdates: boolean, keychainAccessGranted: boolean, }; diff --git a/src/components/KeychainPermissionModal.tsx b/src/components/KeychainPermissionModal.tsx index 59e3a1a..79e5ce2 100644 --- a/src/components/KeychainPermissionModal.tsx +++ b/src/components/KeychainPermissionModal.tsx @@ -19,6 +19,7 @@ export const KeychainPermissionModal: React.FC = () => { try { const result = await invoke('grant_keychain_access'); if (result.persistent) { + useSettingsStore.setState({ keychainAccessGranted: true }); await useSettingsStore.getState().hydratePairingToken(); setShowKeychainModal(false); } else { diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx index d6c6422..dd3e4ce 100644 --- a/src/components/SettingsView.tsx +++ b/src/components/SettingsView.tsx @@ -8,7 +8,7 @@ import { } from '../store/useSettingsStore'; import { Download, Palette, Globe, Folder, Key, - Moon, Terminal, Puzzle, Info, Plus, Trash2, Copy, RefreshCw, Code, ShieldAlert + Moon, Terminal, Puzzle, Info, Plus, Trash2, Copy, RefreshCw, Code, ShieldAlert, Check } from 'lucide-react'; import { open } from '@tauri-apps/plugin-dialog'; import { getVersion } from '@tauri-apps/api/app'; @@ -1055,7 +1055,19 @@ className="app-button px-3 py-1.5 text-[12px] flex items-center gap-1.5 disabled - {!settings.isPairingTokenPersistent && ( + {settings.isPairingTokenPersistent ? ( +
+
+ +
+
+

Keychain Access Granted

+

+ Your pairing token is securely saved and will persist across restarts. +

+
+
+ ) : (
@@ -1066,7 +1078,7 @@ className="app-button px-3 py-1.5 text-[12px] flex items-center gap-1.5 disabled

diff --git a/src/store/useSettingsStore.ts b/src/store/useSettingsStore.ts index 3a7c4bd..beeca6d 100644 --- a/src/store/useSettingsStore.ts +++ b/src/store/useSettingsStore.ts @@ -106,6 +106,7 @@ export interface SettingsState { siteLogins: SiteLogin[]; extensionPairingToken: string; isPairingTokenPersistent: boolean; + keychainAccessGranted: boolean; autoCheckUpdates: boolean; showKeychainModal: boolean; @@ -178,7 +179,7 @@ const generateSecureToken = () => { export const useSettingsStore = create()( persist( - (set) => ({ + (set, get) => ({ theme: 'system', baseDownloadFolder: '~/Downloads', categorySubfolders: { ...DEFAULT_CATEGORY_SUBFOLDERS }, @@ -224,6 +225,7 @@ export const useSettingsStore = create()( siteLogins: [], extensionPairingToken: '', isPairingTokenPersistent: true, + keychainAccessGranted: false, autoCheckUpdates: true, showKeychainModal: false, @@ -314,7 +316,8 @@ export const useSettingsStore = create()( set({ extensionPairingToken: token }); }, hydratePairingToken: async () => { - const result = await invoke('hydrate_extension_pairing_token'); + const granted = get().keychainAccessGranted; + const result = await invoke(granted ? 'grant_keychain_access' : 'hydrate_extension_pairing_token'); set({ extensionPairingToken: result.token, isPairingTokenPersistent: result.persistent @@ -324,8 +327,8 @@ export const useSettingsStore = create()( } return result.tokenChanged; }, - setAutoCheckUpdates: (autoCheckUpdates) => set({ autoCheckUpdates }), - setShowKeychainModal: (show) => set({ showKeychainModal: show }), + setAutoCheckUpdates: (autoCheckUpdates: boolean) => set({ autoCheckUpdates }), + setShowKeychainModal: (show: boolean) => set({ showKeychainModal: show }), }), { name: 'firelink-settings', @@ -393,6 +396,7 @@ export const useSettingsStore = create()( preventsSleepWhileDownloading: state.preventsSleepWhileDownloading, mediaCookieSource: state.mediaCookieSource, siteLogins: state.siteLogins, + keychainAccessGranted: state.keychainAccessGranted, autoCheckUpdates: state.autoCheckUpdates }), merge: (persistedState: unknown, currentState) => {