diff --git a/CHANGELOG.md b/CHANGELOG.md index 3200550..3c69d9d 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 added `SessionCookieStore` to persist, restore and clear 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..52f3677 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 + SessionCookieStore.restore() MxConfiguration.update(from: MendixApp.init( identifier: nil, @@ -32,6 +33,14 @@ class AppDelegate: RCTAppDelegate { return true } + override func applicationDidEnterBackground(_ application: UIApplication) { + SessionCookieStore.persist() + } + + override func applicationWillTerminate(_ application: UIApplication) { + SessionCookieStore.persist() + } + 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/AppUrl/AppUrl.swift b/ios/Modules/AppUrl/AppUrl.swift index fa996a3..fc172dd 100644 --- a/ios/Modules/AppUrl/AppUrl.swift +++ b/ios/Modules/AppUrl/AppUrl.swift @@ -55,7 +55,7 @@ public class AppUrl: NSObject { } // MARK: - Private Helper Methods - private static func createUrl(_ url: String, path: UrlPath?, port: Int? = nil, query: String? = nil, concatPath: Bool = false) -> URL? { + private static func createUrl(_ url: String, path: UrlPath?, port: Int? = nil, query: String? = nil, concatPath: Bool = true) -> URL? { let processedUrl = ensureProtocol(removeTrailingSlash(url)) guard var components = URLComponents(string: processedUrl) ?? URLComponents(string: defaultUrlString) else { return nil diff --git a/ios/Modules/NativeCookieModule/NativeCookieModule.swift b/ios/Modules/NativeCookieModule/NativeCookieModule.swift index 83b48cb..c7985e4 100644 --- a/ios/Modules/NativeCookieModule/NativeCookieModule.swift +++ b/ios/Modules/NativeCookieModule/NativeCookieModule.swift @@ -12,5 +12,6 @@ public class NativeCookieModule: NSObject { for cookie in (storage.cookies ?? []) { storage.deleteCookie(cookie) } + SessionCookieStore.clear() } } diff --git a/ios/Modules/NativeCookieModule/SessionCookieStore.swift b/ios/Modules/NativeCookieModule/SessionCookieStore.swift new file mode 100644 index 0000000..9312e2c --- /dev/null +++ b/ios/Modules/NativeCookieModule/SessionCookieStore.swift @@ -0,0 +1,85 @@ +import Foundation + +public class SessionCookieStore { + + // MARK: - Private properties + 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 cookies = get(key: storageKey) 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() // Clear stored cookies after restoration to avoid any side effects + } + + public static func persist() { + queue.async { + let sessionCookies = HTTPCookieStorage.shared.cookies?.filter { isSessionCookie($0) } ?? [] + guard !sessionCookies.isEmpty else { + clear() + NSLog("SessionCookieStore: Clear existing session cookies from storage") + return + } + set(key: storageKey, cookies: sessionCookies) + } + } + + public static func clear() { + clear(key: storageKey) + } + + // MARK: - Private API + private static func isSessionCookie(_ cookie: HTTPCookie) -> Bool { + return cookie.expiresDate == nil + } + + private static func set(key: String, cookies: [HTTPCookie]) { + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: cookies, requiringSecureCoding: false) + let storeQuery = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecValueData: data] as CFDictionary + SecItemDelete(storeQuery) + let status = SecItemAdd(storeQuery, nil) + if status != noErr { + NSLog("SessionCookieStore: Failed to persist session cookies with status: \(status)") + } + } catch { + NSLog("SessionCookieStore: Failed to persist session cookies: \(error.localizedDescription)") + } + } + + private static func get(key: String) -> [HTTPCookie]? { + do { + let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecReturnData: true] + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + if status == errSecSuccess, let data = item as? Data { + let cookies = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, HTTPCookie.self], from: data) as? [HTTPCookie] + return cookies + } else { + NSLog("SessionCookieStore: No session cookies found with status: \(status)") + return nil + } + } catch { + NSLog("SessionCookieStore: Failed to retrieve session cookies: \(error.localizedDescription)") + return nil + } + } + + private static func clear(key: String) { + let query = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecReturnData: true] as CFDictionary + let status = SecItemDelete(query) + if status != errSecSuccess { + NSLog("SessionCookieStore: Failed to clear cookies with status: \(status)") + } + } +}