From a07a3e67a9f828bc3266e650c8aef0c6a15590f4 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:56:40 +0530 Subject: [PATCH 01/23] chore: revamp --- .gitignore | 2 + .../react/devsupport/DevInternalSettings.kt | 10 - .../devsupport/DevSupportManagerHelpers.kt | 33 ---- .../mendix/mendixnative/MendixInitializer.kt | 6 +- .../mendix/mendixnative/react/ClearData.kt | 61 ++++-- .../com/mendix/mendixnative/react/CloseApp.kt | 15 +- .../mendixnative/react/NativeErrorHandler.kt | 16 +- .../mendixnative/react/fs/NativeFsModule.kt | 2 +- .../mendixnative/util/ReflectionUtils.java | 122 +++--------- .../com/mendixnative/MendixNativeModule.kt | 183 ------------------ .../com/mendixnative/MendixNativePackage.kt | 64 ++++-- .../configuration/MxConfigurationModule.kt | 24 +++ .../com/mendixnative/cookie/MxCookieModule.kt | 24 +++ .../mendixnative/download/MxDownloadModule.kt | 25 +++ .../encryption/MxEncryptionModule.kt | 40 ++++ .../com/mendixnative/error/MxErrorModule.kt | 24 +++ .../com/mendixnative/fs/MxFileSystemModule.kt | 70 +++++++ .../navigation/MxNavigationModule.kt | 27 +++ .../java/com/mendixnative/ota/MxOtaModule.kt | 29 +++ .../com/mendixnative/reload/MxReloadModule.kt | 30 +++ .../splash/MxSplashScreenModule.kt | 31 +++ .../mendixnative/storage/MxStorageModule.kt | 46 +++++ example/__tests__/splash-screen.harness.ts | 2 +- .../xcschemes/MendixNativeExample.xcscheme | 7 + .../ios/MendixNativeExample/AppDelegate.swift | 57 ++---- example/rn-harness.config.mjs | 4 +- ios/MendixNative.h | 8 - ios/MendixNative.mm | 153 --------------- ios/Modules/Helper/DevHelper.swift | 63 ++---- ios/Modules/Helper/ReactAppProvider.swift | 41 ++-- .../MxConfiguration/MxConfigurationModule.h | 4 + .../MxConfiguration/MxConfigurationModule.mm | 20 ++ ios/TurboModules/MxCookie/MxCookie.h | 4 + ios/TurboModules/MxCookie/MxCookie.mm | 22 +++ ios/TurboModules/MxDownload/MxDownload.h | 4 + ios/TurboModules/MxDownload/MxDownload.mm | 27 +++ ios/TurboModules/MxEncryption/MxEncryption.h | 4 + ios/TurboModules/MxEncryption/MxEncryption.mm | 48 +++++ ios/TurboModules/MxError/MxError.h | 4 + ios/TurboModules/MxError/MxError.mm | 21 ++ ios/TurboModules/MxFileSystem/MxFileSystem.h | 4 + ios/TurboModules/MxFileSystem/MxFileSystem.mm | 87 +++++++++ ios/TurboModules/MxNavigation/MxNavigation.h | 4 + ios/TurboModules/MxNavigation/MxNavigation.mm | 24 +++ ios/TurboModules/MxOta/MxOta.h | 4 + ios/TurboModules/MxOta/MxOta.mm | 30 +++ ios/TurboModules/MxReload/MxReload.h | 4 + ios/TurboModules/MxReload/MxReload.mm | 28 +++ .../MxSplashScreen/MxSplashScreen.h | 4 + .../MxSplashScreen/MxSplashScreen.mm | 24 +++ ios/TurboModules/MxStorage/MxStorage.h | 4 + ios/TurboModules/MxStorage/MxStorage.mm | 58 ++++++ src/cookie.ts | 5 - src/cookie/NativeMxCookie.ts | 7 + src/cookie/index.ts | 5 + src/dev-settings.ts | 100 ++++++++++ src/download-handler.ts | 6 - src/download-handler/NativeMxDownload.ts | 30 +++ src/download-handler/index.ts | 9 + src/encrypted-storage.ts | 13 +- src/encryption/NativeMxEncryption.ts | 46 +++++ src/encryption/index.ts | 74 +++++++ src/error-handler.ts | 5 - src/error/NativeMxError.ts | 8 + src/error/index.ts | 5 + src/events.ts | 7 +- src/file-system/NativeMxFileSystem.ts | 41 ++++ src/{file-system.ts => file-system/index.ts} | 28 +-- src/index.ts | 10 +- src/mx-configuration.ts | 3 - src/mx-configuration/NativeMxConfiguration.ts | 31 +++ src/mx-configuration/index.ts | 3 + src/navigation-mode.ts | 6 - src/navigation-mode/NativeMxNavigation.ts | 9 + src/navigation-mode/index.ts | 6 + src/ota.ts | 9 - src/ota/NativeMxOta.ts | 48 +++++ src/ota/index.ts | 13 ++ src/reload-handler.ts | 6 - src/reload-handler/NativeMxReload.ts | 10 + src/reload-handler/index.ts | 6 + src/specs/NativeMendixNative.ts | 126 ------------ src/splash-screen.ts | 6 - src/splash-screen/NativeMxSplashScreen.ts | 22 +++ src/splash-screen/index.ts | 36 ++++ src/storage/NativeMxStorage.ts | 8 + src/storage/index.ts | 97 ++++++++++ 87 files changed, 1675 insertions(+), 821 deletions(-) delete mode 100644 android/src/main/java/com/facebook/react/devsupport/DevInternalSettings.kt delete mode 100644 android/src/main/java/com/facebook/react/devsupport/DevSupportManagerHelpers.kt delete mode 100644 android/src/main/java/com/mendixnative/MendixNativeModule.kt create mode 100644 android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt create mode 100644 android/src/main/java/com/mendixnative/cookie/MxCookieModule.kt create mode 100644 android/src/main/java/com/mendixnative/download/MxDownloadModule.kt create mode 100644 android/src/main/java/com/mendixnative/encryption/MxEncryptionModule.kt create mode 100644 android/src/main/java/com/mendixnative/error/MxErrorModule.kt create mode 100644 android/src/main/java/com/mendixnative/fs/MxFileSystemModule.kt create mode 100644 android/src/main/java/com/mendixnative/navigation/MxNavigationModule.kt create mode 100644 android/src/main/java/com/mendixnative/ota/MxOtaModule.kt create mode 100644 android/src/main/java/com/mendixnative/reload/MxReloadModule.kt create mode 100644 android/src/main/java/com/mendixnative/splash/MxSplashScreenModule.kt create mode 100644 android/src/main/java/com/mendixnative/storage/MxStorageModule.kt delete mode 100644 ios/MendixNative.h delete mode 100644 ios/MendixNative.mm create mode 100644 ios/TurboModules/MxConfiguration/MxConfigurationModule.h create mode 100644 ios/TurboModules/MxConfiguration/MxConfigurationModule.mm create mode 100644 ios/TurboModules/MxCookie/MxCookie.h create mode 100644 ios/TurboModules/MxCookie/MxCookie.mm create mode 100644 ios/TurboModules/MxDownload/MxDownload.h create mode 100644 ios/TurboModules/MxDownload/MxDownload.mm create mode 100644 ios/TurboModules/MxEncryption/MxEncryption.h create mode 100644 ios/TurboModules/MxEncryption/MxEncryption.mm create mode 100644 ios/TurboModules/MxError/MxError.h create mode 100644 ios/TurboModules/MxError/MxError.mm create mode 100644 ios/TurboModules/MxFileSystem/MxFileSystem.h create mode 100644 ios/TurboModules/MxFileSystem/MxFileSystem.mm create mode 100644 ios/TurboModules/MxNavigation/MxNavigation.h create mode 100644 ios/TurboModules/MxNavigation/MxNavigation.mm create mode 100644 ios/TurboModules/MxOta/MxOta.h create mode 100644 ios/TurboModules/MxOta/MxOta.mm create mode 100644 ios/TurboModules/MxReload/MxReload.h create mode 100644 ios/TurboModules/MxReload/MxReload.mm create mode 100644 ios/TurboModules/MxSplashScreen/MxSplashScreen.h create mode 100644 ios/TurboModules/MxSplashScreen/MxSplashScreen.mm create mode 100644 ios/TurboModules/MxStorage/MxStorage.h create mode 100644 ios/TurboModules/MxStorage/MxStorage.mm delete mode 100644 src/cookie.ts create mode 100644 src/cookie/NativeMxCookie.ts create mode 100644 src/cookie/index.ts create mode 100644 src/dev-settings.ts delete mode 100644 src/download-handler.ts create mode 100644 src/download-handler/NativeMxDownload.ts create mode 100644 src/download-handler/index.ts create mode 100644 src/encryption/NativeMxEncryption.ts create mode 100644 src/encryption/index.ts delete mode 100644 src/error-handler.ts create mode 100644 src/error/NativeMxError.ts create mode 100644 src/error/index.ts create mode 100644 src/file-system/NativeMxFileSystem.ts rename src/{file-system.ts => file-system/index.ts} (55%) delete mode 100644 src/mx-configuration.ts create mode 100644 src/mx-configuration/NativeMxConfiguration.ts create mode 100644 src/mx-configuration/index.ts delete mode 100644 src/navigation-mode.ts create mode 100644 src/navigation-mode/NativeMxNavigation.ts create mode 100644 src/navigation-mode/index.ts delete mode 100644 src/ota.ts create mode 100644 src/ota/NativeMxOta.ts create mode 100644 src/ota/index.ts delete mode 100644 src/reload-handler.ts create mode 100644 src/reload-handler/NativeMxReload.ts create mode 100644 src/reload-handler/index.ts delete mode 100644 src/specs/NativeMendixNative.ts delete mode 100644 src/splash-screen.ts create mode 100644 src/splash-screen/NativeMxSplashScreen.ts create mode 100644 src/splash-screen/index.ts create mode 100644 src/storage/NativeMxStorage.ts create mode 100644 src/storage/index.ts diff --git a/.gitignore b/.gitignore index d8bf499..2b842a0 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,5 @@ lib/ # Fastlane.swift runner binary **/fastlane/FastlaneRunner + +.claude/ diff --git a/android/src/main/java/com/facebook/react/devsupport/DevInternalSettings.kt b/android/src/main/java/com/facebook/react/devsupport/DevInternalSettings.kt deleted file mode 100644 index ceefb9d..0000000 --- a/android/src/main/java/com/facebook/react/devsupport/DevInternalSettings.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.facebook.react.devsupport - -import com.facebook.react.modules.debug.interfaces.DeveloperSettings -import com.mendix.mendixnative.activity.MendixReactActivity -import com.mendix.mendixnative.util.ReflectionUtils - -fun getDevInternalSettings(activity: MendixReactActivity): DeveloperSettings? = - (activity.currentDevSupportManager as? DevSupportManagerBase)?.let { - return ReflectionUtils.getField(it, "mDevSettings") - } diff --git a/android/src/main/java/com/facebook/react/devsupport/DevSupportManagerHelpers.kt b/android/src/main/java/com/facebook/react/devsupport/DevSupportManagerHelpers.kt deleted file mode 100644 index 7b3dcf4..0000000 --- a/android/src/main/java/com/facebook/react/devsupport/DevSupportManagerHelpers.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.facebook.react.devsupport - -import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener -import com.facebook.react.devsupport.interfaces.DevSupportManager -import com.mendix.mendixnative.util.ReflectionUtils - -fun setBundleDownloadListener( - devSupportManager: DevSupportManager?, - listener: DevBundleDownloadListener -) { - devSupportManager?.apply { - ReflectionUtils.setFieldOfSuperclass( - this, - listener, - "mBundleDownloadListener", - "devBundleDownloadListener" - ) - } -} - -fun overrideDevLoadingViewController( - devSupportManager: DevSupportManager, - devLoadingViewController: DefaultDevLoadingViewImplementation -) { - devSupportManager.apply { - ReflectionUtils.setFieldOfSuperclass( - this, - devLoadingViewController, - "mDevLoadingViewManager", - "devLoadingViewManager" - ) - } -} diff --git a/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt b/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt index c4db2f3..53274b6 100644 --- a/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt +++ b/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt @@ -21,7 +21,7 @@ import com.mendix.mendixnative.react.clearCachedReactNativeDevBundle import com.mendix.mendixnative.react.clearData import com.mendix.mendixnative.react.closeSqlDatabaseConnection import com.mendix.mendixnative.react.toggleElementInspector -import com.mendixnative.MendixNativeModule +import com.mendix.mendixnative.react.NativeReloadHandler class MendixInitializer( private val context: Activity, @@ -70,7 +70,9 @@ class MendixInitializer( devMenuTouchEventHandler = DevMenuTouchEventHandler(object : DevMenuTouchEventHandler.DevMenuTouchListener { override fun onTap() { - reactNativeHost.typeSafeNativeModule()?.reloadClientWithState() + reactNativeHost.reactApplicationContext()?.let { + NativeReloadHandler(it).reload() + } } override fun onLongPress() { diff --git a/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt b/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt index 9670f69..37b6a1b 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt @@ -88,30 +88,63 @@ fun clearDataWithReactContext(applicationContext: Application, reactNativeHost: } fun deleteAppDatabaseAsync(reactContext: ReactContext?, cb: BooleanCallback) { - reactContext?.typeSafeNativeModule()?.let { - it.deleteAllDBs() - cb(true) - } ?: cb(false) + val opSQLiteModule = reactContext?.getNativeModule(OPSQLiteModule::class.java) + if (opSQLiteModule != null) { + try { + opSQLiteModule.deleteAllDBs() + cb(true) + } catch (e: Exception) { + Log.e("ClearData", "Failed to delete databases: ${e.message}") + cb(false) + } + } else { + cb(false) + } } +/** + * Clears all AsyncStorage data. + * + * Note: Previous implementation only checked module availability without clearing. + * This now actually clears the storage using AsyncStorageModule.clear(). + */ fun clearAsyncStorage(reactNativeHost: ReactNativeHost): Boolean { - val module = reactNativeHost.typeSafeNativeModule() - if (module != null) { - return true - } else { - return false + val asyncStorageModule = reactNativeHost.reactContext()?.getNativeModule(AsyncStorageModule::class.java) + if (asyncStorageModule != null) { + try { + // Clear AsyncStorage synchronously - clear() expects a callback but we're using fire-and-forget + asyncStorageModule.clear { error -> + if (error != null) { + Log.e("ClearData", "AsyncStorage clear error: $error") + } + } + return true + } catch (e: Exception) { + Log.e("ClearData", "Failed to clear AsyncStorage: ${e.message}") + return false + } } + return false } fun clearSecureStorage(context: Context?): Boolean = context?.let { MendixEncryptedStorage.getMendixEncryptedStorage(it).clear() } ?: false -fun clearCookiesAsync(reactContext: ReactContext?, cb: (success: Boolean) -> Unit) = - reactContext?.typeSafeNativeModule()?.let { module -> - module.clearCookies { - cb(it[0] as Boolean) +fun clearCookiesAsync(reactContext: ReactContext?, cb: (success: Boolean) -> Unit) { + val networkingModule = reactContext?.getNativeModule(NetworkingModule::class.java) + if (networkingModule != null) { + try { + networkingModule.clearCookies { result -> + cb(result[0] as Boolean) + } + } catch (e: Exception) { + Log.e("ClearData", "Failed to clear cookies: ${e.message}") + cb(false) } - } ?: cb(false) + } else { + cb(false) + } +} fun clearCachedReactNativeDevBundle(applicationContext: Application) { try { diff --git a/android/src/main/java/com/mendix/mendixnative/react/CloseApp.kt b/android/src/main/java/com/mendix/mendixnative/react/CloseApp.kt index 775d252..2159c20 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/CloseApp.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/CloseApp.kt @@ -1,8 +1,21 @@ package com.mendix.mendixnative.react +import android.util.Log import com.facebook.react.bridge.ReactContext import com.op.sqlite.OPSQLiteModule +/** + * Closes all SQLite database connections. + * + * This is called during app shutdown to gracefully close database connections. + */ fun closeSqlDatabaseConnection(reactContext: ReactContext?) { - reactContext?.typeSafeNativeModule()?.closeAllConnections() + val opSQLiteModule = reactContext?.getNativeModule(OPSQLiteModule::class.java) + if (opSQLiteModule != null) { + try { + opSQLiteModule.closeAllConnections() + } catch (e: Exception) { + Log.e("CloseApp", "Failed to close database connections: ${e.message}") + } + } } 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 3eca055..515ec0d 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.kt @@ -3,16 +3,22 @@ package com.mendix.mendixnative.react import com.facebook.common.logging.FLog import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.WritableMap -import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.modules.core.ExceptionsManagerModule +/** + * Handles JavaScript exceptions by reporting them to React Native's ExceptionsManager. + * + * This bridges JavaScript errors to the native exception handling system. + */ class NativeErrorHandler(val reactContext: ReactApplicationContext) { fun handle(message: String?, stackTrace: ReadableArray?) { - reactContext.typeSafeNativeModule()?.reportSoftException(message, stackTrace, 0.0) - // updateExceptionMessage is not available in RN 0.77.1 + // Use typed module access instead of generic typeSafeNativeModule + val exceptionsManagerModule = reactContext.getNativeModule(ExceptionsManagerModule::class.java) + exceptionsManagerModule?.reportSoftException(message, stackTrace, 0.0) + + // Note: updateExceptionMessage is not available in RN 0.77.1+ // ref: https://github.com/facebook/react-native/commit/4f47439a02183205ff6f68b1fc3bc392e78e4cb4 - // exceptionsManagerModule.updateExceptionMessage(message, stackTrace, 0); + FLog.e(javaClass, "Received JS exception: $message") } } 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 84300cc..5d522e8 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 @@ -30,7 +30,7 @@ class NativeFsModule(private val reactContext: ReactApplicationContext) { fun save(blob: ReadableMap, filePath: String, promise: Promise) { val blobModule = reactContext.getNativeModule(BlobModule::class.java) - val blobId: String = blob.getString("blobId") ?: run {"" + val blobId: String = blob.getString("blobId") ?: run { promise.reject(ERROR_INVALID_BLOB, "The specified blob is invalid") return } diff --git a/android/src/main/java/com/mendix/mendixnative/util/ReflectionUtils.java b/android/src/main/java/com/mendix/mendixnative/util/ReflectionUtils.java index 4627e25..b1fbc0f 100644 --- a/android/src/main/java/com/mendix/mendixnative/util/ReflectionUtils.java +++ b/android/src/main/java/com/mendix/mendixnative/util/ReflectionUtils.java @@ -1,10 +1,16 @@ package com.mendix.mendixnative.util; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +/** + * Minimal reflection utilities for accessing React Native private fields. + * + * **Usage:** Only used by MendixShakeDetector to swap React Native's shake detector. + * There is no public React Native API for this functionality. + * + * **Note:** Reflection should be avoided where possible. This class is kept minimal + * and only exposes methods that are actively used. + */ public class ReflectionUtils { private static Field findDeclaredField(Class objectClass, String... fieldNames) { NoSuchFieldException lastException = null; @@ -20,95 +26,27 @@ private static Field findDeclaredField(Class objectClass, String... fieldName throw new RuntimeException(lastException); } - public static ConstructorWrapper findConstructor(String className, Class... parameterTypes) { - try { - Constructor constructor = Class.forName(className).getDeclaredConstructor(parameterTypes); - constructor.setAccessible(true); - return new ConstructorWrapper(constructor); - } catch (ClassNotFoundException | NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - // TODO: replace this with a lambda after upgrading to Java 8 - public static class ConstructorWrapper { - private final Constructor constructor; - - ConstructorWrapper(Constructor constructor) { - this.constructor = constructor; - } - - public T newInstance(Object... args) { - try { - return (T) constructor.newInstance(args); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - } - - public static MethodWrapper findMethod(Object object, String methodName, Class... parameterTypes) { - try { - Method method = object.getClass().getDeclaredMethod(methodName, parameterTypes); - method.setAccessible(true); - return new MethodWrapper(method, object); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - // TODO: replace this with a lambda after upgrading to Java 8 - public static class MethodWrapper { - private final Method method; - private final Object object; - - MethodWrapper(Method method, Object object) { - this.method = method; - this.object = object; - } - - public void invoke(Object... args) { - try { - method.invoke(object, args); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - } - - public static void setFieldOfSuperclass(Object object, String fieldName, Object value) { - setFieldOfSuperclass(object, value, fieldName); - } - + /** + * Sets a field on the superclass of the given object. + * Tries multiple field names to handle React Native version differences. + * + * @param object The object whose superclass field should be set + * @param value The value to set + * @param fieldNames Field names to try (in order of preference) + */ public static void setFieldOfSuperclass(Object object, Object value, String... fieldNames) { Field field = findDeclaredField(object.getClass().getSuperclass(), fieldNames); setField(object, field, value); } - public static void setField(Object object, String fieldName, Object value) { - try { - Field field = object.getClass().getDeclaredField(fieldName); - setField(object, field, value); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - - private static void setField(Object object, Field field, Object value) { - try { - field.setAccessible(true); - field.set(object, value); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } finally { - field.setAccessible(false); - } - } - - public static T getFieldOfSuperclass(Object object, String fieldName) { - return getFieldOfSuperclass(object, new String[] { fieldName }); - } - + /** + * Gets a field from the superclass of the given object. + * Tries multiple field names to handle React Native version differences. + * + * @param object The object whose superclass field should be retrieved + * @param fieldNames Field names to try (in order of preference) + * @return The field value + */ public static T getFieldOfSuperclass(Object object, String... fieldNames) { try { Field field = findDeclaredField(object.getClass().getSuperclass(), fieldNames); @@ -119,14 +57,14 @@ public static T getFieldOfSuperclass(Object object, String... fieldNames) { } } - public static T getField(Object object, String fieldName) { + private static void setField(Object object, Field field, Object value) { try { - Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true); - return (T) field.get(object); - } catch (NoSuchFieldException | IllegalAccessException | ClassCastException e) { + field.set(object, value); + } catch (IllegalAccessException e) { throw new RuntimeException(e); + } finally { + field.setAccessible(false); } } - } diff --git a/android/src/main/java/com/mendixnative/MendixNativeModule.kt b/android/src/main/java/com/mendixnative/MendixNativeModule.kt deleted file mode 100644 index c050cd5..0000000 --- a/android/src/main/java/com/mendixnative/MendixNativeModule.kt +++ /dev/null @@ -1,183 +0,0 @@ -package com.mendixnative - -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.WritableMap -import com.facebook.react.module.annotations.ReactModule -import com.mendix.mendixnative.encryption.MendixEncryptedStorageModule -import com.mendix.mendixnative.react.MxConfiguration -import com.mendix.mendixnative.react.NativeErrorHandler -import com.mendix.mendixnative.react.NativeReloadHandler -import com.mendix.mendixnative.react.NavigationModeModule -import com.mendix.mendixnative.react.cookie.NativeCookieModule -import com.mendix.mendixnative.react.download.NativeDownloadModule -import com.mendix.mendixnative.react.fs.NativeFsModule -import com.mendix.mendixnative.react.ota.NativeOtaModule -import com.mendix.mendixnative.react.splash.MendixSplashScreenModule -import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter - -@ReactModule(name = MendixNativeModule.NAME) -class MendixNativeModule(reactContext: ReactApplicationContext) : NativeMendixNativeSpec(reactContext) { - - var presenter: MendixSplashScreenPresenter? = null - - override fun getName(): String { - return NAME - } - - override fun encryptedStorageSetItem(key: String, value: String, promise: Promise) { - MendixEncryptedStorageModule(reactApplicationContext).setItem(key, value, promise) - } - - override fun encryptedStorageGetItem(key: String, promise: Promise) { - MendixEncryptedStorageModule(reactApplicationContext).getItem(key, promise) - } - - override fun encryptedStorageRemoveItem(key: String, promise: Promise) { - MendixEncryptedStorageModule(reactApplicationContext).removeItem(key, promise) - } - - override fun encryptedStorageClear(promise: Promise) { - MendixEncryptedStorageModule(reactApplicationContext).clear(promise) - } - - override fun encryptedStorageIsEncrypted(): Boolean { - return MendixEncryptedStorageModule(reactApplicationContext).isEncrypted - } - - override fun splashScreenShow() { - MendixSplashScreenModule(reactApplicationContext).show(presenter) - } - - override fun splashScreenHide() { - MendixSplashScreenModule(reactApplicationContext).hide(presenter) - } - - override fun cookieClearAll(promise: Promise) { - NativeCookieModule(reactApplicationContext).clearAll(promise) - } - - override fun reloadHandlerReload(promise: Promise) { - NativeReloadHandler(reactApplicationContext).reload() - promise.resolve(null) - } - - fun reloadClientWithState() { - emitOnReloadWithState() - } - - override fun reloadHandlerExitApp(promise: Promise) { - NativeReloadHandler(reactApplicationContext).exitApp() - promise.resolve(null) - } - - override fun downloadHandlerDownload( - url: String, - downloadPath: String, - config: ReadableMap, - promise: Promise - ) { - NativeDownloadModule(reactApplicationContext).download( - url, downloadPath, config, promise - ) - } - - override fun mxConfigurationGetConfig(): WritableMap? { - return MxConfiguration(reactApplicationContext).getConstants() - } - - override fun otaDownload( - config: ReadableMap, - promise: Promise - ) { - NativeOtaModule(reactApplicationContext).download(config, promise) - } - - override fun otaDeploy( - config: ReadableMap, - promise: Promise - ) { - NativeOtaModule(reactApplicationContext).deploy(config, promise) - } - - override fun fsSetEncryptionEnabled(enabled: Boolean) { - NativeFsModule(reactApplicationContext).setEncryptionEnabled(enabled) - } - - override fun fsSave( - blob: ReadableMap, - filePath: String, - promise: Promise - ) { - NativeFsModule(reactApplicationContext).save(blob, filePath, promise) - } - - override fun fsRead(filePath: String, promise: Promise) { - NativeFsModule(reactApplicationContext).read(filePath, promise) - } - - override fun fsMove( - filePath: String, - newPath: String, - promise: Promise - ) { - NativeFsModule(reactApplicationContext).move(filePath, newPath, promise) - } - - override fun fsRemove(filePath: String, promise: Promise) { - NativeFsModule(reactApplicationContext).remove(filePath, promise) - } - - override fun fsList(dirPath: String, promise: Promise) { - NativeFsModule(reactApplicationContext).list(dirPath, promise) - } - - override fun fsReadAsDataURL( - filePath: String, - promise: Promise - ) { - NativeFsModule(reactApplicationContext).readAsDataURL(filePath, promise) - } - - override fun fsReadAsText(filePath: String, promise: Promise) { - NativeFsModule(reactApplicationContext).readAsText(filePath, promise) - } - - override fun fsFileExists(filePath: String, promise: Promise) { - NativeFsModule(reactApplicationContext).fileExists(filePath, promise) - } - - override fun fsWriteJson( - data: ReadableMap, - filepath: String, - promise: Promise - ) { - NativeFsModule(reactApplicationContext).writeJson(data, filepath, promise) - } - - override fun fsReadJson(filepath: String, promise: Promise) { - NativeFsModule(reactApplicationContext).readJson(filepath, promise) - } - - override fun fsConstants(): WritableMap? { - return NativeFsModule(reactApplicationContext).getConstants() - } - - override fun errorHandlerHandle(message: String, stackTrace: ReadableArray) { - NativeErrorHandler(reactApplicationContext).handle(message, stackTrace) - } - - override fun navigationModeIsNavigationBarActive(): Boolean { - return NavigationModeModule(reactApplicationContext).isNavigationBarActive() - } - - override fun navigationModeGetNavigationBarHeight(): Double { - return NavigationModeModule(reactApplicationContext).getNavigationBarHeight() - } - - companion object { - const val NAME = "MendixNative" - } -} diff --git a/android/src/main/java/com/mendixnative/MendixNativePackage.kt b/android/src/main/java/com/mendixnative/MendixNativePackage.kt index 882a1d6..e70de7b 100644 --- a/android/src/main/java/com/mendixnative/MendixNativePackage.kt +++ b/android/src/main/java/com/mendixnative/MendixNativePackage.kt @@ -6,6 +6,17 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter +import com.mendixnative.configuration.MxConfigurationModule +import com.mendixnative.cookie.MxCookieModule +import com.mendixnative.download.MxDownloadModule +import com.mendixnative.encryption.MxEncryptionModule +import com.mendixnative.error.MxErrorModule +import com.mendixnative.fs.MxFileSystemModule +import com.mendixnative.navigation.MxNavigationModule +import com.mendixnative.ota.MxOtaModule +import com.mendixnative.reload.MxReloadModule +import com.mendixnative.splash.MxSplashScreenModule +import com.mendixnative.storage.MxStorageModule import java.util.HashMap class MendixNativePackage : BaseReactPackage() { @@ -13,26 +24,51 @@ class MendixNativePackage : BaseReactPackage() { var splashScreenPresenter: MendixSplashScreenPresenter? = null override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { - return if (name == MendixNativeModule.NAME) { - val module = MendixNativeModule(reactContext) - module.presenter = splashScreenPresenter - return module - } else { - null + return when (name) { + MxEncryptionModule.NAME -> MxEncryptionModule(reactContext) + MxSplashScreenModule.NAME -> { + val module = MxSplashScreenModule(reactContext) + module.presenter = splashScreenPresenter + module + } + MxFileSystemModule.NAME -> MxFileSystemModule(reactContext) + MxStorageModule.NAME -> MxStorageModule(reactContext) + MxOtaModule.NAME -> MxOtaModule(reactContext) + MxDownloadModule.NAME -> MxDownloadModule(reactContext) + MxReloadModule.NAME -> MxReloadModule(reactContext) + MxConfigurationModule.NAME -> MxConfigurationModule(reactContext) + MxCookieModule.NAME -> MxCookieModule(reactContext) + MxErrorModule.NAME -> MxErrorModule(reactContext) + MxNavigationModule.NAME -> MxNavigationModule(reactContext) + else -> null } } override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { return ReactModuleInfoProvider { val moduleInfos: MutableMap = HashMap() - moduleInfos[MendixNativeModule.NAME] = ReactModuleInfo( - MendixNativeModule.NAME, - MendixNativeModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - false, // isCxxModule - true // isTurboModule - ) + listOf( + MxEncryptionModule.NAME, + MxSplashScreenModule.NAME, + MxFileSystemModule.NAME, + MxStorageModule.NAME, + MxOtaModule.NAME, + MxDownloadModule.NAME, + MxReloadModule.NAME, + MxConfigurationModule.NAME, + MxCookieModule.NAME, + MxErrorModule.NAME, + MxNavigationModule.NAME + ).forEach { + moduleInfos[it] = ReactModuleInfo( + name = it, + className = it, + canOverrideExistingModule = false, + needsEagerInit = false, + isCxxModule = false, + isTurboModule = true + ) + } moduleInfos } } diff --git a/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt b/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt new file mode 100644 index 0000000..8af3df0 --- /dev/null +++ b/android/src/main/java/com/mendixnative/configuration/MxConfigurationModule.kt @@ -0,0 +1,24 @@ +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 + +@ReactModule(name = MxConfigurationModule.NAME) +class MxConfigurationModule(reactContext: ReactApplicationContext) : + NativeMxConfigurationSpec(reactContext) { + + private val configuration = MxConfiguration(reactContext) + + override fun getName(): String = NAME + + override fun getConfig(): WritableMap? { + return configuration.getConstants() + } + + companion object { + const val NAME = "MxConfiguration" + } +} diff --git a/android/src/main/java/com/mendixnative/cookie/MxCookieModule.kt b/android/src/main/java/com/mendixnative/cookie/MxCookieModule.kt new file mode 100644 index 0000000..edcb843 --- /dev/null +++ b/android/src/main/java/com/mendixnative/cookie/MxCookieModule.kt @@ -0,0 +1,24 @@ +package com.mendixnative.cookie + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.annotations.ReactModule +import com.mendix.mendixnative.react.cookie.NativeCookieModule +import com.mendixnative.NativeMxCookieSpec + +@ReactModule(name = MxCookieModule.NAME) +class MxCookieModule(reactContext: ReactApplicationContext) : + NativeMxCookieSpec(reactContext) { + + private val cookieModule = NativeCookieModule(reactContext) + + override fun getName(): String = NAME + + override fun clearAll(promise: Promise) { + cookieModule.clearAll(promise) + } + + companion object { + const val NAME = "MxCookie" + } +} diff --git a/android/src/main/java/com/mendixnative/download/MxDownloadModule.kt b/android/src/main/java/com/mendixnative/download/MxDownloadModule.kt new file mode 100644 index 0000000..843f0ad --- /dev/null +++ b/android/src/main/java/com/mendixnative/download/MxDownloadModule.kt @@ -0,0 +1,25 @@ +package com.mendixnative.download + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.module.annotations.ReactModule +import com.mendix.mendixnative.react.download.NativeDownloadModule +import com.mendixnative.NativeMxDownloadSpec + +@ReactModule(name = MxDownloadModule.NAME) +class MxDownloadModule(reactContext: ReactApplicationContext) : + NativeMxDownloadSpec(reactContext) { + + private val downloadModule = NativeDownloadModule(reactContext) + + override fun getName(): String = NAME + + override fun download(url: String, downloadPath: String, config: ReadableMap, promise: Promise) { + downloadModule.download(url, downloadPath, config, promise) + } + + companion object { + const val NAME = "MxDownload" + } +} diff --git a/android/src/main/java/com/mendixnative/encryption/MxEncryptionModule.kt b/android/src/main/java/com/mendixnative/encryption/MxEncryptionModule.kt new file mode 100644 index 0000000..9c4f9e7 --- /dev/null +++ b/android/src/main/java/com/mendixnative/encryption/MxEncryptionModule.kt @@ -0,0 +1,40 @@ +package com.mendixnative.encryption + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.annotations.ReactModule +import com.mendix.mendixnative.encryption.MendixEncryptedStorageModule +import com.mendixnative.NativeMxEncryptionSpec + +@ReactModule(name = MxEncryptionModule.NAME) +class MxEncryptionModule(reactContext: ReactApplicationContext) : + NativeMxEncryptionSpec(reactContext) { + + private val encryptedStorage = MendixEncryptedStorageModule(reactContext) + + override fun getName(): String = NAME + + override fun setItem(key: String, value: String, promise: Promise) { + encryptedStorage.setItem(key, value, promise) + } + + override fun getItem(key: String, promise: Promise) { + encryptedStorage.getItem(key, promise) + } + + override fun removeItem(key: String, promise: Promise) { + encryptedStorage.removeItem(key, promise) + } + + override fun clear(promise: Promise) { + encryptedStorage.clear(promise) + } + + override fun isEncrypted(): Boolean { + return encryptedStorage.isEncrypted + } + + companion object { + const val NAME = "MxEncryption" + } +} diff --git a/android/src/main/java/com/mendixnative/error/MxErrorModule.kt b/android/src/main/java/com/mendixnative/error/MxErrorModule.kt new file mode 100644 index 0000000..0e0703c --- /dev/null +++ b/android/src/main/java/com/mendixnative/error/MxErrorModule.kt @@ -0,0 +1,24 @@ +package com.mendixnative.error + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.module.annotations.ReactModule +import com.mendix.mendixnative.react.NativeErrorHandler +import com.mendixnative.NativeMxErrorSpec + +@ReactModule(name = MxErrorModule.NAME) +class MxErrorModule(reactContext: ReactApplicationContext) : + NativeMxErrorSpec(reactContext) { + + private val errorHandler = NativeErrorHandler(reactContext) + + override fun getName(): String = NAME + + override fun handle(message: String, stackTrace: ReadableArray) { + errorHandler.handle(message, stackTrace) + } + + companion object { + const val NAME = "MxError" + } +} diff --git a/android/src/main/java/com/mendixnative/fs/MxFileSystemModule.kt b/android/src/main/java/com/mendixnative/fs/MxFileSystemModule.kt new file mode 100644 index 0000000..2bef53e --- /dev/null +++ b/android/src/main/java/com/mendixnative/fs/MxFileSystemModule.kt @@ -0,0 +1,70 @@ +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 + +@ReactModule(name = MxFileSystemModule.NAME) +class MxFileSystemModule(reactContext: ReactApplicationContext) : + NativeMxFileSystemSpec(reactContext) { + + private val fsModule = NativeFsModule(reactContext) + + override fun getName(): String = NAME + + override fun constants(): WritableMap? { + return fsModule.getConstants() + } + + override fun save(blob: ReadableMap, filePath: String, promise: Promise) { + fsModule.save(blob, filePath, promise) + } + + override fun read(filePath: String, promise: Promise) { + fsModule.read(filePath, promise) + } + + override fun move(filePath: String, newPath: String, promise: Promise) { + fsModule.move(filePath, newPath, promise) + } + + override fun remove(filePath: String, promise: Promise) { + fsModule.remove(filePath, promise) + } + + override fun list(dirPath: String, promise: Promise) { + fsModule.list(dirPath, promise) + } + + override fun readAsDataURL(filePath: String, promise: Promise) { + fsModule.readAsDataURL(filePath, promise) + } + + override fun readAsText(filePath: String, promise: Promise) { + fsModule.readAsText(filePath, promise) + } + + override fun fileExists(filePath: String, promise: Promise) { + fsModule.fileExists(filePath, promise) + } + + override fun writeJson(data: ReadableMap, filepath: String, promise: Promise) { + fsModule.writeJson(data, filepath, promise) + } + + override fun readJson(filepath: String, promise: Promise) { + fsModule.readJson(filepath, promise) + } + + override fun setEncryptionEnabled(enabled: Boolean) { + fsModule.setEncryptionEnabled(enabled) + } + + companion object { + const val NAME = "MxFileSystem" + } +} diff --git a/android/src/main/java/com/mendixnative/navigation/MxNavigationModule.kt b/android/src/main/java/com/mendixnative/navigation/MxNavigationModule.kt new file mode 100644 index 0000000..fb52f19 --- /dev/null +++ b/android/src/main/java/com/mendixnative/navigation/MxNavigationModule.kt @@ -0,0 +1,27 @@ +package com.mendixnative.navigation + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.annotations.ReactModule +import com.mendix.mendixnative.react.NavigationModeModule +import com.mendixnative.NativeMxNavigationSpec + +@ReactModule(name = MxNavigationModule.NAME) +class MxNavigationModule(reactContext: ReactApplicationContext) : + NativeMxNavigationSpec(reactContext) { + + private val navigationMode = NavigationModeModule(reactContext) + + override fun getName(): String = NAME + + override fun isNavigationBarActive(): Boolean { + return navigationMode.isNavigationBarActive() + } + + override fun getNavigationBarHeight(): Double { + return navigationMode.getNavigationBarHeight() + } + + companion object { + const val NAME = "MxNavigation" + } +} diff --git a/android/src/main/java/com/mendixnative/ota/MxOtaModule.kt b/android/src/main/java/com/mendixnative/ota/MxOtaModule.kt new file mode 100644 index 0000000..8ff1258 --- /dev/null +++ b/android/src/main/java/com/mendixnative/ota/MxOtaModule.kt @@ -0,0 +1,29 @@ +package com.mendixnative.ota + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.module.annotations.ReactModule +import com.mendix.mendixnative.react.ota.NativeOtaModule +import com.mendixnative.NativeMxOtaSpec + +@ReactModule(name = MxOtaModule.NAME) +class MxOtaModule(reactContext: ReactApplicationContext) : + NativeMxOtaSpec(reactContext) { + + private val otaModule = NativeOtaModule(reactContext) + + override fun getName(): String = NAME + + override fun download(config: ReadableMap, promise: Promise) { + otaModule.download(config, promise) + } + + override fun deploy(config: ReadableMap, promise: Promise) { + otaModule.deploy(config, promise) + } + + companion object { + const val NAME = "MxOta" + } +} diff --git a/android/src/main/java/com/mendixnative/reload/MxReloadModule.kt b/android/src/main/java/com/mendixnative/reload/MxReloadModule.kt new file mode 100644 index 0000000..dd6e66d --- /dev/null +++ b/android/src/main/java/com/mendixnative/reload/MxReloadModule.kt @@ -0,0 +1,30 @@ +package com.mendixnative.reload + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.annotations.ReactModule +import com.mendix.mendixnative.react.NativeReloadHandler +import com.mendixnative.NativeMxReloadSpec + +@ReactModule(name = MxReloadModule.NAME) +class MxReloadModule(reactContext: ReactApplicationContext) : + NativeMxReloadSpec(reactContext) { + + private val reloadHandler = NativeReloadHandler(reactContext) + + override fun getName(): String = NAME + + override fun reload(promise: Promise) { + reloadHandler.reload() + promise.resolve(null) + } + + override fun exitApp(promise: Promise) { + reloadHandler.exitApp() + promise.resolve(null) + } + + companion object { + const val NAME = "MxReload" + } +} diff --git a/android/src/main/java/com/mendixnative/splash/MxSplashScreenModule.kt b/android/src/main/java/com/mendixnative/splash/MxSplashScreenModule.kt new file mode 100644 index 0000000..86eb385 --- /dev/null +++ b/android/src/main/java/com/mendixnative/splash/MxSplashScreenModule.kt @@ -0,0 +1,31 @@ +package com.mendixnative.splash + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.annotations.ReactModule +import com.mendix.mendixnative.react.splash.MendixSplashScreenModule +import com.mendix.mendixnative.react.splash.MendixSplashScreenPresenter +import com.mendixnative.NativeMxSplashScreenSpec + +@ReactModule(name = MxSplashScreenModule.NAME) +class MxSplashScreenModule(reactContext: ReactApplicationContext) : + NativeMxSplashScreenSpec(reactContext) { + + private val splashScreenModule = MendixSplashScreenModule(reactContext) + + // Presenter is injected by MendixNativePackage + var presenter: MendixSplashScreenPresenter? = null + + override fun getName(): String = NAME + + override fun show() { + splashScreenModule.show(presenter) + } + + override fun hide() { + splashScreenModule.hide(presenter) + } + + companion object { + const val NAME = "MxSplashScreen" + } +} diff --git a/android/src/main/java/com/mendixnative/storage/MxStorageModule.kt b/android/src/main/java/com/mendixnative/storage/MxStorageModule.kt new file mode 100644 index 0000000..b726c9c --- /dev/null +++ b/android/src/main/java/com/mendixnative/storage/MxStorageModule.kt @@ -0,0 +1,46 @@ +package com.mendixnative.storage + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.annotations.ReactModule +import com.mendixnative.NativeMxStorageSpec +import com.op.sqlite.OPSQLiteModule + +@ReactModule(name = MxStorageModule.NAME) +class MxStorageModule(reactContext: ReactApplicationContext) : + NativeMxStorageSpec(reactContext) { + + override fun getName(): String = NAME + + override fun clearDatabases(promise: Promise) { + val opSQLiteModule = reactApplicationContext.getNativeModule(OPSQLiteModule::class.java) + if (opSQLiteModule != null) { + try { + opSQLiteModule.deleteAllDBs() + promise.resolve(null) + } catch (e: Exception) { + promise.reject("STORAGE_CLEAR_FAILED", "Failed to clear databases: ${e.message}", e) + } + } else { + promise.reject("MODULE_NOT_FOUND", "OPSQLiteModule not available") + } + } + + override fun closeDatabaseConnections(promise: Promise) { + val opSQLiteModule = reactApplicationContext.getNativeModule(OPSQLiteModule::class.java) + if (opSQLiteModule != null) { + try { + opSQLiteModule.closeAllConnections() + promise.resolve(null) + } catch (e: Exception) { + promise.reject("STORAGE_CLOSE_FAILED", "Failed to close database connections: ${e.message}", e) + } + } else { + promise.reject("MODULE_NOT_FOUND", "OPSQLiteModule not available") + } + } + + companion object { + const val NAME = "MxStorage" + } +} diff --git a/example/__tests__/splash-screen.harness.ts b/example/__tests__/splash-screen.harness.ts index 85dc188..9846f2f 100644 --- a/example/__tests__/splash-screen.harness.ts +++ b/example/__tests__/splash-screen.harness.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'react-native-harness'; -import { MendixSplashScreen } from 'mendix-native'; +import { MxSplashScreen as MendixSplashScreen } from 'mendix-native'; describe('MendixSplashScreen', () => { describe('API surface', () => { diff --git a/example/ios/MendixNativeExample.xcodeproj/xcshareddata/xcschemes/MendixNativeExample.xcscheme b/example/ios/MendixNativeExample.xcodeproj/xcshareddata/xcschemes/MendixNativeExample.xcscheme index dc9a1e7..c12b556 100644 --- a/example/ios/MendixNativeExample.xcodeproj/xcshareddata/xcschemes/MendixNativeExample.xcscheme +++ b/example/ios/MendixNativeExample.xcodeproj/xcshareddata/xcschemes/MendixNativeExample.xcscheme @@ -60,6 +60,13 @@ ReferencedContainer = "container:MendixNativeExample.xcodeproj"> + + + + Bool { - - SessionCookieStore.restore() setUpProvider() - - guard let bundleUrl = bundleURL() else { - let message = "No script URL provided. Make sure the metro packager is running or you have embedded a JS bundle in your application bundle." - NativeErrorHandler().handle(message: message, stackTrace: []) - return false - } - - ReactNative.shared.setup( - MendixApp.init( - identifier: nil, - bundleUrl: bundleUrl, - runtimeUrl: URL(string: "http://localhost:8081")!, - warningsFilter: .none, - isDeveloperApp: false, - clearDataAtLaunch: false, - splashScreenPresenter: nil, - reactLoading: nil, - enableThreeFingerGestures: false - ), - launchOptions: launchOptions + let mendixApp = MendixApp.init( + identifier: nil, + bundleUrl: bundleUrl, + runtimeUrl: URL(string: "http://localhost:8081")!, + warningsFilter: .none, + isDeveloperApp: false, + clearDataAtLaunch: false, + splashScreenPresenter: nil, + reactLoading: nil, + enableThreeFingerGestures: false ) + ReactNative.shared.setup(mendixApp, launchOptions: nil) ReactNative.shared.start() return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - func applicationDidEnterBackground(_ application: UIApplication) { - SessionCookieStore.persist() - } - - func applicationWillTerminate(_ application: UIApplication) { - SessionCookieStore.persist() - } - - override func sourceURL(for bridge: RCTBridge) -> URL? { - self.bundleURL() - } - - override func bundleURL() -> URL? { - #if DEBUG - RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") - #else - Bundle.main.url(forResource: "main", withExtension: "jsbundle") - #endif + var bundleUrl: URL { +#if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")! +#else + Bundle.main.url(forResource: "main", withExtension: "jsbundle")! +#endif } } diff --git a/example/rn-harness.config.mjs b/example/rn-harness.config.mjs index 3190d96..ed8ecfe 100644 --- a/example/rn-harness.config.mjs +++ b/example/rn-harness.config.mjs @@ -20,12 +20,12 @@ const config = { runners: [ androidPlatform({ name: 'android', - device: androidEmulator('Pixel_API_35'), + device: androidEmulator('Pixel_6'), bundleId: 'mendixnative.example', }), applePlatform({ name: 'ios', - device: appleSimulator('iPhone 17', '26.2'), + device: appleSimulator('iPhone 17', '26.0'), bundleId: 'mendixnative.example', }), ], diff --git a/ios/MendixNative.h b/ios/MendixNative.h deleted file mode 100644 index 57f561f..0000000 --- a/ios/MendixNative.h +++ /dev/null @@ -1,8 +0,0 @@ -#import -#import -#import "RNCAsyncStorage.h" -#import "RCTAppDelegate.h" - -@interface MendixNative : NativeMendixNativeSpecBase -- (void)reloadClientWithState; -@end diff --git a/ios/MendixNative.mm b/ios/MendixNative.mm deleted file mode 100644 index 7907018..0000000 --- a/ios/MendixNative.mm +++ /dev/null @@ -1,153 +0,0 @@ -#import "MendixNative.h" -#import "MendixNative-Swift.h" - -@implementation MendixNative - -RCT_EXPORT_MODULE() - -- (std::shared_ptr)getTurboModule: -(const facebook::react::ObjCTurboModule::InitParams &)params -{ - return std::make_shared(params); -} - -- (void)encryptedStorageSetItem:(nonnull NSString *)key value:(nonnull NSString *)value resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - Promise *promise = [[Promise alloc] initWithResolve:resolve reject:reject]; - [[[EncryptedStorage alloc] init] setItemWithKey:key value:value promise:promise]; -} - -- (void)encryptedStorageGetItem:(nonnull NSString *)key resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - Promise *promise = [[Promise alloc] initWithResolve:resolve reject:reject]; - [[[EncryptedStorage alloc] init] getItemWithKey:key promise:promise]; -} - -- (void)encryptedStorageRemoveItem:(nonnull NSString *)key resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - Promise *promise = [[Promise alloc] initWithResolve:resolve reject:reject]; - [[[EncryptedStorage alloc] init] removeItemWithKey:key promise:promise]; -} - -- (void)encryptedStorageClear:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - Promise *promise = [[Promise alloc] initWithResolve:resolve reject:reject]; - [[[EncryptedStorage alloc] init] clearWithPromise:promise]; -} - -- (nonnull NSNumber *)encryptedStorageIsEncrypted { - return [NSNumber numberWithBool: [EncryptedStorage isEncrypted]]; -} - -- (void)splashScreenShow { - [[[MendixSplashScreen alloc] init] show]; -} - -- (void)splashScreenHide { - [[[MendixSplashScreen alloc] init] hide]; -} - -- (void)cookieClearAll:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - Promise *promise = [Promise instance:resolve reject:reject]; - [[[NativeCookieModule alloc] init] clearAll:promise]; -} - -- (void)reloadHandlerReload:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[ReloadHandler alloc] init] reload]; - resolve(nil); -} - -- (void)reloadClientWithState { - [self emitOnReloadWithState]; -} - -- (void)sendDownloadProgressEvent: (NSDictionary *) data { - [self emitOnDownloadProgress:data]; -} - -- (void)reloadHandlerExitApp:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[ReloadHandler alloc] init] exitApp]; - resolve(nil); -} - -- (void)downloadHandlerDownload:(nonnull NSString *)url downloadPath:(nonnull NSString *)downloadPath config:(nonnull NSDictionary *)config resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - Promise *promise = [Promise instance:resolve reject:reject]; - [[[NativeDownloadModule alloc] init] download:url downloadPath:downloadPath config:config onProgress:^(NSDictionary * _Nonnull data) { - [self emitOnDownloadProgress:data]; - } promise:promise]; -} - -- (nonnull NSDictionary *)mxConfigurationGetConfig { - return [[[MxConfiguration alloc] init] constants]; -} - -- (void)otaDeploy:(nonnull NSDictionary *)config resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - Promise *promise = [Promise instance:resolve reject:reject]; - [[[NativeOtaModule alloc] init] deploy:config promise:promise]; -} - -- (void)otaDownload:(nonnull NSDictionary *)config resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - Promise *promise = [Promise instance:resolve reject:reject]; - [[[NativeOtaModule alloc] init] download:config promise:promise]; -} - -- (void)fsSetEncryptionEnabled:(BOOL)enabled { - [[[NativeFsModule alloc] init] setEncryptionEnabled:enabled]; -} - -- (void)fsSave:(nonnull NSDictionary *)blob filePath:(nonnull NSString *)filePath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[NativeFsModule alloc] init] save:blob filepath:filePath resolve:resolve reject:reject]; -} - -- (void)fsRead:(nonnull NSString *)filePath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[NativeFsModule alloc] init] read:filePath resolve:resolve reject:reject]; -} - -- (void)fsMove:(nonnull NSString *)filePath newPath:(nonnull NSString *)newPath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[NativeFsModule alloc] init] move:filePath newPath:newPath resolve:resolve reject:reject]; -} - -- (void)fsRemove:(nonnull NSString *)filePath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[NativeFsModule alloc] init] remove:filePath resolve:resolve reject:reject]; -} - -- (void)fsList:(nonnull NSString *)dirPath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[NativeFsModule alloc] init] list:dirPath resolve:resolve reject:reject]; -} - -- (void)fsReadAsDataURL:(nonnull NSString *)filePath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[NativeFsModule alloc] init] readAsDataURL:filePath resolve:resolve reject:reject]; -} - -- (void)fsFileExists:(nonnull NSString *)filePath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[NativeFsModule alloc] init] fileExists:filePath resolve:resolve reject:reject]; -} - -- (void)fsWriteJson:(nonnull NSDictionary *)data filepath:(nonnull NSString *)filepath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[NativeFsModule alloc] init] writeJson:data filepath:filepath resolve:resolve reject:reject]; -} - - -- (void)fsReadJson:(nonnull NSString *)filepath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [[[NativeFsModule alloc] init] readJson:filepath resolve:resolve reject:reject]; -} - -- (nonnull NSDictionary *)fsConstants { - return [[[NativeFsModule alloc] init] constants]; -} - -- (void)errorHandlerHandle:(nonnull NSString *)message stackTrace:(nonnull NSArray *)stackTrace { - [[[NativeErrorHandler alloc] init] handleWithMessage:message stackTrace:stackTrace]; -} - -- (void)fsReadAsText:(nonnull NSString *)filePath resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - reject(@"NOT_SPPORTED", @"Read as text is not supported on iOS", nil); -} - -- (nonnull NSNumber *)navigationModeGetNavigationBarHeight { - return [NSNumber numberWithBool:NO]; -} - - -- (nonnull NSNumber *)navigationModeIsNavigationBarActive { - return [NSNumber numberWithDouble:0.0]; -} - - -@end diff --git a/ios/Modules/Helper/DevHelper.swift b/ios/Modules/Helper/DevHelper.swift index 1878441..9931549 100644 --- a/ios/Modules/Helper/DevHelper.swift +++ b/ios/Modules/Helper/DevHelper.swift @@ -2,72 +2,43 @@ import Foundation import React public class DevHelper { - + public static func showAppMenu() { devMenu?.show() } - + public static func toggleElementInspector() { devSettings?.toggleElementInspector() } - + + // Modern Architecture: Access dev modules through ReactAppProvider's moduleRegistry + // Works with RN 0.83+ where RCTBridge.current() returns nil public static var devSettings: RCTDevSettings? { return ReactAppProvider.getModule(type: RCTDevSettings.self) } - + public static var devMenu: RCTDevMenu? { return ReactAppProvider.getModule(type: RCTDevMenu.self) } - + public static func setDebugMode(enabled: Bool) { AppPreferences.remoteDebuggingEnabled = enabled - // RN <= 0.82 exposed isDebuggingRemotely on RCTDevSettings. - // RN 0.83+ removed that toggle and moved to on-device debugging tooling. - if let devSettings, - devSettings.responds(to: NSSelectorFromString("setIsDebuggingRemotely:")) { - devSettings.setValue(enabled, forKey: "isDebuggingRemotely") - return - } - - if enabled { - openDebuggerIfAvailable() - } else { - disableDebuggerIfAvailable() - } + // Modern Architecture (RN 0.83+): + // Debug operations are controlled from JavaScript via the DevSettings TurboModule. + // Native code no longer needs to control debugging - it's all JavaScript-driven. + // + // To open the debugger from JavaScript: + // import { DevSettings } from 'mendix-native'; + // DevSettings.openDebugger(); + // + // This method now only stores the preference. Actual debug control is via JavaScript. } - private static func openDebuggerIfAvailable() { - guard let bundleURL = ReactNative.shared.bundleURL(), - let inspectorHelperClass = NSClassFromString("RCTInspectorDevServerHelper") as? NSObject.Type else { - return - } - - let selector = NSSelectorFromString("openDebugger:withErrorMessage:") - if inspectorHelperClass.responds(to: selector) { - _ = inspectorHelperClass.perform( - selector, - with: bundleURL, - with: "Failed to open debugger. Please check that the dev server is running and reload the app." - ) - } - } - - private static func disableDebuggerIfAvailable() { - guard let inspectorHelperClass = NSClassFromString("RCTInspectorDevServerHelper") as? NSObject.Type else { - return - } - - let selector = NSSelectorFromString("disableDebugger") - if inspectorHelperClass.responds(to: selector) { - _ = inspectorHelperClass.perform(selector) - } - } - public static func hideDevLoadingView() { devLoadingView?.hide() } - + public static var devLoadingView: RCTDevLoadingView? { return ReactAppProvider.getModule(type: RCTDevLoadingView.self) } diff --git a/ios/Modules/Helper/ReactAppProvider.swift b/ios/Modules/Helper/ReactAppProvider.swift index 6d70941..17bc7a1 100644 --- a/ios/Modules/Helper/ReactAppProvider.swift +++ b/ios/Modules/Helper/ReactAppProvider.swift @@ -37,12 +37,12 @@ open class ReactAppProvider: RCTDefaultReactNativeFactoryDelegate, UIApplication open override func sourceURL(for bridge: RCTBridge) -> URL? { return self.bundleURL() } - + public func setReactViewController(_ controller: UIViewController) { controller.view = reactAppView() changeRoot(to: controller) } - + public func reactAppView() -> UIView? { guard let view = reactNativeFactory?.rootViewFactory.view(withModuleName: reactRootViewName) else { return nil @@ -51,40 +51,41 @@ open class ReactAppProvider: RCTDefaultReactNativeFactoryDelegate, UIApplication view.frame = window?.rootViewController?.view.frame ?? .zero return view } - + public func startReactApp() { - + } - + public func stopReactApp() { } - + public static func shared() -> ReactAppProvider? { return UIApplication.shared.delegate as? ReactAppProvider } - - public static func isReactAppActive() -> Bool { - return unsafeBridge != nil - } - + public func changeRoot(to controller: UIViewController) { window?.rootViewController = controller window?.makeKeyAndVisible() } - + public var rootView: UIView? { return window?.rootViewController?.view } - + + // Check if React Native app is active and running + public static func isReactAppActive() -> Bool { + return RCTBridge.current() != nil + } + + // Dev-only module access (RCTDevMenu, RCTDevSettings, RCTDevLoadingView) + // These modules are not TurboModules and are only available in DEV mode + // Using optional RCTBridge.current() - returns nil gracefully when bridge unavailable + // Note: In RN 0.83+, dev modules may not be available if using new architecture exclusively public static func getModule(type: T.Type) -> T? { - return unsafeBridge?.moduleRegistry.module(for: type.self) as? T + return RCTBridge.current()?.moduleRegistry.module(for: type.self) as? T } - + public static func getModule(name: String) -> Any? { - return unsafeBridge?.moduleRegistry.module(forName: name) - } - - public static var unsafeBridge: RCTBridge? { - return RCTBridge.current() + return RCTBridge.current()?.moduleRegistry.module(forName: name) } } diff --git a/ios/TurboModules/MxConfiguration/MxConfigurationModule.h b/ios/TurboModules/MxConfiguration/MxConfigurationModule.h new file mode 100644 index 0000000..c07ad52 --- /dev/null +++ b/ios/TurboModules/MxConfiguration/MxConfigurationModule.h @@ -0,0 +1,4 @@ +#import + +@interface MxConfigurationModule : NativeMxConfigurationSpecBase +@end diff --git a/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm b/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm new file mode 100644 index 0000000..c14ebe0 --- /dev/null +++ b/ios/TurboModules/MxConfiguration/MxConfigurationModule.mm @@ -0,0 +1,20 @@ +#import "MxConfigurationModule.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxConfigurationModule + +RCT_EXPORT_MODULE(MxConfiguration) + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (nonnull NSDictionary *)getConfig { + return [[[MxConfiguration alloc] init] constants]; +} + +@end diff --git a/ios/TurboModules/MxCookie/MxCookie.h b/ios/TurboModules/MxCookie/MxCookie.h new file mode 100644 index 0000000..1151e33 --- /dev/null +++ b/ios/TurboModules/MxCookie/MxCookie.h @@ -0,0 +1,4 @@ +#import + +@interface MxCookie : NativeMxCookieSpecBase +@end diff --git a/ios/TurboModules/MxCookie/MxCookie.mm b/ios/TurboModules/MxCookie/MxCookie.mm new file mode 100644 index 0000000..96808d7 --- /dev/null +++ b/ios/TurboModules/MxCookie/MxCookie.mm @@ -0,0 +1,22 @@ +#import "MxCookie.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxCookie + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (void)clearAll:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + Promise *promise = [Promise instance:resolve reject:reject]; + [[[NativeCookieModule alloc] init] clearAll:promise]; +} + +@end diff --git a/ios/TurboModules/MxDownload/MxDownload.h b/ios/TurboModules/MxDownload/MxDownload.h new file mode 100644 index 0000000..daa52f5 --- /dev/null +++ b/ios/TurboModules/MxDownload/MxDownload.h @@ -0,0 +1,4 @@ +#import + +@interface MxDownload : NativeMxDownloadSpecBase +@end diff --git a/ios/TurboModules/MxDownload/MxDownload.mm b/ios/TurboModules/MxDownload/MxDownload.mm new file mode 100644 index 0000000..fb92377 --- /dev/null +++ b/ios/TurboModules/MxDownload/MxDownload.mm @@ -0,0 +1,27 @@ +#import "MxDownload.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxDownload + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (void)download:(nonnull NSString *)url + downloadPath:(nonnull NSString *)downloadPath + config:(nonnull NSDictionary *)config + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + Promise *promise = [Promise instance:resolve reject:reject]; + // Note: Progress events are not emitted from this module + // Use MxOta module for progress events + [[[NativeDownloadModule alloc] init] download:url downloadPath:downloadPath config:config onProgress:nil promise:promise]; +} + +@end diff --git a/ios/TurboModules/MxEncryption/MxEncryption.h b/ios/TurboModules/MxEncryption/MxEncryption.h new file mode 100644 index 0000000..7b661f2 --- /dev/null +++ b/ios/TurboModules/MxEncryption/MxEncryption.h @@ -0,0 +1,4 @@ +#import + +@interface MxEncryption : NativeMxEncryptionSpecBase +@end diff --git a/ios/TurboModules/MxEncryption/MxEncryption.mm b/ios/TurboModules/MxEncryption/MxEncryption.mm new file mode 100644 index 0000000..1476dec --- /dev/null +++ b/ios/TurboModules/MxEncryption/MxEncryption.mm @@ -0,0 +1,48 @@ +#import "MxEncryption.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxEncryption + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (void)setItem:(nonnull NSString *)key + value:(nonnull NSString *)value + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + Promise *promise = [[Promise alloc] initWithResolve:resolve reject:reject]; + [[[EncryptedStorage alloc] init] setItemWithKey:key value:value promise:promise]; +} + +- (void)getItem:(nonnull NSString *)key + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + Promise *promise = [[Promise alloc] initWithResolve:resolve reject:reject]; + [[[EncryptedStorage alloc] init] getItemWithKey:key promise:promise]; +} + +- (void)removeItem:(nonnull NSString *)key + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + Promise *promise = [[Promise alloc] initWithResolve:resolve reject:reject]; + [[[EncryptedStorage alloc] init] removeItemWithKey:key promise:promise]; +} + +- (void)clear:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + Promise *promise = [[Promise alloc] initWithResolve:resolve reject:reject]; + [[[EncryptedStorage alloc] init] clearWithPromise:promise]; +} + +- (nonnull NSNumber *)isEncrypted { + return [NSNumber numberWithBool: [EncryptedStorage isEncrypted]]; +} + +@end diff --git a/ios/TurboModules/MxError/MxError.h b/ios/TurboModules/MxError/MxError.h new file mode 100644 index 0000000..7b6709e --- /dev/null +++ b/ios/TurboModules/MxError/MxError.h @@ -0,0 +1,4 @@ +#import + +@interface MxError : NativeMxErrorSpecBase +@end diff --git a/ios/TurboModules/MxError/MxError.mm b/ios/TurboModules/MxError/MxError.mm new file mode 100644 index 0000000..8f400a1 --- /dev/null +++ b/ios/TurboModules/MxError/MxError.mm @@ -0,0 +1,21 @@ +#import "MxError.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxError + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (void)handle:(nonnull NSString *)message + stackTrace:(nonnull NSArray *)stackTrace { + [[[NativeErrorHandler alloc] init] handleWithMessage:message stackTrace:stackTrace]; +} + +@end diff --git a/ios/TurboModules/MxFileSystem/MxFileSystem.h b/ios/TurboModules/MxFileSystem/MxFileSystem.h new file mode 100644 index 0000000..27f5bbe --- /dev/null +++ b/ios/TurboModules/MxFileSystem/MxFileSystem.h @@ -0,0 +1,4 @@ +#import + +@interface MxFileSystem : NativeMxFileSystemSpecBase +@end diff --git a/ios/TurboModules/MxFileSystem/MxFileSystem.mm b/ios/TurboModules/MxFileSystem/MxFileSystem.mm new file mode 100644 index 0000000..f67a9c1 --- /dev/null +++ b/ios/TurboModules/MxFileSystem/MxFileSystem.mm @@ -0,0 +1,87 @@ +#import "MxFileSystem.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxFileSystem + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (nonnull NSDictionary *)constants { + return [[[NativeFsModule alloc] init] constants]; +} + +- (void)save:(nonnull NSDictionary *)blob + filePath:(nonnull NSString *)filePath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[NativeFsModule alloc] init] save:blob filepath:filePath resolve:resolve reject:reject]; +} + +- (void)read:(nonnull NSString *)filePath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[NativeFsModule alloc] init] read:filePath resolve:resolve reject:reject]; +} + +- (void)move:(nonnull NSString *)filePath + newPath:(nonnull NSString *)newPath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[NativeFsModule alloc] init] move:filePath newPath:newPath resolve:resolve reject:reject]; +} + +- (void)remove:(nonnull NSString *)filePath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[NativeFsModule alloc] init] remove:filePath resolve:resolve reject:reject]; +} + +- (void)list:(nonnull NSString *)dirPath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[NativeFsModule alloc] init] list:dirPath resolve:resolve reject:reject]; +} + +- (void)readAsDataURL:(nonnull NSString *)filePath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[NativeFsModule alloc] init] readAsDataURL:filePath resolve:resolve reject:reject]; +} + +- (void)readAsText:(nonnull NSString *)filePath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + reject(@"NOT_SUPPORTED", @"Read as text is not supported on iOS", nil); +} + +- (void)fileExists:(nonnull NSString *)filePath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[NativeFsModule alloc] init] fileExists:filePath resolve:resolve reject:reject]; +} + +- (void)writeJson:(nonnull NSDictionary *)data + filepath:(nonnull NSString *)filepath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[NativeFsModule alloc] init] writeJson:data filepath:filepath resolve:resolve reject:reject]; +} + +- (void)readJson:(nonnull NSString *)filepath + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[NativeFsModule alloc] init] readJson:filepath resolve:resolve reject:reject]; +} + +- (void)setEncryptionEnabled:(BOOL)enabled { + [[[NativeFsModule alloc] init] setEncryptionEnabled:enabled]; +} + +@end diff --git a/ios/TurboModules/MxNavigation/MxNavigation.h b/ios/TurboModules/MxNavigation/MxNavigation.h new file mode 100644 index 0000000..0e4c4ce --- /dev/null +++ b/ios/TurboModules/MxNavigation/MxNavigation.h @@ -0,0 +1,4 @@ +#import + +@interface MxNavigation : NativeMxNavigationSpecBase +@end diff --git a/ios/TurboModules/MxNavigation/MxNavigation.mm b/ios/TurboModules/MxNavigation/MxNavigation.mm new file mode 100644 index 0000000..3f7af41 --- /dev/null +++ b/ios/TurboModules/MxNavigation/MxNavigation.mm @@ -0,0 +1,24 @@ +#import "MxNavigation.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxNavigation + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (nonnull NSNumber *)isNavigationBarActive { + return [NSNumber numberWithBool:NO]; +} + +- (nonnull NSNumber *)getNavigationBarHeight { + return [NSNumber numberWithDouble:0.0]; +} + +@end diff --git a/ios/TurboModules/MxOta/MxOta.h b/ios/TurboModules/MxOta/MxOta.h new file mode 100644 index 0000000..42638a7 --- /dev/null +++ b/ios/TurboModules/MxOta/MxOta.h @@ -0,0 +1,4 @@ +#import + +@interface MxOta : NativeMxOtaSpecBase +@end diff --git a/ios/TurboModules/MxOta/MxOta.mm b/ios/TurboModules/MxOta/MxOta.mm new file mode 100644 index 0000000..72d7e7e --- /dev/null +++ b/ios/TurboModules/MxOta/MxOta.mm @@ -0,0 +1,30 @@ +#import "MxOta.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxOta + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (void)download:(nonnull NSDictionary *)config + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + Promise *promise = [Promise instance:resolve reject:reject]; + [[[NativeOtaModule alloc] init] download:config promise:promise]; +} + +- (void)deploy:(nonnull NSDictionary *)config + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + Promise *promise = [Promise instance:resolve reject:reject]; + [[[NativeOtaModule alloc] init] deploy:config promise:promise]; +} + +@end diff --git a/ios/TurboModules/MxReload/MxReload.h b/ios/TurboModules/MxReload/MxReload.h new file mode 100644 index 0000000..8127fed --- /dev/null +++ b/ios/TurboModules/MxReload/MxReload.h @@ -0,0 +1,4 @@ +#import + +@interface MxReload : NativeMxReloadSpecBase +@end diff --git a/ios/TurboModules/MxReload/MxReload.mm b/ios/TurboModules/MxReload/MxReload.mm new file mode 100644 index 0000000..f3f0358 --- /dev/null +++ b/ios/TurboModules/MxReload/MxReload.mm @@ -0,0 +1,28 @@ +#import "MxReload.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxReload + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (void)reload:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[ReloadHandler alloc] init] reload]; + resolve(nil); +} + +- (void)exitApp:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [[[ReloadHandler alloc] init] exitApp]; + resolve(nil); +} + +@end diff --git a/ios/TurboModules/MxSplashScreen/MxSplashScreen.h b/ios/TurboModules/MxSplashScreen/MxSplashScreen.h new file mode 100644 index 0000000..0bdb97b --- /dev/null +++ b/ios/TurboModules/MxSplashScreen/MxSplashScreen.h @@ -0,0 +1,4 @@ +#import + +@interface MxSplashScreen : NativeMxSplashScreenSpecBase +@end diff --git a/ios/TurboModules/MxSplashScreen/MxSplashScreen.mm b/ios/TurboModules/MxSplashScreen/MxSplashScreen.mm new file mode 100644 index 0000000..7808289 --- /dev/null +++ b/ios/TurboModules/MxSplashScreen/MxSplashScreen.mm @@ -0,0 +1,24 @@ +#import "MxSplashScreen.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxSplashScreen + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (void)show { + [[[MendixSplashScreen alloc] init] show]; +} + +- (void)hide { + [[[MendixSplashScreen alloc] init] hide]; +} + +@end diff --git a/ios/TurboModules/MxStorage/MxStorage.h b/ios/TurboModules/MxStorage/MxStorage.h new file mode 100644 index 0000000..5d66c3b --- /dev/null +++ b/ios/TurboModules/MxStorage/MxStorage.h @@ -0,0 +1,4 @@ +#import + +@interface MxStorage : NativeMxStorageSpecBase +@end diff --git a/ios/TurboModules/MxStorage/MxStorage.mm b/ios/TurboModules/MxStorage/MxStorage.mm new file mode 100644 index 0000000..fceb73c --- /dev/null +++ b/ios/TurboModules/MxStorage/MxStorage.mm @@ -0,0 +1,58 @@ +#import "MxStorage.h" +#import "RCTAppDelegate.h" +#import +#import "MendixNative-Swift.h" + +@implementation MxStorage + +RCT_EXPORT_MODULE() + +- (std::shared_ptr)getTurboModule: +(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +- (void)clearDatabases:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + // NOTE: Using self.bridge for OPSQLite module access + // TODO: Move to JavaScript orchestration - JavaScript can call OPSQLite TurboModule directly + // if it exposes deleteAllDBs() method. This would eliminate native module lookup entirely. + id opSQLiteModule = [self.bridge moduleForName:@"OPSQLite"]; + + if (!opSQLiteModule) { + reject(@"MODULE_NOT_FOUND", @"OPSQLiteModule not available", nil); + return; + } + + SEL deleteAllSelector = NSSelectorFromString(@"deleteAllDBs"); + if ([opSQLiteModule respondsToSelector:deleteAllSelector]) { + [opSQLiteModule performSelector:deleteAllSelector]; + resolve(nil); + } else { + reject(@"METHOD_NOT_FOUND", @"deleteAllDBs method not available on OPSQLite", nil); + } +} + +- (void)closeDatabaseConnections:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + // NOTE: Using self.bridge for OPSQLite module access + // TODO: Move to JavaScript orchestration - JavaScript can call OPSQLite TurboModule directly + // if it exposes closeAllConnections() method. This would eliminate native module lookup entirely. + id opSQLiteModule = [self.bridge moduleForName:@"OPSQLite"]; + + if (!opSQLiteModule) { + reject(@"MODULE_NOT_FOUND", @"OPSQLiteModule not available", nil); + return; + } + + SEL closeAllSelector = NSSelectorFromString(@"closeAllConnections"); + if ([opSQLiteModule respondsToSelector:closeAllSelector]) { + [opSQLiteModule performSelector:closeAllSelector]; + resolve(nil); + } else { + reject(@"METHOD_NOT_FOUND", @"closeAllConnections method not available on OPSQLite", nil); + } +} + +@end diff --git a/src/cookie.ts b/src/cookie.ts deleted file mode 100644 index c85345b..0000000 --- a/src/cookie.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Mx from './specs/NativeMendixNative'; - -export const NativeCookie = { - clearAll: Mx.cookieClearAll, -}; diff --git a/src/cookie/NativeMxCookie.ts b/src/cookie/NativeMxCookie.ts new file mode 100644 index 0000000..f778253 --- /dev/null +++ b/src/cookie/NativeMxCookie.ts @@ -0,0 +1,7 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { + clearAll(): Promise; +} + +export default TurboModuleRegistry.getEnforcing('MxCookie'); diff --git a/src/cookie/index.ts b/src/cookie/index.ts new file mode 100644 index 0000000..ff8e8c9 --- /dev/null +++ b/src/cookie/index.ts @@ -0,0 +1,5 @@ +import NativeMxCookie from './NativeMxCookie'; + +export const NativeCookie = { + clearAll: NativeMxCookie.clearAll, +}; diff --git a/src/dev-settings.ts b/src/dev-settings.ts new file mode 100644 index 0000000..7e10e94 --- /dev/null +++ b/src/dev-settings.ts @@ -0,0 +1,100 @@ +import { NativeModules, Platform } from 'react-native'; + +const NativeDevSettings = NativeModules.DevSettings; + +/** + * Controls debugging and development settings using React Native's built-in APIs. + * + * **Modern Architecture:** Calls React Native's DevSettings TurboModule directly + * without using the legacy RCTBridge, providing better performance and compatibility + * with React Native's New Architecture. + * + * In RN 0.83+, the deprecated `setIsDebuggingRemotely` API was removed and replaced + * with modern on-device debugging tools accessed through these methods. + */ +export const DevSettings = { + /** + * Opens the debugger (Chrome DevTools or Hermes debugger). + * In RN 0.83+, this replaces the deprecated `setIsDebuggingRemotely` API. + * + * Calls React Native's DevSettings TurboModule directly. + */ + openDebugger(): void { + if (!__DEV__) return; + + // Call React Native's DevSettings TurboModule directly (no bridge) + NativeDevSettings?.openDebugger?.(); + }, + + /** + * Toggles the element inspector overlay. + * + * Calls React Native's DevSettings TurboModule directly. + */ + toggleElementInspector(): void { + if (!__DEV__) return; + + // Call React Native's DevSettings TurboModule directly (no bridge) + NativeDevSettings?.toggleElementInspector?.(); + }, + + /** + * Reloads the JavaScript bundle. + * + * Calls React Native's DevSettings TurboModule directly. + * + * @param reason Optional reason for the reload + */ + reload(reason?: string): void { + if (!__DEV__) return; + + // Call React Native's DevSettings TurboModule directly (no bridge) + if (NativeDevSettings?.reloadWithReason) { + NativeDevSettings.reloadWithReason(reason ?? 'Manual reload from JS'); + } else if (NativeDevSettings?.reload) { + NativeDevSettings.reload(); + } + }, + + /** + * Controls hot reloading (Fast Refresh). + */ + setHotLoadingEnabled(enabled: boolean): void { + if (__DEV__ && NativeDevSettings?.setHotLoadingEnabled) { + NativeDevSettings.setHotLoadingEnabled(enabled); + } + }, + + /** + * Controls React profiling. + */ + setProfilingEnabled(enabled: boolean): void { + if (__DEV__ && NativeDevSettings?.setProfilingEnabled) { + NativeDevSettings.setProfilingEnabled(enabled); + } + }, + + /** + * Controls shake gesture for dev menu (iOS only). + */ + setShakeToShowDevMenuEnabled(enabled: boolean): void { + if ( + __DEV__ && + Platform.OS === 'ios' && + NativeDevSettings?.setIsShakeToShowDevMenuEnabled + ) { + NativeDevSettings.setIsShakeToShowDevMenuEnabled(enabled); + } + }, + + /** + * Adds a custom menu item to the dev menu. + */ + addMenuItem(title: string, _handler: () => void): void { + if (__DEV__ && NativeDevSettings?.addMenuItem) { + NativeDevSettings.addMenuItem(title); + // Note: Event listener setup would need NativeEventEmitter + // See DevSettings.js in react-native for full implementation + } + }, +}; diff --git a/src/download-handler.ts b/src/download-handler.ts deleted file mode 100644 index 2472bef..0000000 --- a/src/download-handler.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Mx, { type DownloadConfig } from './specs/NativeMendixNative'; - -export const NativeDownloadHandler = { - download: (url: string, downloadPath: string, config: DownloadConfig) => - Mx.downloadHandlerDownload(url, downloadPath, config), -}; diff --git a/src/download-handler/NativeMxDownload.ts b/src/download-handler/NativeMxDownload.ts new file mode 100644 index 0000000..9921e6d --- /dev/null +++ b/src/download-handler/NativeMxDownload.ts @@ -0,0 +1,30 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; +import type { CodegenTypes } from 'react-native'; + +type GenericType = + | string + | number + | boolean + | null + | undefined + | { [key: string]: GenericType } + | GenericType[]; + +type GenericMap = { [key: string]: GenericType }; + +type DownloadConfig = { + connectionTimeout?: CodegenTypes.Int32; + mimeType?: string; +}; + +export interface Spec extends TurboModule { + download( + url: string, + downloadPath: string, + config: GenericMap + ): Promise; +} + +export default TurboModuleRegistry.getEnforcing('MxDownload'); + +export type { GenericMap, DownloadConfig }; diff --git a/src/download-handler/index.ts b/src/download-handler/index.ts new file mode 100644 index 0000000..d2de2a1 --- /dev/null +++ b/src/download-handler/index.ts @@ -0,0 +1,9 @@ +import NativeMxDownload from './NativeMxDownload'; + +// Re-export DownloadConfig type for backward compatibility +export type { DownloadConfig } from './NativeMxDownload'; + +export const NativeDownloadHandler = { + download: (url: string, downloadPath: string, config: Record) => + NativeMxDownload.download(url, downloadPath, config), +}; diff --git a/src/encrypted-storage.ts b/src/encrypted-storage.ts index 23ef80d..ed528a3 100644 --- a/src/encrypted-storage.ts +++ b/src/encrypted-storage.ts @@ -1,9 +1,10 @@ -import Mx from './specs/NativeMendixNative'; +import NativeMxEncryption from './encryption/NativeMxEncryption'; +// Legacy API - uses new MxEncryption module under the hood export const RNMendixEncryptedStorage = { - getItem: Mx.encryptedStorageGetItem, - setItem: Mx.encryptedStorageSetItem, - removeItem: Mx.encryptedStorageRemoveItem, - clear: Mx.encryptedStorageClear, - IS_ENCRYPTED: Mx.encryptedStorageIsEncrypted(), //This one is constant and not a function hence invoked here + getItem: NativeMxEncryption.getItem, + setItem: NativeMxEncryption.setItem, + removeItem: NativeMxEncryption.removeItem, + clear: NativeMxEncryption.clear, + IS_ENCRYPTED: NativeMxEncryption.isEncrypted(), //This one is constant and not a function hence invoked here }; diff --git a/src/encryption/NativeMxEncryption.ts b/src/encryption/NativeMxEncryption.ts new file mode 100644 index 0000000..2603f48 --- /dev/null +++ b/src/encryption/NativeMxEncryption.ts @@ -0,0 +1,46 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +/** + * MxEncryption TurboModule + * + * Provides secure encrypted storage using platform-native keychains: + * - iOS: Uses Keychain Services + * - Android: Uses EncryptedSharedPreferences with AES256 + */ +export interface Spec extends TurboModule { + /** + * Store an encrypted key-value pair + * @param key The key to store under + * @param value The value to encrypt and store + * @returns Promise that resolves when stored + */ + setItem(key: string, value: string): Promise; + + /** + * Retrieve a decrypted value by key + * @param key The key to retrieve + * @returns Promise that resolves to the decrypted value, or null if not found + */ + getItem(key: string): Promise; + + /** + * Remove an encrypted key-value pair + * @param key The key to remove + * @returns Promise that resolves when removed + */ + removeItem(key: string): Promise; + + /** + * Clear all encrypted storage + * @returns Promise that resolves when cleared + */ + clear(): Promise; + + /** + * Check if storage is encrypted + * @returns true if encrypted, false otherwise (always true on iOS, may vary on Android) + */ + isEncrypted(): boolean; +} + +export default TurboModuleRegistry.getEnforcing('MxEncryption'); diff --git a/src/encryption/index.ts b/src/encryption/index.ts new file mode 100644 index 0000000..b801574 --- /dev/null +++ b/src/encryption/index.ts @@ -0,0 +1,74 @@ +import NativeMxEncryption from './NativeMxEncryption'; + +/** + * MxEncryption - Secure encrypted storage + * + * Provides a simple key-value store with automatic encryption using platform-native APIs: + * - iOS: Keychain Services + * - Android: EncryptedSharedPreferences (AES256-GCM) + * + * @example + * ```typescript + * import { MxEncryption } from 'mendix-native'; + * + * // Store encrypted data + * await MxEncryption.setItem('auth_token', 'secret123'); + * + * // Retrieve decrypted data + * const token = await MxEncryption.getItem('auth_token'); + * + * // Remove item + * await MxEncryption.removeItem('auth_token'); + * + * // Clear all encrypted storage + * await MxEncryption.clear(); + * + * // Check if encrypted + * const encrypted = MxEncryption.isEncrypted(); // true + * ``` + */ +export const MxEncryption = { + /** + * Store an encrypted key-value pair + * @param key The key to store under + * @param value The value to encrypt and store + * @returns Promise that resolves when stored + */ + async setItem(key: string, value: string): Promise { + return NativeMxEncryption.setItem(key, value); + }, + + /** + * Retrieve a decrypted value by key + * @param key The key to retrieve + * @returns Promise that resolves to the decrypted value, or null if not found + */ + async getItem(key: string): Promise { + return NativeMxEncryption.getItem(key); + }, + + /** + * Remove an encrypted key-value pair + * @param key The key to remove + * @returns Promise that resolves when removed + */ + async removeItem(key: string): Promise { + return NativeMxEncryption.removeItem(key); + }, + + /** + * Clear all encrypted storage + * @returns Promise that resolves when cleared + */ + async clear(): Promise { + return NativeMxEncryption.clear(); + }, + + /** + * Check if storage is encrypted + * @returns true if encrypted, false otherwise (always true on iOS, may vary on Android) + */ + isEncrypted(): boolean { + return NativeMxEncryption.isEncrypted(); + }, +}; diff --git a/src/error-handler.ts b/src/error-handler.ts deleted file mode 100644 index 5d62411..0000000 --- a/src/error-handler.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Mx from './specs/NativeMendixNative'; - -export const NativeErrorHandler = { - handle: Mx.errorHandlerHandle, -}; diff --git a/src/error/NativeMxError.ts b/src/error/NativeMxError.ts new file mode 100644 index 0000000..a2ec891 --- /dev/null +++ b/src/error/NativeMxError.ts @@ -0,0 +1,8 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; +import type { StackFrame } from 'stacktrace-parser'; + +export interface Spec extends TurboModule { + handle(message: string, stackTrace: StackFrame[]): void; +} + +export default TurboModuleRegistry.getEnforcing('MxError'); diff --git a/src/error/index.ts b/src/error/index.ts new file mode 100644 index 0000000..c7fade6 --- /dev/null +++ b/src/error/index.ts @@ -0,0 +1,5 @@ +import NativeMxError from './NativeMxError'; + +export const NativeErrorHandler = { + handle: NativeMxError.handle, +}; diff --git a/src/events.ts b/src/events.ts index 7b8bca7..b7c10d1 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,4 +1,5 @@ -import Mx from './specs/NativeMendixNative'; +import NativeMxReload from './reload-handler/NativeMxReload'; +import NativeMxOta from './ota/NativeMxOta'; -export const onReloadWithStateEvent = Mx.onReloadWithState; -export const onDownloadProgressEvent = Mx.onDownloadProgress; +export const onReloadWithStateEvent = NativeMxReload.onReloadWithState; +export const onDownloadProgressEvent = NativeMxOta.onDownloadProgress; diff --git a/src/file-system/NativeMxFileSystem.ts b/src/file-system/NativeMxFileSystem.ts new file mode 100644 index 0000000..bafbe87 --- /dev/null +++ b/src/file-system/NativeMxFileSystem.ts @@ -0,0 +1,41 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +type BlobData = { + blobId: string; + offset: number; + size: number; + name?: string; + type?: string; + lastModified?: number; +}; + +type GenericType = + | string + | number + | boolean + | null + | undefined + | { [key: string]: GenericType } + | GenericType[]; + +type GenericMap = { [key: string]: GenericType }; +type GenericArray = GenericType[]; + +export interface Spec extends TurboModule { + constants(): GenericMap; + save(blob: GenericMap, filePath: string): Promise; + read(filePath: string): Promise; + move(filePath: string, newPath: string): Promise; + remove(filePath: string): Promise; + list(dirPath: string): Promise; + readAsDataURL(filePath: string): Promise; + readAsText(filePath: string): Promise; + fileExists(filePath: string): Promise; + writeJson(data: GenericMap, filepath: string): Promise; + readJson(filepath: string): Promise; + setEncryptionEnabled(enabled: boolean): void; +} + +export default TurboModuleRegistry.getEnforcing('MxFileSystem'); + +export type { BlobData, GenericMap, GenericArray }; diff --git a/src/file-system.ts b/src/file-system/index.ts similarity index 55% rename from src/file-system.ts rename to src/file-system/index.ts index a0ec8a8..9106703 100644 --- a/src/file-system.ts +++ b/src/file-system/index.ts @@ -1,11 +1,11 @@ -import Mx, { type BlobData } from './specs/NativeMendixNative'; +import NativeMxFileSystem, { type BlobData } from './NativeMxFileSystem'; const initFs = () => { const { DocumentDirectoryPath, SUPPORTS_DIRECTORY_MOVE, SUPPORTS_ENCRYPTION, - } = Mx.fsConstants(); + } = NativeMxFileSystem.constants(); const docDirPath = DocumentDirectoryPath as string; return { //Constants @@ -14,20 +14,22 @@ const initFs = () => { SUPPORTS_ENCRYPTION: cast(SUPPORTS_ENCRYPTION), //Methods - signature matches with specs - read: Mx.fsRead, - list: Mx.fsList, - readAsDataURL: Mx.fsReadAsDataURL, - readAsText: Mx.fsReadAsText, //Android only - fileExists: Mx.fsFileExists, - move: Mx.fsMove, - remove: Mx.fsRemove, - setEncryptionEnabled: Mx.fsSetEncryptionEnabled, + read: NativeMxFileSystem.read, + list: NativeMxFileSystem.list, + readAsDataURL: NativeMxFileSystem.readAsDataURL, + readAsText: NativeMxFileSystem.readAsText, //Android only + fileExists: NativeMxFileSystem.fileExists, + move: NativeMxFileSystem.move, + remove: NativeMxFileSystem.remove, + setEncryptionEnabled: NativeMxFileSystem.setEncryptionEnabled, //Methods - signature modified since specs does not recognize Record and generics - save: (blob: BlobData, filePath: string) => Mx.fsSave(blob, filePath), + save: (blob: BlobData, filePath: string) => + NativeMxFileSystem.save(blob, filePath), writeJson: (data: Record, filepath: string) => - Mx.fsWriteJson(data, filepath), - readJson: (filepath: string) => Mx.fsReadJson(filepath) as Promise, + NativeMxFileSystem.writeJson(data, filepath), + readJson: (filepath: string) => + NativeMxFileSystem.readJson(filepath) as Promise, //Helpers relativeToDocumentsAbsolutePath: (path: string) => diff --git a/src/index.ts b/src/index.ts index 77b7b17..75523a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,8 @@ -export * from './splash-screen'; +// Modern TurboModule exports (Mx prefix) +export * from './encryption'; // MxEncryption +export * from './splash-screen'; // MxSplashScreen + +// Legacy exports (keep for backward compatibility) export * from './mx-configuration'; export * from './cookie'; export * from './events'; @@ -6,6 +10,8 @@ export * from './ota'; export * from './download-handler'; export * from './reload-handler'; export * from './encrypted-storage'; -export * from './error-handler'; +export * from './error'; export * from './file-system'; export * from './navigation-mode'; +export * from './dev-settings'; +export * from './storage'; diff --git a/src/mx-configuration.ts b/src/mx-configuration.ts deleted file mode 100644 index 4ff47c8..0000000 --- a/src/mx-configuration.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Mx from './specs/NativeMendixNative'; - -export const MxConfiguration = Mx.mxConfigurationGetConfig(); diff --git a/src/mx-configuration/NativeMxConfiguration.ts b/src/mx-configuration/NativeMxConfiguration.ts new file mode 100644 index 0000000..eb1e855 --- /dev/null +++ b/src/mx-configuration/NativeMxConfiguration.ts @@ -0,0 +1,31 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; +import type { CodegenTypes } from 'react-native'; + +type Configuration = { + RUNTIME_URL: string; + APP_NAME: string | null; + /** + * Do not use directly + * @deprecated + */ + FILES_DIRECTORY_NAME: string; + DATABASE_NAME: string; + WARNINGS_FILTER_LEVEL: string; + OTA_MANIFEST_PATH: string; + NATIVE_DEPENDENCIES?: { [key: string]: string }; + IS_DEVELOPER_APP?: boolean; + /** + * @deprecated + */ + CODE_PUSH_KEY?: string; + NATIVE_BINARY_VERSION?: CodegenTypes.Int32; + APP_SESSION_ID?: string; +}; + +export interface Spec extends TurboModule { + getConfig(): Configuration; +} + +export default TurboModuleRegistry.getEnforcing('MxConfiguration'); + +export type { Configuration }; diff --git a/src/mx-configuration/index.ts b/src/mx-configuration/index.ts new file mode 100644 index 0000000..59abeb5 --- /dev/null +++ b/src/mx-configuration/index.ts @@ -0,0 +1,3 @@ +import NativeMxConfiguration from './NativeMxConfiguration'; + +export const MxConfiguration = NativeMxConfiguration.getConfig(); diff --git a/src/navigation-mode.ts b/src/navigation-mode.ts deleted file mode 100644 index 5788ff4..0000000 --- a/src/navigation-mode.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Mx from './specs/NativeMendixNative'; - -export const AndroidNavigationBar = { - height: Mx.navigationModeGetNavigationBarHeight(), - isActive: Mx.navigationModeIsNavigationBarActive(), -}; diff --git a/src/navigation-mode/NativeMxNavigation.ts b/src/navigation-mode/NativeMxNavigation.ts new file mode 100644 index 0000000..eaa3dc3 --- /dev/null +++ b/src/navigation-mode/NativeMxNavigation.ts @@ -0,0 +1,9 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; +import type { CodegenTypes } from 'react-native'; + +export interface Spec extends TurboModule { + isNavigationBarActive(): boolean; + getNavigationBarHeight(): CodegenTypes.Double; +} + +export default TurboModuleRegistry.getEnforcing('MxNavigation'); diff --git a/src/navigation-mode/index.ts b/src/navigation-mode/index.ts new file mode 100644 index 0000000..04209dd --- /dev/null +++ b/src/navigation-mode/index.ts @@ -0,0 +1,6 @@ +import NativeMxNavigation from './NativeMxNavigation'; + +export const AndroidNavigationBar = { + height: NativeMxNavigation.getNavigationBarHeight(), + isActive: NativeMxNavigation.isNavigationBarActive(), +}; diff --git a/src/ota.ts b/src/ota.ts deleted file mode 100644 index 639a709..0000000 --- a/src/ota.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Mx, { - type OtaDeployConfig, - type OtaDownloadConfig, -} from './specs/NativeMendixNative'; - -export const NativeOta = { - download: (config: OtaDownloadConfig) => Mx.otaDownload(config), - deploy: (config: OtaDeployConfig) => Mx.otaDeploy(config), -}; diff --git a/src/ota/NativeMxOta.ts b/src/ota/NativeMxOta.ts new file mode 100644 index 0000000..486d662 --- /dev/null +++ b/src/ota/NativeMxOta.ts @@ -0,0 +1,48 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; +import type { CodegenTypes } from 'react-native'; + +type GenericType = + | string + | number + | boolean + | null + | undefined + | { [key: string]: GenericType } + | GenericType[]; + +type GenericMap = { [key: string]: GenericType }; + +type OtaDownloadConfig = { + url: string; +}; + +type OtaDeployConfig = { + otaDeploymentID: string; + otaPackage: string; + extractionDir: string; +}; + +type OtaDownloadResponse = { + otaPackage: string; +}; + +type DownloadProgress = { + receivedBytes: CodegenTypes.Double; + totalBytes: CodegenTypes.Double; +}; + +export interface Spec extends TurboModule { + download(config: GenericMap): Promise; + deploy(config: GenericMap): Promise; + readonly onDownloadProgress: CodegenTypes.EventEmitter; +} + +export default TurboModuleRegistry.getEnforcing('MxOta'); + +export type { + GenericMap, + OtaDownloadConfig, + OtaDeployConfig, + OtaDownloadResponse, + DownloadProgress, +}; diff --git a/src/ota/index.ts b/src/ota/index.ts new file mode 100644 index 0000000..f1435f9 --- /dev/null +++ b/src/ota/index.ts @@ -0,0 +1,13 @@ +import NativeMxOta from './NativeMxOta'; + +// Re-export types for backward compatibility +export type { + OtaDeployConfig, + OtaDownloadConfig, + OtaDownloadResponse, +} from './NativeMxOta'; + +export const NativeOta = { + download: (config: Record) => NativeMxOta.download(config), + deploy: (config: Record) => NativeMxOta.deploy(config), +}; diff --git a/src/reload-handler.ts b/src/reload-handler.ts deleted file mode 100644 index 889ed74..0000000 --- a/src/reload-handler.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Mx from './specs/NativeMendixNative'; - -export const NativeReloadHandler = { - reload: Mx.reloadHandlerReload, - exitApp: Mx.reloadHandlerExitApp, -}; diff --git a/src/reload-handler/NativeMxReload.ts b/src/reload-handler/NativeMxReload.ts new file mode 100644 index 0000000..673d08b --- /dev/null +++ b/src/reload-handler/NativeMxReload.ts @@ -0,0 +1,10 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; +import type { CodegenTypes } from 'react-native'; + +export interface Spec extends TurboModule { + reload(): Promise; + exitApp(): Promise; + readonly onReloadWithState: CodegenTypes.EventEmitter; +} + +export default TurboModuleRegistry.getEnforcing('MxReload'); diff --git a/src/reload-handler/index.ts b/src/reload-handler/index.ts new file mode 100644 index 0000000..ca324d9 --- /dev/null +++ b/src/reload-handler/index.ts @@ -0,0 +1,6 @@ +import NativeMxReload from './NativeMxReload'; + +export const NativeReloadHandler = { + reload: NativeMxReload.reload, + exitApp: NativeMxReload.exitApp, +}; diff --git a/src/specs/NativeMendixNative.ts b/src/specs/NativeMendixNative.ts deleted file mode 100644 index 37c256f..0000000 --- a/src/specs/NativeMendixNative.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { TurboModuleRegistry, type TurboModule } from 'react-native'; -import type { StackFrame } from 'stacktrace-parser'; -import type { CodegenTypes } from 'react-native'; - -export interface Spec extends TurboModule { - encryptedStorageSetItem(key: string, value: string): Promise; - encryptedStorageGetItem(key: string): Promise; - encryptedStorageRemoveItem(key: string): Promise; - encryptedStorageClear(): Promise; - encryptedStorageIsEncrypted(): boolean; - - splashScreenShow(): void; - splashScreenHide(): void; - - cookieClearAll(): Promise; - - reloadHandlerReload(): Promise; - reloadHandlerExitApp(): Promise; - - downloadHandlerDownload( - url: string, - downloadPath: string, - config: DownloadConfig - ): Promise; - - mxConfigurationGetConfig(): Configuration; - - otaDownload(config: OtaDownloadConfig): Promise; - otaDeploy(config: OtaDeployConfig): Promise; - - fsConstants(): FsConstants; - fsSave(blob: BlobData, filePath: string): Promise; - fsRead(filePath: string): Promise; - fsMove(filePath: string, newPath: string): Promise; - fsRemove(filePath: string): Promise; - fsList(dirPath: string): Promise; - fsReadAsDataURL(filePath: string): Promise; - fsReadAsText(filePath: string): Promise; //Android only - fsFileExists(filePath: string): Promise; - fsWriteJson(data: CodegenTypes.UnsafeObject, filepath: string): Promise; - fsReadJson(filepath: string): Promise; - fsSetEncryptionEnabled(enabled: boolean): void; - - errorHandlerHandle(message: string, stackTrace: StackFrame[]): void; - - navigationModeIsNavigationBarActive(): boolean; - navigationModeGetNavigationBarHeight(): CodegenTypes.Double; - - readonly onReloadWithState: CodegenTypes.EventEmitter; - readonly onDownloadProgress: CodegenTypes.EventEmitter; -} - -export default TurboModuleRegistry.getEnforcing('MendixNative'); - -// Codegen could not recognize types placed in other files hence placed here - -type BlobData = { - blobId: string; - offset: number; - size: number; - name?: string; - type?: string; - lastModified?: number; -}; - -type Configuration = { - RUNTIME_URL: string; - APP_NAME: string | null; - /** - * Do not use directly - * @deprecated - */ - FILES_DIRECTORY_NAME: string; - DATABASE_NAME: string; - WARNINGS_FILTER_LEVEL: string; - OTA_MANIFEST_PATH: string; - NATIVE_DEPENDENCIES?: { [key: string]: string }; - IS_DEVELOPER_APP?: boolean; - /** - * @deprecated - */ - CODE_PUSH_KEY?: string; - NATIVE_BINARY_VERSION?: CodegenTypes.Int32; - APP_SESSION_ID?: string; -}; - -type FsConstants = { - DocumentDirectoryPath: string; - SUPPORTS_DIRECTORY_MOVE: boolean; - SUPPORTS_ENCRYPTION: boolean; -}; - -type DownloadConfig = { - connectionTimeout?: CodegenTypes.Int32; - mimeType?: string; -}; - -type OtaDownloadConfig = { - url: string; -}; - -type OtaDeployConfig = { - otaDeploymentID: string; - otaPackage: string; - extractionDir: string; -}; - -type OtaDownloadResponse = { - otaPackage: string; -}; - -type DownloadProgress = { - receivedBytes: CodegenTypes.Double; - totalBytes: CodegenTypes.Double; -}; - -export type { - BlobData, - Configuration, - FsConstants, - DownloadConfig, - OtaDownloadConfig, - OtaDeployConfig, - OtaDownloadResponse, - DownloadProgress, -}; diff --git a/src/splash-screen.ts b/src/splash-screen.ts deleted file mode 100644 index 9204e49..0000000 --- a/src/splash-screen.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Mx from './specs/NativeMendixNative'; - -export const MendixSplashScreen = { - show: Mx.splashScreenShow, - hide: Mx.splashScreenHide, -}; diff --git a/src/splash-screen/NativeMxSplashScreen.ts b/src/splash-screen/NativeMxSplashScreen.ts new file mode 100644 index 0000000..000822a --- /dev/null +++ b/src/splash-screen/NativeMxSplashScreen.ts @@ -0,0 +1,22 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +/** + * MxSplashScreen TurboModule + * + * Controls the native splash screen display during app launch. + */ +export interface Spec extends TurboModule { + /** + * Show the splash screen + * Displays the native splash screen overlay + */ + show(): void; + + /** + * Hide the splash screen + * Removes the native splash screen overlay with animation + */ + hide(): void; +} + +export default TurboModuleRegistry.getEnforcing('MxSplashScreen'); diff --git a/src/splash-screen/index.ts b/src/splash-screen/index.ts new file mode 100644 index 0000000..b072ee5 --- /dev/null +++ b/src/splash-screen/index.ts @@ -0,0 +1,36 @@ +import NativeMxSplashScreen from './NativeMxSplashScreen'; + +/** + * MxSplashScreen - Native splash screen control + * + * Controls the display of the native splash screen during app launch. + * Typically used to hide the splash screen once the app is ready. + * + * @example + * ```typescript + * import { MxSplashScreen } from 'mendix-native'; + * + * // Show splash screen (usually called automatically on launch) + * MxSplashScreen.show(); + * + * // Hide splash screen when app is ready + * MxSplashScreen.hide(); + * ``` + */ +export const MxSplashScreen = { + /** + * Show the splash screen + * Displays the native splash screen overlay + */ + show(): void { + NativeMxSplashScreen.show(); + }, + + /** + * Hide the splash screen + * Removes the native splash screen overlay with animation + */ + hide(): void { + NativeMxSplashScreen.hide(); + }, +}; diff --git a/src/storage/NativeMxStorage.ts b/src/storage/NativeMxStorage.ts new file mode 100644 index 0000000..f127bd1 --- /dev/null +++ b/src/storage/NativeMxStorage.ts @@ -0,0 +1,8 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { + clearDatabases(): Promise; + closeDatabaseConnections(): Promise; +} + +export default TurboModuleRegistry.getEnforcing('MxStorage'); diff --git a/src/storage/index.ts b/src/storage/index.ts new file mode 100644 index 0000000..6062c57 --- /dev/null +++ b/src/storage/index.ts @@ -0,0 +1,97 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import NativeMxStorage from './NativeMxStorage'; + +/** + * Storage API for managing app data (AsyncStorage, SQLite databases). + * + * Provides a unified, type-safe interface for clearing and managing storage + * across the Mendix Native application. + * + * **Modern Architecture:** This API calls TurboModules directly without using + * the legacy RCTBridge, providing better performance and compatibility with + * React Native's New Architecture. + * + * @example + * ```typescript + * import { Storage } from 'mendix-native'; + * + * // Clear all storage + * await Storage.clearAll(); + * + * // Clear specific storage + * await Storage.clearAsyncStorage(); + * await Storage.clearDatabases(); + * ``` + */ +export const Storage = { + /** + * Clears all AsyncStorage data. + * + * This removes all key-value pairs stored via React Native's AsyncStorage. + * Calls the AsyncStorage TurboModule directly for optimal performance. + * + * @throws {Error} If AsyncStorage module is not available or clear fails + * @example + * ```typescript + * await Storage.clearAsyncStorage(); + * ``` + */ + async clearAsyncStorage(): Promise { + // Call AsyncStorage TurboModule directly (no bridge, no wrapper) + await AsyncStorage.clear(); + }, + + /** + * Deletes all SQLite databases. + * + * This removes all databases created by the op-sqlite module. + * Use with caution as this is irreversible. + * + * @throws {Error} If OPSQLite module is not available + * @example + * ```typescript + * await Storage.clearDatabases(); + * ``` + */ + async clearDatabases(): Promise { + return NativeMxStorage.clearDatabases(); + }, + + /** + * Closes all SQLite database connections. + * + * This gracefully closes all open database connections without deleting data. + * Useful before app termination or data clearing operations. + * + * @throws {Error} If OPSQLite module is not available + * @example + * ```typescript + * await Storage.closeDatabaseConnections(); + * ``` + */ + async closeDatabaseConnections(): Promise { + return NativeMxStorage.closeDatabaseConnections(); + }, + + /** + * Clears all app storage (AsyncStorage + SQLite databases). + * + * This is a convenience method that clears both AsyncStorage and databases + * in a single call. Operations are performed sequentially to ensure proper cleanup. + * + * JavaScript orchestrates the clearing process, calling each TurboModule directly + * for optimal performance. + * + * @throws {Error} If any storage module is not available + * @example + * ```typescript + * // Clear everything + * await Storage.clearAll(); + * ``` + */ + async clearAll(): Promise { + // JavaScript orchestrates - call each TurboModule directly + await this.clearAsyncStorage(); + await this.clearDatabases(); + }, +}; From ce52a85f1a66dc38a5f1e4f765d39abadb62fc03 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:38:23 +0530 Subject: [PATCH 02/23] test: add new tests --- example/__tests__/dev-settings.harness.ts | 21 +++ example/__tests__/download-handler.harness.ts | 47 +++++++ example/__tests__/encryption.harness.ts | 44 +++++++ example/__tests__/error.harness.ts | 24 ++++ example/__tests__/events.harness.ts | 32 +++++ example/__tests__/reload-handler.harness.ts | 14 ++ example/__tests__/storage.harness.ts | 122 ++++++++++++++++++ 7 files changed, 304 insertions(+) create mode 100644 example/__tests__/dev-settings.harness.ts create mode 100644 example/__tests__/download-handler.harness.ts create mode 100644 example/__tests__/encryption.harness.ts create mode 100644 example/__tests__/error.harness.ts create mode 100644 example/__tests__/events.harness.ts create mode 100644 example/__tests__/reload-handler.harness.ts create mode 100644 example/__tests__/storage.harness.ts diff --git a/example/__tests__/dev-settings.harness.ts b/example/__tests__/dev-settings.harness.ts new file mode 100644 index 0000000..52979ca --- /dev/null +++ b/example/__tests__/dev-settings.harness.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'react-native-harness'; +import { DevSettings } from 'mendix-native'; + +describe('DevSettings', () => { + test('safe toggle APIs can be invoked repeatedly', () => { + expect(DevSettings.setHotLoadingEnabled(true)).toBeUndefined(); + expect(DevSettings.setHotLoadingEnabled(false)).toBeUndefined(); + + expect(DevSettings.setProfilingEnabled(true)).toBeUndefined(); + expect(DevSettings.setProfilingEnabled(false)).toBeUndefined(); + + expect(DevSettings.setShakeToShowDevMenuEnabled(true)).toBeUndefined(); + expect(DevSettings.setShakeToShowDevMenuEnabled(false)).toBeUndefined(); + }); + + test('menu items can be registered without throwing', () => { + expect( + DevSettings.addMenuItem('Harness menu item', () => {}) + ).toBeUndefined(); + }); +}); diff --git a/example/__tests__/download-handler.harness.ts b/example/__tests__/download-handler.harness.ts new file mode 100644 index 0000000..8751d16 --- /dev/null +++ b/example/__tests__/download-handler.harness.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, test } from 'react-native-harness'; +import { NativeDownloadHandler, NativeFileSystem } from 'mendix-native'; + +const downloadPath = NativeFileSystem.relativeToDocumentsAbsolutePath( + 'downloads/invalid-url.txt' +); + +describe('NativeDownloadHandler', () => { + beforeEach(async () => { + try { + await NativeFileSystem.remove(downloadPath); + } catch { + // Cleanup is best-effort. + } + }); + + test('rejects malformed URLs without creating a destination file', async () => { + const config = { + connectionTimeout: 25, + mimeType: 'text/plain', + }; + + await expect( + NativeDownloadHandler.download( + '://definitely-invalid-url', + downloadPath, + config + ) + ).rejects.toBeDefined(); + + expect(await NativeFileSystem.fileExists(downloadPath)).toBe(false); + }); + + test('does not mutate the config object while rejecting invalid downloads', async () => { + const config = { + connectionTimeout: 10, + mimeType: 'application/json', + }; + const originalConfig = { ...config }; + + await expect( + NativeDownloadHandler.download('://still-invalid', downloadPath, config) + ).rejects.toBeDefined(); + + expect(config).toEqual(originalConfig); + }); +}); diff --git a/example/__tests__/encryption.harness.ts b/example/__tests__/encryption.harness.ts new file mode 100644 index 0000000..02c3c29 --- /dev/null +++ b/example/__tests__/encryption.harness.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, test } from 'react-native-harness'; +import { MxEncryption, RNMendixEncryptedStorage } from 'mendix-native'; + +describe('MxEncryption', () => { + beforeEach(async () => { + await MxEncryption.clear(); + }); + + test('stores and retrieves values through the modern API', async () => { + await MxEncryption.setItem('modern-key', 'modern-value'); + + await expect(MxEncryption.getItem('modern-key')).resolves.toBe( + 'modern-value' + ); + }); + + test('shares the same backing store as the legacy encrypted storage API', async () => { + await MxEncryption.setItem('shared-key', 'set-by-modern'); + await expect(RNMendixEncryptedStorage.getItem('shared-key')).resolves.toBe( + 'set-by-modern' + ); + + await RNMendixEncryptedStorage.setItem('shared-key', 'set-by-legacy'); + await expect(MxEncryption.getItem('shared-key')).resolves.toBe( + 'set-by-legacy' + ); + }); + + test('removeItem is visible across both exported wrappers', async () => { + await RNMendixEncryptedStorage.setItem('cross-remove-key', 'value'); + + await MxEncryption.removeItem('cross-remove-key'); + + await expect( + RNMendixEncryptedStorage.getItem('cross-remove-key') + ).resolves.toBe(null); + }); + + test('isEncrypted matches the legacy constant contract', () => { + expect(MxEncryption.isEncrypted()).toBe( + RNMendixEncryptedStorage.IS_ENCRYPTED + ); + }); +}); diff --git a/example/__tests__/error.harness.ts b/example/__tests__/error.harness.ts new file mode 100644 index 0000000..9ba408b --- /dev/null +++ b/example/__tests__/error.harness.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from 'react-native-harness'; +import { NativeErrorHandler } from 'mendix-native'; + +describe('NativeErrorHandler', () => { + test('accepts normalized stack frame payloads synchronously', () => { + const stackTrace = [ + { + column: 12, + file: 'example/__tests__/error.harness.ts', + lineNumber: 8, + methodName: 'accepts normalized stack frame payloads synchronously', + }, + ] as any; + + expect(() => { + const result = NativeErrorHandler.handle( + 'Harness error contract check', + stackTrace + ); + + expect(result).toBeUndefined(); + }).not.toThrow(); + }); +}); diff --git a/example/__tests__/events.harness.ts b/example/__tests__/events.harness.ts new file mode 100644 index 0000000..c71799f --- /dev/null +++ b/example/__tests__/events.harness.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from 'react-native-harness'; +import { onDownloadProgressEvent, onReloadWithStateEvent } from 'mendix-native'; + +describe('Module events', () => { + test('download progress exposes a removable subscription', () => { + let callbackCount = 0; + + const subscription = onDownloadProgressEvent(() => { + callbackCount += 1; + }); + + expect(subscription).toBeDefined(); + expect(typeof subscription.remove).toBe('function'); + expect(callbackCount).toBe(0); + + subscription.remove(); + }); + + test('reload state exposes a removable subscription', () => { + let callbackCount = 0; + + const subscription = onReloadWithStateEvent(() => { + callbackCount += 1; + }); + + expect(subscription).toBeDefined(); + expect(typeof subscription.remove).toBe('function'); + expect(callbackCount).toBe(0); + + subscription.remove(); + }); +}); diff --git a/example/__tests__/reload-handler.harness.ts b/example/__tests__/reload-handler.harness.ts new file mode 100644 index 0000000..f19887f --- /dev/null +++ b/example/__tests__/reload-handler.harness.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from 'react-native-harness'; +import { NativeReloadHandler, onReloadWithStateEvent } from 'mendix-native'; + +describe('NativeReloadHandler', () => { + test('exposes async control methods and a removable state listener', () => { + const subscription = onReloadWithStateEvent(() => {}); + + expect(typeof NativeReloadHandler.reload).toBe('function'); + expect(typeof NativeReloadHandler.exitApp).toBe('function'); + expect(typeof subscription.remove).toBe('function'); + + subscription.remove(); + }); +}); diff --git a/example/__tests__/storage.harness.ts b/example/__tests__/storage.harness.ts new file mode 100644 index 0000000..f78a3fc --- /dev/null +++ b/example/__tests__/storage.harness.ts @@ -0,0 +1,122 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { open } from '@op-engineering/op-sqlite'; +import { beforeEach, describe, expect, test } from 'react-native-harness'; +import { Storage } from 'mendix-native'; + +const DB_NAME = 'storage-harness.sqlite'; +const TABLE_NAME = 'storage_harness_records'; + +async function seedDatabase(): Promise { + const db = open({ name: DB_NAME }); + + await db.execute( + `CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (id INTEGER PRIMARY KEY NOT NULL, value TEXT NOT NULL)` + ); + await db.execute(`DELETE FROM ${TABLE_NAME}`); + await db.execute(`INSERT INTO ${TABLE_NAME} (value) VALUES (?)`, ['fixture']); + + db.close(); +} + +async function getRowCount(): Promise { + const db = open({ name: DB_NAME }); + const result = await db.execute( + `SELECT COUNT(*) AS count FROM ${TABLE_NAME}` + ); + const rowCount = Number(result.rows?.[0]?.count ?? 0); + + db.close(); + + return rowCount; +} + +async function tableExists(): Promise { + const db = open({ name: DB_NAME }); + const result = await db.execute( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", + [TABLE_NAME] + ); + const exists = result.rows.length > 0; + + db.close(); + + return exists; +} + +describe('Storage', () => { + beforeEach(async () => { + await AsyncStorage.clear(); + + try { + await Storage.closeDatabaseConnections(); + } catch { + // Ignore cleanup failures so the real test can surface the error. + } + + try { + await Storage.clearDatabases(); + } catch { + // Ignore cleanup failures so the real test can surface the error. + } + }); + + describe('clearAsyncStorage', () => { + test('removes previously persisted AsyncStorage keys', async () => { + await AsyncStorage.setItem('storage-harness:key-1', 'value-1'); + await AsyncStorage.setItem('storage-harness:key-2', 'value-2'); + + await Storage.clearAsyncStorage(); + + const values = await AsyncStorage.multiGet([ + 'storage-harness:key-1', + 'storage-harness:key-2', + ]); + + expect(values).toEqual([ + ['storage-harness:key-1', null], + ['storage-harness:key-2', null], + ]); + }); + }); + + describe('closeDatabaseConnections', () => { + test('closes active sqlite connections without deleting persisted data', async () => { + const db = open({ name: DB_NAME }); + + await db.execute( + `CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (id INTEGER PRIMARY KEY NOT NULL, value TEXT NOT NULL)` + ); + await db.execute(`DELETE FROM ${TABLE_NAME}`); + await db.execute(`INSERT INTO ${TABLE_NAME} (value) VALUES (?)`, [ + 'fixture', + ]); + + await Storage.closeDatabaseConnections(); + + expect(await getRowCount()).toBe(1); + }); + }); + + describe('clearDatabases', () => { + test('removes sqlite schema created through op-sqlite', async () => { + await seedDatabase(); + expect(await tableExists()).toBe(true); + + await Storage.clearDatabases(); + + expect(await tableExists()).toBe(false); + }); + }); + + describe('clearAll', () => { + test('clears AsyncStorage and sqlite state in one call', async () => { + await AsyncStorage.setItem('storage-harness:combined', 'present'); + await seedDatabase(); + + await Storage.clearAll(); + + expect(await AsyncStorage.getItem('storage-harness:combined')).toBe(null); + expect(await tableExists()).toBe(false); + }); + }); +}); From 083cfeb000f3e71d52ed93243708008fb2fa1dba Mon Sep 17 00:00:00 2001 From: vadymv-mendix Date: Fri, 8 May 2026 15:47:34 +0200 Subject: [PATCH 03/23] chore: update RN to v084 --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2bdd152..b8eae33 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "jest": "29.7.0", "lefthook": "2.1.3", "prettier": "3.4.2", - "react": "19.2.3", + "react": "19.2.4", "react-native": "0.84.1", "react-native-builder-bob": "0.40.18", "react-native-gesture-handler": "2.31.2", diff --git a/yarn.lock b/yarn.lock index cb221e4..8b8163b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9256,7 +9256,7 @@ __metadata: jest: "npm:29.7.0" lefthook: "npm:2.1.3" prettier: "npm:3.4.2" - react: "npm:19.2.3" + react: "npm:19.2.4" react-native: "npm:0.84.1" react-native-builder-bob: "npm:0.40.18" react-native-gesture-handler: "npm:2.31.2" From 5b6ca0329684ad0eb87777acb6d2e95ec8b90132 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Tue, 12 May 2026 15:23:37 +0530 Subject: [PATCH 04/23] refactor: streamline type definitions and remove unused code --- src/download-handler/NativeMxDownload.ts | 15 ++--------- src/download-handler/index.ts | 3 --- src/file-system/NativeMxFileSystem.ts | 32 +++++++++++------------- src/ota/NativeMxOta.ts | 16 ++---------- src/ota/index.ts | 14 ++++------- 5 files changed, 24 insertions(+), 56 deletions(-) diff --git a/src/download-handler/NativeMxDownload.ts b/src/download-handler/NativeMxDownload.ts index 9921e6d..6173c46 100644 --- a/src/download-handler/NativeMxDownload.ts +++ b/src/download-handler/NativeMxDownload.ts @@ -1,17 +1,6 @@ import { TurboModuleRegistry, type TurboModule } from 'react-native'; import type { CodegenTypes } from 'react-native'; -type GenericType = - | string - | number - | boolean - | null - | undefined - | { [key: string]: GenericType } - | GenericType[]; - -type GenericMap = { [key: string]: GenericType }; - type DownloadConfig = { connectionTimeout?: CodegenTypes.Int32; mimeType?: string; @@ -21,10 +10,10 @@ export interface Spec extends TurboModule { download( url: string, downloadPath: string, - config: GenericMap + config: DownloadConfig ): Promise; } export default TurboModuleRegistry.getEnforcing('MxDownload'); -export type { GenericMap, DownloadConfig }; +export type { DownloadConfig }; diff --git a/src/download-handler/index.ts b/src/download-handler/index.ts index d2de2a1..1dc9972 100644 --- a/src/download-handler/index.ts +++ b/src/download-handler/index.ts @@ -1,8 +1,5 @@ import NativeMxDownload from './NativeMxDownload'; -// Re-export DownloadConfig type for backward compatibility -export type { DownloadConfig } from './NativeMxDownload'; - export const NativeDownloadHandler = { download: (url: string, downloadPath: string, config: Record) => NativeMxDownload.download(url, downloadPath, config), diff --git a/src/file-system/NativeMxFileSystem.ts b/src/file-system/NativeMxFileSystem.ts index bafbe87..0505202 100644 --- a/src/file-system/NativeMxFileSystem.ts +++ b/src/file-system/NativeMxFileSystem.ts @@ -1,4 +1,8 @@ -import { TurboModuleRegistry, type TurboModule } from 'react-native'; +import { + TurboModuleRegistry, + type TurboModule, + type CodegenTypes, +} from 'react-native'; type BlobData = { blobId: string; @@ -9,21 +13,15 @@ type BlobData = { lastModified?: number; }; -type GenericType = - | string - | number - | boolean - | null - | undefined - | { [key: string]: GenericType } - | GenericType[]; - -type GenericMap = { [key: string]: GenericType }; -type GenericArray = GenericType[]; +type FsConstants = { + DocumentDirectoryPath: string; + SUPPORTS_DIRECTORY_MOVE: boolean; + SUPPORTS_ENCRYPTION: boolean; +}; export interface Spec extends TurboModule { - constants(): GenericMap; - save(blob: GenericMap, filePath: string): Promise; + constants(): FsConstants; + save(blob: BlobData, filePath: string): Promise; read(filePath: string): Promise; move(filePath: string, newPath: string): Promise; remove(filePath: string): Promise; @@ -31,11 +29,11 @@ export interface Spec extends TurboModule { readAsDataURL(filePath: string): Promise; readAsText(filePath: string): Promise; fileExists(filePath: string): Promise; - writeJson(data: GenericMap, filepath: string): Promise; - readJson(filepath: string): Promise; + writeJson(data: CodegenTypes.UnsafeObject, filepath: string): Promise; + readJson(filepath: string): Promise; setEncryptionEnabled(enabled: boolean): void; } export default TurboModuleRegistry.getEnforcing('MxFileSystem'); -export type { BlobData, GenericMap, GenericArray }; +export type { BlobData, FsConstants }; diff --git a/src/ota/NativeMxOta.ts b/src/ota/NativeMxOta.ts index 486d662..eb89732 100644 --- a/src/ota/NativeMxOta.ts +++ b/src/ota/NativeMxOta.ts @@ -1,17 +1,6 @@ import { TurboModuleRegistry, type TurboModule } from 'react-native'; import type { CodegenTypes } from 'react-native'; -type GenericType = - | string - | number - | boolean - | null - | undefined - | { [key: string]: GenericType } - | GenericType[]; - -type GenericMap = { [key: string]: GenericType }; - type OtaDownloadConfig = { url: string; }; @@ -32,15 +21,14 @@ type DownloadProgress = { }; export interface Spec extends TurboModule { - download(config: GenericMap): Promise; - deploy(config: GenericMap): Promise; + download(config: OtaDownloadConfig): Promise; + deploy(config: OtaDeployConfig): Promise; readonly onDownloadProgress: CodegenTypes.EventEmitter; } export default TurboModuleRegistry.getEnforcing('MxOta'); export type { - GenericMap, OtaDownloadConfig, OtaDeployConfig, OtaDownloadResponse, diff --git a/src/ota/index.ts b/src/ota/index.ts index f1435f9..4f9fefb 100644 --- a/src/ota/index.ts +++ b/src/ota/index.ts @@ -1,13 +1,9 @@ -import NativeMxOta from './NativeMxOta'; - -// Re-export types for backward compatibility -export type { - OtaDeployConfig, - OtaDownloadConfig, - OtaDownloadResponse, +import NativeMxOta, { + type OtaDeployConfig, + type OtaDownloadConfig, } from './NativeMxOta'; export const NativeOta = { - download: (config: Record) => NativeMxOta.download(config), - deploy: (config: Record) => NativeMxOta.deploy(config), + download: (config: OtaDownloadConfig) => NativeMxOta.download(config), + deploy: (config: OtaDeployConfig) => NativeMxOta.deploy(config), }; From 2b582de7b0a290521afe2035a77721a442ebb850 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Tue, 12 May 2026 15:28:31 +0530 Subject: [PATCH 05/23] chore: update hermes-engine and React-Core-prebuilt checksums in Podfile.lock --- example/ios/Podfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 2fb0a75..b271ea1 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2124,7 +2124,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: e97c19a5a442429d1988f182a1940fb08df514da - hermes-engine: 22eccaf19c439374e5cd56a3e71cf0ba6d19c2b4 + hermes-engine: a7179a4cd45fa3f8143712e52bd3c2d20b5274a0 MendixNative: 27efb5f8b34da919431b0ee534de0d552c58b259 op-sqlite: e9ef65bcf95a97863874cee87841425bb71c8396 OpenSSL-Universal: 9110d21982bb7e8b22a962b6db56a8aa805afde7 @@ -2136,7 +2136,7 @@ SPEC CHECKSUMS: React: 1ba7d364ade7d883a1ec055bfc3606f35fdee17b React-callinvoker: bc2a26f8d84fb01f003fc6de6c9337b64715f95b React-Core: 7840d3a80b43a95c5e80ef75146bd70925ebab0f - React-Core-prebuilt: 47c5816551bb8ab769edb186fa6b2290ad5fcbc5 + React-Core-prebuilt: a666604237ac1fb51754ee37234701cf197d851b React-CoreModules: 2eb010400b63b89e53a324ffb3c112e4c7c3ce42 React-cxxreact: a558e92199d26f145afa9e62c4233cf8e7950efe React-debug: 755200a6e7f5e6e0a40ff8d215493d43cce285fc @@ -2197,7 +2197,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: e96e93b493d8d86eeaee3e590ba0be53f6abe46f ReactCodegen: 797de5178718324c6eba3327b07f9a423fbd5787 ReactCommon: 07572bf9e687c8a52fbe4a3641e9e3a1a477c78e - ReactNativeDependencies: 8fb48ccd03b7da33b8d37ae401be080e71f782df + ReactNativeDependencies: 9ed9fcba1a22047730c895521de699bacbb90312 RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4 RNGestureHandler: b4f6de20b30325ee40b693f558ad02213201ce91 SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef From 8eab392438776f4835151026ca1eb6d5a4ebd317 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Tue, 12 May 2026 16:47:38 +0530 Subject: [PATCH 06/23] feat: add RN_HARNESS configuration for app registry and view flattening --- example/.harness/manifest.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 example/.harness/manifest.js diff --git a/example/.harness/manifest.js b/example/.harness/manifest.js new file mode 100644 index 0000000..2ea24cc --- /dev/null +++ b/example/.harness/manifest.js @@ -0,0 +1,4 @@ +global.RN_HARNESS = { + appRegistryComponentName: 'App', + disableViewFlattening: false, +}; From 4886110ebeec468026c1464bd1eed44e9c67ec67 Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Wed, 13 May 2026 17:36:26 +0530 Subject: [PATCH 07/23] chore: replace deprecated apis with modern --- .github/CI_DOCUMENTATION.md | 2 +- MendixNative.podspec | 4 +- android/build.gradle | 4 +- .../mendix/mendixnative/MendixInitializer.kt | 58 +-------- .../activity/MendixReactActivity.kt | 4 +- .../fragment/MendixReactFragment.kt | 4 +- .../mendix/mendixnative/react/ClearData.kt | 16 +-- .../com/mendix/mendixnative/react/CloseApp.kt | 2 +- .../mendix/mendixnative/react/ModuleHelper.kt | 24 +--- .../mendixnative/react/NativeErrorHandler.kt | 2 +- .../react/ToggleElementInspector.kt | 11 -- .../mendixnative/react/fs/NativeFsModule.kt | 7 +- .../mendixnative/react/menu/DevAppMenu.kt | 113 ---------------- .../com/mendixnative/MendixNativePackage.kt | 3 - .../mendixnative/storage/MxStorageModule.kt | 46 ------- .../src/main/res/layout/app_menu_layout.xml | 94 -------------- android/src/main/res/values/strings.xml | 17 --- example/__tests__/download-handler.harness.ts | 8 +- example/__tests__/storage.harness.ts | 122 ------------------ .../ios/MendixNativeExample/AppDelegate.swift | 40 +++++- example/ios/Podfile.lock | 2 +- example/src/App.tsx | 33 ++++- ios/Modules/Helper/DevHelper.swift | 6 +- ios/Modules/Helper/ReactAppProvider.swift | 45 ++----- ios/Modules/Helper/ReactHostHelper.h | 20 +++ ios/Modules/Helper/ReactHostHelper.mm | 66 ++++++++++ ios/Modules/Helper/UnsafeMxFunction.swift | 33 ----- .../NativeDownloadHandler.swift | 27 ++-- .../NativeFsModule/NativeFsModule.swift | 2 +- .../NativeOtaModule/NativeOtaModule.swift | 38 +++++- ios/Modules/ReactNative.swift | 2 +- ios/TurboModules/MxDownload/MxDownload.mm | 13 +- ios/TurboModules/MxOta/MxOta.mm | 19 ++- ios/TurboModules/MxReload/MxReload.h | 3 + ios/TurboModules/MxReload/MxReload.mm | 4 + ios/TurboModules/MxStorage/MxStorage.h | 4 - ios/TurboModules/MxStorage/MxStorage.mm | 58 --------- src/file-system/NativeMxFileSystem.ts | 2 +- src/index.ts | 1 - src/storage/NativeMxStorage.ts | 8 -- src/storage/index.ts | 97 -------------- 41 files changed, 278 insertions(+), 786 deletions(-) delete mode 100644 android/src/main/java/com/mendix/mendixnative/react/ToggleElementInspector.kt delete mode 100644 android/src/main/java/com/mendix/mendixnative/react/menu/DevAppMenu.kt delete mode 100644 android/src/main/java/com/mendixnative/storage/MxStorageModule.kt delete mode 100644 android/src/main/res/layout/app_menu_layout.xml delete mode 100644 android/src/main/res/values/strings.xml delete mode 100644 example/__tests__/storage.harness.ts create mode 100644 ios/Modules/Helper/ReactHostHelper.h create mode 100644 ios/Modules/Helper/ReactHostHelper.mm delete mode 100644 ios/Modules/Helper/UnsafeMxFunction.swift delete mode 100644 ios/TurboModules/MxStorage/MxStorage.h delete mode 100644 ios/TurboModules/MxStorage/MxStorage.mm delete mode 100644 src/storage/NativeMxStorage.ts delete mode 100644 src/storage/index.ts diff --git a/.github/CI_DOCUMENTATION.md b/.github/CI_DOCUMENTATION.md index da7baeb..677f51c 100644 --- a/.github/CI_DOCUMENTATION.md +++ b/.github/CI_DOCUMENTATION.md @@ -20,7 +20,7 @@ All workflows use standardized tooling versions to ensure consistency: | ---------------- | --------- | ----------------------------- | | **Node.js** | `24` | `.nvmrc` | | **Yarn** | `4.12.0` | `package.json#packageManager` | -| **React Native** | `0.83.4` | `package.json` | +| **React Native** | `0.84.1` | `package.json` | | **React** | `19.2.14` | `package.json` | | **TypeScript** | `5.9.3` | `package.json` | diff --git a/MendixNative.podspec b/MendixNative.podspec index 8ed9239..0328b65 100644 --- a/MendixNative.podspec +++ b/MendixNative.podspec @@ -14,8 +14,8 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/mendix/mendix-native.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" - s.public_header_files = "ios/**/*.h" - s.private_header_files = "ios/**/*.h" + s.public_header_files = "ios/Modules/Helper/ReactHostHelper.h" + s.private_header_files = "ios/TurboModules/**/*.h" s.dependency "SSZipArchive" s.dependency "RNCAsyncStorage" diff --git a/android/build.gradle b/android/build.gradle index 2a0193c..b7a6b0f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -36,8 +36,6 @@ android { } buildFeatures { - dataBinding true - viewBinding true buildConfig true } @@ -88,7 +86,7 @@ dependencies { kapt 'com.github.bumptech.glide:compiler:4.12.0' - api "com.facebook.react:react-android:0.83.4" + api "com.facebook.react:react-android:0.84.1" api project(':op-engineering_op-sqlite') api project(':react-native-async-storage_async-storage') api project(':react-native-gesture-handler') diff --git a/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt b/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt index 53274b6..362988d 100644 --- a/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt +++ b/android/src/main/java/com/mendix/mendixnative/MendixInitializer.kt @@ -3,9 +3,6 @@ package com.mendix.mendixnative import android.app.Activity import android.view.MotionEvent import com.facebook.react.ReactHost -import com.facebook.react.ReactInstanceEventListener -import com.facebook.react.ReactNativeHost -import com.facebook.react.bridge.ReactContext import com.facebook.react.common.ShakeDetector import com.facebook.react.devsupport.DevSupportManagerBase import com.facebook.react.devsupport.attachMendixSupportManagerShakeDetector @@ -20,25 +17,16 @@ import com.mendix.mendixnative.react.MxConfiguration import com.mendix.mendixnative.react.clearCachedReactNativeDevBundle import com.mendix.mendixnative.react.clearData import com.mendix.mendixnative.react.closeSqlDatabaseConnection -import com.mendix.mendixnative.react.toggleElementInspector -import com.mendix.mendixnative.react.NativeReloadHandler class MendixInitializer( private val context: Activity, private val reactHost: ReactHost, - private val reactNativeHost: ReactNativeHost, private val hasRNDeveloperSupport: Boolean = false, -) : ReactInstanceEventListener { +) { private var shakeDetector: ShakeDetector? = null private var devMenuTouchEventHandler: DevMenuTouchEventHandler? = null - fun onCreate( - mendixApp: MendixApp, - devAppMenuHandler: DevAppMenuHandler = object : DevAppMenuHandler { - override fun showDevAppMenu() {} - }, - clearData: Boolean, - ) { + fun onCreate(mendixApp: MendixApp, clearData: Boolean) { // Assign mendix xas id interceptor to okhttp CookieEncryption.init(this.context) if (CookieEncryption.isCookieEncryptionEnabled()) { @@ -54,33 +42,9 @@ class MendixInitializer( MxConfiguration.warningsFilter = mendixApp.warningsFilter // This is here to make sure that a clean host instance is initialised. - restartReactInstanceManager() + reactHost.invalidate() if (clearData) clearData(context.application) if (hasRNDeveloperSupport) setupDeveloperApp(runtimeUrl, mendixApp) - if (mendixApp.attachCustomDeveloperMenu) attachCustomDeveloperMenu(devAppMenuHandler) - } - - private fun restartReactInstanceManager() { - if (reactNativeHost.hasInstance()) reactNativeHost.clear() - // Pre-initialize reactInstanceManager to be available for other methods - if (reactNativeHost.hasInstance()) reactNativeHost.reactInstanceManager - } - - private fun attachCustomDeveloperMenu(devAppMenuHandler: DevAppMenuHandler) { - devMenuTouchEventHandler = - DevMenuTouchEventHandler(object : DevMenuTouchEventHandler.DevMenuTouchListener { - override fun onTap() { - reactNativeHost.reactApplicationContext()?.let { - NativeReloadHandler(it).reload() - } - } - - override fun onLongPress() { - devAppMenuHandler.showDevAppMenu() - } - }) - - attachShakeDetector(devAppMenuHandler) } fun onDestroy() { @@ -89,29 +53,18 @@ class MendixInitializer( if (hasRNDeveloperSupport) { AppPreferences(context.applicationContext).setElementInspector(false) - reactHost.removeReactInstanceEventListener(this) } - - // We need to clear the host to allow for reinitialization of the Native Modules - // Especially for when switching between apps - reactNativeHost.clear() + reactHost.invalidate() // We need to close all databases separately to avoid hitting a read only state exception // Databases need to close after we are done closing the react native host to avoid db locks - closeSqlDatabaseConnection(reactNativeHost.reactApplicationContext()) + closeSqlDatabaseConnection(reactHost.currentReactContext) } fun stopShakeDetector() { shakeDetector?.stop() } - override fun onReactContextInitialized(context: ReactContext) { - val preferences = AppPreferences(context) - if (preferences.isElementInspectorEnabled) { - toggleElementInspector(context) - } - } - fun dispatchTouchEvent(ev: MotionEvent?): Boolean { return devMenuTouchEventHandler?.handle(ev) ?: false } @@ -139,7 +92,6 @@ class MendixInitializer( preferences.setDevMode((mendixApp.showExtendedDevMenu)) clearCachedReactNativeDevBundle(context.application) - reactHost.addReactInstanceEventListener(this) } } 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 1e2b04a..d7651fd 100644 --- a/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt +++ b/android/src/main/java/com/mendix/mendixnative/activity/MendixReactActivity.kt @@ -31,8 +31,8 @@ open class MendixReactActivity : ReactActivity(), DevAppMenuHandler, LaunchScree ?: throw ClassCastException("Application needs to implement MendixApplication") mendixInitializer = - MendixInitializer(this, reactHost, reactNativeHost, mendixApplication.useDeveloperSupport) - mendixInitializer.onCreate(mendixApp!!, this, intent.getBooleanExtra(CLEAR_DATA, false)) + MendixInitializer(this, reactHost, mendixApplication.useDeveloperSupport) + mendixInitializer.onCreate(mendixApp!!, intent.getBooleanExtra(CLEAR_DATA, false)) super.onCreate(null) } 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 2d44156..3ed4782 100644 --- a/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt +++ b/android/src/main/java/com/mendix/mendixnative/fragment/MendixReactFragment.kt @@ -67,8 +67,8 @@ open class MendixReactFragment : ReactFragment(), MendixReactFragmentView { val hasRNDeveloperSupport = requireArguments().getBoolean(ARG_USE_DEVELOPER_SUPPORT, false) mendixInitializer = - MendixInitializer(requireActivity(), reactHost!!, reactNativeHost, hasRNDeveloperSupport).also { - it.onCreate(mendixApp!!, this, clearData) + MendixInitializer(requireActivity(), reactHost!!, hasRNDeveloperSupport).also { + it.onCreate(mendixApp!!, clearData) } super.onCreate(savedInstanceState) diff --git a/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt b/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt index 37b6a1b..6e654db 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/ClearData.kt @@ -5,7 +5,7 @@ import android.content.Context import android.util.Log import android.webkit.CookieManager import android.widget.Toast -import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactHost import com.facebook.react.bridge.ReactContext import com.facebook.react.modules.network.NetworkingModule import com.mendix.mendixnative.encryption.MendixEncryptedStorage @@ -34,9 +34,9 @@ fun clearData(applicationContext: Application) = clearCookies().also { fileBackend.deleteDirectory(applicationContext.filesDir) } -fun clearDataWithReactContext(applicationContext: Application, reactNativeHost: ReactNativeHost, cb: (success: Boolean) -> Unit) { +fun clearDataWithReactContext(applicationContext: Application, reactHost: ReactHost, cb: (success: Boolean) -> Unit) { clearCachedReactNativeDevBundle(applicationContext) - val reactContext = reactNativeHost.reactContext() + val reactContext = reactHost.currentReactContext val fileBackend = FileBackend(applicationContext) fileBackend.deleteDirectory(applicationContext.filesDir) val errorString = "Clearing %s failed. Please clear your data from the launch screen." @@ -57,7 +57,7 @@ fun clearDataWithReactContext(applicationContext: Application, reactNativeHost: reportError("database") } - if (!clearAsyncStorage(reactNativeHost)) { + if (!clearAsyncStorage(reactHost)) { reportError("async storage") } @@ -88,7 +88,7 @@ fun clearDataWithReactContext(applicationContext: Application, reactNativeHost: } fun deleteAppDatabaseAsync(reactContext: ReactContext?, cb: BooleanCallback) { - val opSQLiteModule = reactContext?.getNativeModule(OPSQLiteModule::class.java) + val opSQLiteModule = reactContext?.nativeModule(OPSQLiteModule.NAME) if (opSQLiteModule != null) { try { opSQLiteModule.deleteAllDBs() @@ -108,8 +108,8 @@ fun deleteAppDatabaseAsync(reactContext: ReactContext?, cb: BooleanCallback) { * Note: Previous implementation only checked module availability without clearing. * This now actually clears the storage using AsyncStorageModule.clear(). */ -fun clearAsyncStorage(reactNativeHost: ReactNativeHost): Boolean { - val asyncStorageModule = reactNativeHost.reactContext()?.getNativeModule(AsyncStorageModule::class.java) +fun clearAsyncStorage(reactHost: ReactHost): Boolean { + val asyncStorageModule = reactHost.nativeModule(AsyncStorageModule.NAME) if (asyncStorageModule != null) { try { // Clear AsyncStorage synchronously - clear() expects a callback but we're using fire-and-forget @@ -131,7 +131,7 @@ fun clearSecureStorage(context: Context?): Boolean = context?.let { MendixEncryptedStorage.getMendixEncryptedStorage(it).clear() } ?: false fun clearCookiesAsync(reactContext: ReactContext?, cb: (success: Boolean) -> Unit) { - val networkingModule = reactContext?.getNativeModule(NetworkingModule::class.java) + val networkingModule = reactContext?.nativeModule(NetworkingModule.NAME) if (networkingModule != null) { try { networkingModule.clearCookies { result -> diff --git a/android/src/main/java/com/mendix/mendixnative/react/CloseApp.kt b/android/src/main/java/com/mendix/mendixnative/react/CloseApp.kt index 2159c20..194d6d3 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/CloseApp.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/CloseApp.kt @@ -10,7 +10,7 @@ import com.op.sqlite.OPSQLiteModule * This is called during app shutdown to gracefully close database connections. */ fun closeSqlDatabaseConnection(reactContext: ReactContext?) { - val opSQLiteModule = reactContext?.getNativeModule(OPSQLiteModule::class.java) + val opSQLiteModule = reactContext?.nativeModule(OPSQLiteModule.NAME) if (opSQLiteModule != null) { try { opSQLiteModule.closeAllConnections() diff --git a/android/src/main/java/com/mendix/mendixnative/react/ModuleHelper.kt b/android/src/main/java/com/mendix/mendixnative/react/ModuleHelper.kt index b39c0f5..7068635 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/ModuleHelper.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/ModuleHelper.kt @@ -1,34 +1,24 @@ package com.mendix.mendixnative.react -import android.annotation.SuppressLint import android.util.Log import com.facebook.react.ReactApplication -import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactHost import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContext - -inline fun ReactContext.typeSafeNativeModule(): T? { +inline fun ReactContext.nativeModule(name: String): T? { return try { - getNativeModule(T::class.java) + getNativeModule(name) as? T } catch (e: Exception) { Log.e("ModuleAccess", "Error getting module ${T::class.simpleName}", e) null } } -inline fun ReactNativeHost.typeSafeNativeModule(): T? { - return reactContext()?.typeSafeNativeModule() -} - -fun ReactNativeHost.reactContext(): ReactContext? { - return reactInstanceManager.currentReactContext +inline fun ReactHost.nativeModule(name: String): T? { + return currentReactContext?.nativeModule(name) } -fun ReactNativeHost.reactApplicationContext(): ReactApplicationContext? { - val context = reactContext() - if (context is ReactApplicationContext) { - return context - } - return null +inline fun ReactApplicationContext.nativeModule(name: String): T? { + return (applicationContext as? ReactApplication)?.reactHost?.nativeModule(name) } 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 515ec0d..a370d21 100644 --- a/android/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.kt +++ b/android/src/main/java/com/mendix/mendixnative/react/NativeErrorHandler.kt @@ -13,7 +13,7 @@ import com.facebook.react.modules.core.ExceptionsManagerModule class NativeErrorHandler(val reactContext: ReactApplicationContext) { fun handle(message: String?, stackTrace: ReadableArray?) { // Use typed module access instead of generic typeSafeNativeModule - val exceptionsManagerModule = reactContext.getNativeModule(ExceptionsManagerModule::class.java) + val exceptionsManagerModule = reactContext.nativeModule(ExceptionsManagerModule.NAME) exceptionsManagerModule?.reportSoftException(message, stackTrace, 0.0) // Note: updateExceptionMessage is not available in RN 0.77.1+ diff --git a/android/src/main/java/com/mendix/mendixnative/react/ToggleElementInspector.kt b/android/src/main/java/com/mendix/mendixnative/react/ToggleElementInspector.kt deleted file mode 100644 index a22381e..0000000 --- a/android/src/main/java/com/mendix/mendixnative/react/ToggleElementInspector.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.mendix.mendixnative.react - -import com.facebook.react.ReactInstanceManager -import com.facebook.react.bridge.ReactContext -import com.facebook.react.modules.core.DeviceEventManagerModule - -@CopiedFrom(ReactInstanceManager::class) -fun toggleElementInspector(context: ReactContext?) { - context?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - ?.emit("toggleElementInspector", null) -} 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 5d522e8..15add2a 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 @@ -13,6 +13,7 @@ import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.ObjectMapper +import com.mendix.mendixnative.react.nativeModule import java.io.File import java.io.FileNotFoundException import java.io.IOException @@ -29,7 +30,7 @@ class NativeFsModule(private val reactContext: ReactApplicationContext) { } fun save(blob: ReadableMap, filePath: String, promise: Promise) { - val blobModule = reactContext.getNativeModule(BlobModule::class.java) + val blobModule = reactContext.nativeModule(BlobModule.NAME) val blobId: String = blob.getString("blobId") ?: run { promise.reject(ERROR_INVALID_BLOB, "The specified blob is invalid") return @@ -149,7 +150,7 @@ class NativeFsModule(private val reactContext: ReactApplicationContext) { fun readAsDataURL(filePath: String, promise: Promise) { try { val fileReaderModule = - reactContext.getNativeModule(FileReaderModule::class.java) + reactContext.nativeModule(FileReaderModule.NAME) fileReaderModule!!.readAsDataURL(read(ensureWhiteListedPath(filePath)), promise) } catch (_: FileNotFoundException) { promise.resolve(null) @@ -241,7 +242,7 @@ class NativeFsModule(private val reactContext: ReactApplicationContext) { private fun read(filePath: String): ReadableMap { val data = fileBackend.read(filePath) - val blobModule = reactContext.getNativeModule(BlobModule::class.java) + val blobModule = reactContext.nativeModule(BlobModule.NAME) val blob: WritableMap = WritableNativeMap() blob.putString("blobId", blobModule!!.store(data)) blob.putInt("offset", 0) diff --git a/android/src/main/java/com/mendix/mendixnative/react/menu/DevAppMenu.kt b/android/src/main/java/com/mendix/mendixnative/react/menu/DevAppMenu.kt deleted file mode 100644 index 51396fb..0000000 --- a/android/src/main/java/com/mendix/mendixnative/react/menu/DevAppMenu.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.mendix.mendixnative.react.menu - -import android.app.Activity -import android.app.AlertDialog -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.widget.Toast -import com.mendix.mendixnative.MendixApplication -import com.mendixnative.R -import com.mendixnative.databinding.AppMenuLayoutBinding -import com.mendix.mendixnative.config.AppPreferences -import com.mendix.mendixnative.react.clearDataWithReactContext -import com.mendix.mendixnative.react.reactContext -import com.mendix.mendixnative.react.toggleElementInspector - -class DevAppMenu( - val activity: Activity, - isDevModeEnabled: Boolean = false, - handleReload: () -> Unit, - onCloseProjectSelected: (() -> Unit)? = null -) : AppMenu { - private val dialog: AlertDialog - - init { - val preferences = AppPreferences(activity.applicationContext) - val binding = AppMenuLayoutBinding.inflate(LayoutInflater.from(activity)) - val view = binding.root - - binding.advancedSettingsButton - - dialog = AlertDialog.Builder(activity) - .setView(view) - .create() - - binding.advancedSettingsContainer.visibility = View.GONE - binding.advancedSettingsButton.visibility = visibleWhenDevModeEnabled(isDevModeEnabled) - binding.advancedSettingsButton.setOnClickListener { - binding.advancedSettingsContainer.visibility = - when (binding.advancedSettingsContainer.visibility) { - (View.GONE) -> View.VISIBLE - else -> View.GONE - } - } - - binding.remoteDebuggingButton.visibility = visibleWhenDevModeEnabled(isDevModeEnabled) - binding.remoteDebuggingButton.text = - activity.resources.getText(remoteDebugginButtonTextResource(preferences.isRemoteJSDebugEnabled)) - binding.remoteDebuggingButton.setOnClickListener { - preferences.setRemoteDebugging(!preferences.isRemoteJSDebugEnabled) - handleReload() - binding.remoteDebuggingButton.text = - activity.resources.getText(remoteDebugginButtonTextResource(preferences.isRemoteJSDebugEnabled)) - dialog.dismiss() - } - - binding.advancedClearData.setOnClickListener { - activity.runOnUiThread { - clearDataWithReactContext( - activity.application, - (activity.application as MendixApplication).reactNativeHost - ) { success: Boolean -> - if (success) { - activity.runOnUiThread { - handleReload() - } - } else { - Toast.makeText(activity, "Clearing data failed.", Toast.LENGTH_LONG).show() - } - } - } - dialog.dismiss() - } - - binding.elementInspectorButton.visibility = visibleWhenDevModeEnabled(isDevModeEnabled) - binding.elementInspectorButton.setOnClickListener { - preferences.setElementInspector(!preferences.isElementInspectorEnabled) - toggleElementInspector((activity.application as MendixApplication).reactNativeHost.reactContext()) - dialog.dismiss() - } - - binding.reloadButton.setOnClickListener { - handleReload() - dialog.dismiss() - } - - binding.closeButton.setOnClickListener { - dialog.dismiss() - onCloseProjectSelected?.invoke() - } - } - - override fun show() { - if (!activity.isDestroyed) { - dialog.show() - } else { - Log.d("DevAppMenu", "Attempted to show dialog in a destroyed activity") - } - } - - private fun visibleWhenDevModeEnabled(devModeEnabled: Boolean): Int = if (devModeEnabled) { - View.VISIBLE - } else { - View.GONE - } - - private fun remoteDebugginButtonTextResource(isRemoteJsDebugEnabled: Boolean): Int = - if (isRemoteJsDebugEnabled) { - R.string.dev_menu_disable_remote_debugging - } else { - R.string.dev_menu_enable_remote_debugging - } -} diff --git a/android/src/main/java/com/mendixnative/MendixNativePackage.kt b/android/src/main/java/com/mendixnative/MendixNativePackage.kt index e70de7b..9560089 100644 --- a/android/src/main/java/com/mendixnative/MendixNativePackage.kt +++ b/android/src/main/java/com/mendixnative/MendixNativePackage.kt @@ -16,7 +16,6 @@ import com.mendixnative.navigation.MxNavigationModule import com.mendixnative.ota.MxOtaModule import com.mendixnative.reload.MxReloadModule import com.mendixnative.splash.MxSplashScreenModule -import com.mendixnative.storage.MxStorageModule import java.util.HashMap class MendixNativePackage : BaseReactPackage() { @@ -32,7 +31,6 @@ class MendixNativePackage : BaseReactPackage() { module } MxFileSystemModule.NAME -> MxFileSystemModule(reactContext) - MxStorageModule.NAME -> MxStorageModule(reactContext) MxOtaModule.NAME -> MxOtaModule(reactContext) MxDownloadModule.NAME -> MxDownloadModule(reactContext) MxReloadModule.NAME -> MxReloadModule(reactContext) @@ -51,7 +49,6 @@ class MendixNativePackage : BaseReactPackage() { MxEncryptionModule.NAME, MxSplashScreenModule.NAME, MxFileSystemModule.NAME, - MxStorageModule.NAME, MxOtaModule.NAME, MxDownloadModule.NAME, MxReloadModule.NAME, diff --git a/android/src/main/java/com/mendixnative/storage/MxStorageModule.kt b/android/src/main/java/com/mendixnative/storage/MxStorageModule.kt deleted file mode 100644 index b726c9c..0000000 --- a/android/src/main/java/com/mendixnative/storage/MxStorageModule.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.mendixnative.storage - -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.module.annotations.ReactModule -import com.mendixnative.NativeMxStorageSpec -import com.op.sqlite.OPSQLiteModule - -@ReactModule(name = MxStorageModule.NAME) -class MxStorageModule(reactContext: ReactApplicationContext) : - NativeMxStorageSpec(reactContext) { - - override fun getName(): String = NAME - - override fun clearDatabases(promise: Promise) { - val opSQLiteModule = reactApplicationContext.getNativeModule(OPSQLiteModule::class.java) - if (opSQLiteModule != null) { - try { - opSQLiteModule.deleteAllDBs() - promise.resolve(null) - } catch (e: Exception) { - promise.reject("STORAGE_CLEAR_FAILED", "Failed to clear databases: ${e.message}", e) - } - } else { - promise.reject("MODULE_NOT_FOUND", "OPSQLiteModule not available") - } - } - - override fun closeDatabaseConnections(promise: Promise) { - val opSQLiteModule = reactApplicationContext.getNativeModule(OPSQLiteModule::class.java) - if (opSQLiteModule != null) { - try { - opSQLiteModule.closeAllConnections() - promise.resolve(null) - } catch (e: Exception) { - promise.reject("STORAGE_CLOSE_FAILED", "Failed to close database connections: ${e.message}", e) - } - } else { - promise.reject("MODULE_NOT_FOUND", "OPSQLiteModule not available") - } - } - - companion object { - const val NAME = "MxStorage" - } -} diff --git a/android/src/main/res/layout/app_menu_layout.xml b/android/src/main/res/layout/app_menu_layout.xml deleted file mode 100644 index ce948e6..0000000 --- a/android/src/main/res/layout/app_menu_layout.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - -