diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 9b0e39b..9dcc6e0 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -689,7 +689,6 @@ impl Browser for WayfernBrowser { "--disable-session-crashed-bubble".to_string(), "--hide-crash-restore-bubble".to_string(), "--disable-infobars".to_string(), - "--disable-quic".to_string(), // Wayfern-specific args for automation "--disable-features=DialMediaRouteProvider".to_string(), "--use-mock-keychain".to_string(), @@ -1166,6 +1165,67 @@ mod tests { assert_eq!(deserialized.host, proxy.host, "Host should match"); assert_eq!(deserialized.port, proxy.port, "Port should match"); } + + #[test] + fn test_wayfern_config_has_no_executable_path() { + // Verify WayfernConfig does not store executable_path + let config = crate::wayfern_manager::WayfernConfig::default(); + let json = serde_json::to_value(&config).unwrap(); + assert!( + json.get("executable_path").is_none(), + "WayfernConfig should not have executable_path field" + ); + } + + #[test] + fn test_camoufox_config_has_no_executable_path() { + // Verify CamoufoxConfig does not store executable_path + let config = crate::camoufox_manager::CamoufoxConfig::default(); + let json = serde_json::to_value(&config).unwrap(); + assert!( + json.get("executable_path").is_none(), + "CamoufoxConfig should not have executable_path field" + ); + } + + #[test] + fn test_profile_data_path_is_dynamic() { + use crate::profile::BrowserProfile; + let profiles_dir = std::path::PathBuf::from("/fake/profiles"); + let profile = BrowserProfile { + id: uuid::Uuid::parse_str("12345678-1234-1234-1234-123456789abc").unwrap(), + name: "test".to_string(), + browser: "wayfern".to_string(), + version: "1.0.0".to_string(), + proxy_id: None, + vpn_id: None, + process_id: None, + last_launch: None, + release_type: "stable".to_string(), + camoufox_config: None, + wayfern_config: None, + group_id: None, + tags: Vec::new(), + note: None, + sync_mode: crate::profile::types::SyncMode::Disabled, + encryption_salt: None, + last_sync: None, + host_os: None, + ephemeral: false, + extension_group_id: None, + proxy_bypass_rules: Vec::new(), + created_by_id: None, + created_by_email: None, + }; + + let path = profile.get_profile_data_path(&profiles_dir); + assert_eq!( + path, + profiles_dir + .join("12345678-1234-1234-1234-123456789abc") + .join("profile") + ); + } } // Global singleton instance diff --git a/src-tauri/src/camoufox_manager.rs b/src-tauri/src/camoufox_manager.rs index c64032f..2d2229f 100644 --- a/src-tauri/src/camoufox_manager.rs +++ b/src-tauri/src/camoufox_manager.rs @@ -21,7 +21,6 @@ pub struct CamoufoxConfig { pub block_images: Option, pub block_webrtc: Option, pub block_webgl: Option, - pub executable_path: Option, pub fingerprint: Option, // JSON string of the complete fingerprint config pub randomize_fingerprint_on_launch: Option, // Generate new fingerprint on every launch pub os: Option, // Operating system for fingerprint generation: "windows", "macos", or "linux" @@ -39,7 +38,6 @@ impl Default for CamoufoxConfig { block_images: None, block_webrtc: None, block_webgl: None, - executable_path: None, fingerprint: None, randomize_fingerprint_on_launch: None, os: None, @@ -129,21 +127,9 @@ impl CamoufoxManager { config: &CamoufoxConfig, ) -> Result> { // Get executable path - let executable_path = if let Some(path) = &config.executable_path { - let p = PathBuf::from(path); - if p.exists() { - p - } else { - log::warn!("Stored Camoufox executable path does not exist: {path}, falling back to dynamic resolution"); - BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))? - } - } else { - BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))? - }; + let executable_path = BrowserRunner::instance() + .get_browser_executable_path(profile) + .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; // Build the config using CamoufoxConfigBuilder let mut builder = CamoufoxConfigBuilder::new() @@ -230,21 +216,9 @@ impl CamoufoxManager { }; // Get executable path - let executable_path = if let Some(path) = &config.executable_path { - let p = PathBuf::from(path); - if p.exists() { - p - } else { - log::warn!("Stored Camoufox executable path does not exist: {path}, falling back to dynamic resolution"); - BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))? - } - } else { - BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))? - }; + let executable_path = BrowserRunner::instance() + .get_browser_executable_path(profile) + .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; // Parse the fingerprint config JSON let fingerprint_config: HashMap = diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index a30cfa4..349b60d 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -207,6 +207,20 @@ impl Extractor { match extraction_result { Ok(path) => { + // Remove quarantine attributes on macOS to prevent + // "app was prevented from modifying data" prompts + #[cfg(target_os = "macos")] + { + let _ = tokio::process::Command::new("xattr") + .args([ + "-dr", + "com.apple.quarantine", + dest_dir.to_str().unwrap_or("."), + ]) + .output() + .await; + } + log::info!( "Successfully extracted {} {} to: {}", browser_type.as_str(), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 168d0a4..c5118f4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -356,12 +356,6 @@ async fn copy_profile_cookies( app_handle: tauri::AppHandle, request: cookie_manager::CookieCopyRequest, ) -> Result, String> { - if !crate::cloud_auth::CLOUD_AUTH - .has_active_paid_subscription() - .await - { - return Err("Cookie copying requires an active Pro subscription".to_string()); - } let target_ids = request.target_profile_ids.clone(); let results = cookie_manager::CookieManager::copy_cookies(&app_handle, request).await?; @@ -397,12 +391,6 @@ async fn import_cookies_from_file( profile_id: String, content: String, ) -> Result { - if !crate::cloud_auth::CLOUD_AUTH - .has_active_paid_subscription() - .await - { - return Err("Cookie import requires an active Pro subscription".to_string()); - } let result = cookie_manager::CookieManager::import_cookies(&app_handle, &profile_id, &content).await?; @@ -426,12 +414,6 @@ async fn import_cookies_from_file( #[tauri::command] async fn export_profile_cookies(profile_id: String, format: String) -> Result { - if !crate::cloud_auth::CLOUD_AUTH - .has_active_paid_subscription() - .await - { - return Err("Cookie export requires an active Pro subscription".to_string()); - } cookie_manager::CookieManager::export_cookies(&profile_id, &format) } diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 9369e6d..77f89fb 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -532,27 +532,6 @@ impl ProfileImporter { let final_camoufox_config = if mapped == "camoufox" { let mut config = camoufox_config.unwrap_or_default(); - if config.executable_path.is_none() { - let mut browser_dir = self.profile_manager.get_binaries_dir(); - browser_dir.push(mapped); - browser_dir.push(&version); - - #[cfg(target_os = "macos")] - let binary_path = browser_dir - .join("Camoufox.app") - .join("Contents") - .join("MacOS") - .join("camoufox"); - - #[cfg(target_os = "windows")] - let binary_path = browser_dir.join("camoufox.exe"); - - #[cfg(target_os = "linux")] - let binary_path = browser_dir.join("camoufox"); - - config.executable_path = Some(binary_path.to_string_lossy().to_string()); - } - if let Some(ref proxy_id_val) = proxy_id { if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_val) { let proxy_url = if let (Some(username), Some(password)) = @@ -631,27 +610,6 @@ impl ProfileImporter { let final_wayfern_config = if mapped == "wayfern" { let mut config = wayfern_config.unwrap_or_default(); - if config.executable_path.is_none() { - let mut browser_dir = self.profile_manager.get_binaries_dir(); - browser_dir.push(mapped); - browser_dir.push(&version); - - #[cfg(target_os = "macos")] - let binary_path = browser_dir - .join("Chromium.app") - .join("Contents") - .join("MacOS") - .join("Chromium"); - - #[cfg(target_os = "windows")] - let binary_path = browser_dir.join("chrome.exe"); - - #[cfg(target_os = "linux")] - let binary_path = browser_dir.join("chrome"); - - config.executable_path = Some(binary_path.to_string_lossy().to_string()); - } - if let Some(ref proxy_id_val) = proxy_id { if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_val) { let proxy_url = if let (Some(username), Some(password)) = diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index a25d795..d78c765 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -1802,8 +1802,11 @@ export function ProfilesDataTable({ const isLaunching = meta.launchingProfiles.has(profile.id); const isStopping = meta.stoppingProfiles.has(profile.id); const isLockedByAnother = meta.isProfileLockedByAnother(profile.id); + const isSyncing = meta.syncStatuses[profile.id]?.status === "syncing"; const canLaunch = - meta.browserState.canLaunchProfile(profile) && !isLockedByAnother; + meta.browserState.canLaunchProfile(profile) && + !isLockedByAnother && + !isSyncing; const lockEmail = meta.getProfileLockEmail(profile.id); const tooltipContent = isLockedByAnother ? meta.t("sync.team.cannotLaunchLocked", { email: lockEmail }) @@ -1831,13 +1834,14 @@ export function ProfilesDataTable({ ); try { await meta.onLaunchProfile(profile); - } catch (error) { + } finally { + // Always clear launching state — the running state is tracked + // separately via profile-running-changed events meta.setLaunchingProfiles((prev: Set) => { const next = new Set(prev); next.delete(profile.id); return next; }); - throw error; } }; @@ -2665,40 +2669,20 @@ export function ProfilesDataTable({ )} {onBulkExtensionGroupAssignment && ( - - - {!crossOsUnlocked && ( - - PRO - - )} - + )} {onBulkCopyCookies && ( - - - {!crossOsUnlocked && ( - - PRO - - )} - + )} {onBulkDelete && ( diff --git a/src/components/profile-info-dialog.tsx b/src/components/profile-info-dialog.tsx index 300d5c1..3fbecf6 100644 --- a/src/components/profile-info-dialog.tsx +++ b/src/components/profile-info-dialog.tsx @@ -266,9 +266,8 @@ export function ProfileInfoDialog({ icon: , label: t("profiles.actions.copyCookiesToProfile"), onClick: () => handleAction(() => onCopyCookiesToProfile?.(profile)), - disabled: isDisabled || !crossOsUnlocked, - proBadge: !crossOsUnlocked, - runningBadge: isRunning && crossOsUnlocked, + disabled: isDisabled, + runningBadge: isRunning, hidden: !isCamoufoxOrWayfern || profile.ephemeral === true || @@ -278,9 +277,8 @@ export function ProfileInfoDialog({ icon: , label: t("profileInfo.actions.manageCookies"), onClick: () => handleAction(() => onOpenCookieManagement?.(profile)), - disabled: isDisabled || !crossOsUnlocked, - proBadge: !crossOsUnlocked, - runningBadge: isRunning && crossOsUnlocked, + disabled: isDisabled, + runningBadge: isRunning, hidden: !isCamoufoxOrWayfern || profile.ephemeral === true ||