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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) }
}

/**
* 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,52 +1,44 @@
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)
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
}
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