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 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 06/14] 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 07/14] 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 08/14] 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 bdfe6e0dd31d9cc0d44c8f314630868c7e4d4791 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 09/14] refactor: remove deprecated APIs --- ios/Modules/Helper/ReactHostHelper.h | 1 - ios/Modules/Helper/ReactHostHelper.mm | 7 -- .../MendixBackwardsCompatUtility.swift | 67 ------------------- .../UnsupportedFeatures.swift | 12 ---- ios/Modules/ReactNative.swift | 52 +------------- 5 files changed, 3 insertions(+), 136 deletions(-) delete mode 100644 ios/Modules/MendixBackwardsCompatUtility/MendixBackwardsCompatUtility.swift delete mode 100644 ios/Modules/MendixBackwardsCompatUtility/UnsupportedFeatures.swift diff --git a/ios/Modules/Helper/ReactHostHelper.h b/ios/Modules/Helper/ReactHostHelper.h index 53b10ad..d381d70 100644 --- a/ios/Modules/Helper/ReactHostHelper.h +++ b/ios/Modules/Helper/ReactHostHelper.h @@ -12,7 +12,6 @@ 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; diff --git a/ios/Modules/Helper/ReactHostHelper.mm b/ios/Modules/Helper/ReactHostHelper.mm index f9ee17f..0800bcc 100644 --- a/ios/Modules/Helper/ReactHostHelper.mm +++ b/ios/Modules/Helper/ReactHostHelper.mm @@ -11,7 +11,6 @@ #import "RCTDefaultReactNativeFactoryDelegate.h" #import "RCTReactNativeFactory.h" #import "MendixNative-Swift.h" -#import "MxReload.h" NS_ASSUME_NONNULL_BEGIN @@ -44,12 +43,6 @@ - (RCTHost *) currentHost { return reactHost; } - -- (void) reloadClientWithState { - MxReload *mxReload = [self moduleForClass: MxReload.class]; - [mxReload emitOnReloadWithState]; -} - - (BOOL)isReactAppActive { if ([NSThread isMainThread]) { return [self currentHost] != nil; 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..0402c21 100644 --- a/ios/Modules/ReactNative.swift +++ b/ios/Modules/ReactNative.swift @@ -5,13 +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 +20,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 +64,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() { @@ -80,33 +74,15 @@ open class ReactNative: NSObject, RCTReloadListener { // MARK: - Reload Methods public func reload() { - guard let mendixApp = mendixApp else { return } + guard mendixApp != nil else { return } // Note: under the New Architecture the bundle URL is resolved fresh in bundleURL(), // 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() - } - // MARK: - RCTReloadListener @objc public func didReceiveReloadCommand() { showSplashScreen() @@ -129,28 +105,6 @@ open class ReactNative: NSObject, RCTReloadListener { remoteDebugging(true) } - // MARK: - Gesture Recognition - private func attachThreeFingerGestures(to window: UIWindow) { - tapGestureHelper?.attach() - } - - private func removeThreeFingerGestures(from window: UIWindow) { - tapGestureHelper?.remove() - window.motionBegan(.motionShake, with: nil) - } - - @objc private func appReloadAction(_ gestureRecognizer: UITapGestureRecognizer) { - if gestureRecognizer.state == .ended && ReactAppProvider.isReactAppActive() == true { - reloadWithState() - } - } - - // MARK: - Legacy Methods (for compatibility) - func useCodePush() -> Bool { - // Implementation depends on your specific CodePush setup - return false - } - public func bundleURL() -> URL? { // New Architecture (Bridgeless): RCTHost re-invokes this provider block on every // reload (via RCTRootViewFactory's bundleURLBlock) instead of consulting the URL set From d42f7952a0690586a442aa8fbe9005b60965f113 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:30:13 +0530 Subject: [PATCH 10/14] refactor: add loop protection for OTA redeployment and update error handling --- .../mendixnative/react/ota/NativeOtaModule.kt | 25 ++++++++++++++ .../OtaJSBundleFileProvider.swift | 6 +--- .../MxConfiguration/MxConfigProxy.swift | 6 ++-- .../NativeDownloadHandler.swift | 16 +++++++++ .../NativeOtaModule/NativeOtaModule.swift | 33 +++++++++++++++++-- .../NativeOtaModule/OtaConstants.swift | 1 + 6 files changed, 76 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/mendix/mendixnative/react/ota/NativeOtaModule.kt b/android/src/main/java/com/mendix/mendixnative/react/ota/NativeOtaModule.kt index bec60fb..a263fa7 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/ota/NativeOtaModule.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/ota/NativeOtaModule.kt @@ -19,6 +19,7 @@ const val OTA_ZIP_FILE_MISSING = "OTA_ZIP_FILE_MISSING" const val OTA_UNZIP_DIR_EXISTS = "OTA_UNZIP_DIR_EXISTS" const val OTA_DEPLOYMENT_FAILED = "OTA_DEPLOYMENT_FAILED" const val OTA_DOWNLOAD_FAILED = "OTA_DOWNLOAD_FAILED" +const val OTA_ALREADY_DEPLOYED = "OTA_ALREADY_DEPLOYED" const val MANIFEST_OTA_DEPLOYMENT_ID_KEY = "otaDeploymentID" const val MANIFEST_RELATIVE_BUNDLE_PATH_KEY = "relativeBundlePath" @@ -141,6 +142,30 @@ class NativeOtaModule( Log.i(TAG, "Deploying ota with id: $otaDeploymentID") + // Loop protection: refuse to redeploy a deployment that is already the active one. + // Redeploying and reloading into an already-active deployment restarts the app into + // the same bundle, causing an infinite download -> deploy -> reload loop. This happens + // when the served OTA bundle's embedded deploymentID never matches the server-advertised + // deploymentID (e.g. a bundle served as an OTA update but built for a different deployment). + // Rejecting here makes the JS OTA flow skip the reload, so the app keeps running the + // currently deployed bundle instead of looping. + if (oldManifest != null && + oldManifest.otaDeploymentID == otaDeploymentID && + File( + resolveAbsolutePathRelativeToOtaDir( + reactApplicationContext, + oldManifest.relativeBundlePath + ) + ).exists() + ) { + zipFile.delete() + return reject( + promise, + OTA_ALREADY_DEPLOYED, + "Deployment $otaDeploymentID is already active. Skipping redeploy to prevent a reload loop." + ) + } + if (!zipFile.exists()) { return reject(promise, OTA_ZIP_FILE_MISSING, "OTA package does not exist") } diff --git a/ios/Modules/JSBundleFileProvider/OtaJSBundleFileProvider.swift b/ios/Modules/JSBundleFileProvider/OtaJSBundleFileProvider.swift index 1b769c3..5c6cb1e 100644 --- a/ios/Modules/JSBundleFileProvider/OtaJSBundleFileProvider.swift +++ b/ios/Modules/JSBundleFileProvider/OtaJSBundleFileProvider.swift @@ -54,10 +54,6 @@ extension OtaJSBundleFileProvider: JSBundleFileProviderProtocol { return nil } - guard let encodedPath = bundlePath.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { - return nil - } - - return URL(string: encodedPath) + return URL(fileURLWithPath: bundlePath) } } diff --git a/ios/Modules/MxConfiguration/MxConfigProxy.swift b/ios/Modules/MxConfiguration/MxConfigProxy.swift index e7cd793..3e94b93 100644 --- a/ios/Modules/MxConfiguration/MxConfigProxy.swift +++ b/ios/Modules/MxConfiguration/MxConfigProxy.swift @@ -8,12 +8,12 @@ public class MxConfigProxy: NSObject { public var filesDirectoryName: String public var warningsFilter: String public var otaManifestPath: String - public var isDeveloperApp: NSNumber + public var isDeveloperApp: Bool 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?) { + init(runtimeUrl: String, appName: String?, databaseName: String, filesDirectoryName: String, warningsFilter: String, otaManifestPath: String, isDeveloperApp: Bool, nativeDependencies: [String : Any], nativeBinaryVersion: NSNumber, appSessionId: String?) { self.runtimeUrl = runtimeUrl self.appName = appName self.databaseName = databaseName @@ -45,7 +45,7 @@ public class MxConfigProxy: NSObject { filesDirectoryName: MxConfiguration.filesDirectoryName, warningsFilter: MxConfiguration.warningsFilter.stringValue, otaManifestPath: OtaHelpers.getOtaManifestFilepath(), - isDeveloperApp: NSNumber(booleanLiteral: MxConfiguration.isDeveloperApp), + isDeveloperApp: MxConfiguration.isDeveloperApp, nativeDependencies: OtaHelpers.getNativeDependencies(), nativeBinaryVersion: NSNumber(integerLiteral: MxConfiguration.nativeBinaryVersion), appSessionId: MxConfiguration.appSessionId diff --git a/ios/Modules/NativeDownloadHandler/NativeDownloadHandler.swift b/ios/Modules/NativeDownloadHandler/NativeDownloadHandler.swift index bf097ee..01274fd 100644 --- a/ios/Modules/NativeDownloadHandler/NativeDownloadHandler.swift +++ b/ios/Modules/NativeDownloadHandler/NativeDownloadHandler.swift @@ -58,6 +58,22 @@ extension NativeDownloadHandler: URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { let fileManager = FileManager.default + // Validate the HTTP status code. URLSession's download task writes the response body + // to `location` even for error responses (e.g. 404/500), so without this check an + // HTML/JSON error body would be saved as the "downloaded" file and later fail to unzip + // with a misleading error. Fail explicitly with the status code instead. + if let httpResponse = downloadTask.response as? HTTPURLResponse, + !(200...299).contains(httpResponse.statusCode) { + let error = NSError( + domain: NSURLErrorDomain, + code: httpResponse.statusCode, + userInfo: [NSLocalizedDescriptionKey: "Download failed with HTTP status \(httpResponse.statusCode)."] + ) + NSLog("%@", formatMessage("Download failed with HTTP status \(httpResponse.statusCode)")) + failCallback?(error) + return + } + // Check MIME type if specified if let expectedMimeType = mimeType, let responseMimeType = downloadTask.response?.mimeType, diff --git a/ios/Modules/NativeOtaModule/NativeOtaModule.swift b/ios/Modules/NativeOtaModule/NativeOtaModule.swift index 25a2237..86885b2 100644 --- a/ios/Modules/NativeOtaModule/NativeOtaModule.swift +++ b/ios/Modules/NativeOtaModule/NativeOtaModule.swift @@ -145,6 +145,25 @@ public class NativeOtaModule: NSObject { let oldManifest = OtaHelpers.readManifestAsDictionary() + // Loop protection: refuse to redeploy a deployment that is already the active one. + // Redeploying and reloading into an already-active deployment restarts the app into + // the same bundle, causing an infinite download -> deploy -> reload loop. This happens + // when the served OTA bundle's embedded deploymentID never matches the server-advertised + // deploymentID (e.g. a bundle served as an OTA update but built for a different deployment). + // Rejecting here makes the JS OTA flow skip the reload, so the app keeps running the + // currently deployed bundle instead of looping. + if let oldManifest = oldManifest, + let deployedID = oldManifest[MANIFEST_OTA_DEPLOYMENT_ID_KEY] as? String, + deployedID == otaDeploymentID, + let deployedBundlePath = oldManifest[MANIFEST_RELATIVE_BUNDLE_PATH_KEY] as? String, + FileManager.default.fileExists(atPath: OtaHelpers.resolveAbsolutePathRelativeToOtaDir("/\(deployedBundlePath)")) { + let message = "[OTA] Deployment \(otaDeploymentID) is already active. Skipping redeploy to prevent a reload loop." + NSLog("%@", message) + removeZipFile(zipPath) + promise.reject(OTA_ALREADY_DEPLOYED, message, nil) + return + } + let fileExists = FileManager.default.fileExists(atPath: zipPath) if !fileExists { let errorMessage = "[OTA] OTA package does not exist." @@ -161,9 +180,17 @@ public class NativeOtaModule: NSObject { let unzipped = SSZipArchive.unzipFile(atPath: zipPath, toDestination: unzipDir, overwrite: false, password: nil, progressHandler: nil) if !unzipped { - NSLog("[OTA] Unzipping OTA failed") - removeZipFile(unzipDir) - promise.reject(OTA_DEPLOYMENT_FAILED, "OTA deployment failed.", nil) + // Diagnostic: the "OTA package" is not a valid zip. This most often means the + // server returned a non-zip response (e.g. an HTML/JSON error body) that was + // saved as the download. Log the size and leading bytes so the real cause is visible. + let fileSize = (try? FileManager.default.attributesOfItem(atPath: zipPath)[.size] as? Int) ?? nil + var preview = "" + if let handle = FileManager.default.contents(atPath: zipPath) { + preview = String(data: handle.prefix(64), encoding: .utf8) ?? handle.prefix(4).map { String(format: "%02x", $0) }.joined() + } + NSLog("[OTA] Unzipping OTA failed. Downloaded file size: \(fileSize ?? -1) bytes. Leading bytes: \(preview)") + removeZipFile(zipPath) + promise.reject(OTA_DEPLOYMENT_FAILED, "OTA deployment failed: downloaded package is not a valid zip.", nil) return } diff --git a/ios/Modules/NativeOtaModule/OtaConstants.swift b/ios/Modules/NativeOtaModule/OtaConstants.swift index 4978b92..2a3c030 100644 --- a/ios/Modules/NativeOtaModule/OtaConstants.swift +++ b/ios/Modules/NativeOtaModule/OtaConstants.swift @@ -12,6 +12,7 @@ let OTA_UNZIP_DIR_EXISTS = "OTA_UNZIP_DIR_EXISTS" let OTA_DEPLOYMENT_FAILED = "OTA_DEPLOYMENT_FAILED" let INVALID_DOWNLOAD_CONFIG = "INVALID_DOWNLOAD_CONFIG" let OTA_DOWNLOAD_FAILED = "OTA_DOWNLOAD_FAILED" +let OTA_ALREADY_DEPLOYED = "OTA_ALREADY_DEPLOYED" // MARK: - Download Config Keys let DOWNLOAD_CONFIG_URL_KEY = "url" From d76066ac8fca9971d27dce9ee46e450b7efc28c6 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:19:51 +0530 Subject: [PATCH 11/14] refactor: update onKeyUp method to require non-null KeyEvent parameter --- .../fragment/MendixReactFragment.kt | 4 +- .../mendixnative/fragment/ReactFragment.kt | 174 ++---------------- 2 files changed, 15 insertions(+), 163 deletions(-) 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 f96a7da..86ffd3d 100644 --- a/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt +++ b/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt @@ -84,7 +84,7 @@ open class MendixReactFragment : ReactFragment(), MendixReactFragmentView { super.onDestroy() } - override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { if (keyCode == KeyEvent.KEYCODE_MENU || doubleTapReloadRecognizer.didDoubleTapBacktick( keyCode, view @@ -109,7 +109,7 @@ open class MendixReactFragment : ReactFragment(), MendixReactFragmentView { } interface MendixReactFragmentView : DevAppMenuHandler, BackButtonHandler { - fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean + fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean } interface BackButtonHandler { 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 d21f152..d18a2a3 100644 --- a/android/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt +++ b/android/src/main/java/com/mendix/mendixnative/fragment/ReactFragment.kt @@ -2,180 +2,32 @@ package com.mendix.mendixnative.fragment import android.content.Intent import android.os.Bundle -import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.facebook.react.ReactApplication -import com.facebook.react.ReactDelegate -import com.facebook.react.ReactHost -import com.facebook.react.modules.core.PermissionAwareActivity -import com.facebook.react.modules.core.PermissionListener -import com.mendix.mendixnative.react.CopiedFrom - /** - * Fragment for creating a React View. This allows the developer to "embed" a React Application - * inside native components such as a Drawer, ViewPager, etc. + * Mendix's [com.facebook.react.ReactFragment] extension. + * + * Adds two behaviors on top of upstream that aren't available in RN itself: + * - tapjacking protection on the react root view + * - always forwarding [onActivityResult] to the React instance, since Mendix embeds React + * fragments inside native navigation (Drawer/ViewPager/Nav) where the host activity's result + * is meant for the React app. */ -@CopiedFrom(com.facebook.react.ReactFragment::class) -open class ReactFragment : Fragment(), PermissionAwareActivity { - private var mReactDelegate: ReactDelegate? = null - private var mPermissionListener: PermissionListener? = null - - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - var mainComponentName: String? = null - var launchOptions: Bundle? = null - if (arguments != null) { - mainComponentName = requireArguments().getString(ARG_COMPONENT_NAME) - launchOptions = requireArguments().getBundle(ARG_LAUNCH_OPTIONS) - } - checkNotNull(mainComponentName) { "Cannot loadApp if component name is null" } - mReactDelegate = activity?.let { ReactDelegate(it, reactHost!!, mainComponentName, launchOptions) } - } - - /** - * 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 reactHost: ReactHost? - get() = (requireActivity().application as ReactApplication).reactHost +open class ReactFragment : com.facebook.react.ReactFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - mReactDelegate!!.loadApp() - // Adds tapjacking protection to the rootview - mReactDelegate!!.reactRootView?.filterTouchesWhenObscured = true - return mReactDelegate!!.reactRootView - } - - override fun onResume() { - super.onResume() - mReactDelegate!!.onHostResume() - } - - override fun onPause() { - super.onPause() - mReactDelegate!!.onHostPause() + val view = super.onCreateView(inflater, container, savedInstanceState) + reactDelegate.reactRootView?.filterTouchesWhenObscured = true + return view } - override fun onDestroy() { - super.onDestroy() - mReactDelegate!!.onHostDestroy() - } - - // endregion + @Suppress("DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - mReactDelegate!!.onActivityResult(requestCode, resultCode, data, true) - } - - /** - * Helper to forward hardware back presses to our React Native Host - * - * - * This must be called via a forward from your host Activity - */ - fun onBackPressed(): Boolean { - return mReactDelegate!!.onBackPressed() - } - - /** - * Helper to forward onKeyUp commands from our host Activity. This allows ReactFragment to handle - * double tap reloads and dev menus - * - * - * This must be called via a forward from your host Activity - * - * @param keyCode keyCode - * @param event event - * @return true if we handled onKeyUp - */ - open fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { - return mReactDelegate!!.shouldShowDevMenuOrReload(keyCode, event) - } - - override fun onRequestPermissionsResult( - requestCode: Int, permissions: Array, grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (mPermissionListener != null - && mPermissionListener!!.onRequestPermissionsResult(requestCode, permissions, grantResults) - ) { - mPermissionListener = null - } - } - - override fun checkPermission(permission: String, pid: Int, uid: Int): Int { - return requireActivity().checkPermission(permission, pid, uid) - } - - override fun checkSelfPermission(permission: String): Int { - return requireActivity().checkSelfPermission(permission) - } - - override fun requestPermissions( - permissions: Array, - requestCode: Int, - listener: PermissionListener? - ) { - mPermissionListener = listener - requestPermissions(permissions, requestCode) - } - - /** Builder class to help instantiate a ReactFragment */ - class Builder { - var mComponentName: String? = null - var mLaunchOptions: Bundle? = null - - /** - * Set the Component name for our React Native instance. - * - * @param componentName The name of the component - * @return Builder - */ - fun setComponentName(componentName: String?): Builder { - mComponentName = componentName - return this - } - - /** - * Set the Launch Options for our React Native instance. - * - * @param launchOptions launchOptions - * @return Builder - */ - fun setLaunchOptions(launchOptions: Bundle?): Builder { - mLaunchOptions = launchOptions - return this - } - - fun build(): ReactFragment { - return newInstance(mComponentName, mLaunchOptions) - } - } - - companion object { - private const val ARG_COMPONENT_NAME = "arg_component_name" - private const val ARG_LAUNCH_OPTIONS = "arg_launch_options" - - /** - * @param componentName The name of the react native component - * @return A new instance of fragment ReactFragment. - */ - private fun newInstance(componentName: String?, launchOptions: Bundle?): ReactFragment { - val fragment = ReactFragment() - val args = Bundle() - args.putString(ARG_COMPONENT_NAME, componentName) - args.putBundle(ARG_LAUNCH_OPTIONS, launchOptions) - fragment.arguments = args - return fragment - } + reactDelegate.onActivityResult(requestCode, resultCode, data, true) } } From edf59905bd89d9cff62c0b1a12a0911ab63a019b Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:08:47 +0530 Subject: [PATCH 12/14] refactor: remove deprecated APIs and improve serialization handling --- .../mendixnative/MendixApplication.java | 1 - .../mendix/mendixnative/MendixInitializer.kt | 1 - .../activity/MendixReactActivity.kt | 23 +++++++++--- .../fragment/MendixReactFragment.kt | 19 +++++++++- .../mendix/mendixnative/react/ClearData.kt | 6 +-- .../react/cookie/NativeCookieModule.kt | 4 +- .../util/MendixBackwardsCompatUtility.kt | 37 ------------------- 7 files changed, 38 insertions(+), 53 deletions(-) delete mode 100644 android/src/main/java/com/mendix/mendixnative/util/MendixBackwardsCompatUtility.kt diff --git a/android/src/main/java/com/mendix/mendixnative/MendixApplication.java b/android/src/main/java/com/mendix/mendixnative/MendixApplication.java index b147638..5b2fe72 100644 --- a/android/src/main/java/com/mendix/mendixnative/MendixApplication.java +++ b/android/src/main/java/com/mendix/mendixnative/MendixApplication.java @@ -5,7 +5,6 @@ import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter; import java.util.List; -import java.util.Map; public interface MendixApplication extends ReactApplication { boolean getUseDeveloperSupport(); diff --git a/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt b/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt index 5961b53..c34eeb9 100644 --- a/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt +++ b/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt @@ -2,7 +2,6 @@ package com.mendix.mendixnative import android.app.Activity import com.facebook.react.ReactHost -import com.facebook.react.devsupport.DevSupportManagerBase import com.facebook.react.modules.network.OkHttpClientProvider import com.mendix.mendixnative.config.AppPreferences import com.mendix.mendixnative.react.* 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 66d08a2..f159f00 100644 --- a/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt +++ b/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt @@ -1,10 +1,10 @@ package com.mendix.mendixnative.activity +import android.os.Build import android.os.Bundle import android.view.KeyEvent import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate -import com.facebook.react.bridge.ReactContext import com.facebook.react.devsupport.interfaces.DevSupportManager import com.mendix.mendixnative.DevAppMenuHandler import com.mendix.mendixnative.MendixApplication @@ -12,6 +12,7 @@ import com.mendix.mendixnative.MendixInitializer import com.mendix.mendixnative.react.MendixApp import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter import com.mendix.mendixnative.util.MendixBackwardsCompatUtility +import java.io.Serializable open class MendixReactActivity : ReactActivity(), DevAppMenuHandler, LaunchScreenHandler { @@ -24,7 +25,7 @@ open class MendixReactActivity : ReactActivity(), DevAppMenuHandler, LaunchScree override fun onCreate(savedInstanceState: Bundle?) { mendixApp = mendixApp - ?: intent.getSerializableExtra(MENDIX_APP_INTENT_KEY) as? MendixApp + ?: getSerializableData(MENDIX_APP_INTENT_KEY, MendixApp::class.java) ?: throw IllegalStateException("MendixApp configuration can't be null") val mendixApplication = application as? MendixApplication ?: throw ClassCastException("Application needs to implement MendixApplication") @@ -36,6 +37,19 @@ open class MendixReactActivity : ReactActivity(), DevAppMenuHandler, LaunchScree super.onCreate(null) } + inline fun getSerializableData(key: String?, clazz: Class): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getSerializableExtra(key, clazz) + } else { + @Suppress("DEPRECATION") + val data = intent.getSerializableExtra(key) + if (data is T) { + return data + } + return null + } + } + override fun onDestroy() { mendixInitializer.onDestroy() super.onDestroy() @@ -46,12 +60,9 @@ open class MendixReactActivity : ReactActivity(), DevAppMenuHandler, LaunchScree } override fun showDevAppMenu() { - currentDevSupportManager?.showDevOptionsDialog(); + currentDevSupportManager?.showDevOptionsDialog() } - private val currentReactContext: ReactContext? - 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 86ffd3d..923d676 100644 --- a/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt +++ b/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt @@ -1,6 +1,7 @@ package com.mendix.mendixnative.fragment import android.content.Intent +import android.os.Build import android.os.Bundle import android.view.KeyEvent import com.facebook.react.devsupport.interfaces.DevSupportManager @@ -9,6 +10,7 @@ import com.mendix.mendixnative.MendixInitializer import com.mendix.mendixnative.activity.LaunchScreenHandler import com.mendix.mendixnative.react.MendixApp import com.mendix.mendixnative.util.MendixDoubleTapRecognizer +import java.io.Serializable /** * Class used for Sample apps @@ -54,7 +56,7 @@ open class MendixReactFragment : ReactFragment(), MendixReactFragmentView { } if (mendixApp == null) { - mendixApp = requireArguments().getSerializable(ARG_MENDIX_APP) as MendixApp? + mendixApp = getSerializableData(ARG_MENDIX_APP, MendixApp::class.java) ?: throw IllegalArgumentException("Mendix app is required") } @@ -73,6 +75,19 @@ open class MendixReactFragment : ReactFragment(), MendixReactFragmentView { super.onCreate(savedInstanceState) } + inline fun getSerializableData(key: String?, clazz: Class): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requireArguments().getSerializable(key, clazz) + } else { + @Suppress("DEPRECATION") + val data = requireArguments().getSerializable(key) + if (data is T) { + return data + } + return null + } + } + fun onNewIntent(intent: Intent) { reactHost?.currentReactContext?.let { it.onNewIntent(it.currentActivity, intent) @@ -100,7 +115,7 @@ open class MendixReactFragment : ReactFragment(), MendixReactFragmentView { override fun showDevAppMenu() { activity?.let { - currentDevSupportManager?.showDevOptionsDialog(); + currentDevSupportManager?.showDevOptionsDialog() } } diff --git a/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt b/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt index bf11f00..ac0baa8 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt @@ -48,12 +48,12 @@ fun clearDataWithReactContext(applicationContext: Application, reactHost: ReactH deleteAppDatabaseAsync(reactContext, object : BooleanCallback { var fired = false - override fun invoke(success: Boolean) { + override fun invoke(res: Boolean) { if (fired) return fired = true - if (!success) { + if (!res) { reportError("database") } @@ -62,7 +62,7 @@ fun clearDataWithReactContext(applicationContext: Application, reactHost: ReactH } clearSecureStorage(reactContext?.applicationContext) - if (!success) { + if (!res) { reportError("encrypted storage") } diff --git a/android/src/main/java/com/mendix/mendixnative/react/cookie/NativeCookieModule.kt b/android/src/main/java/com/mendix/mendixnative/react/cookie/NativeCookieModule.kt index 68aeff1..dbc2465 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/cookie/NativeCookieModule.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/cookie/NativeCookieModule.kt @@ -2,13 +2,11 @@ package com.mendix.mendixnative.react.cookie import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.WritableMap -import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.modules.network.ForwardingCookieHandler class NativeCookieModule(val reactContext: ReactApplicationContext) { fun clearAll(promise: Promise) { - ForwardingCookieHandler(reactContext).clearCookies { + ForwardingCookieHandler().clearCookies { promise.resolve(null) } } diff --git a/android/src/main/java/com/mendix/mendixnative/util/MendixBackwardsCompatUtility.kt b/android/src/main/java/com/mendix/mendixnative/util/MendixBackwardsCompatUtility.kt deleted file mode 100644 index 4c4d4ea..0000000 --- a/android/src/main/java/com/mendix/mendixnative/util/MendixBackwardsCompatUtility.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.mendix.mendixnative.util - -data class UnsupportedFeatures(val reloadInClient: Boolean = false, val hideSplashScreenInClient: Boolean = false); - -class MendixBackwardsCompatUtility private constructor() { - companion object { - @Volatile - private var INSTANCE: MendixBackwardsCompatUtility? = null - - fun getInstance(): MendixBackwardsCompatUtility { - return synchronized(this) { - if (INSTANCE != null) { - INSTANCE!! - } else { - INSTANCE = MendixBackwardsCompatUtility() - INSTANCE!! - } - - } - } - - fun update(forVersion: String) { - getInstance().update(forVersion) - } - } - - private val versionMap = emptyMap() - var unsupportedFeatures: UnsupportedFeatures = UnsupportedFeatures() - private set - - private fun update(forVersion: String) { - val versionParts = forVersion.split(".") - unsupportedFeatures = versionMap[versionParts.take(versionParts.size.coerceAtMost(3)).joinToString(".")] - ?: versionMap[versionParts.take(versionParts.size.coerceAtMost(2)).joinToString(".")] - ?: versionMap[versionParts[0]] ?: UnsupportedFeatures() - } -} From 860d7f066156371e814e62437a0dcb0950d16a73 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:28:06 +0530 Subject: [PATCH 13/14] refactor: simplify splash screen handling by removing compatibility check --- .../com/mendix/mendixnative/activity/MendixReactActivity.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 f159f00..b1e3257 100644 --- a/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt +++ b/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt @@ -11,7 +11,6 @@ import com.mendix.mendixnative.MendixApplication import com.mendix.mendixnative.MendixInitializer import com.mendix.mendixnative.react.MendixApp import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter -import com.mendix.mendixnative.util.MendixBackwardsCompatUtility import java.io.Serializable open class MendixReactActivity : ReactActivity(), DevAppMenuHandler, LaunchScreenHandler { @@ -81,9 +80,7 @@ open class MendixReactActivity : ReactActivity(), DevAppMenuHandler, LaunchScree } override fun showLaunchScreen() { - if (!MendixBackwardsCompatUtility.getInstance().unsupportedFeatures.hideSplashScreenInClient && splashScreenPresenter != null) { - splashScreenPresenter?.show(this) - } + splashScreenPresenter?.show(this) } override fun hideLaunchScreen() { From a8b8e7097728f9405d6b6a86586d80798e5a5eff Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:02:54 +0530 Subject: [PATCH 14/14] refactor: remove deprecated APIs and clean up unused code --- .../react/devsupport/MendixShakeDetector.kt | 46 ------ .../mendix/mendixnative/api/RuntimeInfo.kt | 6 +- .../mendixnative/config/AppPreferences.java | 8 +- .../mendix/mendixnative/react/menu/AppMenu.kt | 5 - .../mendixnative/react/ota/OtaHelpers.kt | 10 +- .../util/MendixDoubleTapRecognizer.kt | 3 +- .../mendixnative/util/ReflectionUtils.java | 132 ------------------ ios/Modules/Extensions/RNCAsyncStorageExt.h | 4 - .../WarningFilter/WarningsFilter.swift | 45 ------ package.json | 2 +- 10 files changed, 12 insertions(+), 249 deletions(-) delete mode 100644 android/src/main/java/com/facebook/react/devsupport/MendixShakeDetector.kt delete mode 100644 android/src/main/java/com/mendix/mendixnative/react/menu/AppMenu.kt delete mode 100644 android/src/main/java/com/mendix/mendixnative/util/ReflectionUtils.java delete mode 100644 ios/Modules/Extensions/RNCAsyncStorageExt.h diff --git a/android/src/main/java/com/facebook/react/devsupport/MendixShakeDetector.kt b/android/src/main/java/com/facebook/react/devsupport/MendixShakeDetector.kt deleted file mode 100644 index a8b601b..0000000 --- a/android/src/main/java/com/facebook/react/devsupport/MendixShakeDetector.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.facebook.react.devsupport - -import android.content.Context -import android.hardware.SensorManager -import com.facebook.react.common.ShakeDetector -import com.facebook.react.devsupport.interfaces.DevSupportManager -import com.mendix.mendixnative.util.ReflectionUtils - -const val SHAKE_DETECTECTOR_VAR = "mShakeDetector" -const val SHAKE_DETECTOR_VAR = "shakeDetector" - -fun makeShakeDetector(applicationContext: Context, onShake: () -> Unit): ShakeDetector { - val shakeDetector = ShakeDetector(onShake) - - shakeDetector.start(applicationContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager) - return shakeDetector -} - -fun attachMendixSupportManagerShakeDetector( - shakeDetector: ShakeDetector, - devSupportManager: DevSupportManager? -) { - val supportManager = devSupportManager ?: return - - try { - val devShakeDetector = - ReflectionUtils.getFieldOfSuperclass( - supportManager, - SHAKE_DETECTECTOR_VAR, - SHAKE_DETECTOR_VAR - ) - - if (devShakeDetector !== shakeDetector) { - devShakeDetector.stop() - } - - ReflectionUtils.setFieldOfSuperclass( - supportManager, - shakeDetector, - SHAKE_DETECTECTOR_VAR, - SHAKE_DETECTOR_VAR - ) - } catch (_: RuntimeException) { - // React Native internals changed; keep the Mendix detector active without replacing RN's. - } -} diff --git a/android/src/main/java/com/mendix/mendixnative/api/RuntimeInfo.kt b/android/src/main/java/com/mendix/mendixnative/api/RuntimeInfo.kt index 2620941..6785e80 100644 --- a/android/src/main/java/com/mendix/mendixnative/api/RuntimeInfo.kt +++ b/android/src/main/java/com/mendix/mendixnative/api/RuntimeInfo.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.mendix.mendixnative.config.AppUrl import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException import java.util.concurrent.TimeUnit @@ -22,10 +23,7 @@ fun getRuntimeInfo(runtimeUrl: String, cb: (info: RuntimeInfoResponse) -> Unit) client.newCall( Request.Builder() .post( - RequestBody.create( - "application/json; charset=utf-8".toMediaTypeOrNull(), - "{\"action\":\"info\"}" - ) + "{\"action\":\"info\"}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) ) .url(AppUrl.removeTrailingSlash(AppUrl.ensureProtocol(runtimeUrl)) + "/xas/") .build() diff --git a/android/src/main/java/com/mendix/mendixnative/config/AppPreferences.java b/android/src/main/java/com/mendix/mendixnative/config/AppPreferences.java index b3f0989..0c1f5f0 100644 --- a/android/src/main/java/com/mendix/mendixnative/config/AppPreferences.java +++ b/android/src/main/java/com/mendix/mendixnative/config/AppPreferences.java @@ -2,7 +2,6 @@ import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; import com.facebook.react.modules.debug.interfaces.DeveloperSettings; import com.facebook.react.packagerconnection.PackagerConnectionSettings; @@ -38,7 +37,12 @@ final public class AppPreferences { private final SharedPreferences preferences; public AppPreferences(Context applicationContext) { - preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext); + // Matches android.preference.PreferenceManager#getDefaultSharedPreferencesName so this + // continues to read/write the same file as React Native's own DeveloperSettings and + // PackagerConnectionSettings (see @CopiedFrom keys above), without depending on the + // deprecated android.preference.PreferenceManager API. + String defaultPreferencesName = applicationContext.getPackageName() + "_preferences"; + preferences = applicationContext.getSharedPreferences(defaultPreferencesName, Context.MODE_PRIVATE); } public String getAppUrl() { diff --git a/android/src/main/java/com/mendix/mendixnative/react/menu/AppMenu.kt b/android/src/main/java/com/mendix/mendixnative/react/menu/AppMenu.kt deleted file mode 100644 index d2cf54e..0000000 --- a/android/src/main/java/com/mendix/mendixnative/react/menu/AppMenu.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.mendix.mendixnative.react.menu - -interface AppMenu { - fun show() -} diff --git a/android/src/main/java/com/mendix/mendixnative/react/ota/OtaHelpers.kt b/android/src/main/java/com/mendix/mendixnative/react/ota/OtaHelpers.kt index e983014..609edfd 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/ota/OtaHelpers.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/ota/OtaHelpers.kt @@ -1,7 +1,6 @@ package com.mendix.mendixnative.react.ota import android.content.Context -import android.os.Build import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper import com.mendix.mendixnative.util.ResourceReader @@ -22,14 +21,7 @@ fun resolveAppVersion(context: Context): String { return context.packageManager.getPackageInfo( context.packageName, 0 - ).let { info -> - info.versionName.let { versionName -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - "$versionName-${info.longVersionCode}" - else - "$versionName-${info.versionCode}" - } - } + ).let { info -> "${info.versionName}-${info.longVersionCode}" } } fun getNativeDependencies(context: Context): Map { diff --git a/android/src/main/java/com/mendix/mendixnative/util/MendixDoubleTapRecognizer.kt b/android/src/main/java/com/mendix/mendixnative/util/MendixDoubleTapRecognizer.kt index 710952d..bdfd399 100644 --- a/android/src/main/java/com/mendix/mendixnative/util/MendixDoubleTapRecognizer.kt +++ b/android/src/main/java/com/mendix/mendixnative/util/MendixDoubleTapRecognizer.kt @@ -1,6 +1,7 @@ package com.mendix.mendixnative.util import android.os.Handler +import android.os.Looper import android.view.KeyEvent import android.view.View import android.widget.EditText @@ -16,7 +17,7 @@ class MendixDoubleTapRecognizer { return true } else { mDoRefresh = true - Handler() + Handler(Looper.getMainLooper()) .postDelayed( { mDoRefresh = false }, DOUBLE_TAP_DELAY) diff --git a/android/src/main/java/com/mendix/mendixnative/util/ReflectionUtils.java b/android/src/main/java/com/mendix/mendixnative/util/ReflectionUtils.java deleted file mode 100644 index 4627e25..0000000 --- a/android/src/main/java/com/mendix/mendixnative/util/ReflectionUtils.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.mendix.mendixnative.util; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public class ReflectionUtils { - private static Field findDeclaredField(Class objectClass, String... fieldNames) { - NoSuchFieldException lastException = null; - - for (String fieldName : fieldNames) { - try { - return objectClass.getDeclaredField(fieldName); - } catch (NoSuchFieldException e) { - lastException = e; - } - } - - throw new RuntimeException(lastException); - } - - public static ConstructorWrapper findConstructor(String className, Class... parameterTypes) { - try { - Constructor constructor = Class.forName(className).getDeclaredConstructor(parameterTypes); - constructor.setAccessible(true); - return new ConstructorWrapper(constructor); - } catch (ClassNotFoundException | NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - // TODO: replace this with a lambda after upgrading to Java 8 - public static class ConstructorWrapper { - private final Constructor constructor; - - ConstructorWrapper(Constructor constructor) { - this.constructor = constructor; - } - - public T newInstance(Object... args) { - try { - return (T) constructor.newInstance(args); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - } - - public static MethodWrapper findMethod(Object object, String methodName, Class... parameterTypes) { - try { - Method method = object.getClass().getDeclaredMethod(methodName, parameterTypes); - method.setAccessible(true); - return new MethodWrapper(method, object); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - // TODO: replace this with a lambda after upgrading to Java 8 - public static class MethodWrapper { - private final Method method; - private final Object object; - - MethodWrapper(Method method, Object object) { - this.method = method; - this.object = object; - } - - public void invoke(Object... args) { - try { - method.invoke(object, args); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - } - - public static void setFieldOfSuperclass(Object object, String fieldName, Object value) { - setFieldOfSuperclass(object, value, fieldName); - } - - public static void setFieldOfSuperclass(Object object, Object value, String... fieldNames) { - Field field = findDeclaredField(object.getClass().getSuperclass(), fieldNames); - setField(object, field, value); - } - - public static void setField(Object object, String fieldName, Object value) { - try { - Field field = object.getClass().getDeclaredField(fieldName); - setField(object, field, value); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - - private static void setField(Object object, Field field, Object value) { - try { - field.setAccessible(true); - field.set(object, value); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } finally { - field.setAccessible(false); - } - } - - public static T getFieldOfSuperclass(Object object, String fieldName) { - return getFieldOfSuperclass(object, new String[] { fieldName }); - } - - public static T getFieldOfSuperclass(Object object, String... fieldNames) { - try { - Field field = findDeclaredField(object.getClass().getSuperclass(), fieldNames); - field.setAccessible(true); - return (T) field.get(object); - } catch (IllegalAccessException | ClassCastException e) { - throw new RuntimeException(e); - } - } - - public static T getField(Object object, String fieldName) { - try { - Field field = object.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return (T) field.get(object); - } catch (NoSuchFieldException | IllegalAccessException | ClassCastException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/ios/Modules/Extensions/RNCAsyncStorageExt.h b/ios/Modules/Extensions/RNCAsyncStorageExt.h deleted file mode 100644 index f657ad2..0000000 --- a/ios/Modules/Extensions/RNCAsyncStorageExt.h +++ /dev/null @@ -1,4 +0,0 @@ -@interface RNCAsyncStorage() -- (dispatch_queue_t)methodQueue; -- (void) multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; -@end diff --git a/ios/Modules/WarningFilter/WarningsFilter.swift b/ios/Modules/WarningFilter/WarningsFilter.swift index 5c7d1c2..7525995 100644 --- a/ios/Modules/WarningFilter/WarningsFilter.swift +++ b/ios/Modules/WarningFilter/WarningsFilter.swift @@ -16,35 +16,6 @@ import Foundation return "none" } } - - /// Static array for Objective-C compatibility (equivalent to WarningsFilter_toString) - static let toString: [String] = [ - "all", - "partial", - "none" - ] - - /// Get string representation for a given index (Objective-C compatibility) - static func string(for index: Int) -> String? { - guard index >= 0 && index < toString.count else { - return nil - } - return toString[index] - } - - /// Create WarningsFilter from string value - static func from(string: String) -> WarningsFilter? { - switch string.lowercased() { - case "all": - return .all - case "partial": - return .partial - case "none": - return WarningsFilter.none - default: - return nil - } - } } // MARK: - CustomStringConvertible @@ -53,19 +24,3 @@ extension WarningsFilter: CustomStringConvertible { return stringValue } } - -// MARK: - Objective-C Bridge Functions -@objc class WarningsFilterBridge: NSObject { - /// Bridge function to get string representation (for Objective-C compatibility) - @objc static func toString(for filter: WarningsFilter) -> String { - return filter.stringValue - } - - /// Bridge function to get WarningsFilter from index (for Objective-C compatibility) - @objc static func filter(for index: Int) -> WarningsFilter { - guard let filter = WarningsFilter(rawValue: index) else { - return .none // Default fallback - } - return filter - } -} diff --git a/package.json b/package.json index d15b9ec..8077677 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "types": "./lib/typescript/src/index.d.ts", "exports": { ".": { - "source": "./src/index.tsx", + "source": "./src/index.ts", "types": "./lib/typescript/src/index.d.ts", "default": "./lib/module/index.js" },