From 977c4d1cd9f5a167a92873ce9fea644a5df63a2e Mon Sep 17 00:00:00 2001 From: floydkim Date: Mon, 16 Mar 2026 01:35:04 +0900 Subject: [PATCH 01/10] refactor: migrate CodePush to TurboModule --- CodePush.podspec | 15 +- android/app/build.gradle | 7 + .../microsoft/codepush/react/CodePush.java | 55 +++- .../codepush/react/CodePushDialog.java | 102 ------- .../codepush/react/CodePushNativeModule.java | 193 +++++-------- ios/CodePush.xcodeproj/project.pbxproj | 12 +- ios/CodePush/CodePush.h | 48 +++- ios/CodePush/{CodePush.m => CodePush.mm} | 124 ++++---- package.json | 13 + react-native.config.js | 4 +- src/AlertAdapter.js | 24 -- src/CodePush.js | 267 ++++++++++++------ src/native/NativeCodePush.ts | 146 ++++++++++ src/package-mixins.js | 12 +- src/specs/NativeCodePush.ts | 43 +++ 15 files changed, 634 insertions(+), 431 deletions(-) delete mode 100644 android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java rename ios/CodePush/{CodePush.m => CodePush.mm} (92%) delete mode 100644 src/AlertAdapter.js create mode 100644 src/native/NativeCodePush.ts create mode 100644 src/specs/NativeCodePush.ts 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/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/ios/CodePush.xcodeproj/project.pbxproj b/ios/CodePush.xcodeproj/project.pbxproj index b36c80a66..490d6da8d 100644 --- a/ios/CodePush.xcodeproj/project.pbxproj +++ b/ios/CodePush.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 13BE3DEE1AC21097009241FE /* CodePush.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* CodePush.m */; }; + 13BE3DEE1AC21097009241FE /* CodePush.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* CodePush.mm */; }; 1B23B9141BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */; }; 1B762E901C9A5E9A006EF800 /* CodePushErrorUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B762E8F1C9A5E9A006EF800 /* CodePushErrorUtils.m */; }; 1BCC09A71CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCC09A61CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m */; }; @@ -82,7 +82,7 @@ 5498D8F61D21F14100B5EB43 /* CodePushUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5498D8F51D21F14100B5EB43 /* CodePushUtils.m */; }; 54FFEDE01BF550630061DD23 /* CodePushDownloadHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */; }; 6463C82D1EBA0CFB0095B8CD /* CodePushUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5498D8F51D21F14100B5EB43 /* CodePushUtils.m */; }; - 6463C82E1EBA0CFB0095B8CD /* CodePush.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* CodePush.m */; }; + 6463C82E1EBA0CFB0095B8CD /* CodePush.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* CodePush.mm */; }; 6463C82F1EBA0CFB0095B8CD /* CodePushConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 81D51F391B6181C2000DA084 /* CodePushConfig.m */; }; 6463C8301EBA0CFB0095B8CD /* CodePushDownloadHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */; }; 6463C8311EBA0CFB0095B8CD /* CodePushErrorUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B762E8F1C9A5E9A006EF800 /* CodePushErrorUtils.m */; }; @@ -125,7 +125,7 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* libCodePush.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCodePush.a; sourceTree = BUILT_PRODUCTS_DIR; }; 13BE3DEC1AC21097009241FE /* CodePush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CodePush.h; path = CodePush/CodePush.h; sourceTree = ""; }; - 13BE3DED1AC21097009241FE /* CodePush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePush.m; path = CodePush/CodePush.m; sourceTree = ""; }; + 13BE3DED1AC21097009241FE /* CodePush.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CodePush.mm; path = CodePush/CodePush.mm; sourceTree = ""; }; 1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RCTConvert+CodePushInstallMode.m"; path = "CodePush/RCTConvert+CodePushInstallMode.m"; sourceTree = ""; }; 1B762E8F1C9A5E9A006EF800 /* CodePushErrorUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushErrorUtils.m; path = CodePush/CodePushErrorUtils.m; sourceTree = ""; }; 1BCC09A61CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RCTConvert+CodePushUpdateState.m"; path = "CodePush/RCTConvert+CodePushUpdateState.m"; sourceTree = ""; }; @@ -277,7 +277,7 @@ FF90DEF92C5A808600CA8692 /* PrivacyInfo.xcprivacy */, 5498D8F51D21F14100B5EB43 /* CodePushUtils.m */, 13BE3DEC1AC21097009241FE /* CodePush.h */, - 13BE3DED1AC21097009241FE /* CodePush.m */, + 13BE3DED1AC21097009241FE /* CodePush.mm */, 81D51F391B6181C2000DA084 /* CodePushConfig.m */, 54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */, 1B762E8F1C9A5E9A006EF800 /* CodePushErrorUtils.m */, @@ -451,7 +451,7 @@ 3221E4912C8ABE1400268379 /* SSZipArchive.m in Sources */, 5421FE311C58AD5A00986A55 /* CodePushTelemetryManager.m in Sources */, 3221E46F2C8ABE1300268379 /* mz_strm_wzaes.c in Sources */, - 13BE3DEE1AC21097009241FE /* CodePush.m in Sources */, + 13BE3DEE1AC21097009241FE /* CodePush.mm in Sources */, 3221E45D2C8ABE1300268379 /* mz_zip.c in Sources */, 1B762E901C9A5E9A006EF800 /* CodePushErrorUtils.m in Sources */, 3221E4832C8ABE1400268379 /* mz_strm_split.c in Sources */, @@ -473,7 +473,7 @@ 3221E4822C8ABE1400268379 /* mz_strm_buf.c in Sources */, 3221E45E2C8ABE1300268379 /* mz_zip.c in Sources */, 6463C82D1EBA0CFB0095B8CD /* CodePushUtils.m in Sources */, - 6463C82E1EBA0CFB0095B8CD /* CodePush.m in Sources */, + 6463C82E1EBA0CFB0095B8CD /* CodePush.mm in Sources */, 6463C82F1EBA0CFB0095B8CD /* CodePushConfig.m in Sources */, 6463C8301EBA0CFB0095B8CD /* CodePushDownloadHandler.m in Sources */, 6463C8311EBA0CFB0095B8CD /* CodePushErrorUtils.m in Sources */, diff --git a/ios/CodePush/CodePush.h b/ios/CodePush/CodePush.h index a5260e814..b5acade4a 100644 --- a/ios/CodePush/CodePush.h +++ b/ios/CodePush/CodePush.h @@ -1,14 +1,40 @@ -#if __has_include() -#import -#elif __has_include("RCTEventEmitter.h") -#import "RCTEventEmitter.h" +#import + +#ifdef RCT_NEW_ARCH_ENABLED + #if defined(__cplusplus) + #if __has_include() + #import + #elif __has_include("RNCodePushSpec.h") + #import "RNCodePushSpec.h" + #endif + #else + #if __has_include() + #import + #elif __has_include("RCTBridgeModule.h") + #import "RCTBridgeModule.h" + #else + #import "React/RCTBridgeModule.h" + #endif + #endif #else -#import "React/RCTEventEmitter.h" // Required when used as a Pod in a Swift project + #if __has_include() + #import + #elif __has_include("RCTEventEmitter.h") + #import "RCTEventEmitter.h" + #else + #import "React/RCTEventEmitter.h" // Required when used as a Pod in a Swift project + #endif #endif -#import - +#ifdef RCT_NEW_ARCH_ENABLED + #if defined(__cplusplus) +@interface CodePush : NativeCodePushSpecBase + #else +@interface CodePush : NSObject + #endif +#else @interface CodePush : RCTEventEmitter +#endif + (NSURL *)binaryBundleURL; /* @@ -68,7 +94,7 @@ * This information will be used to decide whether the application * should ignore the update or not. */ -+ (NSDictionary*)getRollbackInfo; ++ (NSDictionary*)getLatestRollbackInfo; /* * This method is used to save information about the latest rollback. * This information will be used to decide whether the application @@ -203,7 +229,13 @@ failCallback:(void (^)(NSError *err))failCallback; @end +#if defined(__cplusplus) +extern "C" { +#endif void CPLog(NSString *formatString, ...); +#if defined(__cplusplus) +} +#endif typedef NS_ENUM(NSInteger, CodePushInstallMode) { CodePushInstallModeImmediate, diff --git a/ios/CodePush/CodePush.m b/ios/CodePush/CodePush.mm similarity index 92% rename from ios/CodePush/CodePush.m rename to ios/CodePush/CodePush.mm index 1180fc188..c27c7c787 100644 --- a/ios/CodePush/CodePush.m +++ b/ios/CodePush/CodePush.mm @@ -8,7 +8,11 @@ #import "CodePush.h" -@interface CodePush () +@interface CodePush () @end @implementation CodePush { @@ -62,6 +66,7 @@ @implementation CodePush { static BOOL isRunningBinaryVersion = NO; static BOOL needToReportRollback = NO; static BOOL testConfigurationFlag = NO; +static BOOL hasInitializedUpdateAfterRestartForCurrentLoad = NO; // These values are used to save the NS bundle, name, extension and subdirectory // for the JS bundle in the binary. @@ -246,6 +251,9 @@ + (void)setUsingTestConfiguration:(BOOL)shouldUseTestConfiguration #pragma mark - Private API methods +#ifdef RCT_NEW_ARCH_ENABLED +@synthesize bridge = _bridge; +#endif @synthesize methodQueue = _methodQueue; @synthesize pauseCallback = _pauseCallback; @synthesize paused = _paused; @@ -269,7 +277,7 @@ - (void)setPaused:(BOOL)paused - (void)clearDebugUpdates { dispatch_async(dispatch_get_main_queue(), ^{ - if ([super.bridge.bundleURL.scheme hasPrefix:@"http"]) { + if ([self.bridge.bundleURL.scheme hasPrefix:@"http"]) { NSError *error; NSString *binaryAppVersion = [[CodePushConfig current] appVersion]; NSDictionary *currentPackageMetadata = [CodePushPackage getCurrentPackage:&error]; @@ -283,28 +291,6 @@ - (void)clearDebugUpdates }); } -/* - * This method is used by the React Native bridge to allow - * our plugin to expose constants to the JS-side. In our case - * we're simply exporting enum values so that the JS and Native - * sides of the plugin can be in sync. - */ -- (NSDictionary *)constantsToExport -{ - // Export the values of the CodePushInstallMode and CodePushUpdateState - // enums so that the script-side can easily stay in sync - return @{ - @"codePushInstallModeOnNextRestart":@(CodePushInstallModeOnNextRestart), - @"codePushInstallModeImmediate": @(CodePushInstallModeImmediate), - @"codePushInstallModeOnNextResume": @(CodePushInstallModeOnNextResume), - @"codePushInstallModeOnNextSuspend": @(CodePushInstallModeOnNextSuspend), - - @"codePushUpdateStateRunning": @(CodePushUpdateStateRunning), - @"codePushUpdateStatePending": @(CodePushUpdateStatePending), - @"codePushUpdateStateLatest": @(CodePushUpdateStateLatest) - }; -}; - + (BOOL)requiresMainQueueSetup { return YES; @@ -318,14 +304,16 @@ - (void)dealloc } - (void)dispatchDownloadProgressEvent { - // Notify the script-side about the progress - [self sendEventWithName:DownloadProgressEvent - body:@{ - @"totalBytes" : [NSNumber - numberWithLongLong:_latestExpectedContentLength], - @"receivedBytes" : [NSNumber - numberWithLongLong:_latestReceivedConentLength] - }]; + NSDictionary *progress = @{ + @"totalBytes" : [NSNumber numberWithLongLong:_latestExpectedContentLength], + @"receivedBytes" : [NSNumber numberWithLongLong:_latestReceivedConentLength] + }; + +#ifndef RCT_NEW_ARCH_ENABLED + [self sendEventWithName:DownloadProgressEvent body:progress]; +#else + [self emitOnDownloadProgress:progress]; +#endif } - (void)dispatchThrottledDownloadProgressEventWithForce:(BOOL)force @@ -385,7 +373,12 @@ - (instancetype)init self = [super init]; if (self) { - [self initializeUpdateAfterRestart]; + @synchronized([CodePush class]) { + if (!hasInitializedUpdateAfterRestartForCurrentLoad) { + hasInitializedUpdateAfterRestartForCurrentLoad = YES; + [self initializeUpdateAfterRestart]; + } + } } return self; @@ -535,6 +528,10 @@ + (BOOL)isPendingUpdate:(NSString*)packageHash */ - (void)loadBundle { + @synchronized([CodePush class]) { + hasInitializedUpdateAfterRestartForCurrentLoad = NO; + } + // This needs to be async dispatched because the bridge is not set on init // when the app first starts, therefore rollbacks will not take effect. dispatch_async(dispatch_get_main_queue(), ^{ @@ -542,8 +539,8 @@ - (void)loadBundle // is debugging and therefore, shouldn't be redirected to a local // file (since Chrome wouldn't support it). Otherwise, update // the current bundle URL to point at the latest update - if ([CodePush isUsingTestConfiguration] || ![super.bridge.bundleURL.scheme hasPrefix:@"http"]) { - [super.bridge setValue:[CodePush bundleURL] forKey:@"bundleURL"]; + if ([CodePush isUsingTestConfiguration] || ![self.bridge.bundleURL.scheme hasPrefix:@"http"]) { + [self.bridge setValue:[CodePush bundleURL] forKey:@"bundleURL"]; } RCTTriggerReloadCommandListeners(@"CodePush reload"); @@ -713,8 +710,8 @@ -(void)loadBundleOnTick:(NSTimer *)timer { */ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage notifyProgress:(BOOL)notifyProgress - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { NSDictionary *mutableUpdatePackage = [updatePackage mutableCopy]; NSURL *binaryBundleURL = [CodePush binaryBundleURL]; @@ -813,7 +810,7 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending * app version, as well as the deployment key that was configured in the Info.plist file. */ RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + reject:(RCTPromiseRejectBlock)reject) { NSDictionary *configuration = [[CodePushConfig current] configuration]; NSError *error; @@ -846,10 +843,11 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending /* * This method is the native side of the CodePush.getUpdateMetadata method. */ -RCT_EXPORT_METHOD(getUpdateMetadata:(CodePushUpdateState)updateState - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(getUpdateMetadata:(double)updateStateValue + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + CodePushUpdateState updateState = (CodePushUpdateState)updateStateValue; NSError *error; NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy]; @@ -895,11 +893,13 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending * This method is the native side of the LocalPackage.install method. */ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage - installMode:(CodePushInstallMode)installMode - minimumBackgroundDuration:(int)minimumBackgroundDuration - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + installMode:(double)installModeValue + minimumBackgroundDuration:(double)minimumBackgroundDurationValue + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + CodePushInstallMode installMode = (CodePushInstallMode)installModeValue; + int minimumBackgroundDuration = (int)minimumBackgroundDurationValue; NSError *error; [CodePushPackage installPackage:updatePackage removePendingUpdate:[[self class] isPendingUpdate:nil] @@ -965,7 +965,7 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending RCT_EXPORT_METHOD(getLatestRollbackInfo:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + reject:(RCTPromiseRejectBlock)reject) { NSDictionary *latestRollbackInfo = [[self class] getLatestRollbackInfo]; resolve(latestRollbackInfo); @@ -977,7 +977,7 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending */ RCT_EXPORT_METHOD(isFirstRun:(NSString *)packageHash resolve:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + reject:(RCTPromiseRejectBlock)reject) { NSError *error; BOOL isFirstRun = _isFirstRunAfterUpdate @@ -992,14 +992,13 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending * This method is the native side of the CodePush.notifyApplicationReady() method. */ RCT_EXPORT_METHOD(notifyApplicationReady:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + reject:(RCTPromiseRejectBlock)reject) { [CodePush removePendingUpdate]; resolve(nil); } -RCT_EXPORT_METHOD(allow:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(allow) { CPLog(@"Re-allowing restarts."); _allowed = YES; @@ -1010,34 +1009,25 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending [_restartQueue removeObjectAtIndex:0]; [self restartAppInternal:buf]; } - - resolve(nil); } -RCT_EXPORT_METHOD(clearPendingRestart:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(clearPendingRestart) { [_restartQueue removeAllObjects]; - resolve(nil); } -RCT_EXPORT_METHOD(disallow:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(disallow) { CPLog(@"Disallowing restarts."); _allowed = NO; - resolve(nil); } /* * This method is the native side of the CodePush.restartApp() method. */ -RCT_EXPORT_METHOD(restartApp:(BOOL)onlyIfUpdateIsPending - resolve:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(restartApp:(BOOL)onlyIfUpdateIsPending) { [self restartAppInternal:onlyIfUpdateIsPending]; - resolve(nil); } /* @@ -1071,7 +1061,7 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending * or an update failed) and return its details (version label, status). */ RCT_EXPORT_METHOD(getNewStatusReport:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + reject:(RCTPromiseRejectBlock)reject) { if (needToReportRollback) { needToReportRollback = NO; @@ -1128,4 +1118,12 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update _didUpdateProgress = NO; } +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#endif + @end diff --git a/package.json b/package.json index 93dce4b43..7c9e4f453 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,19 @@ "react-native": "src/CodePush.js", "source": "src/CodePush.js", "types": "typings/react-native-code-push.d.ts", + "codegenConfig": { + "name": "RNCodePushSpec", + "type": "modules", + "jsSrcsDir": "src/specs", + "android": { + "javaPackageName": "com.microsoft.codepush.react" + }, + "ios": { + "modulesProvider": { + "CodePush": "CodePush" + } + } + }, "homepage": "https://microsoft.github.io/code-push", "keywords": [ "react-native", diff --git a/react-native.config.js b/react-native.config.js index 71e17c948..81f9bd45a 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -5,7 +5,9 @@ module.exports = { packageImportPath: "import com.microsoft.codepush.react.CodePush;", packageInstance: "CodePush.getInstance(getApplicationContext(), BuildConfig.DEBUG)", - sourceDir: './android/app' + sourceDir: './android/app', + libraryName: 'RNCodePushSpec', + cmakeListsPath: 'build/generated/source/codegen/jni/CMakeLists.txt', } } } diff --git a/src/AlertAdapter.js b/src/AlertAdapter.js deleted file mode 100644 index ff0b4e67d..000000000 --- a/src/AlertAdapter.js +++ /dev/null @@ -1,24 +0,0 @@ -import React, { Platform } from "react-native"; -let { Alert } = React; - -if (Platform.OS === "android") { - const { NativeModules: { CodePushDialog } } = React; - - Alert = { - alert(title, message, buttons) { - if (buttons.length > 2) { - throw "Can only show 2 buttons for Android dialog."; - } - - const button1Text = buttons[0] ? buttons[0].text : null, - button2Text = buttons[1] ? buttons[1].text : null; - - CodePushDialog.showDialog( - title, message, button1Text, button2Text, - (buttonId) => { buttons[buttonId].onPress && buttons[buttonId].onPress(); }, - (error) => { throw error; }); - } - }; -} - -module.exports = { Alert }; diff --git a/src/CodePush.js b/src/CodePush.js index 965685c21..4a80919c2 100644 --- a/src/CodePush.js +++ b/src/CodePush.js @@ -1,14 +1,37 @@ -import { Alert } from "./AlertAdapter"; -import { AppState, Platform } from "react-native"; +import { Alert, AppState, Platform } from "react-native"; import log from "./logging"; import hoistStatics from 'hoist-non-react-statics'; import { SemverVersioning } from './versioning/SemverVersioning' -let NativeCodePush = require("react-native").NativeModules.CodePush; -const PackageMixins = require("./package-mixins")(NativeCodePush); +const NativeCodePushModule = require("./native/NativeCodePush"); +let NativeCodePush = NativeCodePushModule.getNativeCodePush?.() ?? NativeCodePushModule.default; +const { InstallMode, UpdateState } = NativeCodePushModule; +let PackageMixins = NativeCodePush ? require("./package-mixins")(NativeCodePush) : null; const DEPLOYMENT_KEY = 'deprecated_deployment_key'; +function getNativeCodePush() { + if (!NativeCodePush) { + const NativeCodePushModule = require("./native/NativeCodePush"); + NativeCodePush = NativeCodePushModule.getNativeCodePush?.() ?? NativeCodePushModule.default; + } + + if (NativeCodePush && !PackageMixins) { + PackageMixins = require("./package-mixins")(NativeCodePush); + } + + return NativeCodePush; +} + +function getPackageMixins() { + getNativeCodePush(); + return PackageMixins; +} + +function logMissingNativeModule() { + log("The CodePush module doesn't appear to be properly installed. Please double-check that everything is setup correctly."); +} + /** * @param deviceId {string} * @returns {number} @@ -217,8 +240,15 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) { return null; } else { - const remotePackage = { ...update, ...PackageMixins.remote() }; - remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash); + const nativeCodePush = getNativeCodePush(); + const packageMixins = getPackageMixins(); + if (!nativeCodePush || !packageMixins) { + logMissingNativeModule(); + return null; + } + + const remotePackage = { ...update, ...packageMixins.remote() }; + remotePackage.failedInstall = await nativeCodePush.isFailedUpdate(remotePackage.packageHash); return remotePackage; } } @@ -258,7 +288,12 @@ const getConfiguration = (() => { } else if (testConfig) { return testConfig; } else { - config = await NativeCodePush.getConfiguration(); + const nativeCodePush = getNativeCodePush(); + if (!nativeCodePush) { + throw new Error("The CodePush native module isn't available yet."); + } + + config = await nativeCodePush.getConfiguration(); return config; } } @@ -269,11 +304,17 @@ async function getCurrentPackage() { } async function getUpdateMetadata(updateState) { - let updateMetadata = await NativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING); + const nativeCodePush = getNativeCodePush(); + const packageMixins = getPackageMixins(); + if (!nativeCodePush || !packageMixins) { + return null; + } + + let updateMetadata = await nativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING); if (updateMetadata) { - updateMetadata = { ...PackageMixins.local, ...updateMetadata }; - updateMetadata.failedInstall = await NativeCodePush.isFailedUpdate(updateMetadata.packageHash); - updateMetadata.isFirstRun = await NativeCodePush.isFirstRun(updateMetadata.packageHash); + updateMetadata = { ...packageMixins.local, ...updateMetadata }; + updateMetadata.failedInstall = await nativeCodePush.isFailedUpdate(updateMetadata.packageHash); + updateMetadata.isFirstRun = await nativeCodePush.isFirstRun(updateMetadata.packageHash); } return updateMetadata; } @@ -292,14 +333,27 @@ const notifyApplicationReady = (() => { })(); async function notifyApplicationReadyInternal() { - await NativeCodePush.notifyApplicationReady(); - const statusReport = await NativeCodePush.getNewStatusReport(); + const nativeCodePush = getNativeCodePush(); + if (!nativeCodePush) { + logMissingNativeModule(); + return null; + } + + await nativeCodePush.notifyApplicationReady(); + const statusReport = await nativeCodePush.getNewStatusReport(); statusReport && tryReportStatus(statusReport); // Don't wait for this to complete. return statusReport; } async function tryReportStatus(statusReport, retryOnAppResume) { + const nativeCodePush = getNativeCodePush(); + if (!nativeCodePush) { + logMissingNativeModule(); + retryOnAppResume && retryOnAppResume.remove(); + return; + } + try { if (statusReport.appVersion) { log(`Reporting binary update (${statusReport.appVersion})`); @@ -310,22 +364,27 @@ async function tryReportStatus(statusReport, retryOnAppResume) { sharedCodePushOptions?.onUpdateSuccess?.(label); } else { log(`Reporting CodePush update rollback (${label})`); - await NativeCodePush.setLatestRollbackInfo(statusReport.package.packageHash); + await nativeCodePush.setLatestRollbackInfo(statusReport.package.packageHash); sharedCodePushOptions?.onUpdateRollback?.(label); } } - NativeCodePush.recordStatusReported(statusReport); + nativeCodePush.recordStatusReported(statusReport); retryOnAppResume && retryOnAppResume.remove(); } catch (e) { log(`${e}`) log(`Report status failed: ${JSON.stringify(statusReport)}`); - NativeCodePush.saveStatusReportForRetry(statusReport); + nativeCodePush.saveStatusReportForRetry(statusReport); // Try again when the app resumes if (!retryOnAppResume) { const resumeListener = AppState.addEventListener("change", async (newState) => { if (newState !== "active") return; - const refreshedStatusReport = await NativeCodePush.getNewStatusReport(); + const refreshedNativeCodePush = getNativeCodePush(); + if (!refreshedNativeCodePush) { + return; + } + + const refreshedStatusReport = await refreshedNativeCodePush.getNewStatusReport(); if (refreshedStatusReport) { tryReportStatus(refreshedStatusReport, resumeListener); } else { @@ -358,7 +417,12 @@ async function shouldUpdateBeIgnored(remotePackage, syncOptions) { return true; } - const latestRollbackInfo = await NativeCodePush.getLatestRollbackInfo(); + const nativeCodePush = getNativeCodePush(); + if (!nativeCodePush) { + return true; + } + + const latestRollbackInfo = await nativeCodePush.getLatestRollbackInfo(); if (!validateLatestRollbackInfo(latestRollbackInfo, remotePackage.packageHash)) { log("The latest rollback info is not valid."); return true; @@ -407,11 +471,20 @@ let testConfig; function setUpTestDependencies(testSdk, providedTestConfig, testNativeBridge) { if (testSdk) module.exports.AcquisitionSdk = testSdk; if (providedTestConfig) testConfig = providedTestConfig; - if (testNativeBridge) NativeCodePush = testNativeBridge; + if (testNativeBridge) { + NativeCodePush = testNativeBridge; + PackageMixins = require("./package-mixins")(NativeCodePush); + } } async function restartApp(onlyIfUpdateIsPending = false) { - NativeCodePush.restartApp(onlyIfUpdateIsPending); + const nativeCodePush = getNativeCodePush(); + if (!nativeCodePush) { + logMissingNativeModule(); + return; + } + + nativeCodePush.restartApp(onlyIfUpdateIsPending); } // This function allows only one syncInternal operation to proceed at any given time. @@ -817,76 +890,88 @@ function codePushify(options = {}) { } } -// If the "NativeCodePush" variable isn't defined, then -// the app didn't properly install the native module, -// and therefore, it doesn't make sense initializing -// the JS interface when it wouldn't work anyways. -if (NativeCodePush) { - CodePush = codePushify; - Object.assign(CodePush, { - checkForUpdate, - getConfiguration, - getCurrentPackage, - getUpdateMetadata, - log, - notifyAppReady: notifyApplicationReady, - notifyApplicationReady, - restartApp, - setUpTestDependencies, - sync, - disallowRestart: NativeCodePush.disallow, - allowRestart: NativeCodePush.allow, - clearUpdates: NativeCodePush.clearUpdates, - InstallMode: { - IMMEDIATE: NativeCodePush.codePushInstallModeImmediate, // Restart the app immediately - ON_NEXT_RESTART: NativeCodePush.codePushInstallModeOnNextRestart, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart - ON_NEXT_RESUME: NativeCodePush.codePushInstallModeOnNextResume, // Restart the app the next time it is resumed from the background - ON_NEXT_SUSPEND: NativeCodePush.codePushInstallModeOnNextSuspend, // Restart the app _while_ it is in the background, - // but only after it has been in the background for "minimumBackgroundDuration" seconds (0 by default), - // so that user context isn't lost unless the app suspension is long enough to not matter - }, - SyncStatus: { - UP_TO_DATE: 0, // The running app is up-to-date - UPDATE_INSTALLED: 1, // The app had an optional/mandatory update that was successfully downloaded and is about to be installed. - UPDATE_IGNORED: 2, // The app had an optional update and the end-user chose to ignore it - UNKNOWN_ERROR: 3, - SYNC_IN_PROGRESS: 4, // There is an ongoing "sync" operation in progress. - CHECKING_FOR_UPDATE: 5, - AWAITING_USER_ACTION: 6, - DOWNLOADING_PACKAGE: 7, - INSTALLING_UPDATE: 8, - }, - CheckFrequency: { - ON_APP_START: 0, - ON_APP_RESUME: 1, - MANUAL: 2, - }, - UpdateState: { - RUNNING: NativeCodePush.codePushUpdateStateRunning, - PENDING: NativeCodePush.codePushUpdateStatePending, - LATEST: NativeCodePush.codePushUpdateStateLatest, - }, - DeploymentStatus: { - FAILED: "DeploymentFailed", - SUCCEEDED: "DeploymentSucceeded", - }, - DEFAULT_UPDATE_DIALOG: { - appendReleaseDescription: false, - descriptionPrefix: " Description: ", - mandatoryContinueButtonLabel: "Continue", - mandatoryUpdateMessage: "An update is available that must be installed.", - optionalIgnoreButtonLabel: "Ignore", - optionalInstallButtonLabel: "Install", - optionalUpdateMessage: "An update is available. Would you like to install it?", - title: "Update available", - }, - DEFAULT_ROLLBACK_RETRY_OPTIONS: { - delayInHours: 24, - maxRetryAttempts: 1, - }, - }); -} else { - log("The CodePush module doesn't appear to be properly installed. Please double-check that everything is setup correctly."); -} +CodePush = codePushify; +Object.assign(CodePush, { + checkForUpdate, + getConfiguration, + getCurrentPackage, + getUpdateMetadata, + log, + notifyAppReady: notifyApplicationReady, + notifyApplicationReady, + restartApp, + setUpTestDependencies, + sync, + disallowRestart: () => { + const nativeCodePush = getNativeCodePush(); + if (!nativeCodePush) { + logMissingNativeModule(); + return; + } + + return nativeCodePush.disallow(); + }, + allowRestart: () => { + const nativeCodePush = getNativeCodePush(); + if (!nativeCodePush) { + logMissingNativeModule(); + return; + } + + return nativeCodePush.allow(); + }, + clearUpdates: () => { + const nativeCodePush = getNativeCodePush(); + if (!nativeCodePush) { + logMissingNativeModule(); + return; + } + + return nativeCodePush.clearUpdates(); + }, + InstallMode: { + IMMEDIATE: InstallMode.IMMEDIATE, // Restart the app immediately + ON_NEXT_RESTART: InstallMode.ON_NEXT_RESTART, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart + ON_NEXT_RESUME: InstallMode.ON_NEXT_RESUME, // Restart the app the next time it is resumed from the background + ON_NEXT_SUSPEND: InstallMode.ON_NEXT_SUSPEND, // Restart the app _while_ it is in the background, + // but only after it has been in the background for "minimumBackgroundDuration" seconds (0 by default), + // so that user context isn't lost unless the app suspension is long enough to not matter + }, + SyncStatus: { + UP_TO_DATE: 0, // The running app is up-to-date + UPDATE_INSTALLED: 1, // The app had an optional/mandatory update that was successfully downloaded and is about to be installed. + UPDATE_IGNORED: 2, // The app had an optional update and the end-user chose to ignore it + UNKNOWN_ERROR: 3, + SYNC_IN_PROGRESS: 4, // There is an ongoing "sync" operation in progress. + CHECKING_FOR_UPDATE: 5, + AWAITING_USER_ACTION: 6, + DOWNLOADING_PACKAGE: 7, + INSTALLING_UPDATE: 8, + }, + CheckFrequency: { + ON_APP_START: 0, + ON_APP_RESUME: 1, + MANUAL: 2, + }, + UpdateState, + DeploymentStatus: { + FAILED: "DeploymentFailed", + SUCCEEDED: "DeploymentSucceeded", + }, + DEFAULT_UPDATE_DIALOG: { + appendReleaseDescription: false, + descriptionPrefix: " Description: ", + mandatoryContinueButtonLabel: "Continue", + mandatoryUpdateMessage: "An update is available that must be installed.", + optionalIgnoreButtonLabel: "Ignore", + optionalInstallButtonLabel: "Install", + optionalUpdateMessage: "An update is available. Would you like to install it?", + title: "Update available", + }, + DEFAULT_ROLLBACK_RETRY_OPTIONS: { + delayInHours: 24, + maxRetryAttempts: 1, + }, +}); module.exports = CodePush; diff --git a/src/native/NativeCodePush.ts b/src/native/NativeCodePush.ts new file mode 100644 index 000000000..0c4e75cde --- /dev/null +++ b/src/native/NativeCodePush.ts @@ -0,0 +1,146 @@ +import { NativeEventEmitter, NativeModules } from 'react-native'; +import { getNativeCodePushTurboModule } from '../specs/NativeCodePush'; +import type { DownloadProgress, Spec as NativeCodePushSpec } from '../specs/NativeCodePush'; + +export const DOWNLOAD_PROGRESS_EVENT_NAME = 'CodePushDownloadProgress'; + +export const InstallMode = Object.freeze({ + IMMEDIATE: 0, + ON_NEXT_RESTART: 1, + ON_NEXT_RESUME: 2, + ON_NEXT_SUSPEND: 3, +}); + +export const UpdateState = Object.freeze({ + RUNNING: 0, + PENDING: 1, + LATEST: 2, +}); + +type Subscription = { + remove: () => void; +}; + +type LegacyCodePushModule = { + addListener?: (eventName: string) => void; + allow: () => void | Promise; + clearPendingRestart: () => void | Promise; + clearUpdates: () => void; + disallow: () => void | Promise; + downloadAndReplaceCurrentBundle?: (remoteBundleUrl: string) => void; + downloadUpdate: (updatePackage: object, notifyProgress: boolean) => Promise; + getConfiguration: () => Promise; + getLatestRollbackInfo: () => Promise; + getNewStatusReport: () => Promise; + getUpdateMetadata: (updateState: number) => Promise; + installUpdate: ( + updatePackage: object, + installMode: number, + minimumBackgroundDuration: number, + ) => Promise; + isFailedUpdate: (packageHash: string) => Promise; + isFirstRun: (packageHash: string) => Promise; + notifyApplicationReady: () => Promise; + recordStatusReported: (statusReport: object) => void; + removeListeners?: (count: number) => void; + restartApp: (onlyIfUpdateIsPending: boolean) => void | Promise; + saveStatusReportForRetry: (statusReport: object) => void; + setLatestRollbackInfo: (packageHash: string) => Promise; +}; + +type CodePushModule = NativeCodePushSpec & LegacyCodePushModule; + +function getTurboModule(): CodePushModule | null { + return getNativeCodePushTurboModule() as CodePushModule | null; +} + +function getLegacyModule(): LegacyCodePushModule | null { + return (NativeModules.CodePush ?? null) as LegacyCodePushModule | null; +} + +function getNativeModule(): CodePushModule | null { + const turboModule = getTurboModule(); + if (turboModule) { + return turboModule; + } + + return getLegacyModule(); +} + +function normalizeStatusReport(statusReport: object | string | null | undefined) { + if (!statusReport || typeof statusReport === 'string') { + return null; + } + + return statusReport; +} + +function addDownloadProgressListener( + listener: (progress: DownloadProgress) => void, +): Subscription { + const nativeModule = getNativeModule(); + if (!nativeModule) { + return { + remove() {}, + }; + } + + const turboModule = getTurboModule(); + if (turboModule?.onDownloadProgress) { + return turboModule.onDownloadProgress(listener); + } + + const legacyModule = getLegacyModule(); + if (!legacyModule) { + return { + remove() {}, + }; + } + + const eventEmitter = new NativeEventEmitter(legacyModule ?? nativeModule); + return eventEmitter.addListener(DOWNLOAD_PROGRESS_EVENT_NAME, listener); +} + +function createNativeCodePush() { + return { + InstallMode, + UpdateState, + addDownloadProgressListener, + allow: () => getNativeModule()?.allow(), + clearPendingRestart: () => getNativeModule()?.clearPendingRestart(), + clearUpdates: () => getNativeModule()?.clearUpdates(), + disallow: () => getNativeModule()?.disallow(), + downloadAndReplaceCurrentBundle: (remoteBundleUrl: string) => + getNativeModule()?.downloadAndReplaceCurrentBundle?.(remoteBundleUrl), + downloadUpdate: (updatePackage: object, notifyProgress: boolean) => + getNativeModule()?.downloadUpdate(updatePackage, notifyProgress), + getConfiguration: () => getNativeModule()?.getConfiguration(), + getLatestRollbackInfo: () => getNativeModule()?.getLatestRollbackInfo(), + getNewStatusReport: async () => + normalizeStatusReport(await getNativeModule()?.getNewStatusReport()), + getUpdateMetadata: (updateState: number) => getNativeModule()?.getUpdateMetadata(updateState), + installUpdate: ( + updatePackage: object, + installMode: number, + minimumBackgroundDuration: number, + ) => getNativeModule()?.installUpdate(updatePackage, installMode, minimumBackgroundDuration), + isFailedUpdate: (packageHash: string) => getNativeModule()?.isFailedUpdate(packageHash), + isFirstRun: (packageHash: string) => getNativeModule()?.isFirstRun(packageHash), + notifyApplicationReady: () => getNativeModule()?.notifyApplicationReady(), + recordStatusReported: (statusReport: object) => + getNativeModule()?.recordStatusReported(statusReport), + restartApp: (onlyIfUpdateIsPending: boolean) => + getNativeModule()?.restartApp(onlyIfUpdateIsPending), + saveStatusReportForRetry: (statusReport: object) => + getNativeModule()?.saveStatusReportForRetry(statusReport), + setLatestRollbackInfo: (packageHash: string) => + getNativeModule()?.setLatestRollbackInfo(packageHash), + }; +} + +export function getNativeCodePush() { + const nativeModule = getNativeModule(); + return nativeModule ? createNativeCodePush() : null; +} + +export default getNativeCodePush(); diff --git a/src/package-mixins.js b/src/package-mixins.js index 1ac6d51e0..8cda03634 100644 --- a/src/package-mixins.js +++ b/src/package-mixins.js @@ -1,4 +1,3 @@ -import { NativeEventEmitter } from "react-native"; import log from "./logging"; // This function is used to augment remote and local @@ -14,10 +13,7 @@ module.exports = (NativeCodePush) => { let downloadProgressSubscription; if (downloadProgressCallback) { - const codePushEventEmitter = new NativeEventEmitter(NativeCodePush); - // Use event subscription to obtain download progress. - downloadProgressSubscription = codePushEventEmitter.addListener( - "CodePushDownloadProgress", + downloadProgressSubscription = NativeCodePush.addDownloadProgressListener( downloadProgressCallback ); } @@ -41,12 +37,12 @@ module.exports = (NativeCodePush) => { }; const local = { - async install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, minimumBackgroundDuration = 0, updateInstalledCallback) { + async install(installMode = NativeCodePush.InstallMode.ON_NEXT_RESTART, minimumBackgroundDuration = 0, updateInstalledCallback) { const localPackage = this; const localPackageCopy = Object.assign({}, localPackage); // In dev mode, React Native deep freezes any object queued over the bridge await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration); updateInstalledCallback && updateInstalledCallback(); - if (installMode == NativeCodePush.codePushInstallModeImmediate) { + if (installMode == NativeCodePush.InstallMode.IMMEDIATE) { NativeCodePush.restartApp(false); } else { NativeCodePush.clearPendingRestart(); @@ -58,4 +54,4 @@ module.exports = (NativeCodePush) => { }; return { local, remote }; -}; \ No newline at end of file +}; diff --git a/src/specs/NativeCodePush.ts b/src/specs/NativeCodePush.ts new file mode 100644 index 000000000..d611cac4a --- /dev/null +++ b/src/specs/NativeCodePush.ts @@ -0,0 +1,43 @@ +import { TurboModuleRegistry } from 'react-native'; +import type { TurboModule } from 'react-native'; +import type { + EventEmitter, + UnsafeObject, +} from 'react-native/Libraries/Types/CodegenTypes'; + +export type DownloadProgress = { + receivedBytes: number; + totalBytes: number; +}; + +export interface Spec extends TurboModule { + downloadUpdate(updatePackage: UnsafeObject, notifyProgress: boolean): Promise; + getConfiguration(): Promise; + getUpdateMetadata(updateState: number): Promise; + getNewStatusReport(): Promise; + installUpdate( + updatePackage: UnsafeObject, + installMode: number, + minimumBackgroundDuration: number, + ): Promise; + isFailedUpdate(packageHash: string): Promise; + getLatestRollbackInfo(): Promise; + setLatestRollbackInfo(packageHash: string): Promise; + isFirstRun(packageHash: string): Promise; + notifyApplicationReady(): Promise; + allow(): void; + disallow(): void; + clearPendingRestart(): void; + restartApp(onlyIfUpdateIsPending: boolean): void; + recordStatusReported(statusReport: UnsafeObject): void; + saveStatusReportForRetry(statusReport: UnsafeObject): void; + clearUpdates(): void; + + readonly onDownloadProgress: EventEmitter; +} + +export function getNativeCodePushTurboModule(): Spec | null { + return TurboModuleRegistry.get('CodePush'); +} + +export default getNativeCodePushTurboModule(); From 94faceaeedac3647b72fe731b03f7a0aae020cc2 Mon Sep 17 00:00:00 2001 From: floydkim Date: Mon, 16 Mar 2026 01:35:04 +0900 Subject: [PATCH 02/10] chore: update example apps Podfile.lock --- Examples/RN0773/ios/Podfile.lock | 6 +++++- Examples/RN0783/ios/Podfile.lock | 6 +++++- Examples/RN0797/ios/Podfile.lock | 6 +++++- Examples/RN0803/ios/Podfile.lock | 6 +++++- Examples/RN0816/ios/Podfile.lock | 6 +++++- Examples/RN0821/ios/Podfile.lock | 6 +++++- Examples/RN0832/ios/Podfile.lock | 6 +++++- Examples/RN0840/ios/Podfile.lock | 6 +++++- 8 files changed, 40 insertions(+), 8 deletions(-) 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 From 9a7b85861ff6922e11ce39e7e2fe53ebd2788c23 Mon Sep 17 00:00:00 2001 From: floydkim Date: Tue, 17 Mar 2026 01:28:40 +0900 Subject: [PATCH 03/10] chore(e2e): clear android auto-link artifacts before running e2e --- e2e/run.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/e2e/run.ts b/e2e/run.ts index 7efe9eb54..8ce247496 100644 --- a/e2e/run.ts +++ b/e2e/run.ts @@ -82,6 +82,10 @@ async function main() { await resetWatchmanProject(repoRoot); await syncLocalLibraryIfAvailable(appPath, options.maestroOnly ?? false); + if (options.platform === "android" && !options.maestroOnly) { + invalidateAndroidAutolinkingArtifacts(appPath); + } + const releaseIdentifier = getCodePushReleaseIdentifier(appPath); try { // 1. Prepare config @@ -608,3 +612,25 @@ function syncLocalLibraryIfAvailable(appPath: string, maestroOnly: boolean): Pro }); }); } + +function invalidateAndroidAutolinkingArtifacts(appPath: string): void { + const androidPath = path.join(appPath, "android"); + const generatedPaths = [ + path.join(androidPath, "build", "generated", "autolinking"), + path.join(androidPath, "app", "build", "generated", "autolinking"), + ]; + + let removedAny = false; + for (const generatedPath of generatedPaths) { + if (!fs.existsSync(generatedPath)) { + continue; + } + + fs.rmSync(generatedPath, { recursive: true, force: true }); + removedAny = true; + } + + if (removedAny) { + console.log("[android-autolinking] cleared generated autolinking artifacts"); + } +} From c08b8efd73c1f6d665e0c987376d7362de8d46b4 Mon Sep 17 00:00:00 2001 From: floydkim Date: Tue, 17 Mar 2026 01:29:16 +0900 Subject: [PATCH 04/10] chore(e2e): add Alert dialog test --- e2e/flows-alert/01-update-dialog-ignore.yaml | 24 ++++++++++ e2e/flows-alert/02-update-dialog-install.yaml | 30 ++++++++++++ e2e/helpers/prepare-config.ts | 32 +++++++++++++ e2e/run.ts | 47 ++++++++++++++++++- 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 e2e/flows-alert/01-update-dialog-ignore.yaml create mode 100644 e2e/flows-alert/02-update-dialog-install.yaml 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*)