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 814bfff..b00d1ff 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,43 +8,37 @@ 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) { - 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.") ) - 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/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/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/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/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/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/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() = "" } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index af703dc..07922b3 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.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2126,7 +2126,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: e97c19a5a442429d1988f182a1940fb08df514da hermes-engine: a7179a4cd45fa3f8143712e52bd3c2d20b5274a0 - MendixNative: 0014d648c1ad67c7da144e99603ad636b017b424 + MendixNative: 7cba944a4e608e6ab40ec281e9b2b0f9f3cdefe0 op-sqlite: e9ef65bcf95a97863874cee87841425bb71c8396 OpenSSL-Universal: 9110d21982bb7e8b22a962b6db56a8aa805afde7 RCTDeprecation: af44b104091a34482596cd9bd7e8d90c4e9b4bd7 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/Helper/ReactHostHelper.h b/ios/Modules/Helper/ReactHostHelper.h index 53b10ad..0961a0c 100644 --- a/ios/Modules/Helper/ReactHostHelper.h +++ b/ios/Modules/Helper/ReactHostHelper.h @@ -6,6 +6,7 @@ // #import +#import NS_ASSUME_NONNULL_BEGIN @@ -13,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN - (nullable id) moduleForClass: (Class) clazz; - (void) reloadClientWithState; +- (void) reload; +- (void) updateBundleURL: (nonnull NSURL*) bundleURL; - (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..271dd2d 100644 --- a/ios/Modules/Helper/ReactHostHelper.mm +++ b/ios/Modules/Helper/ReactHostHelper.mm @@ -7,7 +7,7 @@ #import "ReactHostHelper.h" #import -#import +#import #import "RCTDefaultReactNativeFactoryDelegate.h" #import "RCTReactNativeFactory.h" #import "MendixNative-Swift.h" @@ -17,18 +17,34 @@ @implementation ReactHostHelper -- (nullable id) moduleForClass: (Class) clazz { +#pragma mark - Thread Helpers + +- (void) runOnUiThread:(dispatch_block_t)block { if ([NSThread isMainThread]) { - return [self getModuleForClass: clazz]; + block(); } else { - __block id result; - dispatch_sync(dispatch_get_main_queue(), ^{ - result = [self getModuleForClass: clazz]; - }); - return result; + dispatch_async(dispatch_get_main_queue(), block); } } +- (void) runOnUiThreadSync:(dispatch_block_t)block { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } +} + +#pragma mark - Module Access + +- (nullable id) moduleForClass: (Class) clazz { + __block id result; + [self runOnUiThreadSync:^{ + result = [self getModuleForClass: clazz]; + }]; + return result; +} + - (nullable id) getModuleForClass: (Class) clazz { RCTHost *reactHost = [self currentHost]; RCTModuleRegistry *moduleRegistry = [reactHost moduleRegistry]; @@ -50,16 +66,26 @@ - (void) reloadClientWithState { [mxReload emitOnReloadWithState]; } +- (void) reload { + [self runOnUiThread:^{ + [[self currentHost] reload]; + }]; +} + +- (void) updateBundleURL:(NSURL *)bundleURL { + [self runOnUiThread:^{ + [[self currentHost] setBundleURLProvider:^NSURL * _Nullable{ + return bundleURL; + }]; + }]; +} + - (BOOL)isReactAppActive { - if ([NSThread isMainThread]) { - return [self currentHost] != nil; - } else { - __block bool result; - dispatch_sync(dispatch_get_main_queue(), ^{ - result = [self currentHost] != nil; - }); - return result; - } + __block BOOL result; + [self runOnUiThreadSync:^{ + result = [self currentHost] != nil; + }]; + return result; } - (void)emitEvent:(nonnull NSString *)eventName payload:(nullable id)payload { 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/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() {} -} diff --git a/ios/Modules/ReactNative.swift b/ios/Modules/ReactNative.swift index caa1ab7..ebf3b83 100644 --- a/ios/Modules/ReactNative.swift +++ b/ios/Modules/ReactNative.swift @@ -84,7 +84,8 @@ open class ReactNative: NSObject, RCTReloadListener { let otaBundleUrl = OtaJSBundleFileProvider.getBundleUrl() if !mendixApp.isDeveloperApp, let otaBundleUrl = otaBundleUrl { - RCTReloadCommandSetBundleURL(otaBundleUrl) + updateBundleURL(otaBundleUrl) + triggerReload() } if mendixApp.isDeveloperApp { @@ -93,15 +94,22 @@ open class ReactNative: NSObject, RCTReloadListener { if response.status == "SUCCESS", let version = response.runtimeInfo?.version { MendixBackwardsCompatUtility.update(version) } - self?.reloadWithBridge() + self?.triggerReload() } } else { - reloadWithBridge() + triggerReload() } } - - private func reloadWithBridge() { - RCTTriggerReloadCommandListeners("Reload command from app") + + private func triggerReload() { + showSplashScreen() + // Note: This bypasses RCTReloadListener notifications (only dev menu Cmd+R triggers those) + ReactHostHelper().reload() + } + + private func updateBundleURL(_ url: URL) { + self.bundleUrl = url + ReactHostHelper().updateBundleURL(url) } public func reloadWithState() { 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/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/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/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 }; 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();