feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,99 @@
apply plugin: 'com.android.library'
// Import autolinking script
apply from: "../scripts/autolinking.gradle"
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useDefaultAndroidSdkVersions()
useExpoPublishing()
static def versionToNumber(major, minor, patch) {
return patch * 100 + minor * 10000 + major * 1000000
}
def getRNVersion() {
def nodeModulesVersion = providers.exec {
workingDir(projectDir)
commandLine("node", "-e", "console.log(require('react-native/package.json').version);")
}.standardOutput.asText.get().trim()
def version = safeExtGet("reactNativeVersion", nodeModulesVersion)
def coreVersion = version.split("-")[0]
def (major, minor, patch) = coreVersion.tokenize('.').collect { it.toInteger() }
return versionToNumber(
major,
minor,
patch
)
}
ensureDependeciesWereEvaluated(project)
group = 'host.exp.exponent'
version = '51.0.39'
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
android {
namespace "expo.core"
defaultConfig {
versionCode 1
versionName "51.0.39"
consumerProguardFiles("proguard-rules.pro")
}
testOptions {
unitTests.includeAndroidResources = true
}
sourceSets {
main {
java {
srcDirs += new File(project.buildDir, generatedFilesSrcDir)
def rnVersion = getRNVersion()
if (rnVersion >= versionToNumber(0, 74, 0)) {
srcDirs += "src/reactWrappers"
}
}
}
}
}
dependencies { dependencyHandler ->
implementation 'com.facebook.react:react-android'
testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.test:core:1.5.0'
testImplementation "com.google.truth:truth:1.1.2"
testImplementation 'io.mockk:mockk:1.13.5'
// Link expo modules as dependencies of the adapter. It uses `api` configuration so they all will be visible for the app as well.
// A collection of the dependencies depends on the options passed to `useExpoModules` in your project's `settings.gradle`.
addExpoModulesDependencies(dependencyHandler, project)
}
// A task generating a package list of expo modules.
task generateExpoModulesPackageListTask {
def modulesConfig = getModulesConfig()
def outputPath = getGenerateExpoModulesPackagesListPath()
if (modulesConfig) {
outputs.file(file(outputPath))
inputs.property("modulesConfig", modulesConfig)
}
// TOOD(@lukmccall): fix not working with configuration cache enabled
doLast {
generateExpoModulesPackageList()
}
}
// Run that task during prebuilding phase.
preBuild.dependsOn "generateExpoModulesPackageListTask"

View File

@@ -0,0 +1,26 @@
# For ReactActivityDelegateWrapper
-keepclassmembers public class com.facebook.react.ReactActivityDelegate {
public *;
protected *;
private ReactDelegate mReactDelegate;
}
# Remove this after react-native 0.74.1
-keepclassmembers public class expo.modules.ReactActivityDelegateWrapper {
protected ReactDelegate getReactDelegate();
}
-keepclassmembers public class com.facebook.react.ReactActivity {
private final ReactActivityDelegate mDelegate;
}
# For ReactNativeHostWrapper
-keepclassmembers public class com.facebook.react.ReactNativeHost {
protected *;
}
# For ExpoModulesPackage autolinking
-keepclassmembers public class expo.modules.ExpoModulesPackageList {
public *;
}
-keepnames class * extends expo.modules.core.BasePackage
-keepnames class * implements expo.modules.core.interfaces.Package

View File

@@ -0,0 +1,3 @@
<manifest>
</manifest>

View File

@@ -0,0 +1,27 @@
package expo.modules
import android.app.Application
import android.content.res.Configuration
import androidx.annotation.UiThread
import expo.modules.core.interfaces.ApplicationLifecycleListener
object ApplicationLifecycleDispatcher {
private var listeners: List<ApplicationLifecycleListener>? = null
@UiThread
private fun getCachedListeners(application: Application): List<ApplicationLifecycleListener> {
return listeners ?: ExpoModulesPackage.packageList
.flatMap { it.createApplicationLifecycleListeners(application) }
.also { listeners = it }
}
@JvmStatic
fun onApplicationCreate(application: Application) {
getCachedListeners(application).forEach { it.onCreate(application) }
}
@JvmStatic
fun onConfigurationChanged(application: Application, newConfig: Configuration) {
getCachedListeners(application).forEach { it.onConfigurationChanged(newConfig) }
}
}

View File

@@ -0,0 +1,41 @@
package expo.modules
import android.util.Log
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import expo.modules.adapters.react.ModuleRegistryAdapter
import expo.modules.core.ModulePriorities
import expo.modules.core.interfaces.Package
import java.lang.Exception
class ExpoModulesPackage : ReactPackage {
val moduleRegistryAdapter = ModuleRegistryAdapter(packageList)
companion object {
@Suppress("unchecked_cast")
val packageList: List<Package> by lazy {
try {
val expoModules = Class.forName("expo.modules.ExpoModulesPackageList")
val getPackageList = expoModules.getMethod("getPackageList")
(getPackageList.invoke(null) as List<Package>)
.sortedByDescending { ModulePriorities.get(it::class.qualifiedName) }
} catch (e: Exception) {
Log.e("ExpoModulesPackage", "Couldn't get expo package list.", e)
emptyList()
}
}
}
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return moduleRegistryAdapter.createNativeModules(reactContext)
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return moduleRegistryAdapter.createViewManagers(reactContext)
}
}

View File

@@ -0,0 +1,161 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules
import android.content.Context
import com.facebook.react.JSEngineResolutionAlgorithm
import com.facebook.react.ReactHost
import com.facebook.react.ReactInstanceEventListener
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.ReactPackageTurboModuleManagerDelegate
import com.facebook.react.bridge.JSBundleLoader
import com.facebook.react.bridge.ReactContext
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.defaults.DefaultComponentsRegistry
import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate
import com.facebook.react.fabric.ComponentFactory
import com.facebook.react.fabric.ReactNativeConfig
import com.facebook.react.interfaces.exceptionmanager.ReactJsExceptionHandler
import com.facebook.react.runtime.BindingsInstaller
import com.facebook.react.runtime.JSCInstance
import com.facebook.react.runtime.JSRuntimeFactory
import com.facebook.react.runtime.ReactHostDelegate
import com.facebook.react.runtime.ReactHostImpl
import com.facebook.react.runtime.hermes.HermesInstance
import java.lang.ref.WeakReference
object ExpoReactHostFactory {
private var reactHost: ReactHost? = null
@UnstableReactNativeAPI
private class ExpoReactHostDelegate(
private val weakContext: WeakReference<Context>,
private val reactNativeHostWrapper: ReactNativeHostWrapper,
override val bindingsInstaller: BindingsInstaller? = null,
private val reactNativeConfig: ReactNativeConfig = ReactNativeConfig.DEFAULT_CONFIG,
override val turboModuleManagerDelegateBuilder: ReactPackageTurboModuleManagerDelegate.Builder =
DefaultTurboModuleManagerDelegate.Builder()
) : ReactHostDelegate {
// Keeps this `_jsBundleLoader` backing property for DevLauncher to replace its internal value
private var _jsBundleLoader: JSBundleLoader? = null
override val jsBundleLoader: JSBundleLoader
get() {
val backingJSBundleLoader = _jsBundleLoader
if (backingJSBundleLoader != null) {
return backingJSBundleLoader
}
val context = weakContext.get() ?: throw IllegalStateException("Unable to get concrete Context")
reactNativeHostWrapper.jsBundleFile?.let { jsBundleFile ->
if (jsBundleFile.startsWith("assets://")) {
return JSBundleLoader.createAssetLoader(context, jsBundleFile, true)
}
return JSBundleLoader.createFileLoader(jsBundleFile)
}
val jsBundleAssetPath = reactNativeHostWrapper.bundleAssetName
return JSBundleLoader.createAssetLoader(context, "assets://$jsBundleAssetPath", true)
}
override val jsMainModulePath: String
get() = reactNativeHostWrapper.jsMainModuleName
override val jsRuntimeFactory: JSRuntimeFactory
get() = if (reactNativeHostWrapper.jsEngineResolutionAlgorithm == JSEngineResolutionAlgorithm.HERMES) {
HermesInstance()
} else {
JSCInstance()
}
override val reactPackages: List<ReactPackage>
get() = reactNativeHostWrapper.packages
override fun getReactNativeConfig(): ReactNativeConfig = reactNativeConfig
override fun handleInstanceException(error: Exception) {
val useDeveloperSupport = reactNativeHostWrapper.useDeveloperSupport
reactNativeHostWrapper.reactNativeHostHandlers.forEach { handler ->
handler.onReactInstanceException(useDeveloperSupport, error)
}
}
}
@OptIn(UnstableReactNativeAPI::class)
@JvmStatic
fun createFromReactNativeHost(
context: Context,
reactNativeHost: ReactNativeHost
): ReactHost {
require(reactNativeHost is ReactNativeHostWrapper) {
"You can call createFromReactNativeHost only with instances of ReactNativeHostWrapper"
}
if (reactHost == null) {
val useDeveloperSupport = reactNativeHost.useDeveloperSupport
val reactHostDelegate = ExpoReactHostDelegate(WeakReference(context), reactNativeHost)
val reactJsExceptionHandler = ReactJsExceptionHandler { _ -> }
val componentFactory = ComponentFactory()
DefaultComponentsRegistry.register(componentFactory)
reactNativeHost.reactNativeHostHandlers.forEach { handler ->
handler.onWillCreateReactInstance(useDeveloperSupport)
}
var reactHostImpl: ReactHostImpl
try {
// react-native 0.75.0 removed the ReactJsExceptionHandler parameter
val constructorWithoutHandler = ReactHostImpl::class.java.getConstructor(
Context::class.java,
ReactHostDelegate::class.java,
ComponentFactory::class.java,
Boolean::class.javaPrimitiveType,
Boolean::class.javaPrimitiveType
)
reactHostImpl = constructorWithoutHandler.newInstance(
context,
reactHostDelegate,
componentFactory,
true,
useDeveloperSupport
)
} catch (e: NoSuchMethodException) {
val constructorWithHandler = ReactHostImpl::class.java.getConstructor(
Context::class.java,
ReactHostDelegate::class.java,
ComponentFactory::class.java,
Boolean::class.javaPrimitiveType,
ReactJsExceptionHandler::class.java,
Boolean::class.javaPrimitiveType
)
reactHostImpl = constructorWithHandler.newInstance(
context,
reactHostDelegate,
componentFactory,
true,
reactJsExceptionHandler,
useDeveloperSupport
)
}
reactHostImpl.apply {
jsEngineResolutionAlgorithm = reactNativeHost.jsEngineResolutionAlgorithm
}
reactNativeHost.reactNativeHostHandlers.forEach { handler ->
handler.onDidCreateDevSupportManager(reactHostImpl.devSupportManager)
}
reactHostImpl.addReactInstanceEventListener(object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
reactNativeHost.reactNativeHostHandlers.forEach { handler ->
handler.onDidCreateReactInstance(useDeveloperSupport, context)
}
}
})
reactHost = reactHostImpl
}
return reactHost as ReactHost
}
}

View File

@@ -0,0 +1,347 @@
package expo.modules
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.KeyEvent
import android.view.ViewGroup
import androidx.collection.ArrayMap
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.ReactDelegate
import com.facebook.react.ReactHost
import com.facebook.react.ReactInstanceEventListener
import com.facebook.react.ReactInstanceManager
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactRootView
import com.facebook.react.bridge.ReactContext
import com.facebook.react.config.ReactFeatureFlags
import com.facebook.react.modules.core.PermissionListener
import expo.modules.core.interfaces.ReactActivityLifecycleListener
import expo.modules.kotlin.Utils
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier
class ReactActivityDelegateWrapper(
private val activity: ReactActivity,
private val isNewArchitectureEnabled: Boolean,
private var delegate: ReactActivityDelegate
) : ReactActivityDelegate(activity, null) {
constructor(activity: ReactActivity, delegate: ReactActivityDelegate) :
this(activity, false, delegate)
private val reactActivityLifecycleListeners = ExpoModulesPackage.packageList
.flatMap { it.createReactActivityLifecycleListeners(activity) }
private val reactActivityHandlers = ExpoModulesPackage.packageList
.flatMap { it.createReactActivityHandlers(activity) }
private val methodMap: ArrayMap<String, Method> = ArrayMap()
private val _reactNativeHost: ReactNativeHost by lazy {
invokeDelegateMethod("getReactNativeHost")
}
private val _reactHost: ReactHost? by lazy {
delegate.reactHost
}
/**
* When the app delay for `loadApp`, the ReactInstanceManager's lifecycle will be disrupted.
* This flag indicates we should emit `onResume` after `loadApp`.
*/
private var shouldEmitPendingResume = false
//region ReactActivityDelegate
override fun getLaunchOptions(): Bundle? {
return invokeDelegateMethod("getLaunchOptions")
}
override fun createRootView(): ReactRootView? {
return invokeDelegateMethod("createRootView")
}
override fun getReactDelegate(): ReactDelegate? {
return invokeDelegateMethod("getReactDelegate")
}
override fun getReactNativeHost(): ReactNativeHost {
return _reactNativeHost
}
override fun getReactHost(): ReactHost? {
return _reactHost
}
override fun getReactInstanceManager(): ReactInstanceManager {
return delegate.reactInstanceManager
}
override fun getMainComponentName(): String? {
return delegate.mainComponentName
}
override fun loadApp(appKey: String?) {
// Give modules a chance to wrap the ReactRootView in a container ViewGroup. If some module
// wants to do this, we override the functionality of `loadApp` and call `setContentView` with
// the new container view instead.
val rootViewContainer = reactActivityHandlers.asSequence()
.mapNotNull { it.createReactRootViewContainer(activity) }
.firstOrNull()
if (rootViewContainer != null) {
val mReactDelegate = ReactActivityDelegate::class.java.getDeclaredField("mReactDelegate")
mReactDelegate.isAccessible = true
val reactDelegate = mReactDelegate[delegate] as ReactDelegate
reactDelegate.loadApp(appKey)
val reactRootView = reactDelegate.reactRootView
(reactRootView?.parent as? ViewGroup)?.removeView(reactRootView)
rootViewContainer.addView(reactRootView, ViewGroup.LayoutParams.MATCH_PARENT)
activity.setContentView(rootViewContainer)
reactActivityLifecycleListeners.forEach { listener ->
listener.onContentChanged(activity)
}
return
}
val delayLoadAppHandler = reactActivityHandlers.asSequence()
.mapNotNull { it.getDelayLoadAppHandler(activity, reactNativeHost) }
.firstOrNull()
if (delayLoadAppHandler != null) {
shouldEmitPendingResume = true
delayLoadAppHandler.whenReady {
Utils.assertMainThread()
invokeDelegateMethod<Unit, String?>("loadApp", arrayOf(String::class.java), arrayOf(appKey))
reactActivityLifecycleListeners.forEach { listener ->
listener.onContentChanged(activity)
}
shouldEmitPendingResume = false
onResume()
}
return
}
invokeDelegateMethod<Unit, String?>("loadApp", arrayOf(String::class.java), arrayOf(appKey))
reactActivityLifecycleListeners.forEach { listener ->
listener.onContentChanged(activity)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
// Give handlers a chance as early as possible to replace the wrapped delegate object.
// If they do, we call the new wrapped delegate's `onCreate` instead of overriding it here.
val newDelegate = reactActivityHandlers.asSequence()
.mapNotNull { it.onDidCreateReactActivityDelegate(activity, this) }
.firstOrNull()
if (newDelegate != null && newDelegate != this) {
val mDelegateField = ReactActivity::class.java.getDeclaredField("mDelegate")
mDelegateField.isAccessible = true
val modifiers = Field::class.java.getDeclaredField("accessFlags")
modifiers.isAccessible = true
modifiers.setInt(mDelegateField, mDelegateField.modifiers and Modifier.FINAL.inv())
mDelegateField.set(activity, newDelegate)
delegate = newDelegate
invokeDelegateMethod<Unit, Bundle?>("onCreate", arrayOf(Bundle::class.java), arrayOf(savedInstanceState))
} else {
// Since we just wrap `ReactActivityDelegate` but not inherit it, in its `onCreate`,
// the calls to `createRootView()` or `getMainComponentName()` have no chances to be our wrapped methods.
// Instead we intercept `ReactActivityDelegate.onCreate` and replace the `mReactDelegate` with our version.
// That's not ideal but works.
val launchOptions = composeLaunchOptions()
val reactDelegate: ReactDelegate
if (ReactFeatureFlags.enableBridgelessArchitecture) {
reactDelegate = ReactDelegate(
plainActivity,
reactHost,
mainComponentName,
launchOptions
)
} else {
reactDelegate = object : ReactDelegate(
plainActivity,
reactNativeHost,
mainComponentName,
launchOptions
) {
override fun createRootView(): ReactRootView {
return this@ReactActivityDelegateWrapper.createRootView() ?: super.createRootView()
}
}
}
val mReactDelegate = ReactActivityDelegate::class.java.getDeclaredField("mReactDelegate")
mReactDelegate.isAccessible = true
mReactDelegate.set(delegate, reactDelegate)
if (mainComponentName != null) {
loadApp(mainComponentName)
}
}
reactActivityLifecycleListeners.forEach { listener ->
listener.onCreate(activity, savedInstanceState)
}
}
override fun onResume() {
if (shouldEmitPendingResume) {
return
}
invokeDelegateMethod<Unit>("onResume")
reactActivityLifecycleListeners.forEach { listener ->
listener.onResume(activity)
}
}
override fun onPause() {
// If app is stopped before delayed `loadApp`, we should cancel the pending resume
if (shouldEmitPendingResume) {
shouldEmitPendingResume = false
}
reactActivityLifecycleListeners.forEach { listener ->
listener.onPause(activity)
}
return invokeDelegateMethod("onPause")
}
override fun onDestroy() {
// If app is stopped before delayed `loadApp`, we should cancel the pending resume
if (shouldEmitPendingResume) {
shouldEmitPendingResume = false
}
reactActivityLifecycleListeners.forEach { listener ->
listener.onDestroy(activity)
}
return invokeDelegateMethod("onDestroy")
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
/**
* Workaround for a problem when results from [onActivityResult] are not properly delivered to modules.
* It happens when Android kills the [Activity] upon low memory scenario and recreates it later on.
*
* In [com.facebook.react.ReactInstanceManager.onActivityResult] you can see that if
* [com.facebook.react.bridge.ReactContext] is null then React would not broadcast the result to the modules
* and thus [expo.modules.kotlin.AppContext] would not be triggered properly.
*
* If [com.facebook.react.bridge.ReactContext] is not available when [onActivityResult] is called then
* let us wait for it and invoke [onActivityResult] once it's available.
*
* TODO (@bbarthec): fix it upstream?
*/
if (!ReactFeatureFlags.enableBridgelessArchitecture && delegate.reactInstanceManager.currentReactContext == null) {
val reactContextListener = object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
delegate.reactInstanceManager.removeReactInstanceEventListener(this)
delegate.onActivityResult(requestCode, resultCode, data)
}
}
return delegate.reactInstanceManager.addReactInstanceEventListener(reactContextListener)
}
delegate.onActivityResult(requestCode, resultCode, data)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// if any of the handlers return true, intentionally consume the event instead of passing it
// through to the delegate
return reactActivityHandlers
.map { it.onKeyDown(keyCode, event) }
.fold(false) { accu, current -> accu || current } || delegate.onKeyDown(keyCode, event)
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
// if any of the handlers return true, intentionally consume the event instead of passing it
// through to the delegate
return reactActivityHandlers
.map { it.onKeyUp(keyCode, event) }
.fold(false) { accu, current -> accu || current } || delegate.onKeyUp(keyCode, event)
}
override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
// if any of the handlers return true, intentionally consume the event instead of passing it
// through to the delegate
return reactActivityHandlers
.map { it.onKeyLongPress(keyCode, event) }
.fold(false) { accu, current -> accu || current } || delegate.onKeyLongPress(keyCode, event)
}
override fun onBackPressed(): Boolean {
val listenerResult = reactActivityLifecycleListeners
.map(ReactActivityLifecycleListener::onBackPressed)
.fold(false) { accu, current -> accu || current }
val delegateResult = delegate.onBackPressed()
return listenerResult || delegateResult
}
override fun onNewIntent(intent: Intent?): Boolean {
val listenerResult = reactActivityLifecycleListeners
.map { it.onNewIntent(intent) }
.fold(false) { accu, current -> accu || current }
val delegateResult = delegate.onNewIntent(intent)
return listenerResult || delegateResult
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
delegate.onWindowFocusChanged(hasFocus)
}
override fun requestPermissions(permissions: Array<out String>?, requestCode: Int, listener: PermissionListener?) {
delegate.requestPermissions(permissions, requestCode, listener)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>?, grantResults: IntArray?) {
delegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun getContext(): Context {
return invokeDelegateMethod("getContext")
}
override fun getPlainActivity(): Activity {
return invokeDelegateMethod("getPlainActivity")
}
override fun isFabricEnabled(): Boolean {
return invokeDelegateMethod("isFabricEnabled")
}
override fun composeLaunchOptions(): Bundle? {
return invokeDelegateMethod("composeLaunchOptions")
}
override fun onConfigurationChanged(newConfig: Configuration?) {
delegate.onConfigurationChanged(newConfig)
}
//endregion
//region Internals
@Suppress("UNCHECKED_CAST")
private fun <T> invokeDelegateMethod(name: String): T {
var method = methodMap[name]
if (method == null) {
method = ReactActivityDelegate::class.java.getDeclaredMethod(name)
method.isAccessible = true
methodMap[name] = method
}
return method!!.invoke(delegate) as T
}
@Suppress("UNCHECKED_CAST")
private fun <T, A> invokeDelegateMethod(
name: String,
argTypes: Array<Class<*>>,
args: Array<A>
): T {
var method = methodMap[name]
if (method == null) {
method = ReactActivityDelegate::class.java.getDeclaredMethod(name, *argTypes)
method.isAccessible = true
methodMap[name] = method
}
return method!!.invoke(delegate, *args) as T
}
//endregion
}

View File

@@ -0,0 +1,106 @@
package expo.modules
import android.app.Application
import androidx.collection.ArrayMap
import com.facebook.react.ReactInstanceEventListener
import com.facebook.react.ReactInstanceManager
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.JavaScriptExecutorFactory
import com.facebook.react.bridge.ReactContext
import java.lang.reflect.Method
open class ReactNativeHostWrapperBase(
application: Application,
protected val host: ReactNativeHost
) : ReactNativeHost(application) {
// TODO: Inherit from DefaultReactNativeHost when we drop SDK 49 support
internal val reactNativeHostHandlers = ExpoModulesPackage.packageList
.flatMap { it.createReactNativeHostHandlers(application) }
private val methodMap: ArrayMap<String, Method> = ArrayMap()
override fun createReactInstanceManager(): ReactInstanceManager {
val developerSupport = useDeveloperSupport
reactNativeHostHandlers.forEach { handler ->
handler.onWillCreateReactInstance(developerSupport)
}
val result = super.createReactInstanceManager()
reactNativeHostHandlers.forEach { handler ->
handler.onDidCreateDevSupportManager(result.devSupportManager)
}
result.addReactInstanceEventListener(object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
reactNativeHostHandlers.forEach { handler ->
handler.onDidCreateReactInstance(developerSupport, context)
}
}
})
injectHostReactInstanceManager(result)
return result
}
override fun getJavaScriptExecutorFactory(): JavaScriptExecutorFactory? {
return reactNativeHostHandlers.asSequence()
.mapNotNull { it.javaScriptExecutorFactory }
.firstOrNull() ?: invokeDelegateMethod("getJavaScriptExecutorFactory")
}
public override fun getJSMainModuleName(): String {
return invokeDelegateMethod("getJSMainModuleName")
}
public override fun getJSBundleFile(): String? {
return reactNativeHostHandlers.asSequence()
.mapNotNull { it.getJSBundleFile(useDeveloperSupport) }
.firstOrNull() ?: invokeDelegateMethod<String?>("getJSBundleFile")
}
public override fun getBundleAssetName(): String? {
return reactNativeHostHandlers.asSequence()
.mapNotNull { it.getBundleAssetName(useDeveloperSupport) }
.firstOrNull() ?: invokeDelegateMethod<String?>("getBundleAssetName")
}
override fun getUseDeveloperSupport(): Boolean {
return reactNativeHostHandlers.asSequence()
.mapNotNull { it.useDeveloperSupport }
.firstOrNull() ?: host.useDeveloperSupport
}
public override fun getPackages(): MutableList<ReactPackage> {
return invokeDelegateMethod("getPackages")
}
//endregion
//region Internals
@Suppress("UNCHECKED_CAST")
internal fun <T> invokeDelegateMethod(name: String): T {
var method = methodMap[name]
if (method == null) {
method = ReactNativeHost::class.java.getDeclaredMethod(name)
method.isAccessible = true
methodMap[name] = method
}
return method!!.invoke(host) as T
}
/**
* Inject the @{ReactInstanceManager} from the wrapper to the wrapped host.
* In case the wrapped host to call `getReactInstanceManager` inside its methods.
*/
private fun injectHostReactInstanceManager(reactInstanceManager: ReactInstanceManager) {
val mReactInstanceManagerField = ReactNativeHost::class.java.getDeclaredField("mReactInstanceManager")
mReactInstanceManagerField.isAccessible = true
mReactInstanceManagerField.set(host, reactInstanceManager)
}
//endregion
}

View File

@@ -0,0 +1,56 @@
package expo.modules
import android.app.Application
import android.content.Context
import com.facebook.react.JSEngineResolutionAlgorithm
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackageTurboModuleManagerDelegate
import com.facebook.react.bridge.UIManagerProvider
import com.facebook.react.common.SurfaceDelegateFactory
import com.facebook.react.devsupport.DevSupportManagerFactory
import com.facebook.react.devsupport.interfaces.RedBoxHandler
class ReactNativeHostWrapper(
application: Application,
host: ReactNativeHost
) : ReactNativeHostWrapperBase(application, host) {
override fun getDevSupportManagerFactory(): DevSupportManagerFactory? {
return reactNativeHostHandlers
.asSequence()
.mapNotNull { it.devSupportManagerFactory }
.firstOrNull() as DevSupportManagerFactory?
?: invokeDelegateMethod("getDevSupportManagerFactory")
}
override fun getReactPackageTurboModuleManagerDelegateBuilder(): ReactPackageTurboModuleManagerDelegate.Builder? {
return invokeDelegateMethod("getReactPackageTurboModuleManagerDelegateBuilder")
}
override fun getUIManagerProvider(): UIManagerProvider? {
return invokeDelegateMethod("getUIManagerProvider")
}
public override fun getJSEngineResolutionAlgorithm(): JSEngineResolutionAlgorithm? {
return invokeDelegateMethod("getJSEngineResolutionAlgorithm")
}
override fun getShouldRequireActivity(): Boolean {
return host.shouldRequireActivity
}
override fun getSurfaceDelegateFactory(): SurfaceDelegateFactory {
return host.surfaceDelegateFactory
}
override fun getRedBoxHandler(): RedBoxHandler? {
return invokeDelegateMethod("getRedBoxHandler")
}
companion object {
@JvmStatic
fun createReactHost(context: Context, reactNativeHost: ReactNativeHost): ReactHost {
return ExpoReactHostFactory.createFromReactNativeHost(context, reactNativeHost)
}
}
}

View File

@@ -0,0 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<!-- Fix unit test manifestMerger build error from expo -> autolinked expo-app-auth -> net.openid.appauth -->
<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
android:exported="true" tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="test" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,109 @@
package expo.modules
import android.content.Context
import android.content.Intent
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.google.common.truth.Truth.assertThat
import expo.modules.core.interfaces.Package
import expo.modules.core.interfaces.ReactActivityHandler
import expo.modules.core.interfaces.ReactActivityLifecycleListener
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Before
import org.junit.Test
internal class ReactActivityDelegateWrapperTest {
lateinit var mockPackage0: MockPackage
lateinit var mockPackage1: MockPackage
@RelaxedMockK
lateinit var activity: ReactActivity
@RelaxedMockK
lateinit var delegate: ReactActivityDelegate
@Before
fun setUp() {
mockPackage0 = MockPackage()
mockPackage1 = MockPackage()
MockKAnnotations.init(this)
mockkObject(ExpoModulesPackage.Companion)
every { ExpoModulesPackage.Companion.packageList } returns listOf(mockPackage0, mockPackage1)
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `onBackPressed should call each handler's callback just once`() {
val delegateWrapper = ReactActivityDelegateWrapper(activity, delegate)
every { mockPackage0.reactActivityLifecycleListener.onBackPressed() } returns true
delegateWrapper.onBackPressed()
verify(exactly = 1) { mockPackage0.reactActivityLifecycleListener.onBackPressed() }
verify(exactly = 1) { mockPackage1.reactActivityLifecycleListener.onBackPressed() }
verify(exactly = 1) { delegate.onBackPressed() }
}
@Test
fun `onBackPressed should return true if someone returns true`() {
val delegateWrapper = ReactActivityDelegateWrapper(activity, delegate)
every { mockPackage0.reactActivityLifecycleListener.onBackPressed() } returns false
every { mockPackage1.reactActivityLifecycleListener.onBackPressed() } returns true
every { delegate.onBackPressed() } returns false
val result = delegateWrapper.onBackPressed()
assertThat(result).isTrue()
}
@Test
fun `onNewIntent should call each handler's callback just once`() {
val intent = mockk<Intent>()
val delegateWrapper = ReactActivityDelegateWrapper(activity, delegate)
every { mockPackage0.reactActivityLifecycleListener.onNewIntent(intent) } returns false
every { mockPackage1.reactActivityLifecycleListener.onNewIntent(intent) } returns true
every { delegate.onNewIntent(intent) } returns false
delegateWrapper.onNewIntent(intent)
verify(exactly = 1) { mockPackage0.reactActivityLifecycleListener.onNewIntent(any()) }
verify(exactly = 1) { mockPackage1.reactActivityLifecycleListener.onNewIntent(any()) }
verify(exactly = 1) { delegate.onNewIntent(any()) }
}
@Test
fun `onNewIntent should return true if someone returns true`() {
val intent = mockk<Intent>()
val delegateWrapper = ReactActivityDelegateWrapper(activity, delegate)
every { mockPackage0.reactActivityLifecycleListener.onNewIntent(intent) } returns false
every { mockPackage1.reactActivityLifecycleListener.onNewIntent(intent) } returns true
every { delegate.onNewIntent(intent) } returns false
val result = delegateWrapper.onNewIntent(intent)
assertThat(result).isTrue()
}
}
internal class MockPackage : Package {
val reactActivityLifecycleListener = mockk<ReactActivityLifecycleListener>(relaxed = true)
val reactActivityHandler = mockk<ReactActivityHandler>(relaxed = true)
override fun createReactActivityLifecycleListeners(activityContext: Context?): List<ReactActivityLifecycleListener> {
return listOf(reactActivityLifecycleListener)
}
override fun createReactActivityHandlers(activityContext: Context?): List<ReactActivityHandler> {
return listOf(reactActivityHandler)
}
}