Skip to content

Commit b5b5f7d

Browse files
Merge pull request #22 from mendix/moo/MOO-2160-persist-session-cookies-on-ios
feat: implement session cookie persistence and restoration on iOS
2 parents 37f4104 + 8d1d399 commit b5b5f7d

7 files changed

Lines changed: 121 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [Unreleased]
1010

11+
- We added `SessionCookieStore` to persist, restore and clear session cookies on iOS.
12+
1113
## [v0.3.0] - 2025-12-09
1214

1315
- We fixed an issue that caused a FileNotFoundException during file deletion operations.

example/ios/MendixNativeExample/AppDelegate.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class AppDelegate: RCTAppDelegate {
1515
super.application(application, didFinishLaunchingWithOptions: launchOptions)
1616

1717
//Start - For MendixApplication compatibility only, not part of React Native template
18+
SessionCookieStore.restore()
1819
MxConfiguration.update(from:
1920
MendixApp.init(
2021
identifier: nil,
@@ -32,6 +33,14 @@ class AppDelegate: RCTAppDelegate {
3233
return true
3334
}
3435

36+
override func applicationDidEnterBackground(_ application: UIApplication) {
37+
SessionCookieStore.persist()
38+
}
39+
40+
override func applicationWillTerminate(_ application: UIApplication) {
41+
SessionCookieStore.persist()
42+
}
43+
3544
override func sourceURL(for bridge: RCTBridge) -> URL? {
3645
self.bundleURL()
3746
}

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ PODS:
88
- hermes-engine (0.78.2):
99
- hermes-engine/Pre-built (= 0.78.2)
1010
- hermes-engine/Pre-built (0.78.2)
11-
- MendixNative (0.1.3):
11+
- MendixNative (0.3.0):
1212
- DoubleConversion
1313
- glog
1414
- hermes-engine
@@ -1851,7 +1851,7 @@ SPEC CHECKSUMS:
18511851
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
18521852
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
18531853
hermes-engine: 2771b98fb813fdc6f92edd7c9c0035ecabf9fee7
1854-
MendixNative: 36190d86a65cb57b351c6396bc1349a7823206b0
1854+
MendixNative: a55e00448d33a66d59bd4b5c5d3057123d34337c
18551855
op-sqlite: 12554de3e1a0cb86cbad3cf1f0c50450f57d3855
18561856
OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2
18571857
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82

ios/Modules/AppPreferences/AppPreferences.swift

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,29 @@ public class AppPreferences: NSObject {
1717
private static var _packagerPort: Int
1818

1919
public static var remoteDebuggingPackagerPort: Int {
20-
get {
21-
return AppUrl.ensurePort(_packagerPort)
22-
}
23-
set {
24-
_packagerPort = newValue
25-
}
20+
get { AppUrl.ensurePort(_packagerPort) }
21+
set { _packagerPort = newValue }
2622
}
2723

28-
public static var appUrl = _appUrl
29-
public static var devModeEnabled = _devModeEnabled
30-
public static var remoteDebuggingEnabled = _remoteDebuggingEnabled
31-
public static var elementInspectorEnabled = _elementInspectorEnabled
24+
public static var appUrl: String? {
25+
get { _appUrl }
26+
set { _appUrl = newValue }
27+
}
28+
29+
public static var devModeEnabled: Bool {
30+
get { _devModeEnabled }
31+
set { _devModeEnabled = newValue }
32+
}
33+
34+
public static var remoteDebuggingEnabled: Bool {
35+
get { _remoteDebuggingEnabled }
36+
set { _remoteDebuggingEnabled = newValue }
37+
}
38+
39+
public static var elementInspectorEnabled: Bool {
40+
get { _elementInspectorEnabled }
41+
set { _elementInspectorEnabled = newValue }
42+
}
3243

3344
public static var safeAppUrl: String {
3445
return appUrl ?? ""

ios/Modules/AppUrl/AppUrl.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public class AppUrl: NSObject {
5555
}
5656

5757
// MARK: - Private Helper Methods
58-
private static func createUrl(_ url: String, path: UrlPath?, port: Int? = nil, query: String? = nil, concatPath: Bool = false) -> URL? {
58+
private static func createUrl(_ url: String, path: UrlPath?, port: Int? = nil, query: String? = nil, concatPath: Bool = true) -> URL? {
5959
let processedUrl = ensureProtocol(removeTrailingSlash(url))
6060
guard var components = URLComponents(string: processedUrl) ?? URLComponents(string: defaultUrlString) else {
6161
return nil

ios/Modules/NativeCookieModule/NativeCookieModule.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ public class NativeCookieModule: NSObject {
1212
for cookie in (storage.cookies ?? []) {
1313
storage.deleteCookie(cookie)
1414
}
15+
SessionCookieStore.clear()
1516
}
1617
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import Foundation
2+
3+
public class SessionCookieStore {
4+
5+
// MARK: - Private properties
6+
private static let bundleIdentifier = Bundle.main.bundleIdentifier ?? "com.mendix.app"
7+
private static let storageKey = bundleIdentifier + "sessionCookies"
8+
private static let queue = DispatchQueue(label: bundleIdentifier + ".session-cookie-store", qos: .utility)
9+
10+
// MARK: - Public API
11+
public static func restore() {
12+
13+
guard let cookies = get(key: storageKey) else {
14+
NSLog("SessionCookieStore: No cookies to restore")
15+
return
16+
}
17+
18+
let storage = HTTPCookieStorage.shared
19+
let existing = Set(storage.cookies ?? [])
20+
cookies.filter { !existing.contains($0) }.forEach { storage.setCookie($0) }
21+
22+
clear() // Clear stored cookies after restoration to avoid any side effects
23+
}
24+
25+
public static func persist() {
26+
queue.async {
27+
let sessionCookies = HTTPCookieStorage.shared.cookies?.filter { isSessionCookie($0) } ?? []
28+
guard !sessionCookies.isEmpty else {
29+
clear()
30+
NSLog("SessionCookieStore: Clear existing session cookies from storage")
31+
return
32+
}
33+
set(key: storageKey, cookies: sessionCookies)
34+
}
35+
}
36+
37+
public static func clear() {
38+
clear(key: storageKey)
39+
}
40+
41+
// MARK: - Private API
42+
private static func isSessionCookie(_ cookie: HTTPCookie) -> Bool {
43+
return cookie.expiresDate == nil
44+
}
45+
46+
private static func set(key: String, cookies: [HTTPCookie]) {
47+
do {
48+
let data = try NSKeyedArchiver.archivedData(withRootObject: cookies, requiringSecureCoding: false)
49+
let storeQuery = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecValueData: data] as CFDictionary
50+
SecItemDelete(storeQuery)
51+
let status = SecItemAdd(storeQuery, nil)
52+
if status != noErr {
53+
NSLog("SessionCookieStore: Failed to persist session cookies with status: \(status)")
54+
}
55+
} catch {
56+
NSLog("SessionCookieStore: Failed to persist session cookies: \(error.localizedDescription)")
57+
}
58+
}
59+
60+
private static func get(key: String) -> [HTTPCookie]? {
61+
do {
62+
let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecReturnData: true]
63+
var item: CFTypeRef?
64+
let status = SecItemCopyMatching(query as CFDictionary, &item)
65+
if status == errSecSuccess, let data = item as? Data {
66+
let cookies = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, HTTPCookie.self], from: data) as? [HTTPCookie]
67+
return cookies
68+
} else {
69+
NSLog("SessionCookieStore: No session cookies found with status: \(status)")
70+
return nil
71+
}
72+
} catch {
73+
NSLog("SessionCookieStore: Failed to retrieve session cookies: \(error.localizedDescription)")
74+
return nil
75+
}
76+
}
77+
78+
private static func clear(key: String) {
79+
let query = [kSecClass: kSecClassGenericPassword, kSecAttrAccount: key, kSecReturnData: true] as CFDictionary
80+
let status = SecItemDelete(query)
81+
if status != errSecSuccess {
82+
NSLog("SessionCookieStore: Failed to clear cookies with status: \(status)")
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)