diff --git a/CodePush.h b/CodePush.h index d24387b..f25f0c2 100644 --- a/CodePush.h +++ b/CodePush.h @@ -1,7 +1,18 @@ #import "RCTBridgeModule.h" -@interface CodePush : NSObject +@interface CodePush : NSObject +/* + * This method is used to retrieve the URL for the most recent + * version of the JavaScript bundle. This could be either the + * bundle that was packaged with the app binary, or the bundle + * that was downloaded as part of a CodePush update. The value returned + * should be used to "bootstrap" the React Native bridge. + * + * This method assumes that your JS bundle is named "main.jsbundle" + * and therefore, if it isn't, you should use either the bundleURLForResource: + * or bundleURLForResource:withExtension: methods to override that behavior. +*/ + (NSURL *)bundleURL; + (NSURL *)bundleURLForResource:(NSString *)resourceName; @@ -25,7 +36,7 @@ @end -@interface CodePushDownloadHandler : NSObject +@interface CodePushDownloadHandler : NSObject @property (strong) NSOutputStream *outputFileStream; @property long expectedContentLength; diff --git a/CodePush.js b/CodePush.js index dc93a5e..073549a 100644 --- a/CodePush.js +++ b/CodePush.js @@ -5,24 +5,20 @@ var Sdk = require("code-push/script/acquisition-sdk").AcquisitionManager; var { NativeCodePush, PackageMixins, Alert } = require("./CodePushNativePlatformAdapter"); function checkForUpdate(deploymentKey = null) { - var config; - var sdk; + var config, sdk; return getConfiguration() - .then((configResult) => { - config = configResult; - + .then((configResult) => { // If a deployment key was explicitly provided, // then let's override the one we retrieved // from the native-side of the app. if (deploymentKey) { - config.deploymentKey = deploymentKey; + config = Object.assign({}, configResult, { deploymentKey }); + } else { + config = configResult; } - return getSdk(); - }) - .then((sdkResult) => { - sdk = sdkResult; + sdk = getSDK(config); // Allow dynamic overwrite of function. This is only to be used for tests. return module.exports.getCurrentPackage(); }) @@ -74,23 +70,6 @@ var getConfiguration = (() => { } })(); -var getSdk = (() => { - var sdk; - return function getSdk() { - if (sdk) { - return Promise.resolve(sdk); - } else if (testSdk) { - return Promise.resolve(testSdk); - } else { - return getConfiguration() - .then((configuration) => { - sdk = new Sdk(requestFetchAdapter, configuration); - return sdk; - }); - } - } -})(); - function getCurrentPackage() { return new Promise((resolve, reject) => { var localPackage; @@ -112,6 +91,14 @@ function getCurrentPackage() { }); } +function getSDK(config) { + if (testSdk) { + return testSdk; + } else { + return new Sdk(requestFetchAdapter, config); + } +} + /* Logs messages to console with the [CodePush] prefix */ function log(message) { console.log(`[CodePush] ${message}`) diff --git a/CodePush.m b/CodePush.m index d0eaa5f..b90be1f 100644 --- a/CodePush.m +++ b/CodePush.m @@ -1,8 +1,9 @@ #import "RCTBridgeModule.h" -#import "RCTEventDispatcher.h" #import "RCTConvert.h" +#import "RCTEventDispatcher.h" #import "RCTRootView.h" #import "RCTUtils.h" + #import "CodePush.h" @implementation CodePush { @@ -15,16 +16,17 @@ static BOOL didUpdate = NO; static NSTimer *_timer; static BOOL usingTestFolder = NO; -static NSString * const FailedUpdatesKey = @"CODE_PUSH_FAILED_UPDATES"; -static NSString * const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; +static NSString *const FailedUpdatesKey = @"CODE_PUSH_FAILED_UPDATES"; +static NSString *const PendingUpdateKey = @"CODE_PUSH_PENDING_UPDATE"; // These keys are already "namespaced" by the PendingUpdateKey, so // their values don't need to be obfuscated to prevent collision with app data -static NSString * const PendingUpdateHashKey = @"hash"; -static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; +static NSString *const PendingUpdateHashKey = @"hash"; +static NSString *const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; @synthesize bridge = _bridge; +// Public Obj-C API (see header for method comments) + (NSURL *)bundleURL { return [self bundleURLForResource:@"main"]; @@ -43,8 +45,7 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; NSString *packageFile = [CodePushPackage getCurrentPackageBundlePath:&error]; NSURL *binaryJsBundleUrl = [[NSBundle mainBundle] URLForResource:resourceName withExtension:resourceExtension]; - if (error || !packageFile) - { + if (error || !packageFile) { return binaryJsBundleUrl; } @@ -61,14 +62,21 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; } } -// Public Obj-C API + (NSString *)getDocumentsDirectory { NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; return documentsDirectory; } -// Internal API methods +// Private API methods + +/* + * This method cancels the currently running rollback + * timer, which has the effect of stopping an automatic + * rollback from occurring. + * + * Note: This method is safe to call from any thread. + */ - (void)cancelRollbackTimer { dispatch_async(dispatch_get_main_queue(), ^{ @@ -76,6 +84,14 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; }); } +/* + * This method checks to see whether a "pending update" has been applied + * (e.g. install was called with a non-immediate mode), but the app hasn't + * yet been restarted (either naturally or programmatically). If there is one, + * it will restart the app (if specified), and start the rollback timer. + * + * Note: This method is safe to call from any thread. + */ - (void)checkForPendingUpdate:(BOOL)needsRestart { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -87,21 +103,26 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; NSString *pendingHash = pendingUpdate[PendingUpdateHashKey]; NSString *currentHash = [CodePushPackage getCurrentPackageHash:&error]; - // If the current hash is equivalent to the pending hash, then the app - // restart "picked up" the new update, but we need to kick off the - // rollback timer and ensure that the necessary state is setup. - if ([pendingHash isEqualToString:currentHash]) { - int rollbackTimeout = [pendingUpdate[PendingUpdateRollbackTimeoutKey] intValue]; - [self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:needsRestart]; - - // Clear the pending update and sync - [preferences removeObjectForKey:PendingUpdateKey]; - [preferences synchronize]; - } + NSAssert([pendingHash isEqualToString:currentHash], @"There is a pending update but it's hash doesn't match that of the current package."); + + // Kick off the rollback timer and ensure that the necessary state is setup for the pending update. + int rollbackTimeout = [pendingUpdate[PendingUpdateRollbackTimeoutKey] intValue]; + [self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:needsRestart]; + + // Clear the pending update and sync + [preferences removeObjectForKey:PendingUpdateKey]; + [preferences synchronize]; } }); } +/* + * This method is meant as a handler for the global app + * resume notification, and therefore, should not be called + * directly. It simply checks to see whether there is a pending + * update that is meant to be installed on resume, and if so + * it applies it and restarts the app. + */ - (void)checkForPendingUpdateDuringResume { // In order to ensure that CodePush doesn't impact the app's @@ -112,6 +133,12 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; } } +/* + * 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 enum @@ -151,6 +178,14 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; return self; } +/* + * This method performs the actual initialization work for an update + * to ensure that the necessary state is setup, including: + * -------------------------------------------------------- + * 1. Updating the current bundle URL to point at the latest update on disk + * 2. Optionally restarting the app to load the new bundle + * 3. Optionally starting the rollback protection timer + */ - (void)initializeUpdateWithRollbackTimeout:(int)rollbackTimeout needsRestart:(BOOL)needsRestart { @@ -167,6 +202,10 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; } } +/* + * This method checks to see whether a specific package hash + * has previously failed installation. + */ - (BOOL)isFailedHash:(NSString*)packageHash { NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; @@ -174,6 +213,11 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; return (failedUpdates != nil && [failedUpdates containsObject:packageHash]); } +/* + * This method updates the React Native bridge's bundle URL + * to point at the latest CodePush update, and then restarts + * the bridge. This isn't meant to be called directly. + */ - (void)loadBundle { // If the current bundle URL is using http(s), then assume the dev @@ -187,6 +231,13 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; [_bridge reload]; } +/* + * This method is used when an update has failed installation + * and the app needs to be rolled back to the previous bundle. + * This method is automatically called when the rollback timer + * expires without the app indicating whether the update succeeded, + * and therefore, it shouldn't be called directly. + */ - (void)rollbackPackage { NSError *error; @@ -201,6 +252,11 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; [self loadBundle]; } +/* + * When an update failed to apply, this method can be called + * to store its hash so that it can be ignored on future + * attempts to check the server for an update. + */ - (void)saveFailedUpdate:(NSString *)packageHash { NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; @@ -218,6 +274,11 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; [preferences synchronize]; } +/* + * When an update is installed whose mode isn't IMMEDIATE, this method + * can be called to store the pending update's metadata (e.g. rollbackTimeout) + * so that it can be used when the actual update application occurs at a later point. + */ - (void)savePendingUpdate:(NSString *)packageHash rollbackTimeout:(int)rollbackTimeout { @@ -232,6 +293,10 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; [preferences synchronize]; } +/* + * This method handles starting the actual rollback timer + * after an update has been installed. + */ - (void)startRollbackTimer:(int)rollbackTimeout { double timeoutInSeconds = rollbackTimeout / 1000; @@ -243,42 +308,57 @@ static NSString * const PendingUpdateRollbackTimeoutKey = @"rollbackTimeout"; } // JavaScript-exported module methods + +/* + * This is native-side of the RemotePackage.download method + */ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [CodePushPackage downloadPackage:updatePackage - progressCallback:^(long expectedContentLength, long receivedContentLength) { - [self.bridge.eventDispatcher - sendAppEventWithName:@"CodePushDownloadProgress" - body:@{ - @"totalBytes":[NSNumber numberWithLong:expectedContentLength], - @"receivedBytes":[NSNumber numberWithLong:receivedContentLength] - }]; - } - doneCallback:^{ - NSError *err; - NSDictionary *newPackage = [CodePushPackage - getPackage:updatePackage[@"packageHash"] - error:&err]; - - if (err) { - return reject(err); - } - - resolve(newPackage); - } - failCallback:^(NSError *err) { - reject(err); - }]; + // The download is progressing forward + progressCallback:^(long expectedContentLength, long receivedContentLength) { + // Notify the script-side about the progress + [self.bridge.eventDispatcher + sendAppEventWithName:@"CodePushDownloadProgress" + body:@{ + @"totalBytes":[NSNumber numberWithLong:expectedContentLength], + @"receivedBytes":[NSNumber numberWithLong:receivedContentLength] + }]; + } + // The download completed + doneCallback:^{ + NSError *err; + NSDictionary *newPackage = [CodePushPackage getPackage:updatePackage[@"packageHash"] error:&err]; + + if (err) { + return reject(err); + } + + resolve(newPackage); + } + // The download failed + failCallback:^(NSError *err) { + reject(err); + }]; } +/* + * This is the native side of the CodePush.getConfiguration method. It isn't + * currently exposed via the "react-native-code-push" module, and is used + * internally only by the CodePush.checkForUpdate method in order to get the + * 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) { resolve([[CodePushConfig current] configuration]); } +/* + * This method is the native side of the CodePush.getCurrentPackage method. + */ RCT_EXPORT_METHOD(getCurrentPackage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -293,6 +373,9 @@ RCT_EXPORT_METHOD(getCurrentPackage:(RCTPromiseResolveBlock)resolve }); } +/* + * This method is the native side of the LocalPackage.install method. + */ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage rollbackTimeout:(int)rollbackTimeout installMode:(CodePushInstallMode)installMode @@ -318,6 +401,10 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage }); } +/* + * This method isn't publicly exposed via the "react-native-code-push" + * module, and is only used internally to populate the RemotePackage.failedApply property. + */ RCT_EXPORT_METHOD(isFailedUpdate:(NSString *)packageHash resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) @@ -326,19 +413,26 @@ RCT_EXPORT_METHOD(isFailedUpdate:(NSString *)packageHash resolve(@(isFailedHash)); } +/* + * This method isn't publicly exposed via the "react-native-code-push" + * module, and is only used internally to populate the LocalPackage.isFirstRun property. + */ RCT_EXPORT_METHOD(isFirstRun:(NSString *)packageHash resolve:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSError *error; BOOL isFirstRun = didUpdate - && nil != packageHash - && [packageHash length] > 0 - && [packageHash isEqualToString:[CodePushPackage getCurrentPackageHash:&error]]; + && nil != packageHash + && [packageHash length] > 0 + && [packageHash isEqualToString:[CodePushPackage getCurrentPackageHash:&error]]; resolve(@(isFirstRun)); } +/* + * This method is the native side of the CodePush.notifyApplicationReady() method. + */ RCT_EXPORT_METHOD(notifyApplicationReady:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { @@ -346,13 +440,18 @@ RCT_EXPORT_METHOD(notifyApplicationReady:(RCTPromiseResolveBlock)resolve resolve([NSNull null]); } -// This function is exposed solely for immediately installed -// update support, and shouldn't be consumed directly by user code. -RCT_EXPORT_METHOD(restartImmedidateUpdate:(int)rollbackTimeout) +/* + * This method isn't publicly exposed via the "react-native-code-push" + * module, and is only used internally to support immediately installed updates. + */ +RCT_EXPORT_METHOD(restartImmediateUpdate:(int)rollbackTimeout) { [self initializeUpdateWithRollbackTimeout:rollbackTimeout needsRestart:YES]; } +/* + * This method is the native side of the CodePush.restartPendingUpdate() method. + */ RCT_EXPORT_METHOD(restartPendingUpdate) { [self checkForPendingUpdate:YES]; diff --git a/package-mixins.js b/package-mixins.js index c808646..8aa9bf2 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -49,7 +49,7 @@ module.exports = (NativeCodePush) => { .then(function() { updateInstalledCallback && updateInstalledCallback(); if (installMode == NativeCodePush.codePushInstallModeImmediate) { - NativeCodePush.restartImmedidateUpdate(rollbackTimeout); + NativeCodePush.restartImmediateUpdate(rollbackTimeout); }; }); }