You've already forked ios-client
mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-05-22 17:10:12 -07:00
0327624b30
* fix layout on ipad * Create multi profiles * multi-profile server config, connection cache, and auth fixes Split ProfilesView into ProfilesListView, AddProfileSheet, ProfileBadge, AddProfileViewModel - Add server URL + setup key fields to Add Profile screen - Store per-profile connection data (ip/fqdn/managementURL) as typed model in ProfileConnectionCache - Show cached connection info immediately on profile switch; empty if no prior data - Fix profile deletion persistence via tombstone in profiles.json - Fix logout to remove both netbird.cfg and state.json (cfg holds auth tokens) - Preserve managementURL in UI after logout via cache fallback - Guard polling from overwriting new profile's data during disconnect/reconnect cycle * fix(multi-profile): reinitialize VPN adapter on profile switch and show current server URL * Update project.pbxproj * fix(multi-profile): prevent default server overwrite on logout/re-login * Update PacketTunnelProvider.swift * fix(multi-profile): address code review findings - Normalize only scheme and host to lowercase when saving management server URL; previously the full URL was lowercased which could corrupt case-sensitive paths on self-hosted servers - Move switchConnectionInfo(to:) inside the do-block so the connection UI is only updated after a successful profile switch - Add ProfileConnectionCache.remove(for:) and clearConnectionData(for:) to prevent stale ip/fqdn/managementURL from persisting after a profile is deleted or logged out; call them from removeProfile and logoutProfile - Use ASWebAuthenticationSession.Callback.customScheme on iOS 17.4+, falling back to the deprecated callbackURLScheme initializer on older versions; add a comment explaining why "http" is used and that a proper fix requires custom-scheme support in the SDK - Guard presentationAnchor against a missing key window with assertionFailure in debug builds instead of silently returning an empty UIWindow
171 lines
6.3 KiB
Swift
171 lines
6.3 KiB
Swift
//
|
|
// Preferences.swift
|
|
// NetBirdiOS
|
|
//
|
|
// Created by Pascal Fischer on 03.08.23.
|
|
//
|
|
|
|
import Foundation
|
|
import NetBirdSDK
|
|
|
|
/// Preferences manages configuration file paths and SDK preferences.
|
|
///
|
|
/// ## Platform Differences
|
|
///
|
|
/// ### iOS
|
|
/// Uses file-based storage via App Group shared container. The main app and extension
|
|
/// can both read/write files to this shared location.
|
|
///
|
|
/// ### tvOS
|
|
/// The App Group shared container does NOT work for IPC between the main app and
|
|
/// Network Extension due to sandbox restrictions. Config is transferred via IPC
|
|
/// (`sendProviderMessage`/`handleAppMessage`) instead. The SDK preferences are not
|
|
/// used on tvOS - settings are managed directly in the extension.
|
|
///
|
|
/// See NetworkExtensionAdapter and PacketTunnelProvider for tvOS config flow details.
|
|
class Preferences {
|
|
|
|
// MARK: - SDK Preferences
|
|
|
|
#if os(iOS)
|
|
/// Creates SDK preferences using the active profile's config/state paths.
|
|
/// iOS only - file-based storage works reliably.
|
|
static func newPreferences() -> NetBirdSDKPreferences {
|
|
guard let configPath = configFile(), let statePath = stateFile() else {
|
|
preconditionFailure("App group container unavailable - check entitlements for '\(GlobalConstants.userPreferencesSuiteName)'")
|
|
}
|
|
guard let preferences = NetBirdSDKNewPreferences(configPath, statePath) else {
|
|
preconditionFailure("Failed to create NetBirdSDKPreferences")
|
|
}
|
|
return preferences
|
|
}
|
|
#else
|
|
/// tvOS does not use SDK preferences - config is transferred via IPC.
|
|
/// Returns nil by design; callers must handle this case.
|
|
static func newPreferences() -> NetBirdSDKPreferences? {
|
|
// tvOS uses IPC-based config transfer, not file-based SDK preferences.
|
|
// The extension manages its own config via UserDefaults.standard after
|
|
// receiving it through handleAppMessage.
|
|
return nil
|
|
}
|
|
#endif
|
|
|
|
// MARK: - File Paths
|
|
|
|
/// Returns the file path for a given filename in the App Group container.
|
|
/// Returns nil if the container is unavailable.
|
|
static func getFilePath(fileName: String) -> String? {
|
|
let fileManager = FileManager.default
|
|
if let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: GlobalConstants.userPreferencesSuiteName) {
|
|
return groupURL.appendingPathComponent(fileName).path
|
|
}
|
|
|
|
#if DEBUG
|
|
// Fallback for testing when app group is not available
|
|
let baseURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
|
|
?? fileManager.urls(for: .documentDirectory, in: .userDomainMask).first
|
|
return (baseURL ?? fileManager.temporaryDirectory).appendingPathComponent(fileName).path
|
|
#else
|
|
AppLogger.shared.log("ERROR: App group '\(GlobalConstants.userPreferencesSuiteName)' not available. Check entitlements.")
|
|
return nil
|
|
#endif
|
|
}
|
|
|
|
static func configFile() -> String? {
|
|
#if os(iOS)
|
|
// Use profile-aware paths on iOS
|
|
return ProfileManager.shared.activeConfigPath()
|
|
#else
|
|
return getFilePath(fileName: GlobalConstants.configFileName)
|
|
#endif
|
|
}
|
|
|
|
static func stateFile() -> String? {
|
|
#if os(iOS)
|
|
// Use profile-aware paths on iOS
|
|
return ProfileManager.shared.activeStatePath()
|
|
#else
|
|
return getFilePath(fileName: GlobalConstants.stateFileName)
|
|
#endif
|
|
}
|
|
|
|
// MARK: - App-Local UserDefaults Storage
|
|
//
|
|
// These methods store config in the App Group UserDefaults for the MAIN APP's
|
|
// own use (e.g., displaying current server URL). On tvOS, this data is NOT
|
|
// shared with the extension - it's app-local only.
|
|
|
|
private static let configJSONKey = "netbird_config_json"
|
|
|
|
/// Get the App Group UserDefaults.
|
|
/// Note: On tvOS, this is app-local only - NOT shared with extension.
|
|
static func sharedUserDefaults() -> UserDefaults? {
|
|
return UserDefaults(suiteName: GlobalConstants.userPreferencesSuiteName)
|
|
}
|
|
|
|
/// Save config JSON to UserDefaults (app-local storage).
|
|
static func saveConfigToUserDefaults(_ configJSON: String) -> Bool {
|
|
guard let defaults = sharedUserDefaults() else {
|
|
return false
|
|
}
|
|
defaults.set(configJSON, forKey: configJSONKey)
|
|
defaults.synchronize()
|
|
return true
|
|
}
|
|
|
|
/// Load config JSON from UserDefaults (app-local storage).
|
|
static func loadConfigFromUserDefaults() -> String? {
|
|
return sharedUserDefaults()?.string(forKey: configJSONKey)
|
|
}
|
|
|
|
/// Check if config exists in UserDefaults.
|
|
static func hasConfigInUserDefaults() -> Bool {
|
|
return sharedUserDefaults()?.string(forKey: configJSONKey) != nil
|
|
}
|
|
|
|
/// Remove config from UserDefaults (for logout).
|
|
static func removeConfigFromUserDefaults() {
|
|
guard let defaults = sharedUserDefaults() else {
|
|
return
|
|
}
|
|
defaults.removeObject(forKey: configJSONKey)
|
|
defaults.removeObject(forKey: managementURLKey)
|
|
defaults.synchronize()
|
|
}
|
|
|
|
// MARK: - Management URL Storage
|
|
//
|
|
// Stored separately because the config JSON from Go SDK serializes ManagementURL
|
|
// as a nested object (not a plain string), making regex/JSON extraction unreliable.
|
|
|
|
private static let managementURLKey = "netbird_management_url"
|
|
|
|
/// Save the management URL explicitly (called when user changes server).
|
|
static func saveManagementURL(_ url: String) {
|
|
sharedUserDefaults()?.set(url, forKey: managementURLKey)
|
|
sharedUserDefaults()?.synchronize()
|
|
}
|
|
|
|
/// Load the explicitly saved management URL.
|
|
static func loadManagementURL() -> String? {
|
|
return sharedUserDefaults()?.string(forKey: managementURLKey)
|
|
}
|
|
|
|
/// Restore config from UserDefaults to the config file path.
|
|
/// iOS only - needed because the Go SDK reads from the file path.
|
|
#if os(iOS)
|
|
static func restoreConfigFromUserDefaults() -> Bool {
|
|
guard let configJSON = loadConfigFromUserDefaults(),
|
|
let path = configFile() else {
|
|
return false
|
|
}
|
|
do {
|
|
try configJSON.write(toFile: path, atomically: false, encoding: .utf8)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
#endif
|
|
}
|