Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<ReactPackage> {
val pkgs: MutableList<ReactPackage> = 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 —
Expand Down Expand Up @@ -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? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) }
Comment thread
YogendraShelke marked this conversation as resolved.
Outdated
}

/**
* 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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,42 @@
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
import com.mendix.mendixnative.react.ota.getOtaManifestFilepath

class MxConfiguration(val reactContext: ReactApplicationContext) {

fun getConstants(): WritableMap? {
fun getConstants(): Map<String, Any?> {
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)
}
constants.putMap("NATIVE_DEPENDENCIES", dependencies)

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(),
"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)
)
return constants
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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>(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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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<String> = 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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any> {
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,7 +13,7 @@ class MxConfigurationModule(reactContext: ReactApplicationContext) :

override fun getName(): String = NAME

override fun getConfig(): WritableMap? {
override fun getTypedExportedConstants(): Map<String, Any?> {
return configuration.getConstants()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,14 +12,35 @@ 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

override fun download(url: String, downloadPath: String, config: ReadableMap, promise: Promise) {
downloadModule.download(url, downloadPath, config, promise)
}

/**
* Emit download progress event.
* This matches the codegen pattern: readonly onDownloadProgress: EventEmitter<DownloadProgress>
* 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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,7 +15,7 @@ class MxFileSystemModule(reactContext: ReactApplicationContext) :

override fun getName(): String = NAME

override fun constants(): WritableMap? {
override fun getTypedExportedConstants(): Map<String, Any> {
return fsModule.getConstants()
}

Expand Down
Loading
Loading