diff --git a/CHANGELOG.md b/CHANGELOG.md index 3200550..79b01bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- We improved `NativeCookieModule` to persist and restore session cookies on iOS. + ## [v0.3.0] - 2025-12-09 - We fixed an issue that caused a FileNotFoundException during file deletion operations. diff --git a/example/ios/MendixNativeExample/AppDelegate.swift b/example/ios/MendixNativeExample/AppDelegate.swift index b6d5e16..d20b021 100644 --- a/example/ios/MendixNativeExample/AppDelegate.swift +++ b/example/ios/MendixNativeExample/AppDelegate.swift @@ -15,6 +15,7 @@ class AppDelegate: RCTAppDelegate { super.application(application, didFinishLaunchingWithOptions: launchOptions) //Start - For MendixApplication compatibility only, not part of React Native template + NativeCookieModule.restoreSessionCookies() MxConfiguration.update(from: MendixApp.init( identifier: nil, @@ -32,6 +33,14 @@ class AppDelegate: RCTAppDelegate { return true } + override func applicationDidEnterBackground(_ application: UIApplication) { + NativeCookieModule.persistSessionCookies() + } + + override func applicationWillTerminate(_ application: UIApplication) { + NativeCookieModule.persistSessionCookies() + } + override func sourceURL(for bridge: RCTBridge) -> URL? { self.bundleURL() } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index fb575c2..d2f85ec 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -8,7 +8,7 @@ PODS: - hermes-engine (0.78.2): - hermes-engine/Pre-built (= 0.78.2) - hermes-engine/Pre-built (0.78.2) - - MendixNative (0.1.3): + - MendixNative (0.3.0): - DoubleConversion - glog - hermes-engine @@ -1851,7 +1851,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 hermes-engine: 2771b98fb813fdc6f92edd7c9c0035ecabf9fee7 - MendixNative: 36190d86a65cb57b351c6396bc1349a7823206b0 + MendixNative: a55e00448d33a66d59bd4b5c5d3057123d34337c op-sqlite: 12554de3e1a0cb86cbad3cf1f0c50450f57d3855 OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2 RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 diff --git a/ios/Modules/AppPreferences/AppPreferences.swift b/ios/Modules/AppPreferences/AppPreferences.swift index d324eaa..eea6433 100644 --- a/ios/Modules/AppPreferences/AppPreferences.swift +++ b/ios/Modules/AppPreferences/AppPreferences.swift @@ -17,18 +17,29 @@ public class AppPreferences: NSObject { private static var _packagerPort: Int public static var remoteDebuggingPackagerPort: Int { - get { - return AppUrl.ensurePort(_packagerPort) - } - set { - _packagerPort = newValue - } + get { AppUrl.ensurePort(_packagerPort) } + set { _packagerPort = newValue } } - public static var appUrl = _appUrl - public static var devModeEnabled = _devModeEnabled - public static var remoteDebuggingEnabled = _remoteDebuggingEnabled - public static var elementInspectorEnabled = _elementInspectorEnabled + public static var appUrl: String? { + get { _appUrl } + set { _appUrl = newValue } + } + + public static var devModeEnabled: Bool { + get { _devModeEnabled } + set { _devModeEnabled = newValue } + } + + public static var remoteDebuggingEnabled: Bool { + get { _remoteDebuggingEnabled } + set { _remoteDebuggingEnabled = newValue } + } + + public static var elementInspectorEnabled: Bool { + get { _elementInspectorEnabled } + set { _elementInspectorEnabled = newValue } + } public static var safeAppUrl: String { return appUrl ?? "" diff --git a/ios/Modules/NativeCookieModule/NativeCookieModule.swift b/ios/Modules/NativeCookieModule/NativeCookieModule.swift index 83b48cb..b7e9ac9 100644 --- a/ios/Modules/NativeCookieModule/NativeCookieModule.swift +++ b/ios/Modules/NativeCookieModule/NativeCookieModule.swift @@ -12,5 +12,70 @@ public class NativeCookieModule: NSObject { for cookie in (storage.cookies ?? []) { storage.deleteCookie(cookie) } + SessionCookieStore.clear() + } + + public static func persistSessionCookies() { + SessionCookieStore.persist() + } + + public static func restoreSessionCookies() { + SessionCookieStore.restore() + } + + final class SessionCookieStore { + + private static let bundleIdentifier = Bundle.main.bundleIdentifier ?? "com.mendix.app" + private static let storageKey = bundleIdentifier + "sessionCookies" + private static let queue = DispatchQueue(label: bundleIdentifier + ".session-cookie-store", qos: .utility) + + // MARK: - Public API + public static func restore() { + + guard + let data = UserDefaults.standard.data(forKey: storageKey), + let cookies = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, HTTPCookie.self], from: data) as? [HTTPCookie] + else { + NSLog("SessionCookieStore: No cookies to restore") + return + } + + let storage = HTTPCookieStorage.shared + let existing = Set(storage.cookies ?? []) + + cookies.filter { !existing.contains($0) }.forEach { storage.setCookie($0) } + + // Clear stored cookies after restoration to avoid any side effects + clear() + } + + public static func persist() { + queue.async { + + let cookies = HTTPCookieStorage.shared.cookies ?? [] + let sessionCookies = cookies.filter { isSessionCookie($0) } + + guard !sessionCookies.isEmpty else { + clear() + NSLog("SessionCookieStore: Clear existing session cookies from storage") + return + } + + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: sessionCookies, requiringSecureCoding: false) + UserDefaults.standard.set(data, forKey: storageKey) + } catch { + NSLog("SessionCookieStore: Failed to persist session cookies: \(error.localizedDescription)") + } + } + } + + public static func clear() { + UserDefaults.standard.removeObject(forKey: storageKey) + } + + public static func isSessionCookie(_ cookie: HTTPCookie) -> Bool { + return cookie.expiresDate == nil + } } }