diff --git a/CodePush.podspec b/CodePush.podspec index d4b845d66..fc1cf8f95 100644 --- a/CodePush.podspec +++ b/CodePush.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = '15.5' s.preserve_paths = '*.js' s.library = 'z' - s.source_files = 'ios/CodePush/*.{h,m}' + s.source_files = 'ios/CodePush/*.{h,m,mm}' s.public_header_files = ['ios/CodePush/CodePush.h'] # Note: Even though there are copy/pasted versions of some of these dependencies in the repo, @@ -22,4 +22,17 @@ Pod::Spec.new do |s| # linked properly at a parent workspace level. s.dependency 'React-Core' s.dependency 'SSZipArchive', '~> 2.5.5' + + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' + s.compiler_flags = '-DRCT_NEW_ARCH_ENABLED=1' + s.pod_target_xcconfig = { + 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17', + 'HEADER_SEARCH_PATHS' => "\"$(PODS_ROOT)/Headers/Public/ReactCodegen\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCodegen/ReactCodegen.framework/Headers\"" + } + + s.dependency 'ReactCodegen' + s.dependency 'RCTRequired' + s.dependency 'RCTTypeSafety' + s.dependency 'ReactCommon/turbomodule/core' + end end diff --git a/Examples/RN0773/ios/Podfile.lock b/Examples/RN0773/ios/Podfile.lock index 4c45f9fee..37dc8744b 100644 --- a/Examples/RN0773/ios/Podfile.lock +++ b/Examples/RN0773/ios/Podfile.lock @@ -1,7 +1,11 @@ PODS: - boost (1.84.0) - CodePush (12.3.2): + - RCTRequired + - RCTTypeSafety - React-Core + - ReactCodegen + - ReactCommon/turbomodule/core - SSZipArchive (~> 2.5.5) - DoubleConversion (1.1.6) - fast_float (6.1.4) @@ -1790,7 +1794,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - CodePush: 65689ae1c412f20483f6488ed373e2688b6cf19d + CodePush: b744a6558bd51db38bdfcb2e2df71b66750f10fc DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 23d8c5470c648a635893dc0956c6dbaead54b656 diff --git a/Examples/RN0783/ios/Podfile.lock b/Examples/RN0783/ios/Podfile.lock index d826dc40b..ce79ee1a5 100644 --- a/Examples/RN0783/ios/Podfile.lock +++ b/Examples/RN0783/ios/Podfile.lock @@ -1,7 +1,11 @@ PODS: - boost (1.84.0) - CodePush (12.3.2): + - RCTRequired + - RCTTypeSafety - React-Core + - ReactCodegen + - ReactCommon/turbomodule/core - SSZipArchive (~> 2.5.5) - DoubleConversion (1.1.6) - fast_float (6.1.4) @@ -1816,7 +1820,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - CodePush: 65689ae1c412f20483f6488ed373e2688b6cf19d + CodePush: b744a6558bd51db38bdfcb2e2df71b66750f10fc DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e053802577a711add20e45bbbf5dd1180b6ca62e diff --git a/Examples/RN0797/ios/Podfile.lock b/Examples/RN0797/ios/Podfile.lock index 04f983171..047726d50 100644 --- a/Examples/RN0797/ios/Podfile.lock +++ b/Examples/RN0797/ios/Podfile.lock @@ -1,7 +1,11 @@ PODS: - boost (1.84.0) - CodePush (12.3.2): + - RCTRequired + - RCTTypeSafety - React-Core + - ReactCodegen + - ReactCommon/turbomodule/core - SSZipArchive (~> 2.5.5) - DoubleConversion (1.1.6) - fast_float (6.1.4) @@ -1966,7 +1970,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - CodePush: 65689ae1c412f20483f6488ed373e2688b6cf19d + CodePush: b744a6558bd51db38bdfcb2e2df71b66750f10fc DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: b60fe06f0f15b7d7408f169442176e69e8eeacde diff --git a/Examples/RN0803/ios/Podfile.lock b/Examples/RN0803/ios/Podfile.lock index c91b52ed8..02cc64151 100644 --- a/Examples/RN0803/ios/Podfile.lock +++ b/Examples/RN0803/ios/Podfile.lock @@ -1,7 +1,11 @@ PODS: - boost (1.84.0) - CodePush (12.3.2): + - RCTRequired + - RCTTypeSafety - React-Core + - ReactCodegen + - ReactCommon/turbomodule/core - SSZipArchive (~> 2.5.5) - DoubleConversion (1.1.6) - fast_float (8.0.0) @@ -2455,7 +2459,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - CodePush: 65689ae1c412f20483f6488ed373e2688b6cf19d + CodePush: b744a6558bd51db38bdfcb2e2df71b66750f10fc DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: fa64fc271e55ebd155a9ac8bec7450b236b43702 diff --git a/Examples/RN0816/ios/Podfile.lock b/Examples/RN0816/ios/Podfile.lock index 937607975..c7af23e8f 100644 --- a/Examples/RN0816/ios/Podfile.lock +++ b/Examples/RN0816/ios/Podfile.lock @@ -1,6 +1,10 @@ PODS: - CodePush (12.3.2): + - RCTRequired + - RCTTypeSafety - React-Core + - ReactCodegen + - ReactCommon/turbomodule/core - SSZipArchive (~> 2.5.5) - FBLazyVector (0.81.6) - hermes-engine (0.81.6): @@ -1925,7 +1929,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - CodePush: 65689ae1c412f20483f6488ed373e2688b6cf19d + CodePush: b744a6558bd51db38bdfcb2e2df71b66750f10fc FBLazyVector: 14ce6e3675cacb2683ad30272f04274a4ee5b67d hermes-engine: 7219f6e751ad6ec7f3d7ec121830ee34dae40749 RCTDeprecation: ff38238d8b6ddfe1fcfeb2718d1c14da9564c1c3 diff --git a/Examples/RN0821/ios/Podfile.lock b/Examples/RN0821/ios/Podfile.lock index f42f49291..15bb1c18d 100644 --- a/Examples/RN0821/ios/Podfile.lock +++ b/Examples/RN0821/ios/Podfile.lock @@ -1,6 +1,10 @@ PODS: - CodePush (12.3.2): + - RCTRequired + - RCTTypeSafety - React-Core + - ReactCodegen + - ReactCommon/turbomodule/core - SSZipArchive (~> 2.5.5) - FBLazyVector (0.82.1) - hermes-engine (0.82.1): @@ -1981,7 +1985,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - CodePush: 65689ae1c412f20483f6488ed373e2688b6cf19d + CodePush: b744a6558bd51db38bdfcb2e2df71b66750f10fc FBLazyVector: 2e5b5553df729e080483373db6f045201ff4e6db hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5 RCTDeprecation: c6b36da89aa26090c8684d29c2868dcca2cd4554 diff --git a/Examples/RN0832/ios/Podfile.lock b/Examples/RN0832/ios/Podfile.lock index d0adf701a..6c2706c9a 100644 --- a/Examples/RN0832/ios/Podfile.lock +++ b/Examples/RN0832/ios/Podfile.lock @@ -1,6 +1,10 @@ PODS: - CodePush (12.3.2): + - RCTRequired + - RCTTypeSafety - React-Core + - ReactCodegen + - ReactCommon/turbomodule/core - SSZipArchive (~> 2.5.5) - FBLazyVector (0.83.2) - hermes-engine (0.14.1): @@ -2091,7 +2095,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - CodePush: 65689ae1c412f20483f6488ed373e2688b6cf19d + CodePush: b744a6558bd51db38bdfcb2e2df71b66750f10fc FBLazyVector: 32e9ed0301d0fcbc1b2b341dd7fcbf291f51eb83 hermes-engine: ac0840c5a51f4cb98852796768c7e0bdb1d0dedc RCTDeprecation: a522c536d2c7be8f518dd834883cf6dce1d4f545 diff --git a/Examples/RN0840/ios/Podfile.lock b/Examples/RN0840/ios/Podfile.lock index 74f2264b1..735ba147b 100644 --- a/Examples/RN0840/ios/Podfile.lock +++ b/Examples/RN0840/ios/Podfile.lock @@ -1,6 +1,10 @@ PODS: - CodePush (12.3.2): + - RCTRequired + - RCTTypeSafety - React-Core + - ReactCodegen + - ReactCommon/turbomodule/core - SSZipArchive (~> 2.5.5) - FBLazyVector (0.84.0) - hermes-engine (250829098.0.7): @@ -2096,7 +2100,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - CodePush: 65689ae1c412f20483f6488ed373e2688b6cf19d + CodePush: b744a6558bd51db38bdfcb2e2df71b66750f10fc FBLazyVector: c12d2108050e27952983d565a232f6f7b1ad5e69 hermes-engine: e2f00b99993f05b6eebd17e9e0de1f490dbf4f19 RCTDeprecation: 3280799c14232a56e5a44f92981a8ee33bc69fd9 diff --git a/android/app/build.gradle b/android/app/build.gradle index 49b72dcca..a04ddb52b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,10 +1,17 @@ apply plugin: "com.android.library" +apply plugin: "com.facebook.react" def DEFAULT_COMPILE_SDK_VERSION = 26 def DEFAULT_BUILD_TOOLS_VERSION = "26.0.3" def DEFAULT_TARGET_SDK_VERSION = 26 def DEFAULT_MIN_SDK_VERSION = 16 +react { + jsRootDir = file("../..") + libraryName = "RNCodePushSpec" + codegenJavaPackageName = "com.microsoft.codepush.react" +} + android { namespace "com.microsoft.codepush.react" diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java index 5c0066f21..2e91f464b 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java @@ -4,18 +4,25 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import com.facebook.react.ReactPackage; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.facebook.react.TurboReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.uimanager.ViewManager; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; -public class CodePush implements ReactPackage { +public class CodePush extends TurboReactPackage { private static boolean sIsRunningBinaryVersion = false; private static boolean sNeedToReportRollback = false; @@ -300,17 +307,45 @@ public void clearUpdates() { } @Override - public List createNativeModules(ReactApplicationContext reactApplicationContext) { - CodePushNativeModule codePushModule = new CodePushNativeModule(reactApplicationContext, this, mUpdateManager, mTelemetryManager, mSettingsManager); - CodePushDialog dialogModule = new CodePushDialog(reactApplicationContext); + public @Nullable NativeModule getModule(String name, ReactApplicationContext reactApplicationContext) { + if (!CodePushNativeModule.NAME.equals(name)) { + return null; + } + + return new CodePushNativeModule( + reactApplicationContext, + this, + mUpdateManager, + mTelemetryManager, + mSettingsManager + ); + } - List nativeModules = new ArrayList<>(); - nativeModules.add(codePushModule); - nativeModules.add(dialogModule); - return nativeModules; + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return new ReactModuleInfoProvider() { + @Override + public Map getReactModuleInfos() { + Map reactModuleInfoMap = new HashMap<>(); + reactModuleInfoMap.put( + CodePushNativeModule.NAME, + new ReactModuleInfo( + CodePushNativeModule.NAME, + CodePushNativeModule.class.getName(), + false, + false, + false, + false, + true + ) + ); + return reactModuleInfoMap; + } + }; } + @Override - public List createViewManagers(ReactApplicationContext reactApplicationContext) { + public List createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) { return new ArrayList<>(); } } diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java deleted file mode 100644 index f1cecddb0..000000000 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.microsoft.codepush.react; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; - -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; - -public class CodePushDialog extends ReactContextBaseJavaModule{ - - public CodePushDialog(ReactApplicationContext reactContext) { - super(reactContext); - } - - @ReactMethod - public void showDialog(final String title, final String message, final String button1Text, - final String button2Text, final Callback successCallback, Callback errorCallback) { - Activity currentActivity = getCurrentActivity(); - if (currentActivity == null || currentActivity.isFinishing()) { - // If getCurrentActivity is null, it could be because the app is backgrounded, - // so we show the dialog when the app resumes) - getReactApplicationContext().addLifecycleEventListener(new LifecycleEventListener() { - @Override - public void onHostResume() { - Activity currentActivity = getCurrentActivity(); - if (currentActivity != null) { - getReactApplicationContext().removeLifecycleEventListener(this); - showDialogInternal(title, message, button1Text, button2Text, successCallback, currentActivity); - } - } - - @Override - public void onHostPause() { - - } - - @Override - public void onHostDestroy() { - - } - }); - } else { - showDialogInternal(title, message, button1Text, button2Text, successCallback, currentActivity); - } - } - - private void showDialogInternal(String title, String message, String button1Text, - String button2Text, final Callback successCallback, Activity currentActivity) { - AlertDialog.Builder builder = new AlertDialog.Builder(currentActivity); - - builder.setCancelable(false); - - DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - try { - dialog.cancel(); - switch (which) { - case DialogInterface.BUTTON_POSITIVE: - successCallback.invoke(0); - break; - case DialogInterface.BUTTON_NEGATIVE: - successCallback.invoke(1); - break; - default: - throw new CodePushUnknownException("Unknown button ID pressed."); - } - } catch (Throwable e) { - CodePushUtils.log(e); - } - } - }; - - if (title != null) { - builder.setTitle(title); - } - - if (message != null) { - builder.setMessage(message); - } - - if (button1Text != null) { - builder.setPositiveButton(button1Text, clickListener); - } - - if (button2Text != null) { - builder.setNegativeButton(button2Text, clickListener); - } - - AlertDialog dialog = builder.create(); - dialog.show(); - } - - @Override - public String getName() { - return "CodePushDialog"; - } -} diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java index 54e303a2c..e363735bb 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java @@ -2,7 +2,6 @@ import android.app.Activity; import android.content.SharedPreferences; -import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.view.View; @@ -20,11 +19,11 @@ import com.facebook.react.bridge.LifecycleEventListener; 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.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.annotations.UnstableReactNativeAPI; +import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.ReactChoreographer; import com.facebook.react.runtime.ReactHostDelegate; @@ -37,15 +36,19 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@ReactModule(name = CodePushNativeModule.NAME) +public class CodePushNativeModule extends NativeCodePushSpec { + public static final String NAME = "CodePush"; -public class CodePushNativeModule extends ReactContextBaseJavaModule { private String mBinaryContentsHash = null; private String mClientUniqueId = null; private LifecycleEventListener mLifecycleEventListener = null; private int mMinimumBackgroundDuration = 0; + private final ExecutorService mBackgroundExecutor = Executors.newSingleThreadExecutor(); private CodePush mCodePush; private SettingsManager mSettingsManager; @@ -76,24 +79,15 @@ public CodePushNativeModule(ReactApplicationContext reactContext, CodePush codeP } @Override - public Map getConstants() { - final Map constants = new HashMap<>(); - - constants.put("codePushInstallModeImmediate", CodePushInstallMode.IMMEDIATE.getValue()); - constants.put("codePushInstallModeOnNextRestart", CodePushInstallMode.ON_NEXT_RESTART.getValue()); - constants.put("codePushInstallModeOnNextResume", CodePushInstallMode.ON_NEXT_RESUME.getValue()); - constants.put("codePushInstallModeOnNextSuspend", CodePushInstallMode.ON_NEXT_SUSPEND.getValue()); - - constants.put("codePushUpdateStateRunning", CodePushUpdateState.RUNNING.getValue()); - constants.put("codePushUpdateStatePending", CodePushUpdateState.PENDING.getValue()); - constants.put("codePushUpdateStateLatest", CodePushUpdateState.LATEST.getValue()); - - return constants; + public String getName() { + return NAME; } @Override - public String getName() { - return "CodePush"; + public void invalidate() { + clearLifecycleEventListener(); + mBackgroundExecutor.shutdownNow(); + super.invalidate(); } private void loadBundleLegacy() { @@ -265,6 +259,21 @@ private Field resolveDeclaredField(Class targetClass, String fieldName) { return null; } + private void executeInBackground(Runnable runnable) { + mBackgroundExecutor.execute(runnable); + } + + private void emitDownloadProgressEvent(DownloadProgress downloadProgress) { + if (mEventEmitterCallback != null) { + emitOnDownloadProgress(downloadProgress.createWritableMap()); + return; + } + + getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, downloadProgress.createWritableMap()); + } + private void restartAppInternal(boolean onlyIfUpdateIsPending) { if (this._restartInProgress) { CodePushUtils.log("Restart request queued until the current restart is completed"); @@ -292,7 +301,7 @@ private void restartAppInternal(boolean onlyIfUpdateIsPending) { } @ReactMethod - public void allow(Promise promise) { + public void allow() { CodePushUtils.log("Re-allowing restarts"); this._allowed = true; @@ -302,42 +311,29 @@ public void allow(Promise promise) { this._restartQueue.remove(0); this.restartAppInternal(buf); } - - promise.resolve(null); - return; } @ReactMethod - public void clearPendingRestart(Promise promise) { + public void clearPendingRestart() { this._restartQueue.clear(); - promise.resolve(null); - return; } @ReactMethod - public void disallow(Promise promise) { + public void disallow() { CodePushUtils.log("Disallowing restarts"); this._allowed = false; - promise.resolve(null); - return; } @ReactMethod - public void restartApp(boolean onlyIfUpdateIsPending, Promise promise) { - try { - restartAppInternal(onlyIfUpdateIsPending); - promise.resolve(null); - } catch(CodePushUnknownException e) { - CodePushUtils.log(e); - promise.reject(e); - } + public void restartApp(boolean onlyIfUpdateIsPending) { + restartAppInternal(onlyIfUpdateIsPending); } @ReactMethod public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) { - AsyncTask asyncTask = new AsyncTask() { + executeInBackground(new Runnable() { @Override - protected Void doInBackground(Void... params) { + public void run() { try { JSONObject mutableUpdatePackage = CodePushUtils.convertReadableToJsonObject(updatePackage); mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() { @@ -351,7 +347,6 @@ public void call(DownloadProgress downloadProgress) { } latestDownloadProgress = downloadProgress; - // If the download is completed, synchronously send the last event. if (latestDownloadProgress.isCompleted()) { dispatchDownloadProgressEvent(); return; @@ -365,24 +360,25 @@ public void call(DownloadProgress downloadProgress) { getReactApplicationContext().runOnUiQueueThread(new Runnable() { @Override public void run() { - ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new Choreographer.FrameCallback() { - @Override - public void doFrame(long frameTimeNanos) { - if (!latestDownloadProgress.isCompleted()) { - dispatchDownloadProgressEvent(); + ReactChoreographer.getInstance().postFrameCallback( + ReactChoreographer.CallbackType.TIMERS_EVENTS, + new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + if (!latestDownloadProgress.isCompleted()) { + dispatchDownloadProgressEvent(); + } + + hasScheduledNextFrame = false; + } } - - hasScheduledNextFrame = false; - } - }); + ); } }); } public void dispatchDownloadProgressEvent() { - getReactApplicationContext() - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap()); + emitDownloadProgressEvent(latestDownloadProgress); } }); @@ -396,12 +392,8 @@ public void dispatchDownloadProgressEvent() { CodePushUtils.log(e); promise.reject(e); } - - return null; } - }; - - asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }); } @ReactMethod @@ -426,16 +418,17 @@ public void getConfiguration(Promise promise) { } @ReactMethod - public void getUpdateMetadata(final int updateState, final Promise promise) { - AsyncTask asyncTask = new AsyncTask() { + public void getUpdateMetadata(final double updateStateValue, final Promise promise) { + executeInBackground(new Runnable() { @Override - protected Void doInBackground(Void... params) { + public void run() { try { + int updateState = (int) updateStateValue; JSONObject currentPackage = mUpdateManager.getCurrentPackage(); if (currentPackage == null) { promise.resolve(null); - return null; + return; } Boolean currentUpdateIsPending = false; @@ -446,38 +439,25 @@ protected Void doInBackground(Void... params) { } if (updateState == CodePushUpdateState.PENDING.getValue() && !currentUpdateIsPending) { - // The caller wanted a pending update - // but there isn't currently one. promise.resolve(null); } else if (updateState == CodePushUpdateState.RUNNING.getValue() && currentUpdateIsPending) { - // The caller wants the running update, but the current - // one is pending, so we need to grab the previous. JSONObject previousPackage = mUpdateManager.getPreviousPackage(); if (previousPackage == null) { promise.resolve(null); - return null; + return; } promise.resolve(CodePushUtils.convertJsonObjectToWritable(previousPackage)); } else { - // The current package satisfies the request: - // 1) Caller wanted a pending, and there is a pending update - // 2) Caller wanted the running update, and there isn't a pending - // 3) Caller wants the latest update, regardless if it's pending or not if (mCodePush.isRunningBinaryVersion()) { - // This only matters in Debug builds. Since we do not clear "outdated" updates, - // we need to indicate to the JS side that somehow we have a current update on - // disk that is not actually running. CodePushUtils.setJSONValueForKey(currentPackage, "_isDebugOnly", true); } - // Enable differentiating pending vs. non-pending updates CodePushUtils.setJSONValueForKey(currentPackage, "isPending", currentUpdateIsPending); promise.resolve(CodePushUtils.convertJsonObjectToWritable(currentPackage)); } } catch (CodePushMalformedDataException e) { - // We need to recover the app in case 'codepush.json' is corrupted CodePushUtils.log(e.getMessage()); clearUpdates(); promise.resolve(null); @@ -485,19 +465,15 @@ protected Void doInBackground(Void... params) { CodePushUtils.log(e); promise.reject(e); } - - return null; } - }; - - asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }); } @ReactMethod public void getNewStatusReport(final Promise promise) { - AsyncTask asyncTask = new AsyncTask() { + executeInBackground(new Runnable() { @Override - protected Void doInBackground(Void... params) { + public void run() { try { if (mCodePush.needToReportRollback()) { mCodePush.setNeedToReportRollback(false); @@ -509,7 +485,7 @@ protected Void doInBackground(Void... params) { WritableMap failedStatusReport = mTelemetryManager.getRollbackReport(lastFailedPackage); if (failedStatusReport != null) { promise.resolve(failedStatusReport); - return null; + return; } } catch (JSONException e) { throw new CodePushUnknownException("Unable to read failed updates information stored in SharedPreferences.", e); @@ -521,41 +497,40 @@ protected Void doInBackground(Void... params) { WritableMap newPackageStatusReport = mTelemetryManager.getUpdateReport(CodePushUtils.convertJsonObjectToWritable(currentPackage)); if (newPackageStatusReport != null) { promise.resolve(newPackageStatusReport); - return null; + return; } } } else if (mCodePush.isRunningBinaryVersion()) { WritableMap newAppVersionStatusReport = mTelemetryManager.getBinaryUpdateReport(mCodePush.getAppVersion()); if (newAppVersionStatusReport != null) { promise.resolve(newAppVersionStatusReport); - return null; + return; } } else { WritableMap retryStatusReport = mTelemetryManager.getRetryStatusReport(); if (retryStatusReport != null) { promise.resolve(retryStatusReport); - return null; + return; } } - promise.resolve(""); + promise.resolve(null); } catch(CodePushUnknownException e) { CodePushUtils.log(e); promise.reject(e); } - return null; } - }; - - asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }); } @ReactMethod - public void installUpdate(final ReadableMap updatePackage, final int installMode, final int minimumBackgroundDuration, final Promise promise) { - AsyncTask asyncTask = new AsyncTask() { + public void installUpdate(final ReadableMap updatePackage, final double installModeValue, final double minimumBackgroundDurationValue, final Promise promise) { + executeInBackground(new Runnable() { @Override - protected Void doInBackground(Void... params) { + public void run() { try { + int installMode = (int) installModeValue; + int minimumBackgroundDuration = (int) minimumBackgroundDurationValue; mUpdateManager.installPackage(CodePushUtils.convertReadableToJsonObject(updatePackage), mSettingsManager.isPendingUpdate(null)); String pendingHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY); @@ -565,20 +540,12 @@ protected Void doInBackground(Void... params) { mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false); } - if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue() || - // We also add the resume listener if the installMode is IMMEDIATE, because - // if the current activity is backgrounded, we want to reload the bundle when - // it comes back into the foreground. - installMode == CodePushInstallMode.IMMEDIATE.getValue() || - installMode == CodePushInstallMode.ON_NEXT_SUSPEND.getValue()) { - - // Store the minimum duration on the native module as an instance - // variable instead of relying on a closure below, so that any - // subsequent resume-based installs could override it. + if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue() + || installMode == CodePushInstallMode.IMMEDIATE.getValue() + || installMode == CodePushInstallMode.ON_NEXT_SUSPEND.getValue()) { CodePushNativeModule.this.mMinimumBackgroundDuration = minimumBackgroundDuration; if (mLifecycleEventListener == null) { - // Ensure we do not add the listener twice. mLifecycleEventListener = new LifecycleEventListener() { private Date lastPausedDate = null; private Handler appSuspendHandler = new Handler(Looper.getMainLooper()); @@ -593,8 +560,6 @@ public void run() { @Override public void onHostResume() { appSuspendHandler.removeCallbacks(loadBundleRunnable); - // As of RN 36, the resume handler fires immediately if the app is in - // the foreground, so explicitly wait for it to be backgrounded first if (lastPausedDate != null) { long durationInBackground = (new Date().getTime() - lastPausedDate.getTime()) / 1000; if (installMode == CodePushInstallMode.IMMEDIATE.getValue() @@ -607,8 +572,6 @@ public void onHostResume() { @Override public void onHostPause() { - // Save the current time so that when the app is later - // resumed, we can detect how long it was in the background. lastPausedDate = new Date(); if (installMode == CodePushInstallMode.ON_NEXT_SUSPEND.getValue() && mSettingsManager.isPendingUpdate(null)) { @@ -625,17 +588,13 @@ public void onHostDestroy() { } } - promise.resolve(""); + promise.resolve(null); } catch(CodePushUnknownException e) { CodePushUtils.log(e); promise.reject(e); } - - return null; } - }; - - asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }); } @ReactMethod @@ -692,7 +651,7 @@ public void isFirstRun(String packageHash, Promise promise) { public void notifyApplicationReady(Promise promise) { try { mSettingsManager.removePendingUpdate(); - promise.resolve(""); + promise.resolve(null); } catch(CodePushUnknownException e) { CodePushUtils.log(e); promise.reject(e); diff --git a/cli/functions/runHermesEmitBinaryCommand.ts b/cli/functions/runHermesEmitBinaryCommand.ts index 80040a062..05eed6f5b 100644 --- a/cli/functions/runHermesEmitBinaryCommand.ts +++ b/cli/functions/runHermesEmitBinaryCommand.ts @@ -5,6 +5,7 @@ import childProcess from "child_process"; import fs from "fs"; import path from "path"; +import { createRequire } from "node:module"; import shell from "shelljs"; /** @@ -14,6 +15,7 @@ import shell from "shelljs"; * @param outputPath {string} Path to output .hbc file * @param sourcemapOutput {string} Path to output sourcemap file (Warning: if sourcemapOutput points to the outputPath, the sourcemap will be included in the CodePush bundle and increase the deployment size) * @param extraHermesFlags {string[]} Additional options to pass to `hermesc` command + * @param projectRoot {string} Root directory of the target app project used to resolve the app's React Native module and locate the matching Hermes compiler. Defaults to the current working directory. * @return {Promise} */ export async function runHermesEmitBinaryCommand( @@ -21,6 +23,7 @@ export async function runHermesEmitBinaryCommand( outputPath: string, sourcemapOutput: string, extraHermesFlags: string[] = [], + projectRoot: string = process.cwd(), ): Promise { const hermesArgs: string[] = [ '-emit-binary', @@ -37,7 +40,7 @@ export async function runHermesEmitBinaryCommand( return new Promise((resolve, reject) => { try { - const hermesCommand = getHermesCommand(); + const hermesCommand = getHermesCommand(projectRoot); const disableAllWarningsArg = '-w'; shell.exec(`${hermesCommand} ${hermesArgs.join(' ')} ${disableAllWarningsArg}`); @@ -58,7 +61,7 @@ export async function runHermesEmitBinaryCommand( } // compose-source-maps.js file path - const composeSourceMapsPath = getComposeSourceMapsPath(); + const composeSourceMapsPath = getComposeSourceMapsPath(projectRoot); if (composeSourceMapsPath === null) { throw new Error('react-native compose-source-maps.js scripts is not found'); } @@ -106,7 +109,7 @@ export async function runHermesEmitBinaryCommand( }); } -function getHermesCommand(): string { +function getHermesCommand(projectRoot: string): string { const fileExists = (file: string): boolean => { try { return fs.statSync(file).isFile(); @@ -115,7 +118,7 @@ function getHermesCommand(): string { } }; - const hermescExecutable = path.join(getHermesCompilerPath(), getHermesOSBin(), getHermesOSExe()); + const hermescExecutable = path.join(getHermesCompilerPath(projectRoot), getHermesOSBin(), getHermesOSExe()); if (fileExists(hermescExecutable)) { return hermescExecutable; } @@ -146,48 +149,75 @@ function getHermesOSExe(): string { } } -function getComposeSourceMapsPath(): string | null { +function getComposeSourceMapsPath(projectRoot: string): string | null { // detect if compose-source-maps.js script exists - const composeSourceMaps = path.join(getReactNativePackagePath(), 'scripts', 'compose-source-maps.js'); + const composeSourceMaps = path.join(getReactNativePackagePath(projectRoot), 'scripts', 'compose-source-maps.js'); if (fs.existsSync(composeSourceMaps)) { return composeSourceMaps; } return null; } -function getReactNativePackagePath(): string { - const result = childProcess.spawnSync('node', [ - '--print', - "require.resolve('react-native/package.json')", - ]); - const packagePath = path.dirname(result.stdout.toString()); - if (result.status === 0 && directoryExistsSync(packagePath)) { +function getReactNativePackagePath(projectRoot: string): string { + const packagePath = resolvePackageRoot(projectRoot, 'react-native'); + if (packagePath !== null) { return packagePath; } - return path.join('node_modules', 'react-native'); + return path.join(projectRoot, 'node_modules', 'react-native'); } -function getHermescDirPathInHermesCompilerPackage() { - const result = childProcess.spawnSync('node', [ - '--print', - "require.resolve('hermes-compiler/package.json')", - ]); - const packagePath = path.dirname(result.stdout.toString()); - const hermescDirPath = path.join(packagePath, 'hermesc'); - if (result.status === 0 && directoryExistsSync(hermescDirPath)) { +function getHermescDirPathInHermesCompilerPackage(projectRoot: string) { + const reactNativePackagePath = getReactNativePackagePath(projectRoot); + const hermescDirPath = path.join(path.dirname(reactNativePackagePath), 'hermes-compiler', 'hermesc'); + + if (directoryExistsSync(hermescDirPath)) { return hermescDirPath; } + return null; } -function getHermesCompilerPath() { - const hermescDirPath = getHermescDirPathInHermesCompilerPackage(); +function getHermesCompilerPath(projectRoot: string) { + const hermescDirPath = getHermescDirPathInHermesCompilerPackage(projectRoot); if (hermescDirPath) { // Since react-native 0.83, Hermes compiler executables are in 'hermes-compiler' package return hermescDirPath } else { - return path.join(getReactNativePackagePath(), 'sdks', 'hermesc'); + return path.join(getReactNativePackagePath(projectRoot), 'sdks', 'hermesc'); + } +} + +function resolvePackageRoot(projectRoot: string, packageName: string): string | null { + try { + const projectRequire = createRequire(path.join(projectRoot, 'package.json')); + const resolvedPath = projectRequire.resolve(packageName); + return findPackageRoot(packageName, resolvedPath); + } catch { + return null; + } +} + +function findPackageRoot(packageName: string, resolvedPath: string): string | null { + let currentPath = path.dirname(resolvedPath); + + while (true) { + const packageJsonPath = path.join(currentPath, 'package.json'); + + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as { name?: string }; + if (packageJson.name === packageName) { + return currentPath; + } + } catch { + // Continue traversing upward until the package root is found. + } + + const parentPath = path.dirname(currentPath); + if (parentPath === currentPath) { + return null; + } + currentPath = parentPath; } } diff --git a/e2e/flows-alert/01-update-dialog-ignore.yaml b/e2e/flows-alert/01-update-dialog-ignore.yaml new file mode 100644 index 000000000..e61523987 --- /dev/null +++ b/e2e/flows-alert/01-update-dialog-ignore.yaml @@ -0,0 +1,24 @@ +appId: ${APP_ID} +--- +- launchApp +- assertVisible: "React Native.*" + +- tapOn: "Clear updates" +- tapOn: "Restart app" +- waitForAnimationToEnd: + timeout: 5000 +- assertVisible: "React Native.*" +- assertNotVisible: "UPDATED!" + +- tapOn: "(?i)^Sync with updateDialog" +- assertVisible: "(?i)^E2E Update Dialog$" +- assertVisible: "(?i)^Install the E2E update now\\?$" +- tapOn: "(?i)^Ignore update$" + +- assertVisible: "Result: UPDATE_IGNORED" +- assertNotVisible: "UPDATED!" + +- tapOn: "Get update metadata" +- waitForAnimationToEnd: + timeout: 3000 +- assertVisible: "METADATA_NULL" diff --git a/e2e/flows-alert/02-update-dialog-install.yaml b/e2e/flows-alert/02-update-dialog-install.yaml new file mode 100644 index 000000000..152888d73 --- /dev/null +++ b/e2e/flows-alert/02-update-dialog-install.yaml @@ -0,0 +1,30 @@ +appId: ${APP_ID} +--- +- launchApp +- assertVisible: "React Native.*" + +- tapOn: "Clear updates" +- tapOn: "Restart app" +- waitForAnimationToEnd: + timeout: 5000 +- assertVisible: "React Native.*" +- assertNotVisible: "UPDATED!" + +- tapOn: "(?i)^Sync with updateDialog" +- assertVisible: "(?i)^E2E Update Dialog$" +- assertVisible: "(?i)^Install the E2E update now\\?$" +- tapOn: "(?i)^Install update$" + +- assertVisible: "Result: UPDATE_INSTALLED" +- assertNotVisible: "UPDATED!" + +- tapOn: "Restart app" +- waitForAnimationToEnd: + timeout: 5000 +- assertVisible: "React Native.*" +- assertVisible: "UPDATED!" + +- tapOn: "Get update metadata" +- waitForAnimationToEnd: + timeout: 3000 +- assertVisible: "METADATA_V1.2.2" diff --git a/e2e/helpers/prepare-config.ts b/e2e/helpers/prepare-config.ts index 8ba6573f7..dffa3c9b7 100644 --- a/e2e/helpers/prepare-config.ts +++ b/e2e/helpers/prepare-config.ts @@ -5,6 +5,11 @@ import { getMockServerHost } from "../config"; const BACKUP_SUFFIX = ".e2e-backup"; const RESUME_SYNC_BUTTON_TITLE = "Sync ON_NEXT_RESUME (20s)"; const SUSPEND_SYNC_BUTTON_TITLE = "Sync ON_NEXT_SUSPEND (20s)"; +const ALERT_SYNC_BUTTON_TITLE = "Sync with updateDialog"; +const ALERT_DIALOG_TITLE = "E2E Update Dialog"; +const ALERT_DIALOG_MESSAGE = "Install the E2E update now?"; +const ALERT_DIALOG_IGNORE_BUTTON = "Ignore update"; +const ALERT_DIALOG_INSTALL_BUTTON = "Install update"; const HANDLE_SYNC_PATTERN = /const handleSync = useCallback\(\(\) => \{\n[\s\S]*?\n {2}\}, \[\]\);/; const DEFAULT_SYNC_BUTTON_PATTERN = /^(\s*)