react-native-codepush-sdk 1.0.1 → 1.0.2

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.
@@ -1,526 +1,526 @@
1
- import AsyncStorage from '@react-native-async-storage/async-storage';
2
- import DeviceInfo from 'react-native-device-info';
3
- import RNFS from 'react-native-fs';
4
- import { unzip } from 'react-native-zip-archive';
5
- import { Platform, NativeModules } from 'react-native';
6
-
7
- export interface CodePushConfiguration {
8
- serverUrl: string;
9
- deploymentKey: string;
10
- appName: string;
11
- checkFrequency?: 'ON_APP_START' | 'ON_APP_RESUME' | 'MANUAL';
12
- installMode?: 'IMMEDIATE' | 'ON_NEXT_RESTART' | 'ON_NEXT_RESUME';
13
- minimumBackgroundDuration?: number;
14
- }
15
-
16
- export interface UpdatePackage {
17
- packageHash: string;
18
- label: string;
19
- appVersion: string;
20
- description: string;
21
- isMandatory: boolean;
22
- packageSize: number;
23
- downloadUrl: string;
24
- rollout?: number;
25
- isDisabled?: boolean;
26
- timestamp: number;
27
- }
28
-
29
- export interface SyncStatus {
30
- status: 'CHECKING_FOR_UPDATE' | 'DOWNLOADING_PACKAGE' | 'INSTALLING_UPDATE' |
31
- 'UP_TO_DATE' | 'UPDATE_INSTALLED' | 'UPDATE_IGNORED' | 'UNKNOWN_ERROR' |
32
- 'AWAITING_USER_ACTION';
33
- progress?: number;
34
- downloadedBytes?: number;
35
- totalBytes?: number;
36
- }
37
-
38
- export interface LocalPackage extends UpdatePackage {
39
- localPath: string;
40
- isFirstRun: boolean;
41
- failedInstall: boolean;
42
- }
43
-
44
- export type SyncStatusCallback = (status: SyncStatus) => void;
45
- export type DownloadProgressCallback = (progress: { receivedBytes: number; totalBytes: number }) => void;
46
-
47
- class CustomCodePush {
48
- private config: CodePushConfiguration;
49
- private currentPackage: LocalPackage | null = null;
50
- // Promise that resolves when directories are initialized and current package loaded
51
- private readyPromise: Promise<void>;
52
- private pendingUpdate: UpdatePackage | null = null;
53
- private isCheckingForUpdate = false;
54
- private isDownloading = false;
55
- private isInstalling = false;
56
-
57
- // Storage keys
58
- private static readonly CURRENT_PACKAGE_KEY = 'CustomCodePush_CurrentPackage';
59
- private static readonly PENDING_UPDATE_KEY = 'CustomCodePush_PendingUpdate';
60
- private static readonly FAILED_UPDATES_KEY = 'CustomCodePush_FailedUpdates';
61
- private static readonly UPDATE_METADATA_KEY = 'CustomCodePush_UpdateMetadata';
62
-
63
- // File paths
64
- private static readonly UPDATES_FOLDER = `${RNFS.DocumentDirectoryPath}/CustomCodePush`;
65
- private static readonly BUNDLES_FOLDER = `${CustomCodePush.UPDATES_FOLDER}/bundles`;
66
- private static readonly DOWNLOADS_FOLDER = `${CustomCodePush.UPDATES_FOLDER}/downloads`;
67
-
68
- constructor(config: CodePushConfiguration) {
69
- this.config = {
70
- checkFrequency: 'ON_APP_START',
71
- installMode: 'ON_NEXT_RESTART',
72
- minimumBackgroundDuration: 0,
73
- ...config,
74
- };
75
- // Initialize directories and load stored package before SDK use
76
- this.readyPromise = (async () => {
77
- await this.initializeDirectories();
78
- await this.loadCurrentPackage();
79
- })();
80
- }
81
-
82
- /**
83
- * Wait for SDK initialization (directories + stored package loaded)
84
- */
85
- public async initialize(): Promise<void> {
86
- return this.readyPromise;
87
- }
88
-
89
- private async initializeDirectories(): Promise<void> {
90
- try {
91
- await RNFS.mkdir(CustomCodePush.UPDATES_FOLDER);
92
- await RNFS.mkdir(CustomCodePush.BUNDLES_FOLDER);
93
- await RNFS.mkdir(CustomCodePush.DOWNLOADS_FOLDER);
94
- } catch (error) {
95
- console.warn('Failed to create directories:', error);
96
- }
97
- }
98
-
99
- private async loadCurrentPackage(): Promise<void> {
100
- try {
101
- const packageData = await AsyncStorage.getItem(CustomCodePush.CURRENT_PACKAGE_KEY);
102
- if (packageData) {
103
- this.currentPackage = JSON.parse(packageData);
104
- }
105
- } catch (error) {
106
- console.warn('Failed to load current package:', error);
107
- }
108
- }
109
-
110
- private async saveCurrentPackage(packageInfo: LocalPackage): Promise<void> {
111
- try {
112
- await AsyncStorage.setItem(CustomCodePush.CURRENT_PACKAGE_KEY, JSON.stringify(packageInfo));
113
- this.currentPackage = packageInfo;
114
- } catch (error) {
115
- console.warn('Failed to save current package:', error);
116
- }
117
- }
118
-
119
- private async getDeviceInfo(): Promise<any> {
120
- return {
121
- platform: Platform.OS,
122
- platformVersion: Platform.Version,
123
- appVersion: await DeviceInfo.getVersion(),
124
- deviceId: await DeviceInfo.getUniqueId(),
125
- deviceModel: await DeviceInfo.getModel(),
126
- clientUniqueId: await DeviceInfo.getUniqueId(),
127
- currentPackageHash: this.currentPackage?.packageHash || null,
128
- };
129
- }
130
-
131
- public async checkForUpdate(): Promise<UpdatePackage | null> {
132
- if (this.isCheckingForUpdate) {
133
- throw new Error('Already checking for update');
134
- }
135
-
136
- this.isCheckingForUpdate = true;
137
-
138
- try {
139
- const deviceInfo = await this.getDeviceInfo();
140
-
141
- const response = await fetch(`${this.config.serverUrl}/v0.1/public/codepush/update_check`, {
142
- method: 'POST',
143
- headers: {
144
- 'Content-Type': 'application/json',
145
- },
146
- body: JSON.stringify({
147
- deploymentKey: this.config.deploymentKey,
148
- appVersion: deviceInfo.appVersion || '1.0.0',
149
- packageHash: this.currentPackage?.packageHash,
150
- clientUniqueId: deviceInfo.clientUniqueId,
151
- label: this.currentPackage?.label,
152
- }),
153
- });
154
-
155
- if (!response.ok) {
156
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
157
- }
158
-
159
- const data = await response.json();
160
-
161
- if (data.updateInfo) {
162
- const updatePackage: UpdatePackage = {
163
- packageHash: data.updateInfo.packageHash,
164
- label: data.updateInfo.label,
165
- appVersion: data.updateInfo.appVersion,
166
- description: data.updateInfo.description || '',
167
- isMandatory: data.updateInfo.isMandatory || false,
168
- packageSize: data.updateInfo.size || 0,
169
- downloadUrl: data.updateInfo.downloadUrl,
170
- rollout: data.updateInfo.rollout,
171
- isDisabled: data.updateInfo.isDisabled,
172
- timestamp: Date.now(),
173
- };
174
-
175
- this.pendingUpdate = updatePackage;
176
- return updatePackage;
177
- }
178
-
179
- return null;
180
- } catch (error) {
181
- console.log('Error checking for update:', error);
182
- console.error('Error checking for update:', error);
183
- throw error;
184
- } finally {
185
- this.isCheckingForUpdate = false;
186
- }
187
- }
188
-
189
- public async downloadUpdate(
190
- updatePackage: UpdatePackage,
191
- progressCallback?: DownloadProgressCallback
192
- ): Promise<LocalPackage> {
193
- if (this.isDownloading) {
194
- throw new Error('Already downloading update');
195
- }
196
-
197
- this.isDownloading = true;
198
-
199
- try {
200
- // Check if it's a JavaScript file (demo bundles)
201
- const isJsFile = updatePackage.downloadUrl.endsWith('.js');
202
- const fileExtension = isJsFile ? 'js' : 'zip';
203
- const downloadPath = `${CustomCodePush.DOWNLOADS_FOLDER}/${updatePackage.packageHash}.${fileExtension}`;
204
-
205
- // Clean up any existing download
206
- if (await RNFS.exists(downloadPath)) {
207
- await RNFS.unlink(downloadPath);
208
- }
209
-
210
- const downloadResult = await RNFS.downloadFile({
211
- fromUrl: updatePackage.downloadUrl,
212
- toFile: downloadPath,
213
- progress: (res) => {
214
- if (progressCallback) {
215
- progressCallback({
216
- receivedBytes: res.bytesWritten,
217
- totalBytes: res.contentLength,
218
- });
219
- }
220
- },
221
- }).promise;
222
-
223
- if (downloadResult.statusCode !== 200) {
224
- throw new Error(`Download failed with status ${downloadResult.statusCode}`);
225
- }
226
-
227
- // Verify file size (approximate for JS files)
228
- const fileStats = await RNFS.stat(downloadPath);
229
- if (isJsFile) {
230
- // For JS files, just check if file exists and has content
231
- if (fileStats.size === 0) {
232
- throw new Error('Downloaded JavaScript file is empty');
233
- }
234
- } else {
235
- // For zip files, check exact size
236
- if (fileStats.size !== updatePackage.packageSize) {
237
- throw new Error('Downloaded file size mismatch');
238
- }
239
- }
240
-
241
- let localPath: string;
242
-
243
- if (isJsFile) {
244
- // For JavaScript files, create a simple structure
245
- localPath = `${CustomCodePush.BUNDLES_FOLDER}/${updatePackage.packageHash}`;
246
- await RNFS.mkdir(localPath);
247
-
248
- // Copy the JS file to the bundle location
249
- const bundlePath = `${localPath}/index.bundle`;
250
- await RNFS.copyFile(downloadPath, bundlePath);
251
-
252
- // Clean up download file
253
- await RNFS.unlink(downloadPath);
254
- } else {
255
- // For zip files, extract as before
256
- localPath = `${CustomCodePush.BUNDLES_FOLDER}/${updatePackage.packageHash}`;
257
- await RNFS.mkdir(localPath);
258
- await unzip(downloadPath, localPath);
259
-
260
- // Clean up download file
261
- await RNFS.unlink(downloadPath);
262
- }
263
-
264
- const localPackage: LocalPackage = {
265
- ...updatePackage,
266
- localPath: localPath,
267
- isFirstRun: false,
268
- failedInstall: false,
269
- };
270
-
271
- // Save update metadata
272
- await AsyncStorage.setItem(
273
- `${CustomCodePush.UPDATE_METADATA_KEY}_${updatePackage.packageHash}`,
274
- JSON.stringify(localPackage)
275
- );
276
-
277
- return localPackage;
278
- } catch (error) {
279
- console.error('Error downloading update:', error);
280
- throw error;
281
- } finally {
282
- this.isDownloading = false;
283
- }
284
- }
285
-
286
- public async installUpdate(localPackage: LocalPackage): Promise<void> {
287
- if (this.isInstalling) {
288
- throw new Error('Already installing update');
289
- }
290
-
291
- this.isInstalling = true;
292
-
293
- try {
294
- // Validate the package
295
- const bundlePath = `${localPackage.localPath}/index.bundle`;
296
- if (!(await RNFS.exists(bundlePath))) {
297
- throw new Error('Bundle file not found in update package');
298
- }
299
-
300
- // Mark as current package
301
- await this.saveCurrentPackage(localPackage);
302
-
303
- // Clear pending update
304
- this.pendingUpdate = null;
305
- await AsyncStorage.removeItem(CustomCodePush.PENDING_UPDATE_KEY);
306
-
307
- // Log installation
308
- await this.logUpdateInstallation(localPackage, true);
309
-
310
- } catch (error) {
311
- console.error('Error installing update:', error);
312
- await this.logUpdateInstallation(localPackage, false);
313
- throw error;
314
- } finally {
315
- this.isInstalling = false;
316
- }
317
- }
318
-
319
- private async logUpdateInstallation(localPackage: LocalPackage, success: boolean): Promise<void> {
320
- try {
321
- const deviceInfo = await this.getDeviceInfo();
322
-
323
- await fetch(`${this.config.serverUrl}/v0.1/public/codepush/report_status/deploy`, {
324
- method: 'POST',
325
- headers: {
326
- 'Content-Type': 'application/json',
327
- },
328
- body: JSON.stringify({
329
- deploymentKey: this.config.deploymentKey,
330
- label: localPackage.label,
331
- status: success ? 'Deployed' : 'Failed',
332
- clientUniqueId: deviceInfo.clientUniqueId,
333
- }),
334
- });
335
- } catch (error) {
336
- console.warn('Failed to log update installation:', error);
337
- }
338
- }
339
-
340
- public async sync(
341
- options: {
342
- installMode?: 'IMMEDIATE' | 'ON_NEXT_RESTART' | 'ON_NEXT_RESUME';
343
- mandatoryInstallMode?: 'IMMEDIATE' | 'ON_NEXT_RESTART' | 'ON_NEXT_RESUME';
344
- updateDialog?: boolean;
345
- rollbackRetryOptions?: {
346
- delayInHours?: number;
347
- maxRetryAttempts?: number;
348
- };
349
- } = {},
350
- statusCallback?: SyncStatusCallback,
351
- downloadProgressCallback?: DownloadProgressCallback
352
- ): Promise<boolean> {
353
- try {
354
- // Check for update
355
- statusCallback?.({ status: 'CHECKING_FOR_UPDATE' });
356
- const updatePackage = await this.checkForUpdate();
357
-
358
- if (!updatePackage) {
359
- statusCallback?.({ status: 'UP_TO_DATE' });
360
- return false;
361
- }
362
-
363
- // Show update dialog if needed
364
- if (options.updateDialog && updatePackage.isMandatory) {
365
- statusCallback?.({ status: 'AWAITING_USER_ACTION' });
366
- // In a real implementation, you would show a native dialog here
367
- // For now, we'll proceed automatically
368
- }
369
-
370
- // Download update
371
- statusCallback?.({ status: 'DOWNLOADING_PACKAGE', progress: 0 });
372
- const localPackage = await this.downloadUpdate(updatePackage, (progress) => {
373
- const progressPercent = (progress.receivedBytes / progress.totalBytes) * 100;
374
- statusCallback?.({
375
- status: 'DOWNLOADING_PACKAGE',
376
- progress: progressPercent,
377
- downloadedBytes: progress.receivedBytes,
378
- totalBytes: progress.totalBytes,
379
- });
380
- downloadProgressCallback?.(progress);
381
- });
382
-
383
- // Install update
384
- statusCallback?.({ status: 'INSTALLING_UPDATE' });
385
- await this.installUpdate(localPackage);
386
-
387
- const installMode = updatePackage.isMandatory
388
- ? (options.mandatoryInstallMode || 'IMMEDIATE')
389
- : (options.installMode || this.config.installMode || 'ON_NEXT_RESTART');
390
-
391
- if (installMode === 'IMMEDIATE') {
392
- // Restart the app immediately
393
- this.restartApp();
394
- }
395
-
396
- statusCallback?.({ status: 'UPDATE_INSTALLED' });
397
- return true;
398
-
399
- } catch (error) {
400
- console.error('Sync error:', error);
401
- statusCallback?.({ status: 'UNKNOWN_ERROR' });
402
- return false;
403
- }
404
- }
405
-
406
- public async getCurrentPackage(): Promise<LocalPackage | null> {
407
- return this.currentPackage;
408
- }
409
-
410
- public async getUpdateMetadata(): Promise<LocalPackage | null> {
411
- return this.currentPackage;
412
- }
413
-
414
- public async clearUpdates(): Promise<void> {
415
- try {
416
- // Clear storage
417
- await AsyncStorage.multiRemove([
418
- CustomCodePush.CURRENT_PACKAGE_KEY,
419
- CustomCodePush.PENDING_UPDATE_KEY,
420
- CustomCodePush.FAILED_UPDATES_KEY,
421
- ]);
422
-
423
- // Clear files
424
- if (await RNFS.exists(CustomCodePush.UPDATES_FOLDER)) {
425
- await RNFS.unlink(CustomCodePush.UPDATES_FOLDER);
426
- }
427
-
428
- // Reinitialize
429
- await this.initializeDirectories();
430
- this.currentPackage = null;
431
- this.pendingUpdate = null;
432
-
433
- } catch (error) {
434
- console.error('Error clearing updates:', error);
435
- throw error;
436
- }
437
- }
438
-
439
- public async rollback(): Promise<void> {
440
- if (!this.currentPackage) {
441
- throw new Error('No current package to rollback from');
442
- }
443
-
444
- try {
445
- // Remove current package
446
- const packagePath = this.currentPackage.localPath;
447
- if (await RNFS.exists(packagePath)) {
448
- await RNFS.unlink(packagePath);
449
- }
450
-
451
- // Clear current package
452
- await AsyncStorage.removeItem(CustomCodePush.CURRENT_PACKAGE_KEY);
453
- this.currentPackage = null;
454
-
455
- // Log rollback
456
- await this.logRollback();
457
-
458
- // Restart app to use original bundle
459
- this.restartApp();
460
-
461
- } catch (error) {
462
- console.error('Error during rollback:', error);
463
- throw error;
464
- }
465
- }
466
-
467
- private async logRollback(): Promise<void> {
468
- try {
469
- const deviceInfo = await this.getDeviceInfo();
470
-
471
- await fetch(`${this.config.serverUrl}/v0.1/public/codepush/report_status/deploy`, {
472
- method: 'POST',
473
- headers: {
474
- 'Content-Type': 'application/json',
475
- },
476
- body: JSON.stringify({
477
- deploymentKey: this.config.deploymentKey,
478
- label: this.currentPackage?.label || 'unknown',
479
- status: 'Rollback',
480
- clientUniqueId: deviceInfo.clientUniqueId,
481
- }),
482
- });
483
- } catch (error) {
484
- console.warn('Failed to log rollback:', error);
485
- }
486
- }
487
-
488
- private restartApp(): void {
489
- // In a real implementation, you would use a native module to restart the app
490
- // For now, we'll just reload the React Native bundle
491
- if (Platform.OS === 'android') {
492
- // Android restart implementation
493
- NativeModules.DevSettings?.reload();
494
- } else {
495
- // iOS restart implementation
496
- NativeModules.DevSettings?.reload();
497
- }
498
- }
499
-
500
- public getBundleUrl(): string | null {
501
- if (this.currentPackage && this.currentPackage.localPath) {
502
- return `file://${this.currentPackage.localPath}/index.bundle`;
503
- }
504
- return null;
505
- }
506
-
507
- // Static methods for easy integration
508
- public static configure(config: CodePushConfiguration): CustomCodePush {
509
- return new CustomCodePush(config);
510
- }
511
-
512
- public static async checkForUpdate(instance: CustomCodePush): Promise<UpdatePackage | null> {
513
- return instance.checkForUpdate();
514
- }
515
-
516
- public static async sync(
517
- instance: CustomCodePush,
518
- options?: any,
519
- statusCallback?: SyncStatusCallback,
520
- downloadProgressCallback?: DownloadProgressCallback
521
- ): Promise<boolean> {
522
- return instance.sync(options, statusCallback, downloadProgressCallback);
523
- }
524
- }
525
-
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+ import DeviceInfo from 'react-native-device-info';
3
+ import RNFS from 'react-native-fs';
4
+ import { unzip } from 'react-native-zip-archive';
5
+ import { Platform, NativeModules } from 'react-native';
6
+
7
+ export interface CodePushConfiguration {
8
+ serverUrl: string;
9
+ deploymentKey: string;
10
+ appName: string;
11
+ checkFrequency?: 'ON_APP_START' | 'ON_APP_RESUME' | 'MANUAL';
12
+ installMode?: 'IMMEDIATE' | 'ON_NEXT_RESTART' | 'ON_NEXT_RESUME';
13
+ minimumBackgroundDuration?: number;
14
+ }
15
+
16
+ export interface UpdatePackage {
17
+ packageHash: string;
18
+ label: string;
19
+ appVersion: string;
20
+ description: string;
21
+ isMandatory: boolean;
22
+ packageSize: number;
23
+ downloadUrl: string;
24
+ rollout?: number;
25
+ isDisabled?: boolean;
26
+ timestamp: number;
27
+ }
28
+
29
+ export interface SyncStatus {
30
+ status: 'CHECKING_FOR_UPDATE' | 'DOWNLOADING_PACKAGE' | 'INSTALLING_UPDATE' |
31
+ 'UP_TO_DATE' | 'UPDATE_INSTALLED' | 'UPDATE_IGNORED' | 'UNKNOWN_ERROR' |
32
+ 'AWAITING_USER_ACTION';
33
+ progress?: number;
34
+ downloadedBytes?: number;
35
+ totalBytes?: number;
36
+ }
37
+
38
+ export interface LocalPackage extends UpdatePackage {
39
+ localPath: string;
40
+ isFirstRun: boolean;
41
+ failedInstall: boolean;
42
+ }
43
+
44
+ export type SyncStatusCallback = (status: SyncStatus) => void;
45
+ export type DownloadProgressCallback = (progress: { receivedBytes: number; totalBytes: number }) => void;
46
+
47
+ class CustomCodePush {
48
+ private config: CodePushConfiguration;
49
+ private currentPackage: LocalPackage | null = null;
50
+ // Promise that resolves when directories are initialized and current package loaded
51
+ private readyPromise: Promise<void>;
52
+ private pendingUpdate: UpdatePackage | null = null;
53
+ private isCheckingForUpdate = false;
54
+ private isDownloading = false;
55
+ private isInstalling = false;
56
+
57
+ // Storage keys
58
+ private static readonly CURRENT_PACKAGE_KEY = 'CustomCodePush_CurrentPackage';
59
+ private static readonly PENDING_UPDATE_KEY = 'CustomCodePush_PendingUpdate';
60
+ private static readonly FAILED_UPDATES_KEY = 'CustomCodePush_FailedUpdates';
61
+ private static readonly UPDATE_METADATA_KEY = 'CustomCodePush_UpdateMetadata';
62
+
63
+ // File paths
64
+ private static readonly UPDATES_FOLDER = `${RNFS.DocumentDirectoryPath}/CustomCodePush`;
65
+ private static readonly BUNDLES_FOLDER = `${CustomCodePush.UPDATES_FOLDER}/bundles`;
66
+ private static readonly DOWNLOADS_FOLDER = `${CustomCodePush.UPDATES_FOLDER}/downloads`;
67
+
68
+ constructor(config: CodePushConfiguration) {
69
+ this.config = {
70
+ checkFrequency: 'ON_APP_START',
71
+ installMode: 'ON_NEXT_RESTART',
72
+ minimumBackgroundDuration: 0,
73
+ ...config,
74
+ };
75
+ // Initialize directories and load stored package before SDK use
76
+ this.readyPromise = (async () => {
77
+ await this.initializeDirectories();
78
+ await this.loadCurrentPackage();
79
+ })();
80
+ }
81
+
82
+ /**
83
+ * Wait for SDK initialization (directories + stored package loaded)
84
+ */
85
+ public async initialize(): Promise<void> {
86
+ return this.readyPromise;
87
+ }
88
+
89
+ private async initializeDirectories(): Promise<void> {
90
+ try {
91
+ await RNFS.mkdir(CustomCodePush.UPDATES_FOLDER);
92
+ await RNFS.mkdir(CustomCodePush.BUNDLES_FOLDER);
93
+ await RNFS.mkdir(CustomCodePush.DOWNLOADS_FOLDER);
94
+ } catch (error) {
95
+ console.warn('Failed to create directories:', error);
96
+ }
97
+ }
98
+
99
+ private async loadCurrentPackage(): Promise<void> {
100
+ try {
101
+ const packageData = await AsyncStorage.getItem(CustomCodePush.CURRENT_PACKAGE_KEY);
102
+ if (packageData) {
103
+ this.currentPackage = JSON.parse(packageData);
104
+ }
105
+ } catch (error) {
106
+ console.warn('Failed to load current package:', error);
107
+ }
108
+ }
109
+
110
+ private async saveCurrentPackage(packageInfo: LocalPackage): Promise<void> {
111
+ try {
112
+ await AsyncStorage.setItem(CustomCodePush.CURRENT_PACKAGE_KEY, JSON.stringify(packageInfo));
113
+ this.currentPackage = packageInfo;
114
+ } catch (error) {
115
+ console.warn('Failed to save current package:', error);
116
+ }
117
+ }
118
+
119
+ private async getDeviceInfo(): Promise<any> {
120
+ return {
121
+ platform: Platform.OS,
122
+ platformVersion: Platform.Version,
123
+ appVersion: await DeviceInfo.getVersion(),
124
+ deviceId: await DeviceInfo.getUniqueId(),
125
+ deviceModel: await DeviceInfo.getModel(),
126
+ clientUniqueId: await DeviceInfo.getUniqueId(),
127
+ currentPackageHash: this.currentPackage?.packageHash || null,
128
+ };
129
+ }
130
+
131
+ public async checkForUpdate(): Promise<UpdatePackage | null> {
132
+ if (this.isCheckingForUpdate) {
133
+ throw new Error('Already checking for update');
134
+ }
135
+
136
+ this.isCheckingForUpdate = true;
137
+
138
+ try {
139
+ const deviceInfo = await this.getDeviceInfo();
140
+
141
+ const response = await fetch(`${this.config.serverUrl}/api/v1/update_check`, {
142
+ method: 'POST',
143
+ headers: {
144
+ 'Content-Type': 'application/json',
145
+ },
146
+ body: JSON.stringify({
147
+ deploymentKey: this.config.deploymentKey,
148
+ appVersion: deviceInfo.appVersion || '1.0.0',
149
+ packageHash: this.currentPackage?.packageHash,
150
+ clientUniqueId: deviceInfo.clientUniqueId,
151
+ label: this.currentPackage?.label,
152
+ }),
153
+ });
154
+
155
+ if (!response.ok) {
156
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
157
+ }
158
+
159
+ const data = await response.json();
160
+
161
+ if (data.updateInfo) {
162
+ const updatePackage: UpdatePackage = {
163
+ packageHash: data.updateInfo.packageHash,
164
+ label: data.updateInfo.label,
165
+ appVersion: data.updateInfo.appVersion,
166
+ description: data.updateInfo.description || '',
167
+ isMandatory: data.updateInfo.isMandatory || false,
168
+ packageSize: data.updateInfo.size || 0,
169
+ downloadUrl: data.updateInfo.downloadUrl,
170
+ rollout: data.updateInfo.rollout,
171
+ isDisabled: data.updateInfo.isDisabled,
172
+ timestamp: Date.now(),
173
+ };
174
+
175
+ this.pendingUpdate = updatePackage;
176
+ return updatePackage;
177
+ }
178
+
179
+ return null;
180
+ } catch (error) {
181
+ console.log('Error checking for update:', error);
182
+ console.error('Error checking for update:', error);
183
+ throw error;
184
+ } finally {
185
+ this.isCheckingForUpdate = false;
186
+ }
187
+ }
188
+
189
+ public async downloadUpdate(
190
+ updatePackage: UpdatePackage,
191
+ progressCallback?: DownloadProgressCallback
192
+ ): Promise<LocalPackage> {
193
+ if (this.isDownloading) {
194
+ throw new Error('Already downloading update');
195
+ }
196
+
197
+ this.isDownloading = true;
198
+
199
+ try {
200
+ // Check if it's a JavaScript file (demo bundles)
201
+ const isJsFile = updatePackage.downloadUrl.endsWith('.js');
202
+ const fileExtension = isJsFile ? 'js' : 'zip';
203
+ const downloadPath = `${CustomCodePush.DOWNLOADS_FOLDER}/${updatePackage.packageHash}.${fileExtension}`;
204
+
205
+ // Clean up any existing download
206
+ if (await RNFS.exists(downloadPath)) {
207
+ await RNFS.unlink(downloadPath);
208
+ }
209
+
210
+ const downloadResult = await RNFS.downloadFile({
211
+ fromUrl: updatePackage.downloadUrl,
212
+ toFile: downloadPath,
213
+ progress: (res) => {
214
+ if (progressCallback) {
215
+ progressCallback({
216
+ receivedBytes: res.bytesWritten,
217
+ totalBytes: res.contentLength,
218
+ });
219
+ }
220
+ },
221
+ }).promise;
222
+
223
+ if (downloadResult.statusCode !== 200) {
224
+ throw new Error(`Download failed with status ${downloadResult.statusCode}`);
225
+ }
226
+
227
+ // Verify file size (approximate for JS files)
228
+ const fileStats = await RNFS.stat(downloadPath);
229
+ if (isJsFile) {
230
+ // For JS files, just check if file exists and has content
231
+ if (fileStats.size === 0) {
232
+ throw new Error('Downloaded JavaScript file is empty');
233
+ }
234
+ } else {
235
+ // For zip files, check exact size
236
+ if (fileStats.size !== updatePackage.packageSize) {
237
+ throw new Error('Downloaded file size mismatch');
238
+ }
239
+ }
240
+
241
+ let localPath: string;
242
+
243
+ if (isJsFile) {
244
+ // For JavaScript files, create a simple structure
245
+ localPath = `${CustomCodePush.BUNDLES_FOLDER}/${updatePackage.packageHash}`;
246
+ await RNFS.mkdir(localPath);
247
+
248
+ // Copy the JS file to the bundle location
249
+ const bundlePath = `${localPath}/index.bundle`;
250
+ await RNFS.copyFile(downloadPath, bundlePath);
251
+
252
+ // Clean up download file
253
+ await RNFS.unlink(downloadPath);
254
+ } else {
255
+ // For zip files, extract as before
256
+ localPath = `${CustomCodePush.BUNDLES_FOLDER}/${updatePackage.packageHash}`;
257
+ await RNFS.mkdir(localPath);
258
+ await unzip(downloadPath, localPath);
259
+
260
+ // Clean up download file
261
+ await RNFS.unlink(downloadPath);
262
+ }
263
+
264
+ const localPackage: LocalPackage = {
265
+ ...updatePackage,
266
+ localPath: localPath,
267
+ isFirstRun: false,
268
+ failedInstall: false,
269
+ };
270
+
271
+ // Save update metadata
272
+ await AsyncStorage.setItem(
273
+ `${CustomCodePush.UPDATE_METADATA_KEY}_${updatePackage.packageHash}`,
274
+ JSON.stringify(localPackage)
275
+ );
276
+
277
+ return localPackage;
278
+ } catch (error) {
279
+ console.error('Error downloading update:', error);
280
+ throw error;
281
+ } finally {
282
+ this.isDownloading = false;
283
+ }
284
+ }
285
+
286
+ public async installUpdate(localPackage: LocalPackage): Promise<void> {
287
+ if (this.isInstalling) {
288
+ throw new Error('Already installing update');
289
+ }
290
+
291
+ this.isInstalling = true;
292
+
293
+ try {
294
+ // Validate the package
295
+ const bundlePath = `${localPackage.localPath}/index.bundle`;
296
+ if (!(await RNFS.exists(bundlePath))) {
297
+ throw new Error('Bundle file not found in update package');
298
+ }
299
+
300
+ // Mark as current package
301
+ await this.saveCurrentPackage(localPackage);
302
+
303
+ // Clear pending update
304
+ this.pendingUpdate = null;
305
+ await AsyncStorage.removeItem(CustomCodePush.PENDING_UPDATE_KEY);
306
+
307
+ // Log installation
308
+ await this.logUpdateInstallation(localPackage, true);
309
+
310
+ } catch (error) {
311
+ console.error('Error installing update:', error);
312
+ await this.logUpdateInstallation(localPackage, false);
313
+ throw error;
314
+ } finally {
315
+ this.isInstalling = false;
316
+ }
317
+ }
318
+
319
+ private async logUpdateInstallation(localPackage: LocalPackage, success: boolean): Promise<void> {
320
+ try {
321
+ const deviceInfo = await this.getDeviceInfo();
322
+
323
+ await fetch(`${this.config.serverUrl}/api/v1/report_status/deploy`, {
324
+ method: 'POST',
325
+ headers: {
326
+ 'Content-Type': 'application/json',
327
+ },
328
+ body: JSON.stringify({
329
+ deploymentKey: this.config.deploymentKey,
330
+ label: localPackage.label,
331
+ status: success ? 'Deployed' : 'Failed',
332
+ clientUniqueId: deviceInfo.clientUniqueId,
333
+ }),
334
+ });
335
+ } catch (error) {
336
+ console.warn('Failed to log update installation:', error);
337
+ }
338
+ }
339
+
340
+ public async sync(
341
+ options: {
342
+ installMode?: 'IMMEDIATE' | 'ON_NEXT_RESTART' | 'ON_NEXT_RESUME';
343
+ mandatoryInstallMode?: 'IMMEDIATE' | 'ON_NEXT_RESTART' | 'ON_NEXT_RESUME';
344
+ updateDialog?: boolean;
345
+ rollbackRetryOptions?: {
346
+ delayInHours?: number;
347
+ maxRetryAttempts?: number;
348
+ };
349
+ } = {},
350
+ statusCallback?: SyncStatusCallback,
351
+ downloadProgressCallback?: DownloadProgressCallback
352
+ ): Promise<boolean> {
353
+ try {
354
+ // Check for update
355
+ statusCallback?.({ status: 'CHECKING_FOR_UPDATE' });
356
+ const updatePackage = await this.checkForUpdate();
357
+
358
+ if (!updatePackage) {
359
+ statusCallback?.({ status: 'UP_TO_DATE' });
360
+ return false;
361
+ }
362
+
363
+ // Show update dialog if needed
364
+ if (options.updateDialog && updatePackage.isMandatory) {
365
+ statusCallback?.({ status: 'AWAITING_USER_ACTION' });
366
+ // In a real implementation, you would show a native dialog here
367
+ // For now, we'll proceed automatically
368
+ }
369
+
370
+ // Download update
371
+ statusCallback?.({ status: 'DOWNLOADING_PACKAGE', progress: 0 });
372
+ const localPackage = await this.downloadUpdate(updatePackage, (progress) => {
373
+ const progressPercent = (progress.receivedBytes / progress.totalBytes) * 100;
374
+ statusCallback?.({
375
+ status: 'DOWNLOADING_PACKAGE',
376
+ progress: progressPercent,
377
+ downloadedBytes: progress.receivedBytes,
378
+ totalBytes: progress.totalBytes,
379
+ });
380
+ downloadProgressCallback?.(progress);
381
+ });
382
+
383
+ // Install update
384
+ statusCallback?.({ status: 'INSTALLING_UPDATE' });
385
+ await this.installUpdate(localPackage);
386
+
387
+ const installMode = updatePackage.isMandatory
388
+ ? (options.mandatoryInstallMode || 'IMMEDIATE')
389
+ : (options.installMode || this.config.installMode || 'ON_NEXT_RESTART');
390
+
391
+ if (installMode === 'IMMEDIATE') {
392
+ // Restart the app immediately
393
+ this.restartApp();
394
+ }
395
+
396
+ statusCallback?.({ status: 'UPDATE_INSTALLED' });
397
+ return true;
398
+
399
+ } catch (error) {
400
+ console.error('Sync error:', error);
401
+ statusCallback?.({ status: 'UNKNOWN_ERROR' });
402
+ return false;
403
+ }
404
+ }
405
+
406
+ public async getCurrentPackage(): Promise<LocalPackage | null> {
407
+ return this.currentPackage;
408
+ }
409
+
410
+ public async getUpdateMetadata(): Promise<LocalPackage | null> {
411
+ return this.currentPackage;
412
+ }
413
+
414
+ public async clearUpdates(): Promise<void> {
415
+ try {
416
+ // Clear storage
417
+ await AsyncStorage.multiRemove([
418
+ CustomCodePush.CURRENT_PACKAGE_KEY,
419
+ CustomCodePush.PENDING_UPDATE_KEY,
420
+ CustomCodePush.FAILED_UPDATES_KEY,
421
+ ]);
422
+
423
+ // Clear files
424
+ if (await RNFS.exists(CustomCodePush.UPDATES_FOLDER)) {
425
+ await RNFS.unlink(CustomCodePush.UPDATES_FOLDER);
426
+ }
427
+
428
+ // Reinitialize
429
+ await this.initializeDirectories();
430
+ this.currentPackage = null;
431
+ this.pendingUpdate = null;
432
+
433
+ } catch (error) {
434
+ console.error('Error clearing updates:', error);
435
+ throw error;
436
+ }
437
+ }
438
+
439
+ public async rollback(): Promise<void> {
440
+ if (!this.currentPackage) {
441
+ throw new Error('No current package to rollback from');
442
+ }
443
+
444
+ try {
445
+ // Remove current package
446
+ const packagePath = this.currentPackage.localPath;
447
+ if (await RNFS.exists(packagePath)) {
448
+ await RNFS.unlink(packagePath);
449
+ }
450
+
451
+ // Clear current package
452
+ await AsyncStorage.removeItem(CustomCodePush.CURRENT_PACKAGE_KEY);
453
+ this.currentPackage = null;
454
+
455
+ // Log rollback
456
+ await this.logRollback();
457
+
458
+ // Restart app to use original bundle
459
+ this.restartApp();
460
+
461
+ } catch (error) {
462
+ console.error('Error during rollback:', error);
463
+ throw error;
464
+ }
465
+ }
466
+
467
+ private async logRollback(): Promise<void> {
468
+ try {
469
+ const deviceInfo = await this.getDeviceInfo();
470
+
471
+ await fetch(`${this.config.serverUrl}/api/v1/report_status/deploy`, {
472
+ method: 'POST',
473
+ headers: {
474
+ 'Content-Type': 'application/json',
475
+ },
476
+ body: JSON.stringify({
477
+ deploymentKey: this.config.deploymentKey,
478
+ label: this.currentPackage?.label || 'unknown',
479
+ status: 'Rollback',
480
+ clientUniqueId: deviceInfo.clientUniqueId,
481
+ }),
482
+ });
483
+ } catch (error) {
484
+ console.warn('Failed to log rollback:', error);
485
+ }
486
+ }
487
+
488
+ private restartApp(): void {
489
+ // In a real implementation, you would use a native module to restart the app
490
+ // For now, we'll just reload the React Native bundle
491
+ if (Platform.OS === 'android') {
492
+ // Android restart implementation
493
+ NativeModules.DevSettings?.reload();
494
+ } else {
495
+ // iOS restart implementation
496
+ NativeModules.DevSettings?.reload();
497
+ }
498
+ }
499
+
500
+ public getBundleUrl(): string | null {
501
+ if (this.currentPackage && this.currentPackage.localPath) {
502
+ return `file://${this.currentPackage.localPath}/index.bundle`;
503
+ }
504
+ return null;
505
+ }
506
+
507
+ // Static methods for easy integration
508
+ public static configure(config: CodePushConfiguration): CustomCodePush {
509
+ return new CustomCodePush(config);
510
+ }
511
+
512
+ public static async checkForUpdate(instance: CustomCodePush): Promise<UpdatePackage | null> {
513
+ return instance.checkForUpdate();
514
+ }
515
+
516
+ public static async sync(
517
+ instance: CustomCodePush,
518
+ options?: any,
519
+ statusCallback?: SyncStatusCallback,
520
+ downloadProgressCallback?: DownloadProgressCallback
521
+ ): Promise<boolean> {
522
+ return instance.sync(options, statusCallback, downloadProgressCallback);
523
+ }
524
+ }
525
+
526
526
  export default CustomCodePush;