diff --git a/CodePush.js b/CodePush.js index e31558c..eee79d8 100644 --- a/CodePush.js +++ b/CodePush.js @@ -9,7 +9,7 @@ const PackageMixins = require("./package-mixins")(NativeCodePush); async function checkForUpdate(deploymentKey = null) { /* * Before we ask the server if an update exists, we - * need to retrieve three pieces of information from the + * need to retrieve three pieces of information from the * native side: deployment key, app version (e.g. 1.0.1) * and the hash of the currently running update (if there is one). * This allows the client to only receive updates which are targetted @@ -17,7 +17,7 @@ async function checkForUpdate(deploymentKey = null) { * different from the CodePush update they have already installed. */ const nativeConfig = await getConfiguration(); - + /* * If a deployment key was explicitly provided, * then let's override the one we retrieved @@ -30,7 +30,7 @@ async function checkForUpdate(deploymentKey = null) { // Use dynamically overridden getCurrentPackage() during tests. const localPackage = await module.exports.getCurrentPackage(); - + /* * If the app has a previously installed update, and that update * was targetted at the same app version that is currently running, @@ -48,9 +48,9 @@ async function checkForUpdate(deploymentKey = null) { queryPackage.packageHash = config.packageHash; } } - + const update = await sdk.queryUpdateWithCurrentPackage(queryPackage); - + /* * There are four cases where checkForUpdate will resolve to null: * ---------------------------------------------------------------- @@ -69,13 +69,13 @@ async function checkForUpdate(deploymentKey = null) { * because we want to avoid having to install diff updates against the binary's * version, which we can't do yet on Android. */ - if (!update || update.updateAppVersion || - localPackage && (update.packageHash === localPackage.packageHash) || + if (!update || update.updateAppVersion || + localPackage && (update.packageHash === localPackage.packageHash) || (!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) { if (update && update.updateAppVersion) { log("An update is available but it is targeting a newer binary version than you are currently running."); } - + return null; } else { const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) }; @@ -93,7 +93,7 @@ const getConfiguration = (() => { } else if (testConfig) { return testConfig; } else { - config = await NativeCodePush.getConfiguration(); + config = await NativeCodePush.getConfiguration(); return config; } } @@ -123,7 +123,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) { } else { resolve(update); } - }); + }); }); }; @@ -135,7 +135,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) { } else { resolve(); } - }); + }); }); }; @@ -147,7 +147,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) { } else { resolve(); } - }); + }); }); }; @@ -159,7 +159,7 @@ function log(message) { console.log(`[CodePush] ${message}`) } -// This ensures that notifyApplicationReadyInternal is only called once +// This ensures that notifyApplicationReadyInternal is only called once // in the lifetime of this module instance. const notifyApplicationReady = (() => { let notifyApplicationReadyPromise; @@ -167,13 +167,13 @@ const notifyApplicationReady = (() => { if (!notifyApplicationReadyPromise) { notifyApplicationReadyPromise = notifyApplicationReadyInternal(); } - + return notifyApplicationReadyPromise; }; })(); async function notifyApplicationReadyInternal() { - await NativeCodePush.notifyApplicationReady(); + await NativeCodePush.notifyApplicationReady(); const statusReport = await NativeCodePush.getNewStatusReport(); if (statusReport) { const config = await getConfiguration(); @@ -208,15 +208,15 @@ function setUpTestDependencies(testSdk, providedTestConfig, testNativeBridge) { const sync = (() => { let syncInProgress = false; const setSyncCompleted = () => { syncInProgress = false; }; - + return (options = {}, syncStatusChangeCallback, downloadProgressCallback) => { if (syncInProgress) { typeof syncStatusChangeCallback === "function" ? syncStatusChangeCallback(CodePush.SyncStatus.SYNC_IN_PROGRESS) : log("Sync already in progress."); return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS); - } - + } + syncInProgress = true; const syncPromise = syncInternal(options, syncStatusChangeCallback, downloadProgressCallback); syncPromise @@ -230,7 +230,7 @@ const sync = (() => { /* * The syncInternal method provides a simple, one-line experience for * incorporating the check, download and installation of an update. - * + * * It simply composes the existing API methods together and adds additional * support for respecting mandatory updates, ignoring previously failed * releases, and displaying a standard confirmation UI to the end-user @@ -245,9 +245,9 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE, minimumBackgroundDuration: 0, updateDialog: null, - ...options + ...options }; - + syncStatusChangeCallback = typeof syncStatusChangeCallback === "function" ? syncStatusChangeCallback : (syncStatus) => { @@ -271,7 +271,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg log("User cancelled the update."); break; case CodePush.SyncStatus.UPDATE_INSTALLED: - /* + /* * If the install mode is IMMEDIATE, this will not get returned as the * app will be restarted to a new Javascript context. */ @@ -290,40 +290,34 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg break; } }; - - downloadProgressCallback = typeof downloadProgressCallback === "function" - ? downloadProgressCallback - : (downloadProgress) => { - log(`Expecting ${downloadProgress.totalBytes} bytes, received ${downloadProgress.receivedBytes} bytes.`); - }; - + try { await CodePush.notifyApplicationReady(); - + syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE); const remotePackage = await checkForUpdate(syncOptions.deploymentKey); - + const doDownloadAndInstall = async () => { syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE); const localPackage = await remotePackage.download(downloadProgressCallback); - + // Determine the correct install mode based on whether the update is mandatory or not. resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode; - + syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE); await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => { syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED); }); - + return CodePush.SyncStatus.UPDATE_INSTALLED; }; - + const updateShouldBeIgnored = remotePackage && (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates); if (!remotePackage || updateShouldBeIgnored) { if (updateShouldBeIgnored) { log("An update is available, but it is being ignored due to having been previously rolled back."); } - + syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE); return CodePush.SyncStatus.UP_TO_DATE; } else if (syncOptions.updateDialog) { @@ -334,24 +328,24 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg } else { syncOptions.updateDialog = { ...CodePush.DEFAULT_UPDATE_DIALOG, ...syncOptions.updateDialog }; } - - return await new Promise((resolve, reject) => { + + return await new Promise((resolve, reject) => { let message = null; const dialogButtons = [{ text: null, - onPress: async () => { + onPress: async () => { resolve(await doDownloadAndInstall()); } }]; - + if (remotePackage.isMandatory) { message = syncOptions.updateDialog.mandatoryUpdateMessage; dialogButtons[0].text = syncOptions.updateDialog.mandatoryContinueButtonLabel; } else { message = syncOptions.updateDialog.optionalUpdateMessage; - dialogButtons[0].text = syncOptions.updateDialog.optionalInstallButtonLabel; + dialogButtons[0].text = syncOptions.updateDialog.optionalInstallButtonLabel; // Since this is an optional update, add another button - // to allow the end-user to ignore it + // to allow the end-user to ignore it dialogButtons.push({ text: syncOptions.updateDialog.optionalIgnoreButtonLabel, onPress: () => { @@ -360,13 +354,13 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg } }); } - + // If the update has a description, and the developer // explicitly chose to display it, then set that as the message if (syncOptions.updateDialog.appendReleaseDescription && remotePackage.description) { - message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`; + message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`; } - + syncStatusChangeCallback(CodePush.SyncStatus.AWAITING_USER_ACTION); Alert.alert(syncOptions.updateDialog.title, message, dialogButtons); }); @@ -375,16 +369,16 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg } } catch (error) { syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR); - log(error.message); + log(error.message); throw error; - } + } }; let CodePush; -// If the "NativeCodePush" variable isn't defined, then +// 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 +// and therefore, it doesn't make sense initializing // the JS interface when it wouldn't work anyways. if (NativeCodePush) { CodePush = { 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 8e4e555..49f0c12 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 @@ -13,6 +13,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.ReactChoreographer; import com.facebook.react.uimanager.ViewManager; import com.facebook.soloader.SoLoader; @@ -25,6 +26,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.provider.Settings; +import android.view.Choreographer; import org.json.JSONArray; import org.json.JSONException; @@ -236,7 +238,7 @@ public class CodePush implements ReactPackage { // Reset the state which indicates that // the app was just freshly updated. didUpdate = false; - + JSONObject pendingUpdate = getPendingUpdate(); if (pendingUpdate != null) { try { @@ -251,7 +253,7 @@ public class CodePush implements ReactPackage { // There is in fact a new update running for the first // time, so update the local state to ensure the client knows. didUpdate = true; - + // Mark that we tried to initialize the new update, so that if it crashes, // we will know that we need to rollback when the app next starts. savePendingUpdate(pendingUpdate.getString(PENDING_UPDATE_HASH_KEY), @@ -439,7 +441,7 @@ public class CodePush implements ReactPackage { } @ReactMethod - public void downloadUpdate(final ReadableMap updatePackage, final Promise promise) { + public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) { AsyncTask asyncTask = new AsyncTask() { @Override protected Void doInBackground(Void... params) { @@ -447,11 +449,48 @@ public class CodePush implements ReactPackage { WritableMap mutableUpdatePackage = CodePushUtils.convertReadableMapToWritableMap(updatePackage); mutableUpdatePackage.putString(BINARY_MODIFIED_TIME_KEY, "" + getBinaryResourcesModifiedTime()); codePushPackage.downloadPackage(mutableUpdatePackage, CodePush.this.assetsBundleFileName, new DownloadProgressCallback() { + private boolean hasScheduledNextFrame = false; + private DownloadProgress latestDownloadProgress = null; + @Override public void call(DownloadProgress downloadProgress) { + if (!notifyProgress) { + return; + } + + latestDownloadProgress = downloadProgress; + // If the download is completed, synchronously send the last event. + if (latestDownloadProgress.isCompleted()) { + dispatchDownloadProgressEvent(); + return; + } + + if (hasScheduledNextFrame) { + return; + } + + hasScheduledNextFrame = true; + 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(); + } + + hasScheduledNextFrame = false; + } + }); + } + }); + } + + public void dispatchDownloadProgressEvent() { getReactApplicationContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(DOWNLOAD_PROGRESS_EVENT_NAME, downloadProgress.createWritableMap()); + .emit(DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap()); } }); diff --git a/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgress.java b/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgress.java index ff4add9..45b5026 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgress.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgress.java @@ -23,4 +23,8 @@ class DownloadProgress { } return map; } + + public boolean isCompleted() { + return this.totalBytes == this.receivedBytes; + } } diff --git a/ios/CodePush/CodePush.h b/ios/CodePush/CodePush.h index b56561e..06ac51b 100644 --- a/ios/CodePush/CodePush.h +++ b/ios/CodePush/CodePush.h @@ -54,11 +54,13 @@ @property (strong) NSOutputStream *outputFileStream; @property long long expectedContentLength; @property long long receivedContentLength; +@property dispatch_queue_t operationQueue; @property (copy) void (^progressCallback)(long long, long long); @property (copy) void (^doneCallback)(BOOL); @property (copy) void (^failCallback)(NSError *err); - (id)init:(NSString *)downloadFilePath +operationQueue:(dispatch_queue_t)operationQueue progressCallback:(void (^)(long long, long long))progressCallback doneCallback:(void (^)(BOOL))doneCallback failCallback:(void (^)(NSError *err))failCallback; @@ -78,6 +80,7 @@ failCallback:(void (^)(NSError *err))failCallback; + (void)downloadPackage:(NSDictionary *)updatePackage expectedBundleFileName:(NSString *)expectedBundleFileName + operationQueue:(dispatch_queue_t)operationQueue progressCallback:(void (^)(long long, long long))progressCallback doneCallback:(void (^)())doneCallback failCallback:(void (^)(NSError *err))failCallback; diff --git a/ios/CodePush/CodePush.m b/ios/CodePush/CodePush.m index 689f943..10ece53 100644 --- a/ios/CodePush/CodePush.m +++ b/ios/CodePush/CodePush.m @@ -7,7 +7,7 @@ #import "CodePush.h" -@interface CodePush () +@interface CodePush () @end @implementation CodePush { @@ -15,6 +15,11 @@ BOOL _isFirstRunAfterUpdate; int _minimumBackgroundDuration; NSDate *_lastResignedDate; + + // Used to coordinate the dispatching of download progress events to JS. + long long _latestExpectedContentLength; + long long _latestReceivedConentLength; + BOOL _didUpdateProgress; } RCT_EXPORT_MODULE() @@ -171,6 +176,8 @@ static NSString *bundleResourceName = @"main"; @synthesize bridge = _bridge; @synthesize methodQueue = _methodQueue; +@synthesize pauseCallback = _pauseCallback; +@synthesize paused = _paused; /* * This method is used to clear updates that are installed @@ -223,6 +230,17 @@ static NSString *bundleResourceName = @"main"; [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (void)dispatchDownloadProgressEvent +{ + // Notify the script-side about the progress + [self.bridge.eventDispatcher + sendDeviceEventWithName:@"CodePushDownloadProgress" + body:@{ + @"totalBytes":[NSNumber numberWithLongLong:_latestExpectedContentLength], + @"receivedBytes":[NSNumber numberWithLongLong:_latestReceivedConentLength] + }]; +} + /* * This method ensures that the app was packaged with a JS bundle * file, and if not, it throws the appropriate exception. @@ -273,7 +291,7 @@ static NSString *bundleResourceName = @"main"; #ifdef DEBUG [self clearDebugUpdates]; #endif - + _paused = YES; NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey]; if (pendingUpdate) { @@ -479,6 +497,7 @@ static NSString *bundleResourceName = @"main"; * This is native-side of the RemotePackage.download method */ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage + notifyProgress:(BOOL)notifyProgress resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -488,44 +507,53 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage [mutableUpdatePackage setValue:[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL] forKey:BinaryBundleDateKey]; } - + + if (notifyProgress) { + // Set up and unpause the frame observer so that it can emit + // progress events every frame if the progress is updated. + _didUpdateProgress = NO; + _paused = NO; + } + [CodePushPackage downloadPackage:mutableUpdatePackage expectedBundleFileName:[bundleResourceName stringByAppendingPathExtension:bundleResourceExtension] + operationQueue:_methodQueue // The download is progressing forward progressCallback:^(long long expectedContentLength, long long receivedContentLength) { - dispatch_async(_methodQueue, ^{ - // Notify the script-side about the progress - [self.bridge.eventDispatcher - sendDeviceEventWithName:@"CodePushDownloadProgress" - body:@{ - @"totalBytes":[NSNumber numberWithLongLong:expectedContentLength], - @"receivedBytes":[NSNumber numberWithLongLong:receivedContentLength] - }]; - }); + // Update the download progress so that the frame observer can notify the JS side + _latestExpectedContentLength = expectedContentLength; + _latestReceivedConentLength = receivedContentLength; + _didUpdateProgress = YES; + + // If the download is completed, stop observing frame + // updates and synchronously send the last event. + if (expectedContentLength == receivedContentLength) { + _didUpdateProgress = NO; + _paused = YES; + [self dispatchDownloadProgressEvent]; + } } // The download completed doneCallback:^{ - dispatch_async(_methodQueue, ^{ - NSError *err; - NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err]; + NSError *err; + NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err]; - if (err) { - return reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err); - } - - resolve(newPackage); - }); + if (err) { + return reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err); + } + resolve(newPackage); } // The download failed failCallback:^(NSError *err) { - dispatch_async(_methodQueue, ^{ - if ([CodePushErrorUtils isCodePushError:err]) { - [self saveFailedUpdate:mutableUpdatePackage]; - } - - reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err); - }); + if ([CodePushErrorUtils isCodePushError:err]) { + [self saveFailedUpdate:mutableUpdatePackage]; + } + + // Stop observing frame updates if the download fails. + _didUpdateProgress = NO; + _paused = YES; + reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err); }]; } @@ -760,4 +788,16 @@ RCT_EXPORT_METHOD(getNewStatusReport:(RCTPromiseResolveBlock)resolve resolve(nil); } +#pragma mark - RCTFrameUpdateObserver Methods + +- (void)didUpdateFrame:(RCTFrameUpdate *)update +{ + if (!_didUpdateProgress) { + return; + } + + [self dispatchDownloadProgressEvent]; + _didUpdateProgress = NO; +} + @end \ No newline at end of file diff --git a/ios/CodePush/CodePushDownloadHandler.m b/ios/CodePush/CodePushDownloadHandler.m index 62fd6f4..6a2b8e6 100644 --- a/ios/CodePush/CodePushDownloadHandler.m +++ b/ios/CodePush/CodePushDownloadHandler.m @@ -6,12 +6,14 @@ } - (id)init:(NSString *)downloadFilePath +operationQueue:(dispatch_queue_t)operationQueue progressCallback:(void (^)(long long, long long))progressCallback doneCallback:(void (^)(BOOL))doneCallback failCallback:(void (^)(NSError *err))failCallback { self.outputFileStream = [NSOutputStream outputStreamToFileAtPath:downloadFilePath append:NO]; self.receivedContentLength = 0; + self.operationQueue = operationQueue; self.progressCallback = progressCallback; self.doneCallback = doneCallback; self.failCallback = failCallback; @@ -22,12 +24,12 @@ failCallback:(void (^)(NSError *err))failCallback { NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0]; - NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]; - [connection scheduleInRunLoop:[NSRunLoop mainRunLoop] - forMode:NSDefaultRunLoopMode]; + NSOperationQueue *delegateQueue = [NSOperationQueue new]; + delegateQueue.underlyingQueue = self.operationQueue; + [connection setDelegateQueue:delegateQueue]; [connection start]; } @@ -51,31 +53,31 @@ failCallback:(void (^)(NSError *err))failCallback { if (headerOffset >= 4) { break; } - + const char *bytes = [data bytes]; _header[headerOffset] = bytes[i]; } } - + self.receivedContentLength = self.receivedContentLength + [data length]; - + NSInteger bytesLeft = [data length]; - + do { NSInteger bytesWritten = [self.outputFileStream write:[data bytes] maxLength:bytesLeft]; if (bytesWritten == -1) { break; } - + bytesLeft -= bytesWritten; } while (bytesLeft > 0); - + self.progressCallback(self.expectedContentLength, self.receivedContentLength); - + // bytesLeft should not be negative. assert(bytesLeft >= 0); - + if (bytesLeft) { [self.outputFileStream close]; [connection cancel]; diff --git a/ios/CodePush/CodePushPackage.m b/ios/CodePush/CodePushPackage.m index 80a481c..caa7d99 100644 --- a/ios/CodePush/CodePushPackage.m +++ b/ios/CodePush/CodePushPackage.m @@ -41,6 +41,7 @@ static NSString *const UnzippedFolderName = @"unzipped"; + (void)downloadPackage:(NSDictionary *)updatePackage expectedBundleFileName:(NSString *)expectedBundleFileName + operationQueue:(dispatch_queue_t)operationQueue progressCallback:(void (^)(long long, long long))progressCallback doneCallback:(void (^)())doneCallback failCallback:(void (^)(NSError *err))failCallback @@ -76,6 +77,7 @@ static NSString *const UnzippedFolderName = @"unzipped"; CodePushDownloadHandler *downloadHandler = [[CodePushDownloadHandler alloc] init:downloadFilePath + operationQueue:operationQueue progressCallback:progressCallback doneCallback:^(BOOL isZip) { NSError *error = nil; diff --git a/package-mixins.js b/package-mixins.js index 4c68404..0357c41 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -14,24 +14,24 @@ module.exports = (NativeCodePush) => { let downloadProgressSubscription; if (downloadProgressCallback) { - // Use event subscription to obtain download progress. + // Use event subscription to obtain download progress. downloadProgressSubscription = DeviceEventEmitter.addListener( "CodePushDownloadProgress", downloadProgressCallback ); } - + // Use the downloaded package info. Native code will save the package info // so that the client knows what the current package version is. - try { - const downloadedPackage = await NativeCodePush.downloadUpdate(this); + try { + const downloadedPackage = await NativeCodePush.downloadUpdate(this, !!downloadProgressCallback); reportStatusDownload && reportStatusDownload(this); return { ...downloadedPackage, ...local }; } finally { downloadProgressSubscription && downloadProgressSubscription.remove(); } }, - + isPending: false // A remote package could never be in a pending state }; }; @@ -47,7 +47,7 @@ module.exports = (NativeCodePush) => { localPackage.isPending = true; // Mark the package as pending since it hasn't been applied yet } }, - + isPending: false // A local package wouldn't be pending until it was installed };