From 07168a2b97a0720416ab8f2c57ae8256a4d80e3b Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Wed, 31 Dec 2025 21:25:23 +0530 Subject: [PATCH 1/5] feat: implement session cookie persistence and restoration on iOS --- .../ios/MendixNativeExample/AppDelegate.swift | 9 +++ example/ios/Podfile.lock | 4 +- .../AppPreferences/AppPreferences.swift | 31 ++++++--- .../NativeCookieModule.swift | 65 +++++++++++++++++++ 4 files changed, 97 insertions(+), 12 deletions(-) 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 + } } } From aa09e91353f491d8406bab0d06ab24c96501d331 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Wed, 31 Dec 2025 21:34:48 +0530 Subject: [PATCH 2/5] chore: update changelog to document session cookie persistence improvements on iOS --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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. From 2ba8d6561285ffcea312219ebd34fabfc61e5de0 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:34:36 +0530 Subject: [PATCH 3/5] chore: refactor --- .../ios/MendixNativeExample/AppDelegate.swift | 6 +- .../NativeCookieModule.swift | 64 -------------- .../SessionCookieStore.swift | 85 +++++++++++++++++++ 3 files changed, 88 insertions(+), 67 deletions(-) create mode 100644 ios/Modules/NativeCookieModule/SessionCookieStore.swift diff --git a/example/ios/MendixNativeExample/AppDelegate.swift b/example/ios/MendixNativeExample/AppDelegate.swift index d20b021..52f3677 100644 --- a/example/ios/MendixNativeExample/AppDelegate.swift +++ b/example/ios/MendixNativeExample/AppDelegate.swift @@ -15,7 +15,7 @@ class AppDelegate: RCTAppDelegate { super.application(application, didFinishLaunchingWithOptions: launchOptions) //Start - For MendixApplication compatibility only, not part of React Native template - NativeCookieModule.restoreSessionCookies() + SessionCookieStore.restore() MxConfiguration.update(from: MendixApp.init( identifier: nil, @@ -34,11 +34,11 @@ class AppDelegate: RCTAppDelegate { } override func applicationDidEnterBackground(_ application: UIApplication) { - NativeCookieModule.persistSessionCookies() + SessionCookieStore.persist() } override func applicationWillTerminate(_ application: UIApplication) { - NativeCookieModule.persistSessionCookies() + SessionCookieStore.persist() } override func sourceURL(for bridge: RCTBridge) -> URL? { diff --git a/ios/Modules/NativeCookieModule/NativeCookieModule.swift b/ios/Modules/NativeCookieModule/NativeCookieModule.swift index b7e9ac9..c7985e4 100644 --- a/ios/Modules/NativeCookieModule/NativeCookieModule.swift +++ b/ios/Modules/NativeCookieModule/NativeCookieModule.swift @@ -14,68 +14,4 @@ public class NativeCookieModule: NSObject { } 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 - } - } } 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)") + } + } +} From ac25eecd763f0533546e06ebe6f135d6a092012c Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:37:23 +0530 Subject: [PATCH 4/5] chore: update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b01bf..3c69d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ 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. +- We added `SessionCookieStore` to persist, restore and clear session cookies on iOS. ## [v0.3.0] - 2025-12-09 From 8d1d3992c3a4a50191f4e00b84a8792b44cc7a7f Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:03:41 +0530 Subject: [PATCH 5/5] fix: update default value for concatPath in createUrl method --- ios/Modules/AppUrl/AppUrl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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