From 2de92942a3a939121d964608693c2c728d752e72 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:21:38 +0530 Subject: [PATCH 1/9] refactor: replace RedBoxHelper with RCTExceptionsManager for error handling --- ios/Modules/ErrorHandler/NativeErrorHandler.swift | 3 ++- ios/Modules/RCTRedBoxHelper/RCTRedBoxHelper.swift | 11 ----------- 2 files changed, 2 insertions(+), 12 deletions(-) delete mode 100644 ios/Modules/RCTRedBoxHelper/RCTRedBoxHelper.swift diff --git a/ios/Modules/ErrorHandler/NativeErrorHandler.swift b/ios/Modules/ErrorHandler/NativeErrorHandler.swift index 1b05dab..7b5886a 100644 --- a/ios/Modules/ErrorHandler/NativeErrorHandler.swift +++ b/ios/Modules/ErrorHandler/NativeErrorHandler.swift @@ -1,8 +1,9 @@ import Foundation +import React @objcMembers public class NativeErrorHandler: NSObject { public func handle(message: String, stackTrace: [[String: Any]]) { - RedBoxHelper.shared.redBox.showErrorMessage(message, withStack: stackTrace) + DevHelper.getModule(type: RCTExceptionsManager.self)?.reportFatalException(message, stack: stackTrace, exceptionId: -1) } } diff --git a/ios/Modules/RCTRedBoxHelper/RCTRedBoxHelper.swift b/ios/Modules/RCTRedBoxHelper/RCTRedBoxHelper.swift deleted file mode 100644 index a1d4548..0000000 --- a/ios/Modules/RCTRedBoxHelper/RCTRedBoxHelper.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation -import React - -final class RedBoxHelper { - - static let shared = RedBoxHelper() - - let redBox: RCTRedBox = RCTRedBox() - - private init() {} -} From 5be47a7125b80e1fef1dc4d11dd788a2e2472566 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:43:26 +0530 Subject: [PATCH 2/9] refactor: implement typed constants spec --- .../mendixnative/react/MxConfiguration.kt | 37 ++++++------- .../mendixnative/react/fs/NativeFsModule.kt | 12 ++--- .../configuration/MxConfigurationModule.kt | 3 +- .../com/mendixnative/fs/MxFileSystemModule.kt | 3 +- .../MxConfiguration/MxConfigProxy.swift | 54 +++++++++++++++++++ .../MxConfiguration/MxConfiguration.swift | 2 +- .../NativeFsModule/NativeFsModule.swift | 6 --- .../MxConfiguration/MxConfigurationModule.mm | 21 +++++++- ios/TurboModules/MxFileSystem/MxFileSystem.mm | 13 ++++- src/file-system/NativeMxFileSystem.ts | 2 +- src/file-system/index.ts | 2 +- src/mx-configuration/NativeMxConfiguration.ts | 2 +- src/mx-configuration/index.ts | 2 +- 13 files changed, 112 insertions(+), 47 deletions(-) create mode 100644 ios/Modules/MxConfiguration/MxConfigProxy.swift diff --git a/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt b/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt index 814bfff..2322361 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt @@ -1,8 +1,6 @@ package com.mendix.mendixnative.react import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.WritableMap -import com.facebook.react.bridge.WritableNativeMap import com.mendix.mendixnative.MendixApplication import com.mendix.mendixnative.config.AppUrl import com.mendix.mendixnative.react.ota.getNativeDependencies @@ -10,7 +8,7 @@ import com.mendix.mendixnative.react.ota.getOtaManifestFilepath class MxConfiguration(val reactContext: ReactApplicationContext) { - fun getConstants(): WritableMap? { + fun getConstants(): Map { val application = (reactContext.applicationContext as MendixApplication) if (runtimeUrl == null) { if (warningsFilter != WarningsFilter.none) { @@ -22,31 +20,26 @@ class MxConfiguration(val reactContext: ReactApplicationContext) { Throwable("Without the runtime URL, the app cannot retrieve any data.\n\nPlease redeploy the app.") ) - return WritableNativeMap() + return emptyMap() } throw IllegalStateException("Runtime URL not set in the MxConfiguration") } - val constants = WritableNativeMap() - constants.putString("RUNTIME_URL", AppUrl.forRuntime(runtimeUrl)) - constants.putString("APP_NAME", defaultAppName) - constants.putString("DATABASE_NAME", defaultDatabaseName) - constants.putString( - "FILES_DIRECTORY_NAME", - defaultFilesDirectoryName - ) // Not to be removed as it is required for backwards compatibility. - constants.putString("WARNINGS_FILTER_LEVEL", warningsFilter.toString()) - constants.putString("OTA_MANIFEST_PATH", getOtaManifestFilepath(reactContext)) - constants.putBoolean("IS_DEVELOPER_APP", application.getUseDeveloperSupport()) - constants.putInt("NATIVE_BINARY_VERSION", NATIVE_BINARY_VERSION) - constants.putString("APP_SESSION_ID", application.getAppSessionId()) - - val dependencies = WritableNativeMap() - getNativeDependencies(reactContext).forEach { - dependencies.putString(it.key, it.value) + val constants = mutableMapOf( + "RUNTIME_URL" to AppUrl.forRuntime(runtimeUrl), + "DATABASE_NAME" to defaultDatabaseName, + "FILES_DIRECTORY_NAME" to defaultFilesDirectoryName, + "WARNINGS_FILTER_LEVEL" to warningsFilter.toString(), + "OTA_MANIFEST_PATH" to getOtaManifestFilepath(reactContext), + "IS_DEVELOPER_APP" to application.getUseDeveloperSupport(), + "NATIVE_BINARY_VERSION" to NATIVE_BINARY_VERSION, + "APP_SESSION_ID" to application.getAppSessionId(), + "NATIVE_DEPENDENCIES" to getNativeDependencies(reactContext) + ) + defaultAppName?.let { + constants.put("APP_NAME", it) } - constants.putMap("NATIVE_DEPENDENCIES", dependencies) return constants } diff --git a/android/src/main/java/com/mendix/mendixnative/react/fs/NativeFsModule.kt b/android/src/main/java/com/mendix/mendixnative/react/fs/NativeFsModule.kt index 15add2a..96a8f29 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/fs/NativeFsModule.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/fs/NativeFsModule.kt @@ -230,12 +230,12 @@ class NativeFsModule(private val reactContext: ReactApplicationContext) { } } - fun getConstants(): WritableMap { - val constants = WritableNativeMap() - constants.putString("DocumentDirectoryPath", filesDir) - constants.putBoolean("SUPPORTS_DIRECTORY_MOVE", true) // Client uses this const to identify if functionality is supported - constants.putBoolean("SUPPORTS_ENCRYPTION", true) - return constants + fun getConstants(): Map { + return mapOf( + "DocumentDirectoryPath" to filesDir, + "SUPPORTS_DIRECTORY_MOVE" to true, // Client uses this const to identify if functionality is supported + "SUPPORTS_ENCRYPTION" to true + ) } @Throws(IOException::class) diff --git a/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt b/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt index 8af3df0..4ac93b4 100644 --- a/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt +++ b/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt @@ -1,7 +1,6 @@ package com.mendixnative.configuration import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.WritableMap import com.facebook.react.module.annotations.ReactModule import com.mendix.mendixnative.react.MxConfiguration import com.mendixnative.NativeMxConfigurationSpec @@ -14,7 +13,7 @@ class MxConfigurationModule(reactContext: ReactApplicationContext) : override fun getName(): String = NAME - override fun getConfig(): WritableMap? { + override fun getTypedExportedConstants(): Map { return configuration.getConstants() } diff --git a/android/src/main/java/com/mendixnative/fs/MxFileSystemModule.kt b/android/src/main/java/com/mendixnative/fs/MxFileSystemModule.kt index 2bef53e..4d87099 100644 --- a/android/src/main/java/com/mendixnative/fs/MxFileSystemModule.kt +++ b/android/src/main/java/com/mendixnative/fs/MxFileSystemModule.kt @@ -3,7 +3,6 @@ package com.mendixnative.fs import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.WritableMap import com.facebook.react.module.annotations.ReactModule import com.mendix.mendixnative.react.fs.NativeFsModule import com.mendixnative.NativeMxFileSystemSpec @@ -16,7 +15,7 @@ class MxFileSystemModule(reactContext: ReactApplicationContext) : override fun getName(): String = NAME - override fun constants(): WritableMap? { + override fun getTypedExportedConstants(): Map { return fsModule.getConstants() } diff --git a/ios/Modules/MxConfiguration/MxConfigProxy.swift b/ios/Modules/MxConfiguration/MxConfigProxy.swift new file mode 100644 index 0000000..e7cd793 --- /dev/null +++ b/ios/Modules/MxConfiguration/MxConfigProxy.swift @@ -0,0 +1,54 @@ +import Foundation + +@objcMembers +public class MxConfigProxy: NSObject { + public var runtimeUrl: String + public var appName: String? + public var databaseName: String + public var filesDirectoryName: String + public var warningsFilter: String + public var otaManifestPath: String + public var isDeveloperApp: NSNumber + public var nativeDependencies: [String: Any] + public var nativeBinaryVersion: NSNumber + public var appSessionId: String? + + init(runtimeUrl: String, appName: String?, databaseName: String, filesDirectoryName: String, warningsFilter: String, otaManifestPath: String, isDeveloperApp: NSNumber, nativeDependencies: [String : Any], nativeBinaryVersion: NSNumber, appSessionId: String?) { + self.runtimeUrl = runtimeUrl + self.appName = appName + self.databaseName = databaseName + self.filesDirectoryName = filesDirectoryName + self.warningsFilter = warningsFilter + self.otaManifestPath = otaManifestPath + self.isDeveloperApp = isDeveloperApp + self.nativeDependencies = nativeDependencies + self.nativeBinaryVersion = nativeBinaryVersion + self.appSessionId = appSessionId + } + + + public static func prepare() -> MxConfigProxy? { + guard let runtimeUrl = MxConfiguration.runtimeUrl?.absoluteString else { + let exception = NSException( + name: NSExceptionName("RUNTIME_URL_MISSING"), + reason: "Runtime URL was not set prior to launch.", + userInfo: nil + ) + exception.raise() + return nil + } + + return MxConfigProxy( + runtimeUrl: runtimeUrl, + appName: MxConfiguration.appName, + databaseName: MxConfiguration.databaseName, + filesDirectoryName: MxConfiguration.filesDirectoryName, + warningsFilter: MxConfiguration.warningsFilter.stringValue, + otaManifestPath: OtaHelpers.getOtaManifestFilepath(), + isDeveloperApp: NSNumber(booleanLiteral: MxConfiguration.isDeveloperApp), + nativeDependencies: OtaHelpers.getNativeDependencies(), + nativeBinaryVersion: NSNumber(integerLiteral: MxConfiguration.nativeBinaryVersion), + appSessionId: MxConfiguration.appSessionId + ) + } +} diff --git a/ios/Modules/MxConfiguration/MxConfiguration.swift b/ios/Modules/MxConfiguration/MxConfiguration.swift index cae46aa..4196fe3 100644 --- a/ios/Modules/MxConfiguration/MxConfiguration.swift +++ b/ios/Modules/MxConfiguration/MxConfiguration.swift @@ -3,7 +3,7 @@ import Foundation @objcMembers public class MxConfiguration: NSObject { - private static let nativeBinaryVersion: Int = 32 + static let nativeBinaryVersion: Int = 32 private static let defaultDatabaseName = "default" private static let defaultFilesDirectoryName = "files/default" diff --git a/ios/Modules/NativeFsModule/NativeFsModule.swift b/ios/Modules/NativeFsModule/NativeFsModule.swift index 92455e2..0d47252 100644 --- a/ios/Modules/NativeFsModule/NativeFsModule.swift +++ b/ios/Modules/NativeFsModule/NativeFsModule.swift @@ -268,12 +268,6 @@ public class NativeFsModule: NSObject { } } - public let constants: NSDictionary = [ - "DocumentDirectoryPath": NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first ?? "", - "SUPPORTS_DIRECTORY_MOVE": true, - "SUPPORTS_ENCRYPTION": true - ] - private func isWhiteListedPath(_ paths: String..., reject: RCTPromiseRejectBlock) -> Bool { do { try NativeFsModule.ensureWhiteListedPath(paths) diff --git a/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm b/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm index c14ebe0..d5b4d2f 100644 --- a/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm +++ b/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm @@ -13,8 +13,25 @@ @implementation MxConfigurationModule return std::make_shared(params); } -- (nonnull NSDictionary *)getConfig { - return [[[MxConfiguration alloc] init] constants]; +- (nonnull facebook::react::ModuleConstants)constantsToExport { + return [self getConstants]; +} + +- (nonnull facebook::react::ModuleConstants)getConstants { + MxConfigProxy *config = [MxConfigProxy prepare]; + return facebook::react::typedConstants({ + .RUNTIME_URL = config.runtimeUrl, + .APP_NAME = config.appName, + .FILES_DIRECTORY_NAME = config.filesDirectoryName, + .DATABASE_NAME = config.databaseName, + .WARNINGS_FILTER_LEVEL = config.warningsFilter, + .OTA_MANIFEST_PATH = config.otaManifestPath, + .NATIVE_DEPENDENCIES = config.nativeDependencies, + .IS_DEVELOPER_APP = config.isDeveloperApp, + .CODE_PUSH_KEY= NULL, + .NATIVE_BINARY_VERSION = [config.nativeBinaryVersion doubleValue], + .APP_SESSION_ID = config.appSessionId + }); } @end diff --git a/ios/TurboModules/MxFileSystem/MxFileSystem.mm b/ios/TurboModules/MxFileSystem/MxFileSystem.mm index f67a9c1..c31fb5a 100644 --- a/ios/TurboModules/MxFileSystem/MxFileSystem.mm +++ b/ios/TurboModules/MxFileSystem/MxFileSystem.mm @@ -13,8 +13,17 @@ @implementation MxFileSystem return std::make_shared(params); } -- (nonnull NSDictionary *)constants { - return [[[NativeFsModule alloc] init] constants]; +- (facebook::react::ModuleConstants)constantsToExport { + return [self getConstants]; +} + +- (facebook::react::ModuleConstants)getConstants { + NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + return facebook::react::typedConstants({ + .DocumentDirectoryPath = path ?: @"", + .SUPPORTS_DIRECTORY_MOVE = true, + .SUPPORTS_ENCRYPTION = true + }); } - (void)save:(nonnull NSDictionary *)blob diff --git a/src/file-system/NativeMxFileSystem.ts b/src/file-system/NativeMxFileSystem.ts index 69386a2..6845bc4 100644 --- a/src/file-system/NativeMxFileSystem.ts +++ b/src/file-system/NativeMxFileSystem.ts @@ -20,7 +20,7 @@ type FsConstants = { }; export interface Spec extends TurboModule { - constants(): FsConstants; + readonly getConstants: () => FsConstants; save(blob: CodegenTypes.UnsafeObject, filePath: string): Promise; read(filePath: string): Promise; move(filePath: string, newPath: string): Promise; diff --git a/src/file-system/index.ts b/src/file-system/index.ts index 9106703..e9704b9 100644 --- a/src/file-system/index.ts +++ b/src/file-system/index.ts @@ -5,7 +5,7 @@ const initFs = () => { DocumentDirectoryPath, SUPPORTS_DIRECTORY_MOVE, SUPPORTS_ENCRYPTION, - } = NativeMxFileSystem.constants(); + } = NativeMxFileSystem.getConstants(); const docDirPath = DocumentDirectoryPath as string; return { //Constants diff --git a/src/mx-configuration/NativeMxConfiguration.ts b/src/mx-configuration/NativeMxConfiguration.ts index eb1e855..2b50057 100644 --- a/src/mx-configuration/NativeMxConfiguration.ts +++ b/src/mx-configuration/NativeMxConfiguration.ts @@ -23,7 +23,7 @@ type Configuration = { }; export interface Spec extends TurboModule { - getConfig(): Configuration; + readonly getConstants: () => Configuration; } export default TurboModuleRegistry.getEnforcing('MxConfiguration'); diff --git a/src/mx-configuration/index.ts b/src/mx-configuration/index.ts index 59abeb5..a55f886 100644 --- a/src/mx-configuration/index.ts +++ b/src/mx-configuration/index.ts @@ -1,3 +1,3 @@ import NativeMxConfiguration from './NativeMxConfiguration'; -export const MxConfiguration = NativeMxConfiguration.getConfig(); +export const MxConfiguration = NativeMxConfiguration.getConstants(); From a8e2c864ce2b5c270e8040f173f9436529ee569b Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:23:36 +0530 Subject: [PATCH 3/9] refactor: update deprecated ReactNativeHost usage to ReactHost across the application refactor: replace ExceptionsManagerModule with DevSupportManager for error handling --- .../mendixnative/MendixReactApplication.kt | 38 ++------------ .../activity/MendixReactActivity.kt | 2 +- .../fragment/MendixReactFragment.kt | 4 +- .../mendixnative/fragment/ReactFragment.kt | 13 ++--- .../mendixnative/react/MxConfiguration.kt | 7 ++- .../mendixnative/react/NativeErrorHandler.kt | 38 +++++++++++++- .../mendixnative/example/MainApplication.kt | 50 +++---------------- 7 files changed, 58 insertions(+), 94 deletions(-) diff --git a/android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt b/android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt index e412ea5..ebed900 100644 --- a/android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt +++ b/android/src/main/java/com/mendix/mendixnative/MendixReactApplication.kt @@ -2,7 +2,6 @@ package com.mendix.mendixnative import android.app.Application import com.facebook.react.ReactHost -import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage import com.facebook.react.bridge.JSBundleLoader import com.facebook.react.bridge.JSBundleLoaderDelegate @@ -28,8 +27,6 @@ import com.mendixnative.MendixNativePackage import java.util.* import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load -import com.facebook.react.defaults.DefaultReactNativeHost - abstract class MendixReactApplication : Application(), MendixApplication, ErrorHandlerFactory { private val appSessionId = "" + Math.random() * 1000 + Date().time override fun getAppSessionId(): String = appSessionId @@ -44,33 +41,11 @@ abstract class MendixReactApplication : Application(), MendixApplication, ErrorH private var jsBundleFileProvider: JSBundleFileProvider? = jsBundleProvider - override var reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { - override fun getUseDeveloperSupport(): Boolean = this@MendixReactApplication.useDeveloperSupport - - override fun getPackages(): List { - val pkgs: MutableList = ArrayList() - // Use the packages provided by the concrete Application subclass. - pkgs.addAll(this@MendixReactApplication.packages) - // Inject splashScreenPresenter into any MendixNativePackage instances without creating duplicates. - applyInternalPackageAugmentations(pkgs) - return pkgs - } - - override fun getJSBundleFile(): String? = this@MendixReactApplication.jsBundleFile - override fun getJSMainModuleName(): String = "index" - override fun getBundleAssetName(): String? = super.getBundleAssetName() - override fun getRedBoxHandler(): RedBoxHandler? = null - - // Hermes & New Arch flags; Hermes executor will be picked automatically when isHermesEnabled is true. - override val isNewArchEnabled: Boolean = true - override val isHermesEnabled: Boolean = true - } - /** - * Build the [ReactHost] ourselves instead of using [DefaultReactHost.getDefaultReactHost], - * because that factory evaluates [ReactNativeHost.getJSBundleFile] once at creation time and - * bakes the result into a fixed [JSBundleLoader]. After an OTA update deploys a new bundle, - * a subsequent [ReactHost.reload] would still load the stale bundle. + * Build the [ReactHost] with a custom [JSBundleLoader] instead of using a static bundle path. + * The default approach evaluates the bundle file path once at creation time and bakes it into + * a fixed [JSBundleLoader]. After an OTA update deploys a new bundle, a subsequent + * [ReactHost.reload] would still load the stale bundle. * * By providing a **dynamic** [JSBundleLoader] whose [JSBundleLoader.loadScript] calls * [getJSBundleFile] on every invocation, each reload picks up the latest bundle path — @@ -133,10 +108,7 @@ abstract class MendixReactApplication : Application(), MendixApplication, ErrorH override fun onCreate() { super.onCreate() SoLoader.init(this, OpenSourceMergedSoMapping) - // Only load the New Architecture entry point when enabled (always true here, but guarded for safety). - if (reactNativeHost is DefaultReactNativeHost) { - load() - } + load() } override fun getJSBundleFile(): String? { diff --git a/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt b/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt index 6b2130e..66d08a2 100644 --- a/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt +++ b/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt @@ -50,7 +50,7 @@ open class MendixReactActivity : ReactActivity(), DevAppMenuHandler, LaunchScree } private val currentReactContext: ReactContext? - get() = if (reactNativeHost.hasInstance()) reactInstanceManager.currentReactContext else null + get() = reactHost.currentReactContext val currentDevSupportManager: DevSupportManager? get() = reactHost.devSupportManager diff --git a/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt b/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt index 7a5c63e..f96a7da 100644 --- a/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt +++ b/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt @@ -74,8 +74,8 @@ open class MendixReactFragment : ReactFragment(), MendixReactFragmentView { } fun onNewIntent(intent: Intent) { - if (reactNativeHost.hasInstance()) { - reactNativeHost.reactInstanceManager.onNewIntent(intent); + reactHost?.currentReactContext?.let { + it.onNewIntent(it.currentActivity, intent) } } diff --git a/android/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt b/android/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt index f1ba4ea..d21f152 100644 --- a/android/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt +++ b/android/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt @@ -10,7 +10,6 @@ import androidx.fragment.app.Fragment import com.facebook.react.ReactApplication import com.facebook.react.ReactDelegate import com.facebook.react.ReactHost -import com.facebook.react.ReactNativeHost import com.facebook.react.modules.core.PermissionAwareActivity import com.facebook.react.modules.core.PermissionListener import com.mendix.mendixnative.react.CopiedFrom @@ -35,17 +34,15 @@ open class ReactFragment : Fragment(), PermissionAwareActivity { launchOptions = requireArguments().getBundle(ARG_LAUNCH_OPTIONS) } checkNotNull(mainComponentName) { "Cannot loadApp if component name is null" } - mReactDelegate = activity?.let { ReactDelegate(it, reactHost, mainComponentName, launchOptions) } + mReactDelegate = activity?.let { ReactDelegate(it, reactHost!!, mainComponentName, launchOptions) } } /** - * Get the [ReactNativeHost] used by this app. By default, assumes [ ][Activity.getApplication] is an instance of [ReactApplication] and calls [ ][ReactApplication.getReactNativeHost]. Override this method if your application class does not - * implement `ReactApplication` or you simply have a different mechanism for storing a - * `ReactNativeHost`, e.g. as a static field somewhere. + * Get the [ReactHost] used by this app. By default, assumes [ ][Activity.getApplication] is an + * instance of [ReactApplication] and calls [ ][ReactApplication.getReactHost]. Override this + * method if your application class does not implement `ReactApplication` or you simply have a + * different mechanism for storing a `ReactHost`, e.g. as a static field somewhere. */ - protected val reactNativeHost: ReactNativeHost - get() = (requireActivity().application as ReactApplication).reactNativeHost - protected val reactHost: ReactHost? get() = (requireActivity().application as ReactApplication).reactHost diff --git a/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt b/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt index 2322361..b00d1ff 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt @@ -12,10 +12,9 @@ class MxConfiguration(val reactContext: ReactApplicationContext) { val application = (reactContext.applicationContext as MendixApplication) if (runtimeUrl == null) { if (warningsFilter != WarningsFilter.none) { - application.reactNativeHost - .reactInstanceManager - .devSupportManager - .showNewJavaError( + application.reactHost + ?.devSupportManager + ?.showNewJavaError( "Runtime URL not specified.", Throwable("Without the runtime URL, the app cannot retrieve any data.\n\nPlease redeploy the app.") ) diff --git a/android/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.kt b/android/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.kt index 9b7487b..ba00c20 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.kt @@ -1,13 +1,47 @@ package com.mendix.mendixnative.react import com.facebook.common.logging.FLog +import com.facebook.react.ReactApplication +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableArray -import com.facebook.react.modules.core.ExceptionsManagerModule +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.devsupport.StackTraceHelper class NativeErrorHandler(val reactContext: ReactApplicationContext) { fun handle(message: String?, stackTrace: ReadableArray?) { - reactContext.nativeModule(ExceptionsManagerModule.NAME)?.reportSoftException(message, stackTrace, 0.0) FLog.e(javaClass, "Received JS exception: $message") + // In bridgeless mode, use DevSupportManager directly for proper error display + val reactHost = (reactContext.applicationContext as? ReactApplication)?.reactHost + reactHost?.devSupportManager?.showNewJSError(message, sanitize(stackTrace), -1) + } + + /** + * Filter out invalid stack frames to prevent parsing errors. + * + * React Native's StackTraceHelper.convertJsStackTrace() uses requireNotNull() for + * methodName and file. Invalid frames cause secondary errors that break RedBox and reload. + * + * Simply skip frames that don't have the required non-null fields. + */ + private fun sanitize(stackTrace: ReadableArray?): ReadableArray { + val filtered = Arguments.createArray() + if (stackTrace == null) return filtered + (0 until stackTrace.size()) + .mapNotNull { stackTrace.getMap(it) } + .filter { isValidFrame(it) } + .forEach { filtered.pushMap(it) } + + return filtered + } + + /** + * Check if a stack frame has the required non-null fields for StackTraceHelper. + * Uses React Native's own key constants to match their validation logic. + */ + private fun isValidFrame(frame: ReadableMap): Boolean { + return arrayOf(StackTraceHelper.FILE_KEY, StackTraceHelper.METHOD_NAME_KEY).all { + frame.hasKey(it) && !frame.isNull(it) + } } } diff --git a/example/android/app/src/main/java/mendixnative/example/MainApplication.kt b/example/android/app/src/main/java/mendixnative/example/MainApplication.kt index e41702a..814c2fa 100644 --- a/example/android/app/src/main/java/mendixnative/example/MainApplication.kt +++ b/example/android/app/src/main/java/mendixnative/example/MainApplication.kt @@ -1,19 +1,8 @@ package mendixnative.example -import android.app.Application import com.facebook.react.PackageList -import com.facebook.react.ReactApplication -import com.facebook.react.ReactHost -import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load -import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost -import com.facebook.react.defaults.DefaultReactNativeHost -import com.facebook.react.soloader.OpenSourceMergedSoMapping -import com.facebook.soloader.SoLoader - -//Start - For MendixApplication compatibility only, not part of React Native template -import com.mendix.mendixnative.MendixApplication +import com.mendix.mendixnative.MendixReactApplication import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter import com.mendix.mendixnative.react.MxConfiguration @@ -21,44 +10,17 @@ class SplashScreenPresenter: MendixSplashScreenPresenter { override fun show(activity: android.app.Activity) {} override fun hide(activity: android.app.Activity) {} } -//End - For MendixApplication compatibility only, not part of React Native template - -class MainApplication : Application(), MendixApplication { - - override val reactNativeHost: ReactNativeHost = - object : DefaultReactNativeHost(this) { - override fun getPackages(): List = - PackageList(this).packages.apply { - // Packages that cannot be autolinked yet can be added manually here, for example: - // add(MyReactNativePackage()) - } - - override fun getJSMainModuleName(): String = "index" - - override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG - - override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED - override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED - } - override val reactHost: ReactHost - get() = getDefaultReactHost(applicationContext, reactNativeHost) +class MainApplication : MendixReactApplication() { override fun onCreate() { super.onCreate() - MxConfiguration.runtimeUrl = "http://10.0.2.2:8081" //For MendixApplication compatibility only, not part of React Native template - SoLoader.init(this, OpenSourceMergedSoMapping) - if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - // If you opted-in for the New Architecture, we load the native entry point for this app. - load() - } + MxConfiguration.runtimeUrl = "http://10.0.2.2:8081" } - //Start - For MendixApplication compatibility only, not part of React Native template - override fun getUseDeveloperSupport() = false + override fun getUseDeveloperSupport() = BuildConfig.DEBUG override fun createSplashScreenPresenter() = SplashScreenPresenter() override fun getPackages(): List = PackageList(this).packages - override fun getJSBundleFile() = null - override fun getAppSessionId() = null - //End - For MendixApplication compatibility only, not part of React Native template + override fun getJSBundleFile(): String? = null + override fun getAppSessionId() = "" } From c6e25307e85f37c72b774f89de7372e4227c50a8 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:36:25 +0530 Subject: [PATCH 4/9] refactor: enhance NativeDownloadModule to support download progress events --- .../react/download/NativeDownloadModule.kt | 23 ++++-------------- .../mendixnative/download/MxDownloadModule.kt | 24 ++++++++++++++++++- ios/TurboModules/MxDownload/MxDownload.mm | 6 ++++- src/download-handler/NativeMxDownload.ts | 10 +++++++- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/android/src/main/java/com/mendix/mendixnative/react/download/NativeDownloadModule.kt b/android/src/main/java/com/mendix/mendixnative/react/download/NativeDownloadModule.kt index dd8ed9c..71cd0d0 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/download/NativeDownloadModule.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/download/NativeDownloadModule.kt @@ -1,13 +1,15 @@ package com.mendix.mendixnative.react.download import com.facebook.react.bridge.* -import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter import okhttp3.OkHttpClient import java.io.IOException import java.net.ConnectException import java.util.concurrent.TimeUnit -class NativeDownloadModule(val context: ReactApplicationContext) { +class NativeDownloadModule( + val context: ReactApplicationContext, + private val eventEmitter: ((Double, Double) -> Unit)? = null +) { val client = OkHttpClient() fun download( @@ -66,30 +68,15 @@ class NativeDownloadModule(val context: ReactApplicationContext) { } } ) { receivedBytes, totalBytes -> - postProgressEvent( - receivedBytes, - totalBytes - ) + eventEmitter?.invoke(receivedBytes, totalBytes) } } - private fun postProgressEvent(receivedBytes: Double, totalBytes: Double) { - val params = Arguments.createMap() - params.putDouble("receivedBytes", receivedBytes) - params.putDouble("totalBytes", totalBytes) - context - .getJSModule(RCTDeviceEventEmitter::class.java) - .emit(DOWNLOAD_PROGRESS_EVENT, params) - } - companion object { - val supportedEvents: Array = arrayOf(DOWNLOAD_PROGRESS_EVENT) - const val TIMEOUT_KEY = "connectionTimeout" const val MIME_TYPE_KEY = "mimeType" const val TIMEOUT = 10000 - const val DOWNLOAD_PROGRESS_EVENT = "NDM_DOWNLOAD_PROGRESS_EVENT" const val ERROR_DOWNLOAD_FAILED = "ERROR_DOWNLOAD_FAILED" const val FILE_ALREADY_EXISTS = "FILE_ALREADY_EXISTS" const val ERROR_CONNECTION_FAILED = "ERROR_CONNECTION_FAILED" diff --git a/android/src/main/java/com/mendixnative/download/MxDownloadModule.kt b/android/src/main/java/com/mendixnative/download/MxDownloadModule.kt index 843f0ad..2de3c8c 100644 --- a/android/src/main/java/com/mendixnative/download/MxDownloadModule.kt +++ b/android/src/main/java/com/mendixnative/download/MxDownloadModule.kt @@ -1,5 +1,6 @@ package com.mendixnative.download +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap @@ -11,7 +12,13 @@ import com.mendixnative.NativeMxDownloadSpec class MxDownloadModule(reactContext: ReactApplicationContext) : NativeMxDownloadSpec(reactContext) { - private val downloadModule = NativeDownloadModule(reactContext) + // Pass event emitter callback to NativeDownloadModule + private val downloadModule = NativeDownloadModule( + reactContext, + eventEmitter = { receivedBytes, totalBytes -> + emitOnDownloadProgress(receivedBytes, totalBytes) + } + ) override fun getName(): String = NAME @@ -19,6 +26,21 @@ class MxDownloadModule(reactContext: ReactApplicationContext) : downloadModule.download(url, downloadPath, config, promise) } + /** + * Emit download progress event. + * This matches the codegen pattern: readonly onDownloadProgress: EventEmitter + * Codegen generates the base addListener/removeListeners methods automatically. + */ + private fun emitOnDownloadProgress(receivedBytes: Double, totalBytes: Double) { + val params = Arguments.createMap().apply { + putDouble("receivedBytes", receivedBytes) + putDouble("totalBytes", totalBytes) + } + // Emit via the codegen-generated event emitter + // Event name matches the spec: onDownloadProgress + emitOnDownloadProgress(params) + } + companion object { const val NAME = "MxDownload" } diff --git a/ios/TurboModules/MxDownload/MxDownload.mm b/ios/TurboModules/MxDownload/MxDownload.mm index 88faa49..100f7fd 100644 --- a/ios/TurboModules/MxDownload/MxDownload.mm +++ b/ios/TurboModules/MxDownload/MxDownload.mm @@ -26,7 +26,11 @@ - (void)download:(nonnull NSString *)url connectionTimeout = @(config.connectionTimeout().value()); } NSString *mimeType = config.mimeType();; - [[[NativeDownloadModule alloc] init] download:url downloadPath:downloadPath connectionTimeout:connectionTimeout mimeType:mimeType onProgress:nil promise:promise]; + NativeDownloadModule *downloader = [[NativeDownloadModule alloc] init]; + [downloader download:url downloadPath:downloadPath connectionTimeout:connectionTimeout mimeType:mimeType onProgress:^(NSDictionary* progress) { +// Uncomment the line below to track progress events. +// [self emitOnDownloadProgress: progress]; + } promise:promise]; } @end diff --git a/src/download-handler/NativeMxDownload.ts b/src/download-handler/NativeMxDownload.ts index 6173c46..cbb4f77 100644 --- a/src/download-handler/NativeMxDownload.ts +++ b/src/download-handler/NativeMxDownload.ts @@ -6,14 +6,22 @@ type DownloadConfig = { mimeType?: string; }; +type DownloadProgress = { + receivedBytes: CodegenTypes.Double; + totalBytes: CodegenTypes.Double; +}; + export interface Spec extends TurboModule { download( url: string, downloadPath: string, config: DownloadConfig ): Promise; + + // Event emitter for download progress + readonly onDownloadProgress: CodegenTypes.EventEmitter; } export default TurboModuleRegistry.getEnforcing('MxDownload'); -export type { DownloadConfig }; +export type { DownloadConfig, DownloadProgress }; From 54358b3f547b1dd567b8f2611322a86566652bfd Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:16:17 +0530 Subject: [PATCH 5/9] chore: update MendixNative version to 0.5.1 in Podfile.lock --- example/ios/Podfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index af703dc..2288d93 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -3,7 +3,7 @@ PODS: - hermes-engine (250829098.0.9): - hermes-engine/Pre-built (= 250829098.0.9) - hermes-engine/Pre-built (250829098.0.9) - - MendixNative (0.4.1): + - MendixNative (0.5.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2126,7 +2126,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: e97c19a5a442429d1988f182a1940fb08df514da hermes-engine: a7179a4cd45fa3f8143712e52bd3c2d20b5274a0 - MendixNative: 0014d648c1ad67c7da144e99603ad636b017b424 + MendixNative: b0ea153b893ce40b90016f397e9a5acfe4444c33 op-sqlite: e9ef65bcf95a97863874cee87841425bb71c8396 OpenSSL-Universal: 9110d21982bb7e8b22a962b6db56a8aa805afde7 RCTDeprecation: af44b104091a34482596cd9bd7e8d90c4e9b4bd7 From 6123968a18758e65108ad314ed9ac5b87c5363da Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:58:41 +0530 Subject: [PATCH 6/9] refactor: update getConstants and getTypedExportedConstants to use nullable types --- .../java/com/mendix/mendixnative/react/MxConfiguration.kt | 7 ++----- .../mendixnative/configuration/MxConfigurationModule.kt | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt b/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt index b00d1ff..be2119e 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/MxConfiguration.kt @@ -8,7 +8,7 @@ import com.mendix.mendixnative.react.ota.getOtaManifestFilepath class MxConfiguration(val reactContext: ReactApplicationContext) { - fun getConstants(): Map { + fun getConstants(): Map { val application = (reactContext.applicationContext as MendixApplication) if (runtimeUrl == null) { if (warningsFilter != WarningsFilter.none) { @@ -27,6 +27,7 @@ class MxConfiguration(val reactContext: ReactApplicationContext) { val constants = mutableMapOf( "RUNTIME_URL" to AppUrl.forRuntime(runtimeUrl), + "APP_NAME" to defaultAppName, "DATABASE_NAME" to defaultDatabaseName, "FILES_DIRECTORY_NAME" to defaultFilesDirectoryName, "WARNINGS_FILTER_LEVEL" to warningsFilter.toString(), @@ -36,10 +37,6 @@ class MxConfiguration(val reactContext: ReactApplicationContext) { "APP_SESSION_ID" to application.getAppSessionId(), "NATIVE_DEPENDENCIES" to getNativeDependencies(reactContext) ) - defaultAppName?.let { - constants.put("APP_NAME", it) - } - return constants } diff --git a/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt b/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt index 4ac93b4..351e215 100644 --- a/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt +++ b/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt @@ -13,7 +13,7 @@ class MxConfigurationModule(reactContext: ReactApplicationContext) : override fun getName(): String = NAME - override fun getTypedExportedConstants(): Map { + override fun getTypedExportedConstants(): Map { return configuration.getConstants() } From 908ddc65ef93d1f5f4559fca1260bca65ab67a53 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:05:46 +0530 Subject: [PATCH 7/9] refactor: fix tests --- .../ios/MendixNativeExample/AppDelegate.swift | 2 +- example/jest.config.js | 1 - .../MxConfiguration/MxConfiguration.swift | 25 ------------------- .../MxConfiguration/MxConfigurationModule.mm | 2 +- 4 files changed, 2 insertions(+), 28 deletions(-) diff --git a/example/ios/MendixNativeExample/AppDelegate.swift b/example/ios/MendixNativeExample/AppDelegate.swift index bf5983c..d86af0f 100644 --- a/example/ios/MendixNativeExample/AppDelegate.swift +++ b/example/ios/MendixNativeExample/AppDelegate.swift @@ -16,7 +16,7 @@ class AppDelegate: ReactAppProvider { bundleUrl: bundleUrl, runtimeUrl: URL(string: "http://localhost:8081")!, warningsFilter: .none, - isDeveloperApp: false, + isDeveloperApp: true, clearDataAtLaunch: false, splashScreenPresenter: nil, reactLoading: nil, diff --git a/example/jest.config.js b/example/jest.config.js index 210c856..af66ed2 100644 --- a/example/jest.config.js +++ b/example/jest.config.js @@ -1,5 +1,4 @@ module.exports = { - forceExit: true, projects: [ { displayName: 'react-native-harness', diff --git a/ios/Modules/MxConfiguration/MxConfiguration.swift b/ios/Modules/MxConfiguration/MxConfiguration.swift index 4196fe3..4bf12d0 100644 --- a/ios/Modules/MxConfiguration/MxConfiguration.swift +++ b/ios/Modules/MxConfiguration/MxConfiguration.swift @@ -52,31 +52,6 @@ public class MxConfiguration: NSObject { set { _warningsFilter = newValue } } - public func constants() -> [String: Any] { - guard let runtimeUrl = MxConfiguration.runtimeUrl else { - let exception = NSException( - name: NSExceptionName("RUNTIME_URL_MISSING"), - reason: "Runtime URL was not set prior to launch.", - userInfo: nil - ) - exception.raise() - return [:] - } - - return [ - "RUNTIME_URL": runtimeUrl.absoluteString, - "APP_NAME": MxConfiguration.appName ?? NSNull(), - "DATABASE_NAME": MxConfiguration.databaseName, - "FILES_DIRECTORY_NAME": MxConfiguration.filesDirectoryName, - "WARNINGS_FILTER_LEVEL": MxConfiguration.warningsFilter.stringValue, - "OTA_MANIFEST_PATH": OtaHelpers.getOtaManifestFilepath(), - "IS_DEVELOPER_APP": NSNumber(value: MxConfiguration.isDeveloperApp), - "NATIVE_DEPENDENCIES": OtaHelpers.getNativeDependencies(), - "NATIVE_BINARY_VERSION": NSNumber(value: MxConfiguration.nativeBinaryVersion), - "APP_SESSION_ID": MxConfiguration.appSessionId ?? NSNull() - ] - } - public static func update(from mendixApp: MendixApp) { MxConfiguration.runtimeUrl = mendixApp.runtimeUrl MxConfiguration.appName = mendixApp.identifier diff --git a/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm b/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm index d5b4d2f..4fe5121 100644 --- a/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm +++ b/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm @@ -21,7 +21,7 @@ @implementation MxConfigurationModule MxConfigProxy *config = [MxConfigProxy prepare]; return facebook::react::typedConstants({ .RUNTIME_URL = config.runtimeUrl, - .APP_NAME = config.appName, + .APP_NAME = config.appName ?: [[NSNull alloc] init], .FILES_DIRECTORY_NAME = config.filesDirectoryName, .DATABASE_NAME = config.databaseName, .WARNINGS_FILTER_LEVEL = config.warningsFilter, From 72bcf3061c4255791f6734d9f218ad2521614f40 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:40:20 +0530 Subject: [PATCH 8/9] refactor: increase Node.js memory limit for E2E tests --- .github/workflows/ios.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index bae9dc6..fc93106 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -84,4 +84,6 @@ jobs: # Run tests - name: Run Harness E2E tests working-directory: example - run: yarn harness:ios + run: | + export NODE_OPTIONS="--max-old-space-size=6144" + yarn harness:ios From a22e1397d27432b778d7d5238cef79e4debc9d2a Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:54:54 +0530 Subject: [PATCH 9/9] refactor: update deprecated APIs and improve Swift interoperability --- MendixNative.podspec | 8 +- .../project.pbxproj | 2 + example/ios/Podfile.lock | 2 +- ios/Modules/Helper/DevHelper.swift | 4 +- ios/Modules/Helper/ReactAppProvider.swift | 14 +++- ios/Modules/Helper/ReactHostHelper.h | 21 ----- ios/Modules/Helper/ReactHostHelper.mm | 78 ------------------- .../MendixBackwardsCompatUtility.swift | 67 ---------------- .../UnsupportedFeatures.swift | 12 --- ios/Modules/ReactNative.swift | 25 +----- .../RuntimeInfoProvider/RuntimeInfo.swift | 3 +- 11 files changed, 27 insertions(+), 209 deletions(-) delete mode 100644 ios/Modules/Helper/ReactHostHelper.h delete mode 100644 ios/Modules/Helper/ReactHostHelper.mm delete mode 100644 ios/Modules/MendixBackwardsCompatUtility/MendixBackwardsCompatUtility.swift delete mode 100644 ios/Modules/MendixBackwardsCompatUtility/UnsupportedFeatures.swift diff --git a/MendixNative.podspec b/MendixNative.podspec index c234757..fb7152b 100644 --- a/MendixNative.podspec +++ b/MendixNative.podspec @@ -14,8 +14,12 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/mendix/mendix-native.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" - s.public_header_files = "ios/Modules/Helper/ReactHostHelper.h" - s.private_header_files = "ios/TurboModules/**/*.h" + s.public_header_files = "ios/TurboModules/**/*.h" + + s.swift_version = "5.9" + s.pod_target_xcconfig = { + "SWIFT_OBJC_INTEROP_MODE" => "objcxx" + } s.dependency "SSZipArchive" s.dependency "RNCAsyncStorage" diff --git a/example/ios/MendixNativeExample.xcodeproj/project.pbxproj b/example/ios/MendixNativeExample.xcodeproj/project.pbxproj index 8d0597b..927b9bf 100644 --- a/example/ios/MendixNativeExample.xcodeproj/project.pbxproj +++ b/example/ios/MendixNativeExample.xcodeproj/project.pbxproj @@ -278,6 +278,7 @@ PRODUCT_BUNDLE_IDENTIFIER = mendixnative.example; PRODUCT_NAME = MendixNativeExample; SWIFT_OBJC_BRIDGING_HEADER = "MendixNativeExample-Bridging-Header.h"; + SWIFT_OBJC_INTEROP_MODE = objcxx; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -306,6 +307,7 @@ PRODUCT_BUNDLE_IDENTIFIER = mendixnative.example; PRODUCT_NAME = MendixNativeExample; SWIFT_OBJC_BRIDGING_HEADER = "MendixNativeExample-Bridging-Header.h"; + SWIFT_OBJC_INTEROP_MODE = objcxx; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 2288d93..cf42674 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2126,7 +2126,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: e97c19a5a442429d1988f182a1940fb08df514da hermes-engine: a7179a4cd45fa3f8143712e52bd3c2d20b5274a0 - MendixNative: b0ea153b893ce40b90016f397e9a5acfe4444c33 + MendixNative: 9b99a46706e69ff5e3db87f52c1818e8f9700d98 op-sqlite: e9ef65bcf95a97863874cee87841425bb71c8396 OpenSSL-Universal: 9110d21982bb7e8b22a962b6db56a8aa805afde7 RCTDeprecation: af44b104091a34482596cd9bd7e8d90c4e9b4bd7 diff --git a/ios/Modules/Helper/DevHelper.swift b/ios/Modules/Helper/DevHelper.swift index b18f8e8..e98e923 100644 --- a/ios/Modules/Helper/DevHelper.swift +++ b/ios/Modules/Helper/DevHelper.swift @@ -10,7 +10,7 @@ public class DevHelper { getModule(type: RCTDevSettings.self)?.isShakeToShowDevMenuEnabled = enabled // This event can be triggered to facilitate communication with the DevSettings JS module. Please refer to dev-settings.ts for further details. - // ReactHostHelper().emitEvent("mendixSetShakeToShowDevMenu", payload: enabled) + // getModule(type: RCTEventEmitter.self)?.sendEvent(withName: "mendixSetShakeToShowDevMenu", body: enabled) } public static func hideDevLoadingView() { @@ -18,6 +18,6 @@ public class DevHelper { } public static func getModule(type: T.Type) -> T? { - return ReactHostHelper().module(for: T.self) as? T + return ReactAppProvider.shared()?.reactHost()?.moduleRegistry.module(for: type.self) as? T } } diff --git a/ios/Modules/Helper/ReactAppProvider.swift b/ios/Modules/Helper/ReactAppProvider.swift index 9b73e91..1489f45 100644 --- a/ios/Modules/Helper/ReactAppProvider.swift +++ b/ios/Modules/Helper/ReactAppProvider.swift @@ -55,7 +55,13 @@ open class ReactAppProvider: UIResponder, UIApplicationDelegate { } public static func shared() -> ReactAppProvider? { - return UIApplication.shared.delegate as? ReactAppProvider + if Thread.isMainThread { + return UIApplication.shared.delegate as? ReactAppProvider + } else { + return DispatchQueue.main.sync { + return UIApplication.shared.delegate as? ReactAppProvider + } + } } public func changeRoot(to controller: UIViewController) { @@ -68,7 +74,11 @@ open class ReactAppProvider: UIResponder, UIApplicationDelegate { } public static func isReactAppActive() -> Bool { - return ReactHostHelper().isReactAppActive() + return shared()?.reactHost() != nil + } + + public func reactHost() -> RCTHost? { + return reactNativeFactory?.rootViewFactory.reactHost } } diff --git a/ios/Modules/Helper/ReactHostHelper.h b/ios/Modules/Helper/ReactHostHelper.h deleted file mode 100644 index 53b10ad..0000000 --- a/ios/Modules/Helper/ReactHostHelper.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// ReactHostHelper.h -// MendixNative -// -// Created by Yogendra Shelke on 13/05/26. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface ReactHostHelper : NSObject - -- (nullable id) moduleForClass: (Class) clazz; -- (void) reloadClientWithState; -- (BOOL) isReactAppActive; -- (void) emitEvent: (nonnull NSString*) eventName payload: (nullable id) payload; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/Modules/Helper/ReactHostHelper.mm b/ios/Modules/Helper/ReactHostHelper.mm deleted file mode 100644 index f9ee17f..0000000 --- a/ios/Modules/Helper/ReactHostHelper.mm +++ /dev/null @@ -1,78 +0,0 @@ -// -// ReactHostHelper.mm -// MendixNative -// -// Created by Yogendra Shelke on 13/05/26. -// - -#import "ReactHostHelper.h" -#import -#import -#import "RCTDefaultReactNativeFactoryDelegate.h" -#import "RCTReactNativeFactory.h" -#import "MendixNative-Swift.h" -#import "MxReload.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation ReactHostHelper - -- (nullable id) moduleForClass: (Class) clazz { - if ([NSThread isMainThread]) { - return [self getModuleForClass: clazz]; - } else { - __block id result; - dispatch_sync(dispatch_get_main_queue(), ^{ - result = [self getModuleForClass: clazz]; - }); - return result; - } -} - -- (nullable id) getModuleForClass: (Class) clazz { - RCTHost *reactHost = [self currentHost]; - RCTModuleRegistry *moduleRegistry = [reactHost moduleRegistry]; - id nativeModule = [moduleRegistry moduleForClass: clazz]; - return nativeModule; -} - -- (RCTHost *) currentHost { - ReactAppProvider *reactAppProvider = (ReactAppProvider *) [[UIApplication sharedApplication] delegate]; - RCTReactNativeFactory *reactNativeFactory = [reactAppProvider reactNativeFactory]; - RCTRootViewFactory *rootViewFactory = [reactNativeFactory rootViewFactory]; - RCTHost *reactHost = [rootViewFactory reactHost]; - return reactHost; -} - - -- (void) reloadClientWithState { - MxReload *mxReload = [self moduleForClass: MxReload.class]; - [mxReload emitOnReloadWithState]; -} - -- (BOOL)isReactAppActive { - if ([NSThread isMainThread]) { - return [self currentHost] != nil; - } else { - __block bool result; - dispatch_sync(dispatch_get_main_queue(), ^{ - result = [self currentHost] != nil; - }); - return result; - } -} - -- (void)emitEvent:(nonnull NSString *)eventName payload:(nullable id)payload { - RCTHost *reactHost = [self currentHost]; - - NSMutableArray *args = [NSMutableArray arrayWithObject:eventName]; - if (payload != nil) { - [args addObject:payload]; - } - - [reactHost callFunctionOnJSModule:@"RCTDeviceEventEmitter" method:@"emit" args:args]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/Modules/MendixBackwardsCompatUtility/MendixBackwardsCompatUtility.swift b/ios/Modules/MendixBackwardsCompatUtility/MendixBackwardsCompatUtility.swift deleted file mode 100644 index 3b9f82e..0000000 --- a/ios/Modules/MendixBackwardsCompatUtility/MendixBackwardsCompatUtility.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation - -public class MendixBackwardsCompatUtility: NSObject { - - private static let versionDictionary: [String: UnsupportedFeatures] = [ - "8.9": UnsupportedFeatures(reloadInClient: true, hideSplashScreenInClient: true), - "8.10": UnsupportedFeatures(reloadInClient: false, hideSplashScreenInClient: true), - "8.11": UnsupportedFeatures(reloadInClient: false, hideSplashScreenInClient: true), - "8.12.0": UnsupportedFeatures(reloadInClient: false, hideSplashScreenInClient: true), - "DEFAULT": UnsupportedFeatures(reloadInClient: false, hideSplashScreenInClient: false) - ] - - private static var _unsupportedFeatures: UnsupportedFeatures? = versionDictionary["DEFAULT"] - private static let lock = NSLock() - - public static func unsupportedFeatures() -> UnsupportedFeatures? { - lock.lock() - defer { lock.unlock() } - return _unsupportedFeatures - } - - public static func update(_ forVersion: String) { - let versionParts = forVersion.components(separatedBy: ".") - let versionDict = versionDictionary - - lock.lock() - defer { lock.unlock() } - - // Try with up to 3 parts (major.minor.patch) - if versionParts.count >= 3 { - let threePartVersion = Array(versionParts.prefix(3)).joined(separator: ".") - if let features = versionDict[threePartVersion] { - _unsupportedFeatures = features - return - } - } - - // Try with 2 parts (major.minor) - if versionParts.count >= 2 { - let twoPartVersion = Array(versionParts.prefix(2)).joined(separator: ".") - if let features = versionDict[twoPartVersion] { - _unsupportedFeatures = features - return - } - } - - // Try with 1 part (major) - if versionParts.count >= 1 { - if let features = versionDict[versionParts[0]] { - _unsupportedFeatures = features - return - } - } - - // Default fallback - _unsupportedFeatures = versionDict["DEFAULT"] - } - - static func isHideSplashScreenInClientSupported() -> Bool { - - if let unsupportedFeatures = Self.unsupportedFeatures() { - return !unsupportedFeatures.hideSplashScreenInClient - } - - return true - } -} diff --git a/ios/Modules/MendixBackwardsCompatUtility/UnsupportedFeatures.swift b/ios/Modules/MendixBackwardsCompatUtility/UnsupportedFeatures.swift deleted file mode 100644 index 28b95a6..0000000 --- a/ios/Modules/MendixBackwardsCompatUtility/UnsupportedFeatures.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation - -public class UnsupportedFeatures: NSObject { - public let reloadInClient: Bool - public let hideSplashScreenInClient: Bool - - public init(reloadInClient: Bool, hideSplashScreenInClient: Bool = false) { - self.reloadInClient = reloadInClient - self.hideSplashScreenInClient = hideSplashScreenInClient - super.init() - } -} diff --git a/ios/Modules/ReactNative.swift b/ios/Modules/ReactNative.swift index 5f3268c..5e1833d 100644 --- a/ios/Modules/ReactNative.swift +++ b/ios/Modules/ReactNative.swift @@ -5,12 +5,10 @@ public protocol ReactNativeDelegateInternal: AnyObject { func onAppClosed() } -@objcMembers open class ReactNative: NSObject, RCTReloadListener { // MARK: - Properties private var mendixApp: MendixApp? private var bundleUrl: URL? - private var mendixOTAEnabled: Bool = false private var tapGestureHelper: TapGestureRecognizerHelper? public weak var delegate: ReactNativeDelegateInternal? @@ -23,10 +21,9 @@ open class ReactNative: NSObject, RCTReloadListener { } // MARK: - Setup Methods - public func setup(_ mendixApp: MendixApp, launchOptions: [AnyHashable: Any]? = nil, mendixOTAEnabled: Bool = false) { + public func setup(_ mendixApp: MendixApp, launchOptions: [AnyHashable: Any]? = nil) { self.mendixApp = mendixApp self.bundleUrl = mendixApp.bundleUrl - self.mendixOTAEnabled = mendixOTAEnabled if let host = bundleUrl?.host, let port = bundleUrl?.port { let jsLocation = "\(host):\(port)" @@ -68,9 +65,7 @@ open class ReactNative: NSObject, RCTReloadListener { // MARK: - Splash Screen Methods public func showSplashScreen() { - if MendixBackwardsCompatUtility.isHideSplashScreenInClientSupported() { - mendixApp?.splashScreenPresenter?.show(ReactAppProvider.shared()?.rootView) - } + mendixApp?.splashScreenPresenter?.show(ReactAppProvider.shared()?.rootView) } public func hideSplashScreen() { @@ -86,25 +81,11 @@ open class ReactNative: NSObject, RCTReloadListener { // which RCTHost re-invokes on reload. RCTReloadCommandSetBundleURL is a legacy-bridge // mechanism that the bridgeless host ignores, so it is intentionally not used here. - if mendixApp.isDeveloperApp { - let runtimeInfoUrl = AppUrl.forRuntimeInfo(mendixApp.runtimeUrl.absoluteString) - RuntimeInfoProvider.getRuntimeInfo(runtimeInfoUrl) { [weak self] response in - if response.status == "SUCCESS", let version = response.runtimeInfo?.version { - MendixBackwardsCompatUtility.update(version) - } - self?.reloadWithBridge() - } - } else { - reloadWithBridge() - } - } - - private func reloadWithBridge() { RCTTriggerReloadCommandListeners("Reload command from app") } public func reloadWithState() { - ReactHostHelper().reloadClientWithState() + DevHelper.getModule(type: MxReload.self)?.emitOnReloadWithState() } // MARK: - RCTReloadListener diff --git a/ios/Modules/RuntimeInfoProvider/RuntimeInfo.swift b/ios/Modules/RuntimeInfoProvider/RuntimeInfo.swift index dc1de1b..7204ba1 100644 --- a/ios/Modules/RuntimeInfoProvider/RuntimeInfo.swift +++ b/ios/Modules/RuntimeInfoProvider/RuntimeInfo.swift @@ -1,6 +1,6 @@ import Foundation -public class RuntimeInfo: NSObject { +public class RuntimeInfo { // MARK: - Properties public let cacheburst: String @@ -14,7 +14,6 @@ public class RuntimeInfo: NSObject { self.cacheburst = cacheburst self.nativeBinaryVersion = nativeBinaryVersion self.packagerPort = packagerPort - super.init() } convenience init(_ dictionary: [String: Any]) {