1
1
#import " RCTBridgeModule.h"
2
- #import " RCTEventDispatcher.h"
3
2
#import " RCTConvert.h"
3
+ #import " RCTEventDispatcher.h"
4
4
#import " RCTRootView.h"
5
5
#import " RCTUtils.h"
6
+
6
7
#import " CodePush.h"
7
8
8
9
@implementation CodePush {
@@ -15,16 +16,17 @@ @implementation CodePush {
15
16
static NSTimer *_timer;
16
17
static BOOL usingTestFolder = NO ;
17
18
18
- static NSString * const FailedUpdatesKey = @" CODE_PUSH_FAILED_UPDATES" ;
19
- static NSString * const PendingUpdateKey = @" CODE_PUSH_PENDING_UPDATE" ;
19
+ static NSString *const FailedUpdatesKey = @" CODE_PUSH_FAILED_UPDATES" ;
20
+ static NSString *const PendingUpdateKey = @" CODE_PUSH_PENDING_UPDATE" ;
20
21
21
22
// These keys are already "namespaced" by the PendingUpdateKey, so
22
23
// their values don't need to be obfuscated to prevent collision with app data
23
- static NSString * const PendingUpdateHashKey = @" hash" ;
24
- static NSString * const PendingUpdateRollbackTimeoutKey = @" rollbackTimeout" ;
24
+ static NSString *const PendingUpdateHashKey = @" hash" ;
25
+ static NSString *const PendingUpdateRollbackTimeoutKey = @" rollbackTimeout" ;
25
26
26
27
@synthesize bridge = _bridge;
27
28
29
+ // Public Obj-C API (see header for method comments)
28
30
+ (NSURL *)bundleURL
29
31
{
30
32
return [self bundleURLForResource: @" main" ];
@@ -43,8 +45,7 @@ + (NSURL *)bundleURLForResource:(NSString *)resourceName
43
45
NSString *packageFile = [CodePushPackage getCurrentPackageBundlePath: &error];
44
46
NSURL *binaryJsBundleUrl = [[NSBundle mainBundle ] URLForResource: resourceName withExtension: resourceExtension];
45
47
46
- if (error || !packageFile)
47
- {
48
+ if (error || !packageFile) {
48
49
return binaryJsBundleUrl;
49
50
}
50
51
@@ -61,21 +62,36 @@ + (NSURL *)bundleURLForResource:(NSString *)resourceName
61
62
}
62
63
}
63
64
64
- // Public Obj-C API
65
65
+ (NSString *)getDocumentsDirectory
66
66
{
67
67
NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES ) objectAtIndex: 0 ];
68
68
return documentsDirectory;
69
69
}
70
70
71
- // Internal API methods
71
+ // Private API methods
72
+
73
+ /*
74
+ * This method cancels the currently running rollback
75
+ * timer, which has the effect of stopping an automatic
76
+ * rollback from occuring.
77
+ *
78
+ * Note: This method is safe to call from any thread.
79
+ */
72
80
- (void )cancelRollbackTimer
73
81
{
74
82
dispatch_async (dispatch_get_main_queue (), ^{
75
83
[_timer invalidate ];
76
84
});
77
85
}
78
86
87
+ /*
88
+ * This method checks to see whether a "pending udpate" has been applied
89
+ * (e.g. install was called with a non-immediate mode), but the app hasn't
90
+ * yet been restarted (either naturally or synthentically). If there is one,
91
+ * it will restart the app (if specified), and start the rollback timer.
92
+ *
93
+ * Note: This method is safe to call from any thread.
94
+ */
79
95
- (void )checkForPendingUpdate : (BOOL )needsRestart
80
96
{
81
97
dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
@@ -93,15 +109,24 @@ - (void)checkForPendingUpdate:(BOOL)needsRestart
93
109
if ([pendingHash isEqualToString: currentHash]) {
94
110
int rollbackTimeout = [pendingUpdate[PendingUpdateRollbackTimeoutKey] intValue ];
95
111
[self initializeUpdateWithRollbackTimeout: rollbackTimeout needsRestart: needsRestart];
96
-
97
- // Clear the pending update and sync
98
- [preferences removeObjectForKey: PendingUpdateKey];
99
- [preferences synchronize ];
112
+ } else {
113
+ // NOTE: We shouldn't ever reach here
100
114
}
115
+
116
+ // Clear the pending update and sync
117
+ [preferences removeObjectForKey: PendingUpdateKey];
118
+ [preferences synchronize ];
101
119
}
102
120
});
103
121
}
104
122
123
+ /*
124
+ * This method is meant as a handler for the global app
125
+ * resume notification, and therefore, should not be called
126
+ * directly. It simply checks to see whether there is a pending
127
+ * update that is meant to be installed on resume, and if so
128
+ * it applies it and restarts the app.
129
+ */
105
130
- (void )checkForPendingUpdateDuringResume
106
131
{
107
132
// In order to ensure that CodePush doesn't impact the app's
@@ -112,6 +137,12 @@ - (void)checkForPendingUpdateDuringResume
112
137
}
113
138
}
114
139
140
+ /*
141
+ * This method is used by the React Native bridge to allow
142
+ * our plugin to expose constants to the JS-side. In our case
143
+ * we're simply exporting enum values so that the JS and Native
144
+ * sides of the plugin can be in sync.
145
+ */
115
146
- (NSDictionary *)constantsToExport
116
147
{
117
148
// Export the values of the CodePushInstallMode enum
@@ -151,6 +182,14 @@ - (instancetype)init
151
182
return self;
152
183
}
153
184
185
+ /*
186
+ * This method performs the actual initialization work for a update
187
+ * to ensure that the neccessary state is setup, including:
188
+ * --------------------------------------------------------
189
+ * 1. Updating the current bundle URL to point at the latest update on disk
190
+ * 2. Optionally restarting the app to load the new bundle
191
+ * 3. Optionally starting the rollback protection timer
192
+ */
154
193
- (void )initializeUpdateWithRollbackTimeout : (int )rollbackTimeout
155
194
needsRestart : (BOOL )needsRestart
156
195
{
@@ -167,13 +206,22 @@ - (void)initializeUpdateWithRollbackTimeout:(int)rollbackTimeout
167
206
}
168
207
}
169
208
209
+ /*
210
+ * This method checks to see whether a specific package hash
211
+ * has previously failed installation.
212
+ */
170
213
- (BOOL )isFailedHash : (NSString *)packageHash
171
214
{
172
215
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults ];
173
216
NSMutableArray *failedUpdates = [preferences objectForKey: FailedUpdatesKey];
174
217
return (failedUpdates != nil && [failedUpdates containsObject: packageHash]);
175
218
}
176
219
220
+ /*
221
+ * This method updates the React Native bridge's bundle URL
222
+ * to point at the latest CodePush update, and then restarts
223
+ * the bridge. This isn't meant to be called directly.
224
+ */
177
225
- (void )loadBundle
178
226
{
179
227
// If the current bundle URL is using http(s), then assume the dev
@@ -187,6 +235,13 @@ - (void)loadBundle
187
235
[_bridge reload ];
188
236
}
189
237
238
+ /*
239
+ * This method is used when an update has failed installation
240
+ * and the app needs to be rolled back to the previous bundle.
241
+ * This method is automatically called when the rollback timer
242
+ * expires without the app indicating whether the update succeeded,
243
+ * and therefore, it shouldn't be called directly.
244
+ */
190
245
- (void )rollbackPackage
191
246
{
192
247
NSError *error;
@@ -201,6 +256,11 @@ - (void)rollbackPackage
201
256
[self loadBundle ];
202
257
}
203
258
259
+ /*
260
+ * When an update failed to apply, this method can be called
261
+ * to store its hash so that it can be ignored on future
262
+ * attempts to check the server for an update.
263
+ */
204
264
- (void )saveFailedUpdate : (NSString *)packageHash
205
265
{
206
266
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults ];
@@ -218,6 +278,11 @@ - (void)saveFailedUpdate:(NSString *)packageHash
218
278
[preferences synchronize ];
219
279
}
220
280
281
+ /*
282
+ * When an update is installed whose mode isn't IMMEDIATE, this method
283
+ * can be called to store the pending update's metadata (e.g. rollbackTimeout)
284
+ * so that it can be used when the actual update application occurs at a later point.
285
+ */
221
286
- (void )savePendingUpdate : (NSString *)packageHash
222
287
rollbackTimeout : (int )rollbackTimeout
223
288
{
@@ -232,6 +297,10 @@ - (void)savePendingUpdate:(NSString *)packageHash
232
297
[preferences synchronize ];
233
298
}
234
299
300
+ /*
301
+ * This method handles starting the actual rollback timer
302
+ * after an update has been installed.
303
+ */
235
304
- (void )startRollbackTimer : (int )rollbackTimeout
236
305
{
237
306
double timeoutInSeconds = rollbackTimeout / 1000 ;
@@ -243,42 +312,57 @@ - (void)startRollbackTimer:(int)rollbackTimeout
243
312
}
244
313
245
314
// JavaScript-exported module methods
315
+
316
+ /*
317
+ * This is native-side of the RemotePackage.download method
318
+ */
246
319
RCT_EXPORT_METHOD (downloadUpdate:(NSDictionary *)updatePackage
247
320
resolver:(RCTPromiseResolveBlock)resolve
248
321
rejecter:(RCTPromiseRejectBlock)reject)
249
322
{
250
323
[CodePushPackage downloadPackage: updatePackage
251
- progressCallback: ^(long expectedContentLength, long receivedContentLength) {
252
- [self .bridge.eventDispatcher
253
- sendAppEventWithName: @" CodePushDownloadProgress"
254
- body: @{
255
- @" totalBytes" :[NSNumber numberWithLong: expectedContentLength],
256
- @" receivedBytes" :[NSNumber numberWithLong: receivedContentLength]
257
- }];
258
- }
259
- doneCallback: ^{
260
- NSError *err;
261
- NSDictionary *newPackage = [CodePushPackage
262
- getPackage: updatePackage[@" packageHash" ]
263
- error: &err];
264
-
265
- if (err) {
266
- return reject (err);
267
- }
268
-
269
- resolve (newPackage);
270
- }
271
- failCallback: ^(NSError *err) {
272
- reject (err);
273
- }];
324
+ // The download is progressing forward
325
+ progressCallback: ^(long expectedContentLength, long receivedContentLength) {
326
+ // Notify the script-side about the progress
327
+ [self .bridge.eventDispatcher
328
+ sendAppEventWithName: @" CodePushDownloadProgress"
329
+ body: @{
330
+ @" totalBytes" :[NSNumber numberWithLong: expectedContentLength],
331
+ @" receivedBytes" :[NSNumber numberWithLong: receivedContentLength]
332
+ }];
333
+ }
334
+ // The download completed
335
+ doneCallback: ^{
336
+ NSError *err;
337
+ NSDictionary *newPackage = [CodePushPackage getPackage: updatePackage[@" packageHash" ] error: &err];
338
+
339
+ if (err) {
340
+ return reject (err);
341
+ }
342
+
343
+ resolve (newPackage);
344
+ }
345
+ // The download failed
346
+ failCallback: ^(NSError *err) {
347
+ reject (err);
348
+ }];
274
349
}
275
350
351
+ /*
352
+ * This is the native side of the CodePush.getConfiguration method. It isn't
353
+ * currently exposed via the "react-native-code-push" module, and is used
354
+ * internally only by the CodePush.checkForUpdate method in order to get the
355
+ * app version, as well as the deployment key that was configured in the Info.plist file.
356
+ */
276
357
RCT_EXPORT_METHOD (getConfiguration:(RCTPromiseResolveBlock)resolve
277
358
rejecter:(RCTPromiseRejectBlock)reject)
278
359
{
279
360
resolve ([[CodePushConfig current ] configuration ]);
280
361
}
281
362
363
+ /*
364
+ * This method is the native side of the CodePush.getCurrentPackage method.
365
+ */
282
366
RCT_EXPORT_METHOD (getCurrentPackage:(RCTPromiseResolveBlock)resolve
283
367
rejecter:(RCTPromiseRejectBlock)reject)
284
368
{
@@ -293,6 +377,9 @@ - (void)startRollbackTimer:(int)rollbackTimeout
293
377
});
294
378
}
295
379
380
+ /*
381
+ * This method is the native side of the LocalPackage.install method.
382
+ */
296
383
RCT_EXPORT_METHOD (installUpdate:(NSDictionary *)updatePackage
297
384
rollbackTimeout:(int )rollbackTimeout
298
385
installMode:(CodePushInstallMode)installMode
@@ -318,6 +405,10 @@ - (void)startRollbackTimer:(int)rollbackTimeout
318
405
});
319
406
}
320
407
408
+ /*
409
+ * This method isn't publically exposed via the "react-native-code-push"
410
+ * module, and is only used internally to populate the RemotePackage.failedApply property.
411
+ */
321
412
RCT_EXPORT_METHOD (isFailedUpdate:(NSString *)packageHash
322
413
resolve:(RCTPromiseResolveBlock)resolve
323
414
reject:(RCTPromiseRejectBlock)reject)
@@ -326,33 +417,45 @@ - (void)startRollbackTimer:(int)rollbackTimeout
326
417
resolve (@(isFailedHash));
327
418
}
328
419
420
+ /*
421
+ * This method isn't publically exposed via the "react-native-code-push"
422
+ * module, and is only used internally to populate the LocalPackage.isFirstRun property.
423
+ */
329
424
RCT_EXPORT_METHOD (isFirstRun:(NSString *)packageHash
330
425
resolve:(RCTPromiseResolveBlock)resolve
331
426
rejecter:(RCTPromiseRejectBlock)reject)
332
427
{
333
428
NSError *error;
334
429
BOOL isFirstRun = didUpdate
335
- && nil != packageHash
336
- && [packageHash length ] > 0
337
- && [packageHash isEqualToString: [CodePushPackage getCurrentPackageHash: &error]];
430
+ && nil != packageHash
431
+ && [packageHash length ] > 0
432
+ && [packageHash isEqualToString: [CodePushPackage getCurrentPackageHash: &error]];
338
433
339
434
resolve (@(isFirstRun));
340
435
}
341
436
437
+ /*
438
+ * This method is the native side of the CodePush.notifyApplicationReady() method.
439
+ */
342
440
RCT_EXPORT_METHOD (notifyApplicationReady:(RCTPromiseResolveBlock)resolve
343
441
rejecter:(RCTPromiseRejectBlock)reject)
344
442
{
345
443
[self cancelRollbackTimer ];
346
444
resolve ([NSNull null ]);
347
445
}
348
446
349
- // This function is exposed solely for immediately installed
350
- // update support, and shouldn't be consumed directly by user code.
447
+ /*
448
+ * This method isn't publically exposed via the "react-native-code-push"
449
+ * module, and is only used internally to support immediately installed updates.
450
+ */
351
451
RCT_EXPORT_METHOD (restartImmedidateUpdate:(int )rollbackTimeout)
352
452
{
353
453
[self initializeUpdateWithRollbackTimeout: rollbackTimeout needsRestart: YES ];
354
454
}
355
455
456
+ /*
457
+ * This method is the native side of the CodePush.restartPendingUpdate() method.
458
+ */
356
459
RCT_EXPORT_METHOD (restartPendingUpdate)
357
460
{
358
461
[self checkForPendingUpdate: YES ];
0 commit comments