@algocare/react-native-code-push 12.6.2 → 12.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CodePush.js CHANGED
@@ -586,6 +586,9 @@ async function syncInternal(
586
586
  }
587
587
  }
588
588
  break
589
+ case CodePush.SyncStatus.PREPARING_UPDATE:
590
+ log('Download complete. Preparing update (unzip/file-copy).')
591
+ break
589
592
  case CodePush.SyncStatus.UNKNOWN_ERROR:
590
593
  log('An unknown error occurred.')
591
594
  break
@@ -608,7 +611,11 @@ async function syncInternal(
608
611
  const doDownloadAndInstall = async () => {
609
612
  syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE)
610
613
  const localPackage = await remotePackage.download(
611
- downloadProgressCallback
614
+ downloadProgressCallback,
615
+ () => {
616
+ // HTTP download complete, post-processing (unzip/file-copy) starts
617
+ syncStatusChangeCallback(CodePush.SyncStatus.PREPARING_UPDATE)
618
+ }
612
619
  )
613
620
 
614
621
  // Determine the correct install mode based on whether the update is mandatory or not.
@@ -924,6 +931,7 @@ if (NativeCodePush) {
924
931
  AWAITING_USER_ACTION: 6,
925
932
  DOWNLOADING_PACKAGE: 7,
926
933
  INSTALLING_UPDATE: 8,
934
+ PREPARING_UPDATE: 9, // HTTP download complete, post-processing (unzip/file-copy) in progress
927
935
  },
928
936
  CheckFrequency: {
929
937
  ON_APP_START: 0,
@@ -108,9 +108,9 @@
108
108
 
109
109
  @end
110
110
 
111
- @interface CodePushDownloadHandler : NSObject <NSURLConnectionDelegate>
111
+ @interface CodePushDownloadHandler : NSObject <NSURLSessionDownloadDelegate>
112
112
 
113
- @property (strong) NSOutputStream *outputFileStream;
113
+ @property (strong) NSString *downloadFilePath;
114
114
  @property long long expectedContentLength;
115
115
  @property long long receivedContentLength;
116
116
  @property dispatch_queue_t operationQueue;
@@ -144,7 +144,9 @@ failCallback:(void (^)(NSError *err))failCallback;
144
144
  operationQueue:(dispatch_queue_t)operationQueue
145
145
  progressCallback:(void (^)(long long, long long))progressCallback
146
146
  doneCallback:(void (^)())doneCallback
147
- failCallback:(void (^)(NSError *err))failCallback;
147
+ failCallback:(void (^)(NSError *err))failCallback
148
+ installProgressCallback:(void (^)(NSString *, NSNumber *, NSNumber *))installProgressCallback
149
+ downloadCompleteCallback:(void (^)())downloadCompleteCallback;
148
150
 
149
151
  + (NSDictionary *)getCurrentPackage:(NSError **)error;
150
152
  + (NSDictionary *)getPreviousPackage:(NSError **)error;
@@ -43,6 +43,8 @@ RCT_EXPORT_MODULE()
43
43
 
44
44
  // These constants represent emitted events
45
45
  static NSString *const DownloadProgressEvent = @"CodePushDownloadProgress";
46
+ static NSString *const DownloadCompleteEvent = @"CodePushDownloadComplete";
47
+ static NSString *const InstallProgressEvent = @"CodePushInstallProgress";
46
48
 
47
49
  // These constants represent valid deployment statuses
48
50
  static NSString *const DeploymentFailed = @"DeploymentFailed";
@@ -640,7 +642,7 @@ static NSString *const LatestRollbackCountKey = @"count";
640
642
  }
641
643
 
642
644
  - (NSArray<NSString *> *)supportedEvents {
643
- return @[DownloadProgressEvent];
645
+ return @[DownloadProgressEvent, DownloadCompleteEvent, InstallProgressEvent];
644
646
  }
645
647
 
646
648
  // Determine how long the app was in the background
@@ -726,6 +728,17 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
726
728
 
727
729
  NSString * publicKey = [[CodePushConfig current] publicKey];
728
730
 
731
+ // Install progress callback for reporting unzip/file-copy progress to JS
732
+ void (^installProgressCallback)(NSString *, NSNumber *, NSNumber *) =
733
+ ^(NSString *phase, NSNumber *current, NSNumber *total) {
734
+ dispatch_async(dispatch_get_main_queue(), ^{
735
+ [self sendEventWithName:InstallProgressEvent
736
+ body:@{@"phase": phase,
737
+ @"current": current,
738
+ @"total": total}];
739
+ });
740
+ };
741
+
729
742
  [CodePushPackage
730
743
  downloadPackage:mutableUpdatePackage
731
744
  expectedBundleFileName:[bundleResourceName stringByAppendingPathExtension:bundleResourceExtension]
@@ -746,7 +759,7 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
746
759
  [self dispatchDownloadProgressEvent];
747
760
  }
748
761
  }
749
- // The download completed
762
+ // The download and post-processing completed
750
763
  doneCallback:^{
751
764
  NSError *err;
752
765
  NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];
@@ -766,6 +779,14 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
766
779
  _didUpdateProgress = NO;
767
780
  self.paused = YES;
768
781
  reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
782
+ }
783
+ // Install progress (unzip/file-copy) callback
784
+ installProgressCallback:installProgressCallback
785
+ // Notify JS that HTTP download is complete and post-processing begins
786
+ downloadCompleteCallback:^{
787
+ dispatch_async(dispatch_get_main_queue(), ^{
788
+ [self sendEventWithName:DownloadCompleteEvent body:@{}];
789
+ });
769
790
  }];
770
791
  }
771
792
 
@@ -1,8 +1,10 @@
1
1
  #import "CodePush.h"
2
2
 
3
+ static const NSTimeInterval kDownloadTimeoutInterval = 60.0;
4
+
3
5
  @implementation CodePushDownloadHandler {
4
- // Header chars used to determine if the file is a zip.
5
- char _header[4];
6
+ NSURLSession *_session;
7
+ BOOL _completed; // 콜백 중복 호출 방지 플래그
6
8
  }
7
9
 
8
10
  - (id)init:(NSString *)downloadFilePath
@@ -10,121 +12,141 @@ operationQueue:(dispatch_queue_t)operationQueue
10
12
  progressCallback:(void (^)(long long, long long))progressCallback
11
13
  doneCallback:(void (^)(BOOL))doneCallback
12
14
  failCallback:(void (^)(NSError *err))failCallback {
13
- self.outputFileStream = [NSOutputStream outputStreamToFileAtPath:downloadFilePath
14
- append:NO];
15
- self.receivedContentLength = 0;
16
- self.operationQueue = operationQueue;
17
- self.progressCallback = progressCallback;
18
- self.doneCallback = doneCallback;
19
- self.failCallback = failCallback;
15
+ self = [super init];
16
+ if (self) {
17
+ self.downloadFilePath = downloadFilePath;
18
+ self.receivedContentLength = 0;
19
+ self.expectedContentLength = 0;
20
+ self.operationQueue = operationQueue;
21
+ self.progressCallback = progressCallback;
22
+ self.doneCallback = doneCallback;
23
+ self.failCallback = failCallback;
24
+ _completed = NO;
25
+ }
20
26
  return self;
21
27
  }
22
28
 
23
29
  - (void)download:(NSString *)url {
24
30
  self.downloadUrl = url;
31
+ NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
32
+ config.timeoutIntervalForRequest = kDownloadTimeoutInterval;
33
+
34
+ NSOperationQueue *delegateQueue = [NSOperationQueue new];
35
+ delegateQueue.underlyingQueue = self.operationQueue;
36
+
37
+ _session = [NSURLSession sessionWithConfiguration:config
38
+ delegate:self
39
+ delegateQueue:delegateQueue];
40
+
25
41
  NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]
26
42
  cachePolicy:NSURLRequestUseProtocolCachePolicy
27
- timeoutInterval:60.0];
28
- NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
29
- delegate:self
30
- startImmediately:NO];
31
- if ([NSOperationQueue instancesRespondToSelector:@selector(setUnderlyingQueue:)]) {
32
- NSOperationQueue *delegateQueue = [NSOperationQueue new];
33
- delegateQueue.underlyingQueue = self.operationQueue;
34
- [connection setDelegateQueue:delegateQueue];
35
- } else {
36
- [connection scheduleInRunLoop:[NSRunLoop mainRunLoop]
37
- forMode:NSDefaultRunLoopMode];
38
- }
43
+ timeoutInterval:kDownloadTimeoutInterval];
44
+ NSURLSessionDownloadTask *task = [_session downloadTaskWithRequest:request];
45
+ [task resume];
46
+ }
39
47
 
40
- [connection start];
48
+ - (void)invokeFailCallback:(NSError *)error {
49
+ if (_completed) return;
50
+ _completed = YES;
51
+ if (self.failCallback) {
52
+ self.failCallback(error);
53
+ }
41
54
  }
42
55
 
43
- #pragma mark NSURLConnection Delegate Methods
56
+ - (void)invokeDoneCallback:(BOOL)isZip {
57
+ if (_completed) return;
58
+ _completed = YES;
59
+ if (self.doneCallback) {
60
+ self.doneCallback(isZip);
61
+ }
62
+ }
44
63
 
45
- - (NSCachedURLResponse *)connection:(NSURLConnection *)connection
46
- willCacheResponse:(NSCachedURLResponse*)cachedResponse {
47
- // Return nil to indicate not necessary to store a cached response for this connection
48
- return nil;
64
+ #pragma mark - NSURLSessionDownloadDelegate
65
+
66
+ - (void)URLSession:(NSURLSession *)session
67
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
68
+ didWriteData:(int64_t)bytesWritten
69
+ totalBytesWritten:(int64_t)totalBytesWritten
70
+ totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
71
+ self.expectedContentLength = totalBytesExpectedToWrite;
72
+ self.receivedContentLength = totalBytesWritten;
73
+ if (self.progressCallback) {
74
+ self.progressCallback(self.expectedContentLength, self.receivedContentLength);
75
+ }
49
76
  }
50
77
 
51
- - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
52
- if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
53
- NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
78
+ - (void)URLSession:(NSURLSession *)session
79
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
80
+ didFinishDownloadingToURL:(NSURL *)location {
81
+ // Check HTTP status code
82
+ if ([downloadTask.response isKindOfClass:[NSHTTPURLResponse class]]) {
83
+ NSInteger statusCode = ((NSHTTPURLResponse *)downloadTask.response).statusCode;
54
84
  if (statusCode >= 400) {
55
- [self.outputFileStream close];
56
- [connection cancel];
57
- NSError *err = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat: @"Received %ld response from %@", (long)statusCode, self.downloadUrl]];
58
- self.failCallback(err);
85
+ NSError *err = [CodePushErrorUtils errorWithMessage:
86
+ [NSString stringWithFormat:@"Received %ld response from %@",
87
+ (long)statusCode, self.downloadUrl]];
88
+ [self invokeFailCallback:err];
59
89
  return;
60
90
  }
61
91
  }
62
-
63
- self.expectedContentLength = response.expectedContentLength;
64
- [self.outputFileStream open];
65
- }
66
92
 
67
- - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
68
- if (self.receivedContentLength < 4) {
69
- for (int i = 0; i < [data length]; i++) {
70
- int headerOffset = (int)self.receivedContentLength + i;
71
- if (headerOffset >= 4) {
72
- break;
73
- }
93
+ // Move downloaded file from temporary location to our target path
94
+ NSFileManager *fileManager = [NSFileManager defaultManager];
95
+ NSError *moveError = nil;
74
96
 
75
- const char *bytes = [data bytes];
76
- _header[headerOffset] = bytes[i];
77
- }
97
+ // Remove existing file if present
98
+ if ([fileManager fileExistsAtPath:self.downloadFilePath]) {
99
+ [fileManager removeItemAtPath:self.downloadFilePath error:nil];
78
100
  }
79
101
 
80
- self.receivedContentLength = self.receivedContentLength + [data length];
81
-
82
- NSInteger bytesLeft = [data length];
102
+ [fileManager moveItemAtURL:location
103
+ toURL:[NSURL fileURLWithPath:self.downloadFilePath]
104
+ error:&moveError];
83
105
 
84
- do {
85
- NSInteger bytesWritten = [self.outputFileStream write:[data bytes]
86
- maxLength:bytesLeft];
87
- if (bytesWritten == -1) {
88
- break;
89
- }
90
-
91
- bytesLeft -= bytesWritten;
92
- } while (bytesLeft > 0);
106
+ if (moveError) {
107
+ [self invokeFailCallback:moveError];
108
+ return;
109
+ }
93
110
 
94
- self.progressCallback(self.expectedContentLength, self.receivedContentLength);
111
+ if (self.receivedContentLength < 1) {
112
+ NSError *err = [CodePushErrorUtils errorWithMessage:
113
+ [NSString stringWithFormat:@"Received empty response from %@",
114
+ self.downloadUrl]];
115
+ [self invokeFailCallback:err];
116
+ return;
117
+ }
95
118
 
96
- // bytesLeft should not be negative.
97
- assert(bytesLeft >= 0);
119
+ // Determine if the downloaded file is a ZIP by reading its header
120
+ BOOL isZip = NO;
121
+ NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:self.downloadFilePath];
122
+ if (fileHandle) {
123
+ NSData *headerData = [fileHandle readDataOfLength:4];
124
+ [fileHandle closeFile];
125
+ if (headerData.length >= 4) {
126
+ const char *bytes = [headerData bytes];
127
+ isZip = bytes[0] == 'P' && bytes[1] == 'K' && bytes[2] == 3 && bytes[3] == 4;
128
+ }
129
+ }
98
130
 
99
- if (bytesLeft) {
100
- [self.outputFileStream close];
101
- [connection cancel];
102
- self.failCallback([self.outputFileStream streamError]);
131
+ // Send final 100% progress callback
132
+ // Use receivedContentLength for both args to guarantee expected == received at completion
133
+ if (self.progressCallback) {
134
+ self.progressCallback(self.receivedContentLength, self.receivedContentLength);
103
135
  }
104
- }
105
136
 
106
- - (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
107
- {
108
- [self.outputFileStream close];
109
- self.failCallback(error);
137
+ [self invokeDoneCallback:isZip];
110
138
  }
111
139
 
112
- - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
113
- [self.outputFileStream close];
114
- if (self.receivedContentLength < 1) {
115
- NSError *err = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Received empty response from %@", self.downloadUrl]];
116
- self.failCallback(err);
117
- return;
118
- }
119
-
120
- // expectedContentLength might be -1 when NSURLConnection don't know the length(e.g. response encode with gzip)
121
- if (self.expectedContentLength > 0) {
122
- // We should have received all of the bytes if this is called.
123
- assert(self.receivedContentLength == self.expectedContentLength);
124
- }
140
+ #pragma mark - NSURLSessionTaskDelegate
125
141
 
126
- BOOL isZip = _header[0] == 'P' && _header[1] == 'K' && _header[2] == 3 && _header[3] == 4;
127
- self.doneCallback(isZip);
142
+ - (void)URLSession:(NSURLSession *)session
143
+ task:(NSURLSessionTask *)task
144
+ didCompleteWithError:(NSError *)error {
145
+ if (error) {
146
+ [self invokeFailCallback:error];
147
+ }
148
+ [session finishTasksAndInvalidate];
149
+ _session = nil; // Break retain cycle (NSURLSession strong-references its delegate)
128
150
  }
129
151
 
130
152
  @end
@@ -50,6 +50,8 @@ static NSString *const UnzippedFolderName = @"unzipped";
50
50
  progressCallback:(void (^)(long long, long long))progressCallback
51
51
  doneCallback:(void (^)())doneCallback
52
52
  failCallback:(void (^)(NSError *err))failCallback
53
+ installProgressCallback:(void (^)(NSString *, NSNumber *, NSNumber *))installProgressCallback
54
+ downloadCompleteCallback:(void (^)())downloadCompleteCallback
53
55
  {
54
56
  NSString *newUpdateHash = updatePackage[@"packageHash"];
55
57
  NSString *newUpdateFolderPath = [self getPackageFolderPath:newUpdateHash];
@@ -85,6 +87,11 @@ static NSString *const UnzippedFolderName = @"unzipped";
85
87
  operationQueue:operationQueue
86
88
  progressCallback:progressCallback
87
89
  doneCallback:^(BOOL isZip) {
90
+ // HTTP 다운로드 완료 → 후처리(unzip/파일복사) 시작 전 알림
91
+ if (downloadCompleteCallback) {
92
+ downloadCompleteCallback();
93
+ }
94
+
88
95
  NSError *error = nil;
89
96
  NSString * unzippedFolderPath = [CodePushPackage getUnzippedFolderPath];
90
97
  NSMutableDictionary * mutableUpdatePackage = [updatePackage mutableCopy];
@@ -101,8 +108,21 @@ static NSString *const UnzippedFolderName = @"unzipped";
101
108
  }
102
109
 
103
110
  NSError *nonFailingError = nil;
104
- [SSZipArchive unzipFileAtPath:downloadFilePath
105
- toDestination:unzippedFolderPath];
111
+ BOOL unzipSuccess = [SSZipArchive unzipFileAtPath:downloadFilePath
112
+ toDestination:unzippedFolderPath
113
+ overwrite:YES
114
+ password:nil
115
+ progressHandler:^(NSString *entry, unz_file_info zipInfo, long entryNumber, long total) {
116
+ if (installProgressCallback && total > 0) {
117
+ installProgressCallback(@"EXTRACTING", @(entryNumber), @(total));
118
+ }
119
+ }
120
+ completionHandler:nil];
121
+ if (!unzipSuccess) {
122
+ error = [CodePushErrorUtils errorWithMessage:@"Failed to unzip the downloaded update."];
123
+ failCallback(error);
124
+ return;
125
+ }
106
126
  [[NSFileManager defaultManager] removeItemAtPath:downloadFilePath
107
127
  error:&nonFailingError];
108
128
  if (nonFailingError) {
package/package-mixins.js CHANGED
@@ -7,19 +7,31 @@ import log from "./logging";
7
7
  module.exports = (NativeCodePush) => {
8
8
  const remote = () => {
9
9
  return {
10
- async download(downloadProgressCallback) {
10
+ async download(downloadProgressCallback, downloadCompleteCallback) {
11
11
  if (!this.downloadUrl) {
12
12
  throw new Error("Cannot download an update without a download url");
13
13
  }
14
14
 
15
15
  let downloadProgressSubscription;
16
- if (downloadProgressCallback) {
16
+ let downloadCompleteSubscription;
17
+ if (downloadProgressCallback || downloadCompleteCallback) {
17
18
  const codePushEventEmitter = new NativeEventEmitter(NativeCodePush);
18
- // Use event subscription to obtain download progress.
19
- downloadProgressSubscription = codePushEventEmitter.addListener(
20
- "CodePushDownloadProgress",
21
- downloadProgressCallback
22
- );
19
+
20
+ if (downloadProgressCallback) {
21
+ // Use event subscription to obtain download progress.
22
+ downloadProgressSubscription = codePushEventEmitter.addListener(
23
+ "CodePushDownloadProgress",
24
+ downloadProgressCallback
25
+ );
26
+ }
27
+
28
+ if (downloadCompleteCallback) {
29
+ // Listen for download complete event (HTTP download done, post-processing starts)
30
+ downloadCompleteSubscription = codePushEventEmitter.addListener(
31
+ "CodePushDownloadComplete",
32
+ downloadCompleteCallback
33
+ );
34
+ }
23
35
  }
24
36
 
25
37
  // Use the downloaded package info. Native code will save the package info
@@ -33,6 +45,7 @@ module.exports = (NativeCodePush) => {
33
45
  return { ...downloadedPackage, ...local };
34
46
  } finally {
35
47
  downloadProgressSubscription && downloadProgressSubscription.remove();
48
+ downloadCompleteSubscription && downloadCompleteSubscription.remove();
36
49
  }
37
50
  },
38
51
 
@@ -58,4 +71,4 @@ module.exports = (NativeCodePush) => {
58
71
  };
59
72
 
60
73
  return { local, remote };
61
- };
74
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algocare/react-native-code-push",
3
- "version": "12.6.2",
3
+ "version": "12.6.3",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "CodePush.js",
6
6
  "typings": "typings/react-native-code-push.d.ts",
@@ -167,9 +167,11 @@ export interface RemotePackage extends Package {
167
167
  * Downloads the available update from the CodePush service.
168
168
  *
169
169
  * @param downloadProgressCallback An optional callback that allows tracking the progress of the update while it is being downloaded.
170
+ * @param downloadCompleteCallback An optional callback that is called when the HTTP download completes and post-processing begins (iOS only).
170
171
  */
171
172
  download(
172
- downloadProgressCallback?: DownloadProgressCallback
173
+ downloadProgressCallback?: DownloadProgressCallback,
174
+ downloadCompleteCallback?: () => void
173
175
  ): Promise<LocalPackage>
174
176
 
175
177
  /**
@@ -490,6 +492,12 @@ declare namespace CodePush {
490
492
  * An available update was downloaded and is about to be installed.
491
493
  */
492
494
  INSTALLING_UPDATE,
495
+
496
+ /**
497
+ * The update has been downloaded and post-processing (unzip/file copy) is in progress.
498
+ * This status is only reported on iOS when the native module supports it.
499
+ */
500
+ PREPARING_UPDATE,
493
501
  }
494
502
 
495
503
  /**