From e0a75fcd5a87f590693a7bf11925157171f4e262 Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Thu, 20 Jul 2023 14:41:46 -0700 Subject: [PATCH] Introduce SampleLegacyModule example in RNTester (#38539) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38539 We will use this example to: - Showcase legacy module support in the TurboModule system in RNTester - E2E test legacy module support in the TurboModule system Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D47529295 fbshipit-source-id: e882e7c13a3d68427d0bb7c1c3d73906d7cd7345 --- .../platform/android/SampleLegacyModule.java | 246 ++++++++++++++++++ .../react/uiapp/RNTesterApplication.java | 17 ++ .../uiapp/RNTesterReactHostDelegate.java | 17 ++ .../TurboModule/SampleLegacyModuleExample.js | 143 ++++++---- .../js/utils/RNTesterList.android.js | 4 + 5 files changed, 376 insertions(+), 51 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleLegacyModule.java diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleLegacyModule.java b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleLegacyModule.java new file mode 100644 index 00000000000000..461be38ad8a71d --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleLegacyModule.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.fbreact.specs; + +import android.app.Activity; +import android.util.DisplayMetrics; +import android.widget.Toast; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.module.annotations.ReactModule; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; + +@ReactModule(name = SampleLegacyModule.NAME) +public class SampleLegacyModule extends ReactContextBaseJavaModule { + + public static final String NAME = "SampleLegacyModule"; + + private static final String TAG = SampleLegacyModule.class.getName(); + private final ReactApplicationContext mContext; + private Toast mToast; + + public SampleLegacyModule(ReactApplicationContext context) { + super(context); + mContext = context; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public boolean getBool(boolean arg) { + log("getBool", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public double getEnum(double arg) { + log("getEnum", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public double getDouble(double arg) { + log("getDouble", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public int getInt(int arg) { + log("getInt", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public float getFloat(float arg) { + log("getFloat", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public double getObjectDouble(Double arg) { + log("getObjectDouble", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public Integer getObjectInteger(Integer arg) { + log("getObjectInteger", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public Float getObjectFloat(Float arg) { + log("getObjectFloat", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public String getString(String arg) { + log("getString", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public double getRootTag(double arg) { + log("getRootTag", arg, arg); + return arg; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod + public void voidFunc() { + log("voidFunc", "", ""); + return; + } + + // This function returns {@link WritableMap} instead of {@link Map} for backward compat with + // existing native modules that use this Writable* as return types or in events. {@link + // WritableMap} is modified in the Java side, and read (or consumed) on the C++ side. + // In the future, all native modules should ideally return an immutable Map + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public WritableMap getObject(ReadableMap arg) { + WritableNativeMap map = new WritableNativeMap(); + map.merge(arg); + log("getObject", arg, map); + return map; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public WritableMap getUnsafeObject(ReadableMap arg) { + WritableNativeMap map = new WritableNativeMap(); + map.merge(arg); + log("getUnsafeObject", arg, map); + return map; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public WritableMap getValue(double numberArg, String stringArg, ReadableMap mapArg) { + WritableMap map = new WritableNativeMap(); + map.putDouble("x", numberArg); + map.putString("y", stringArg); + WritableMap zMap = new WritableNativeMap(); + zMap.merge(mapArg); + map.putMap("z", zMap); + log( + "getValue", + MapBuilder.of("1-numberArg", numberArg, "2-stringArg", stringArg, "3-mapArg", mapArg), + map); + return map; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod + public void getValueWithCallback(final Callback callback) { + String result = "Value From Callback"; + log("Callback", "Return Time", result); + callback.invoke(result); + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod(isBlockingSynchronousMethod = true) + public WritableArray getArray(ReadableArray arg) { + if (arg == null || Arguments.toList(arg) == null) { + // Returning an empty array, since the super class always returns non-null + return new WritableNativeArray(); + } + WritableArray result = Arguments.makeNativeArray(Arguments.toList(arg)); + log("getArray", arg, result); + return result; + } + + @DoNotStrip + @SuppressWarnings("unused") + @ReactMethod + public void getValueWithPromise(boolean error, Promise promise) { + if (error) { + promise.reject( + "code 1", + "intentional promise rejection", + new Throwable("promise intentionally rejected")); + } else { + promise.resolve("result"); + } + } + + @Override + public final @Nullable Map getConstants() { + Map result = new HashMap<>(); + DisplayMetrics displayMetrics = new DisplayMetrics(); + Activity activity = mContext.getCurrentActivity(); + if (activity != null) { + result.put("const2", 390); + } + result.put("const1", true); + result.put("const3", "something"); + log("constantsToExport", "", result); + return result; + } + + private void log(String method, Object input, Object output) { + if (mToast != null) { + mToast.cancel(); + } + StringBuilder message = new StringBuilder("Method :"); + message + .append(method) + .append("\nInputs: ") + .append(input.toString()) + .append("\nOutputs: ") + .append(output.toString()); + mToast = Toast.makeText(mContext, message.toString(), Toast.LENGTH_LONG); + mToast.show(); + } + + public void invalidate() {} + + @Override + public String getName() { + return NAME; + } +} diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java index 1ce23983101170..7a2dd4930d902d 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java @@ -9,6 +9,7 @@ import android.app.Application; import androidx.annotation.NonNull; +import com.facebook.fbreact.specs.SampleLegacyModule; import com.facebook.fbreact.specs.SampleTurboModule; import com.facebook.react.JSEngineResolutionAlgorithm; import com.facebook.react.ReactApplication; @@ -79,6 +80,10 @@ public NativeModule getModule( return new SampleTurboModule(reactContext); } + if (SampleLegacyModule.NAME.equals(name)) { + return new SampleLegacyModule(reactContext); + } + return null; } @@ -101,6 +106,18 @@ public Map getReactModuleInfos() { false, // isCxxModule true // isTurboModule )); + + moduleInfos.put( + SampleLegacyModule.NAME, + new ReactModuleInfo( + SampleLegacyModule.NAME, + "SampleLegacyModule", + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + false // isTurboModule + )); } return moduleInfos; } diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterReactHostDelegate.java b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterReactHostDelegate.java index ef3eb92712d8f0..50e883382f8e9f 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterReactHostDelegate.java +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterReactHostDelegate.java @@ -10,6 +10,7 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.facebook.fbreact.specs.SampleLegacyModule; import com.facebook.fbreact.specs.SampleTurboModule; import com.facebook.react.JSEngineResolutionAlgorithm; import com.facebook.react.ReactPackage; @@ -111,6 +112,10 @@ public NativeModule getModule( return new SampleTurboModule(reactContext); } + if (SampleLegacyModule.NAME.equals(name)) { + return new SampleLegacyModule(reactContext); + } + return null; } @@ -133,6 +138,18 @@ public Map getReactModuleInfos() { false, // isCxxModule true // isTurboModule )); + + moduleInfos.put( + SampleLegacyModule.NAME, + new ReactModuleInfo( + SampleLegacyModule.NAME, + "SampleLegacyModule", + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + false // isTurboModule + )); } return moduleInfos; } diff --git a/packages/rn-tester/js/examples/TurboModule/SampleLegacyModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleLegacyModuleExample.js index 1be5e965ec6848..ffd7b846e74f72 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleLegacyModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleLegacyModuleExample.js @@ -18,6 +18,7 @@ import { Text, TouchableOpacity, View, + Platform, } from 'react-native'; import styles from './TurboModuleExampleCommon'; @@ -58,57 +59,97 @@ class SampleLegacyModuleExample extends React.Component<{||}, State> { // Add calls to methods in TurboModule here // $FlowFixMe[missing-local-annot] - _tests = { - voidFunc: () => getSampleLegacyModule()?.voidFunc(), - getBool: () => getSampleLegacyModule()?.getBool(true), - getEnum: () => getSampleLegacyModule()?.getEnum(1.0), - getNumber: () => getSampleLegacyModule()?.getNumber(99.95), - getFloat: () => getSampleLegacyModule()?.getNumber(99.95), - getInt: () => getSampleLegacyModule()?.getInt(99), - getLongLong: () => getSampleLegacyModule()?.getLongLong(99), - getUnsignedLongLong: () => getSampleLegacyModule()?.getUnsignedLongLong(99), - getNSInteger: () => getSampleLegacyModule()?.getNSInteger(99), - getNSUInteger: () => getSampleLegacyModule()?.getNSUInteger(99), - getArray: () => - getSampleLegacyModule()?.getArray([ - {a: 1, b: 'foo'}, - {a: 2, b: 'bar'}, - null, - ]), - getObject: () => - getSampleLegacyModule()?.getObject({a: 1, b: 'foo', c: null}), - getString: () => getSampleLegacyModule()?.getString('Hello'), - getNullString: () => getSampleLegacyModule()?.getString(null), - getNSNumber: () => getSampleLegacyModule()?.getNSNumber(20.0), - getUnsafeObject: () => - getSampleLegacyModule()?.getObject({a: 1, b: 'foo', c: null}), - getRootTag: () => getSampleLegacyModule()?.getRootTag(this.context), - getValue: () => - getSampleLegacyModule()?.getValue(5, 'test', {a: 1, b: 'foo'}), - callback: () => - getSampleLegacyModule()?.getValueWithCallback(callbackValue => - this._setResult('callback', callbackValue), - ), - promise: () => - getSampleLegacyModule() - ?.getValueWithPromise(false) - .then(valuePromise => this._setResult('promise', valuePromise)), - rejectPromise: () => - getSampleLegacyModule() - ?.getValueWithPromise(true) - .then(() => {}) - .catch(e => this._setResult('rejectPromise', e.message)), - // voidFuncThrows: () => getSampleLegacyModule()?.voidFuncThrows(), - // getObjectThrows: () => getSampleLegacyModule()?.getObjectThrows({}), - // promiseThrows: () => getSampleLegacyModule()?.promiseThrows(true), - // voidFuncAssert: () => getSampleLegacyModule()?.voidFuncAssert(), - // getObjectAssert: () => getSampleLegacyModule()?.getObjectAssert({}), - // promiseAssert: () => getSampleLegacyModule()?.promiseAssert(true), - getConstants: () => getSampleLegacyModule()?.getConstants(), - getConst1: () => getSampleLegacyModule()?.const1, - getConst2: () => getSampleLegacyModule()?.const2, - getConst3: () => getSampleLegacyModule()?.const3, - }; + _tests = + Platform.OS === 'ios' + ? { + voidFunc: () => getSampleLegacyModule()?.voidFunc(), + getBool: () => getSampleLegacyModule()?.getBool(true), + getEnum: () => getSampleLegacyModule()?.getEnum(1.0), + getNumber: () => getSampleLegacyModule()?.getNumber(99.95), + getFloat: () => getSampleLegacyModule()?.getNumber(99.95), + getInt: () => getSampleLegacyModule()?.getInt(99), + getLongLong: () => getSampleLegacyModule()?.getLongLong(99), + getUnsignedLongLong: () => + getSampleLegacyModule()?.getUnsignedLongLong(99), + getNSInteger: () => getSampleLegacyModule()?.getNSInteger(99), + getNSUInteger: () => getSampleLegacyModule()?.getNSUInteger(99), + getArray: () => + getSampleLegacyModule()?.getArray([ + {a: 1, b: 'foo'}, + {a: 2, b: 'bar'}, + null, + ]), + getObject: () => + getSampleLegacyModule()?.getObject({a: 1, b: 'foo', c: null}), + getString: () => getSampleLegacyModule()?.getString('Hello'), + getNullString: () => getSampleLegacyModule()?.getString(null), + getNSNumber: () => getSampleLegacyModule()?.getNSNumber(20.0), + getUnsafeObject: () => + getSampleLegacyModule()?.getObject({a: 1, b: 'foo', c: null}), + getRootTag: () => getSampleLegacyModule()?.getRootTag(this.context), + getValue: () => + getSampleLegacyModule()?.getValue(5, 'test', {a: 1, b: 'foo'}), + callback: () => + getSampleLegacyModule()?.getValueWithCallback(callbackValue => + this._setResult('callback', callbackValue), + ), + promise: () => + getSampleLegacyModule() + ?.getValueWithPromise(false) + .then(valuePromise => this._setResult('promise', valuePromise)), + rejectPromise: () => + getSampleLegacyModule() + ?.getValueWithPromise(true) + .then(() => {}) + .catch(e => this._setResult('rejectPromise', e.message)), + getConstants: () => getSampleLegacyModule()?.getConstants(), + getConst1: () => getSampleLegacyModule()?.const1, + getConst2: () => getSampleLegacyModule()?.const2, + getConst3: () => getSampleLegacyModule()?.const3, + } + : { + voidFunc: () => getSampleLegacyModule()?.voidFunc(), + getBool: () => getSampleLegacyModule()?.getBool(true), + getEnum: () => getSampleLegacyModule()?.getEnum(1.0), + getDouble: () => getSampleLegacyModule()?.getDouble(99.95), + getInt: () => getSampleLegacyModule()?.getInt(99), + getFloat: () => getSampleLegacyModule()?.getFloat(99.95), + getObjectDouble: () => + getSampleLegacyModule()?.getObjectDouble(99.95), + getObjectInteger: () => getSampleLegacyModule()?.getObjectInteger(99), + getObjectFloat: () => getSampleLegacyModule()?.getObjectFloat(99.95), + getString: () => getSampleLegacyModule()?.getString('Hello'), + getRootTag: () => getSampleLegacyModule()?.getRootTag(this.context), + getObject: () => + getSampleLegacyModule()?.getObject({a: 1, b: 'foo', c: null}), + getUnsafeObject: () => + getSampleLegacyModule()?.getObject({a: 1, b: 'foo', c: null}), + getValue: () => + getSampleLegacyModule()?.getValue(5, 'test', {a: 1, b: 'foo'}), + callback: () => + getSampleLegacyModule()?.getValueWithCallback(callbackValue => + this._setResult('callback', callbackValue), + ), + getArray: () => + getSampleLegacyModule()?.getArray([ + {a: 1, b: 'foo'}, + {a: 2, b: 'bar'}, + null, + ]), + promise: () => + getSampleLegacyModule() + ?.getValueWithPromise(false) + .then(valuePromise => this._setResult('promise', valuePromise)), + rejectPromise: () => + getSampleLegacyModule() + ?.getValueWithPromise(true) + .then(() => {}) + .catch(e => this._setResult('rejectPromise', e.message)), + getConstants: () => getSampleLegacyModule()?.getConstants(), + getConst1: () => getSampleLegacyModule()?.const1, + getConst2: () => getSampleLegacyModule()?.const2, + getConst3: () => getSampleLegacyModule()?.const3, + }; _setResult(name: string, result: mixed) { this.setState(({testResults}) => ({ diff --git a/packages/rn-tester/js/utils/RNTesterList.android.js b/packages/rn-tester/js/utils/RNTesterList.android.js index 429b19428a9b79..306dcfa41fa8f7 100644 --- a/packages/rn-tester/js/utils/RNTesterList.android.js +++ b/packages/rn-tester/js/utils/RNTesterList.android.js @@ -307,6 +307,10 @@ const APIs: Array = ([ category: 'Basic', module: require('../examples/TurboModule/TurboModuleExample'), }, + { + key: 'LegacyModuleExample', + module: require('../examples/TurboModule/LegacyModuleExample'), + }, { key: 'TurboCxxModuleExample', category: 'Basic',