Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions example/ios/MendixNativeExample/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
}
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 21 additions & 10 deletions ios/Modules/AppPreferences/AppPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? ""
Expand Down
2 changes: 1 addition & 1 deletion ios/Modules/AppUrl/AppUrl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions ios/Modules/NativeCookieModule/NativeCookieModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public class NativeCookieModule: NSObject {
for cookie in (storage.cookies ?? []) {
storage.deleteCookie(cookie)
}
SessionCookieStore.clear()
}
}
85 changes: 85 additions & 0 deletions ios/Modules/NativeCookieModule/SessionCookieStore.swift
Original file line number Diff line number Diff line change
@@ -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)")
}
}
}