react-native-codepush-sdk 1.0.0
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/LICENSE +21 -0
- package/README.md +278 -0
- package/dist/hooks/useFrameworkReady.js +11 -0
- package/dist/index.js +36 -0
- package/dist/services/codepushService.js +164 -0
- package/dist/src/components/UpdateChecker.js +230 -0
- package/dist/src/sdk/CodePushProvider.js +181 -0
- package/dist/src/sdk/CustomCodePush.js +405 -0
- package/dist/src/utils/BundleManager.js +124 -0
- package/dist/types/codepush.js +2 -0
- package/hooks/useFrameworkReady.ts +17 -0
- package/index.ts +10 -0
- package/package.json +72 -0
- package/services/codepushService.ts +181 -0
- package/src/components/UpdateChecker.tsx +303 -0
- package/src/sdk/CodePushProvider.tsx +184 -0
- package/src/sdk/CustomCodePush.ts +526 -0
- package/src/utils/BundleManager.ts +140 -0
- package/types/codepush.ts +44 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
|
|
7
|
+
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
8
|
+
const react_native_fs_1 = __importDefault(require("react-native-fs"));
|
|
9
|
+
const react_native_zip_archive_1 = require("react-native-zip-archive");
|
|
10
|
+
const react_native_1 = require("react-native");
|
|
11
|
+
class CustomCodePush {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.currentPackage = null;
|
|
14
|
+
this.pendingUpdate = null;
|
|
15
|
+
this.isCheckingForUpdate = false;
|
|
16
|
+
this.isDownloading = false;
|
|
17
|
+
this.isInstalling = false;
|
|
18
|
+
this.config = Object.assign({ checkFrequency: 'ON_APP_START', installMode: 'ON_NEXT_RESTART', minimumBackgroundDuration: 0 }, config);
|
|
19
|
+
// Initialize directories and load stored package before SDK use
|
|
20
|
+
this.readyPromise = (async () => {
|
|
21
|
+
await this.initializeDirectories();
|
|
22
|
+
await this.loadCurrentPackage();
|
|
23
|
+
})();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Wait for SDK initialization (directories + stored package loaded)
|
|
27
|
+
*/
|
|
28
|
+
async initialize() {
|
|
29
|
+
return this.readyPromise;
|
|
30
|
+
}
|
|
31
|
+
async initializeDirectories() {
|
|
32
|
+
try {
|
|
33
|
+
await react_native_fs_1.default.mkdir(CustomCodePush.UPDATES_FOLDER);
|
|
34
|
+
await react_native_fs_1.default.mkdir(CustomCodePush.BUNDLES_FOLDER);
|
|
35
|
+
await react_native_fs_1.default.mkdir(CustomCodePush.DOWNLOADS_FOLDER);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.warn('Failed to create directories:', error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async loadCurrentPackage() {
|
|
42
|
+
try {
|
|
43
|
+
const packageData = await async_storage_1.default.getItem(CustomCodePush.CURRENT_PACKAGE_KEY);
|
|
44
|
+
if (packageData) {
|
|
45
|
+
this.currentPackage = JSON.parse(packageData);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.warn('Failed to load current package:', error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async saveCurrentPackage(packageInfo) {
|
|
53
|
+
try {
|
|
54
|
+
await async_storage_1.default.setItem(CustomCodePush.CURRENT_PACKAGE_KEY, JSON.stringify(packageInfo));
|
|
55
|
+
this.currentPackage = packageInfo;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.warn('Failed to save current package:', error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async getDeviceInfo() {
|
|
62
|
+
var _a;
|
|
63
|
+
return {
|
|
64
|
+
platform: react_native_1.Platform.OS,
|
|
65
|
+
platformVersion: react_native_1.Platform.Version,
|
|
66
|
+
appVersion: await react_native_device_info_1.default.getVersion(),
|
|
67
|
+
deviceId: await react_native_device_info_1.default.getUniqueId(),
|
|
68
|
+
deviceModel: await react_native_device_info_1.default.getModel(),
|
|
69
|
+
clientUniqueId: await react_native_device_info_1.default.getUniqueId(),
|
|
70
|
+
currentPackageHash: ((_a = this.currentPackage) === null || _a === void 0 ? void 0 : _a.packageHash) || null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async checkForUpdate() {
|
|
74
|
+
var _a, _b;
|
|
75
|
+
if (this.isCheckingForUpdate) {
|
|
76
|
+
throw new Error('Already checking for update');
|
|
77
|
+
}
|
|
78
|
+
this.isCheckingForUpdate = true;
|
|
79
|
+
try {
|
|
80
|
+
const deviceInfo = await this.getDeviceInfo();
|
|
81
|
+
const response = await fetch(`${this.config.serverUrl}/v0.1/public/codepush/update_check`, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
deploymentKey: this.config.deploymentKey,
|
|
88
|
+
appVersion: deviceInfo.appVersion || '1.0.0',
|
|
89
|
+
packageHash: (_a = this.currentPackage) === null || _a === void 0 ? void 0 : _a.packageHash,
|
|
90
|
+
clientUniqueId: deviceInfo.clientUniqueId,
|
|
91
|
+
label: (_b = this.currentPackage) === null || _b === void 0 ? void 0 : _b.label,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
if (data.updateInfo) {
|
|
99
|
+
const updatePackage = {
|
|
100
|
+
packageHash: data.updateInfo.packageHash,
|
|
101
|
+
label: data.updateInfo.label,
|
|
102
|
+
appVersion: data.updateInfo.appVersion,
|
|
103
|
+
description: data.updateInfo.description || '',
|
|
104
|
+
isMandatory: data.updateInfo.isMandatory || false,
|
|
105
|
+
packageSize: data.updateInfo.size || 0,
|
|
106
|
+
downloadUrl: data.updateInfo.downloadUrl,
|
|
107
|
+
rollout: data.updateInfo.rollout,
|
|
108
|
+
isDisabled: data.updateInfo.isDisabled,
|
|
109
|
+
timestamp: Date.now(),
|
|
110
|
+
};
|
|
111
|
+
this.pendingUpdate = updatePackage;
|
|
112
|
+
return updatePackage;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.log('Error checking for update:', error);
|
|
118
|
+
console.error('Error checking for update:', error);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
this.isCheckingForUpdate = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async downloadUpdate(updatePackage, progressCallback) {
|
|
126
|
+
if (this.isDownloading) {
|
|
127
|
+
throw new Error('Already downloading update');
|
|
128
|
+
}
|
|
129
|
+
this.isDownloading = true;
|
|
130
|
+
try {
|
|
131
|
+
// Check if it's a JavaScript file (demo bundles)
|
|
132
|
+
const isJsFile = updatePackage.downloadUrl.endsWith('.js');
|
|
133
|
+
const fileExtension = isJsFile ? 'js' : 'zip';
|
|
134
|
+
const downloadPath = `${CustomCodePush.DOWNLOADS_FOLDER}/${updatePackage.packageHash}.${fileExtension}`;
|
|
135
|
+
// Clean up any existing download
|
|
136
|
+
if (await react_native_fs_1.default.exists(downloadPath)) {
|
|
137
|
+
await react_native_fs_1.default.unlink(downloadPath);
|
|
138
|
+
}
|
|
139
|
+
const downloadResult = await react_native_fs_1.default.downloadFile({
|
|
140
|
+
fromUrl: updatePackage.downloadUrl,
|
|
141
|
+
toFile: downloadPath,
|
|
142
|
+
progress: (res) => {
|
|
143
|
+
if (progressCallback) {
|
|
144
|
+
progressCallback({
|
|
145
|
+
receivedBytes: res.bytesWritten,
|
|
146
|
+
totalBytes: res.contentLength,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
}).promise;
|
|
151
|
+
if (downloadResult.statusCode !== 200) {
|
|
152
|
+
throw new Error(`Download failed with status ${downloadResult.statusCode}`);
|
|
153
|
+
}
|
|
154
|
+
// Verify file size (approximate for JS files)
|
|
155
|
+
const fileStats = await react_native_fs_1.default.stat(downloadPath);
|
|
156
|
+
if (isJsFile) {
|
|
157
|
+
// For JS files, just check if file exists and has content
|
|
158
|
+
if (fileStats.size === 0) {
|
|
159
|
+
throw new Error('Downloaded JavaScript file is empty');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// For zip files, check exact size
|
|
164
|
+
if (fileStats.size !== updatePackage.packageSize) {
|
|
165
|
+
throw new Error('Downloaded file size mismatch');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let localPath;
|
|
169
|
+
if (isJsFile) {
|
|
170
|
+
// For JavaScript files, create a simple structure
|
|
171
|
+
localPath = `${CustomCodePush.BUNDLES_FOLDER}/${updatePackage.packageHash}`;
|
|
172
|
+
await react_native_fs_1.default.mkdir(localPath);
|
|
173
|
+
// Copy the JS file to the bundle location
|
|
174
|
+
const bundlePath = `${localPath}/index.bundle`;
|
|
175
|
+
await react_native_fs_1.default.copyFile(downloadPath, bundlePath);
|
|
176
|
+
// Clean up download file
|
|
177
|
+
await react_native_fs_1.default.unlink(downloadPath);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// For zip files, extract as before
|
|
181
|
+
localPath = `${CustomCodePush.BUNDLES_FOLDER}/${updatePackage.packageHash}`;
|
|
182
|
+
await react_native_fs_1.default.mkdir(localPath);
|
|
183
|
+
await (0, react_native_zip_archive_1.unzip)(downloadPath, localPath);
|
|
184
|
+
// Clean up download file
|
|
185
|
+
await react_native_fs_1.default.unlink(downloadPath);
|
|
186
|
+
}
|
|
187
|
+
const localPackage = Object.assign(Object.assign({}, updatePackage), { localPath: localPath, isFirstRun: false, failedInstall: false });
|
|
188
|
+
// Save update metadata
|
|
189
|
+
await async_storage_1.default.setItem(`${CustomCodePush.UPDATE_METADATA_KEY}_${updatePackage.packageHash}`, JSON.stringify(localPackage));
|
|
190
|
+
return localPackage;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
console.error('Error downloading update:', error);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
this.isDownloading = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async installUpdate(localPackage) {
|
|
201
|
+
if (this.isInstalling) {
|
|
202
|
+
throw new Error('Already installing update');
|
|
203
|
+
}
|
|
204
|
+
this.isInstalling = true;
|
|
205
|
+
try {
|
|
206
|
+
// Validate the package
|
|
207
|
+
const bundlePath = `${localPackage.localPath}/index.bundle`;
|
|
208
|
+
if (!(await react_native_fs_1.default.exists(bundlePath))) {
|
|
209
|
+
throw new Error('Bundle file not found in update package');
|
|
210
|
+
}
|
|
211
|
+
// Mark as current package
|
|
212
|
+
await this.saveCurrentPackage(localPackage);
|
|
213
|
+
// Clear pending update
|
|
214
|
+
this.pendingUpdate = null;
|
|
215
|
+
await async_storage_1.default.removeItem(CustomCodePush.PENDING_UPDATE_KEY);
|
|
216
|
+
// Log installation
|
|
217
|
+
await this.logUpdateInstallation(localPackage, true);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
console.error('Error installing update:', error);
|
|
221
|
+
await this.logUpdateInstallation(localPackage, false);
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
this.isInstalling = false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async logUpdateInstallation(localPackage, success) {
|
|
229
|
+
try {
|
|
230
|
+
const deviceInfo = await this.getDeviceInfo();
|
|
231
|
+
await fetch(`${this.config.serverUrl}/v0.1/public/codepush/report_status/deploy`, {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: {
|
|
234
|
+
'Content-Type': 'application/json',
|
|
235
|
+
},
|
|
236
|
+
body: JSON.stringify({
|
|
237
|
+
deploymentKey: this.config.deploymentKey,
|
|
238
|
+
label: localPackage.label,
|
|
239
|
+
status: success ? 'Deployed' : 'Failed',
|
|
240
|
+
clientUniqueId: deviceInfo.clientUniqueId,
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.warn('Failed to log update installation:', error);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async sync(options = {}, statusCallback, downloadProgressCallback) {
|
|
249
|
+
try {
|
|
250
|
+
// Check for update
|
|
251
|
+
statusCallback === null || statusCallback === void 0 ? void 0 : statusCallback({ status: 'CHECKING_FOR_UPDATE' });
|
|
252
|
+
const updatePackage = await this.checkForUpdate();
|
|
253
|
+
if (!updatePackage) {
|
|
254
|
+
statusCallback === null || statusCallback === void 0 ? void 0 : statusCallback({ status: 'UP_TO_DATE' });
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
// Show update dialog if needed
|
|
258
|
+
if (options.updateDialog && updatePackage.isMandatory) {
|
|
259
|
+
statusCallback === null || statusCallback === void 0 ? void 0 : statusCallback({ status: 'AWAITING_USER_ACTION' });
|
|
260
|
+
// In a real implementation, you would show a native dialog here
|
|
261
|
+
// For now, we'll proceed automatically
|
|
262
|
+
}
|
|
263
|
+
// Download update
|
|
264
|
+
statusCallback === null || statusCallback === void 0 ? void 0 : statusCallback({ status: 'DOWNLOADING_PACKAGE', progress: 0 });
|
|
265
|
+
const localPackage = await this.downloadUpdate(updatePackage, (progress) => {
|
|
266
|
+
const progressPercent = (progress.receivedBytes / progress.totalBytes) * 100;
|
|
267
|
+
statusCallback === null || statusCallback === void 0 ? void 0 : statusCallback({
|
|
268
|
+
status: 'DOWNLOADING_PACKAGE',
|
|
269
|
+
progress: progressPercent,
|
|
270
|
+
downloadedBytes: progress.receivedBytes,
|
|
271
|
+
totalBytes: progress.totalBytes,
|
|
272
|
+
});
|
|
273
|
+
downloadProgressCallback === null || downloadProgressCallback === void 0 ? void 0 : downloadProgressCallback(progress);
|
|
274
|
+
});
|
|
275
|
+
// Install update
|
|
276
|
+
statusCallback === null || statusCallback === void 0 ? void 0 : statusCallback({ status: 'INSTALLING_UPDATE' });
|
|
277
|
+
await this.installUpdate(localPackage);
|
|
278
|
+
const installMode = updatePackage.isMandatory
|
|
279
|
+
? (options.mandatoryInstallMode || 'IMMEDIATE')
|
|
280
|
+
: (options.installMode || this.config.installMode || 'ON_NEXT_RESTART');
|
|
281
|
+
if (installMode === 'IMMEDIATE') {
|
|
282
|
+
// Restart the app immediately
|
|
283
|
+
this.restartApp();
|
|
284
|
+
}
|
|
285
|
+
statusCallback === null || statusCallback === void 0 ? void 0 : statusCallback({ status: 'UPDATE_INSTALLED' });
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
console.error('Sync error:', error);
|
|
290
|
+
statusCallback === null || statusCallback === void 0 ? void 0 : statusCallback({ status: 'UNKNOWN_ERROR' });
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async getCurrentPackage() {
|
|
295
|
+
return this.currentPackage;
|
|
296
|
+
}
|
|
297
|
+
async getUpdateMetadata() {
|
|
298
|
+
return this.currentPackage;
|
|
299
|
+
}
|
|
300
|
+
async clearUpdates() {
|
|
301
|
+
try {
|
|
302
|
+
// Clear storage
|
|
303
|
+
await async_storage_1.default.multiRemove([
|
|
304
|
+
CustomCodePush.CURRENT_PACKAGE_KEY,
|
|
305
|
+
CustomCodePush.PENDING_UPDATE_KEY,
|
|
306
|
+
CustomCodePush.FAILED_UPDATES_KEY,
|
|
307
|
+
]);
|
|
308
|
+
// Clear files
|
|
309
|
+
if (await react_native_fs_1.default.exists(CustomCodePush.UPDATES_FOLDER)) {
|
|
310
|
+
await react_native_fs_1.default.unlink(CustomCodePush.UPDATES_FOLDER);
|
|
311
|
+
}
|
|
312
|
+
// Reinitialize
|
|
313
|
+
await this.initializeDirectories();
|
|
314
|
+
this.currentPackage = null;
|
|
315
|
+
this.pendingUpdate = null;
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
console.error('Error clearing updates:', error);
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async rollback() {
|
|
323
|
+
if (!this.currentPackage) {
|
|
324
|
+
throw new Error('No current package to rollback from');
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
// Remove current package
|
|
328
|
+
const packagePath = this.currentPackage.localPath;
|
|
329
|
+
if (await react_native_fs_1.default.exists(packagePath)) {
|
|
330
|
+
await react_native_fs_1.default.unlink(packagePath);
|
|
331
|
+
}
|
|
332
|
+
// Clear current package
|
|
333
|
+
await async_storage_1.default.removeItem(CustomCodePush.CURRENT_PACKAGE_KEY);
|
|
334
|
+
this.currentPackage = null;
|
|
335
|
+
// Log rollback
|
|
336
|
+
await this.logRollback();
|
|
337
|
+
// Restart app to use original bundle
|
|
338
|
+
this.restartApp();
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
console.error('Error during rollback:', error);
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async logRollback() {
|
|
346
|
+
var _a;
|
|
347
|
+
try {
|
|
348
|
+
const deviceInfo = await this.getDeviceInfo();
|
|
349
|
+
await fetch(`${this.config.serverUrl}/v0.1/public/codepush/report_status/deploy`, {
|
|
350
|
+
method: 'POST',
|
|
351
|
+
headers: {
|
|
352
|
+
'Content-Type': 'application/json',
|
|
353
|
+
},
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
deploymentKey: this.config.deploymentKey,
|
|
356
|
+
label: ((_a = this.currentPackage) === null || _a === void 0 ? void 0 : _a.label) || 'unknown',
|
|
357
|
+
status: 'Rollback',
|
|
358
|
+
clientUniqueId: deviceInfo.clientUniqueId,
|
|
359
|
+
}),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
console.warn('Failed to log rollback:', error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
restartApp() {
|
|
367
|
+
var _a, _b;
|
|
368
|
+
// In a real implementation, you would use a native module to restart the app
|
|
369
|
+
// For now, we'll just reload the React Native bundle
|
|
370
|
+
if (react_native_1.Platform.OS === 'android') {
|
|
371
|
+
// Android restart implementation
|
|
372
|
+
(_a = react_native_1.NativeModules.DevSettings) === null || _a === void 0 ? void 0 : _a.reload();
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
// iOS restart implementation
|
|
376
|
+
(_b = react_native_1.NativeModules.DevSettings) === null || _b === void 0 ? void 0 : _b.reload();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
getBundleUrl() {
|
|
380
|
+
if (this.currentPackage && this.currentPackage.localPath) {
|
|
381
|
+
return `file://${this.currentPackage.localPath}/index.bundle`;
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
// Static methods for easy integration
|
|
386
|
+
static configure(config) {
|
|
387
|
+
return new CustomCodePush(config);
|
|
388
|
+
}
|
|
389
|
+
static async checkForUpdate(instance) {
|
|
390
|
+
return instance.checkForUpdate();
|
|
391
|
+
}
|
|
392
|
+
static async sync(instance, options, statusCallback, downloadProgressCallback) {
|
|
393
|
+
return instance.sync(options, statusCallback, downloadProgressCallback);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Storage keys
|
|
397
|
+
CustomCodePush.CURRENT_PACKAGE_KEY = 'CustomCodePush_CurrentPackage';
|
|
398
|
+
CustomCodePush.PENDING_UPDATE_KEY = 'CustomCodePush_PendingUpdate';
|
|
399
|
+
CustomCodePush.FAILED_UPDATES_KEY = 'CustomCodePush_FailedUpdates';
|
|
400
|
+
CustomCodePush.UPDATE_METADATA_KEY = 'CustomCodePush_UpdateMetadata';
|
|
401
|
+
// File paths
|
|
402
|
+
CustomCodePush.UPDATES_FOLDER = `${react_native_fs_1.default.DocumentDirectoryPath}/CustomCodePush`;
|
|
403
|
+
CustomCodePush.BUNDLES_FOLDER = `${CustomCodePush.UPDATES_FOLDER}/bundles`;
|
|
404
|
+
CustomCodePush.DOWNLOADS_FOLDER = `${CustomCodePush.UPDATES_FOLDER}/downloads`;
|
|
405
|
+
exports.default = CustomCodePush;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BundleManager = void 0;
|
|
7
|
+
const react_native_fs_1 = __importDefault(require("react-native-fs"));
|
|
8
|
+
const react_native_1 = require("react-native");
|
|
9
|
+
class BundleManager {
|
|
10
|
+
/**
|
|
11
|
+
* Get the path to the current bundle that should be loaded
|
|
12
|
+
*/
|
|
13
|
+
static getCurrentBundlePath() {
|
|
14
|
+
// In a real implementation, this would check if a custom bundle exists
|
|
15
|
+
// and return its path, otherwise return the original bundle path
|
|
16
|
+
return BundleManager.CUSTOM_BUNDLE_PATH;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if a custom bundle exists
|
|
20
|
+
*/
|
|
21
|
+
static async hasCustomBundle() {
|
|
22
|
+
try {
|
|
23
|
+
return await react_native_fs_1.default.exists(BundleManager.CUSTOM_BUNDLE_PATH);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Copy a bundle from source to the current bundle location
|
|
31
|
+
*/
|
|
32
|
+
static async installBundle(sourcePath) {
|
|
33
|
+
try {
|
|
34
|
+
const bundlePath = `${sourcePath}/index.bundle`;
|
|
35
|
+
if (!(await react_native_fs_1.default.exists(bundlePath))) {
|
|
36
|
+
throw new Error('Bundle file not found in update package');
|
|
37
|
+
}
|
|
38
|
+
// Ensure directory exists
|
|
39
|
+
const bundleDir = BundleManager.CUSTOM_BUNDLE_PATH.substring(0, BundleManager.CUSTOM_BUNDLE_PATH.lastIndexOf('/'));
|
|
40
|
+
await react_native_fs_1.default.mkdir(bundleDir);
|
|
41
|
+
// Copy bundle
|
|
42
|
+
await react_native_fs_1.default.copyFile(bundlePath, BundleManager.CUSTOM_BUNDLE_PATH);
|
|
43
|
+
// Copy assets if they exist
|
|
44
|
+
const assetsSourcePath = `${sourcePath}/assets`;
|
|
45
|
+
const assetsDestPath = `${bundleDir}/assets`;
|
|
46
|
+
if (await react_native_fs_1.default.exists(assetsSourcePath)) {
|
|
47
|
+
// Custom folder copy implementation needed here. For now, this is a placeholder.
|
|
48
|
+
// TODO: Implement folder copy logic or use a third-party utility.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error('Failed to install bundle:', error);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Remove the current custom bundle and revert to original
|
|
58
|
+
*/
|
|
59
|
+
static async removeCustomBundle() {
|
|
60
|
+
try {
|
|
61
|
+
if (await react_native_fs_1.default.exists(BundleManager.CUSTOM_BUNDLE_PATH)) {
|
|
62
|
+
await react_native_fs_1.default.unlink(BundleManager.CUSTOM_BUNDLE_PATH);
|
|
63
|
+
}
|
|
64
|
+
// Also remove assets directory
|
|
65
|
+
const bundleDir = BundleManager.CUSTOM_BUNDLE_PATH.substring(0, BundleManager.CUSTOM_BUNDLE_PATH.lastIndexOf('/'));
|
|
66
|
+
const assetsPath = `${bundleDir}/assets`;
|
|
67
|
+
if (await react_native_fs_1.default.exists(assetsPath)) {
|
|
68
|
+
await react_native_fs_1.default.unlink(assetsPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error('Failed to remove custom bundle:', error);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate that a bundle is properly formatted
|
|
78
|
+
*/
|
|
79
|
+
static async validateBundle(bundlePath) {
|
|
80
|
+
try {
|
|
81
|
+
// Check if bundle file exists
|
|
82
|
+
if (!(await react_native_fs_1.default.exists(bundlePath))) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
// Check file size (should be > 0)
|
|
86
|
+
const stats = await react_native_fs_1.default.stat(bundlePath);
|
|
87
|
+
if (stats.size === 0) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
// Additional validation could include:
|
|
91
|
+
// - Checking bundle format
|
|
92
|
+
// - Verifying bundle signature
|
|
93
|
+
// - Testing bundle loading
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.error('Bundle validation failed:', error);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get bundle metadata
|
|
103
|
+
*/
|
|
104
|
+
static async getBundleMetadata(bundlePath) {
|
|
105
|
+
try {
|
|
106
|
+
const metadataPath = `${bundlePath}/metadata.json`;
|
|
107
|
+
if (await react_native_fs_1.default.exists(metadataPath)) {
|
|
108
|
+
const metadataContent = await react_native_fs_1.default.readFile(metadataPath, 'utf8');
|
|
109
|
+
return JSON.parse(metadataContent);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error('Failed to read bundle metadata:', error);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.BundleManager = BundleManager;
|
|
120
|
+
BundleManager.ORIGINAL_BUNDLE_PATH = react_native_1.Platform.select({
|
|
121
|
+
ios: `${react_native_fs_1.default.MainBundlePath}/main.jsbundle`,
|
|
122
|
+
android: 'assets://index.android.bundle',
|
|
123
|
+
});
|
|
124
|
+
BundleManager.CUSTOM_BUNDLE_PATH = `${react_native_fs_1.default.DocumentDirectoryPath}/CustomCodePush/current.bundle`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
frameworkReady?: () => void;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare const window: any;
|
|
10
|
+
|
|
11
|
+
export function useFrameworkReady() {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (typeof window !== 'undefined' && window.frameworkReady) {
|
|
14
|
+
window.frameworkReady();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Main entry point for React Native CodePush SDK
|
|
2
|
+
export { CodePushProvider, useCodePush } from './src/sdk/CodePushProvider';
|
|
3
|
+
export { default as CustomCodePush } from './src/sdk/CustomCodePush';
|
|
4
|
+
export { default as UpdateChecker } from './src/components/UpdateChecker';
|
|
5
|
+
export { BundleManager } from './src/utils/BundleManager';
|
|
6
|
+
export { codePushService } from './services/codepushService';
|
|
7
|
+
export { useFrameworkReady } from './hooks/useFrameworkReady';
|
|
8
|
+
|
|
9
|
+
// Export types
|
|
10
|
+
export * from './types/codepush';
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-codepush-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A React Native CodePush SDK for over-the-air updates",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc --outDir dist",
|
|
9
|
+
"dev": "cd example && npm run start",
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"lint": "eslint .",
|
|
12
|
+
"prepare": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react-native",
|
|
16
|
+
"codepush",
|
|
17
|
+
"over-the-air",
|
|
18
|
+
"updates",
|
|
19
|
+
"mobile",
|
|
20
|
+
"SDK"
|
|
21
|
+
],
|
|
22
|
+
"author": "Bùi Sĩ Nam - hyper-mind.dev",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/picassio/react-native-codepush-sdk.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/picassio/react-native-codepush-sdk#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/picassio/react-native-codepush-sdk/issues"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=16.8.0",
|
|
37
|
+
"react-native": ">=0.60.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@react-native-async-storage/async-storage": "^1.23.1",
|
|
41
|
+
"react-native-device-info": "^10.13.0",
|
|
42
|
+
"react-native-fs": "^2.20.0",
|
|
43
|
+
"react-native-zip-archive": "^6.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@babel/core": "^7.20.0",
|
|
47
|
+
"@babel/preset-env": "^7.20.0",
|
|
48
|
+
"@babel/runtime": "^7.20.0",
|
|
49
|
+
"@types/react": "^18.2.6",
|
|
50
|
+
"@types/react-native": "^0.72.0",
|
|
51
|
+
"babel-jest": "^29.6.3",
|
|
52
|
+
"eslint": "^8.19.0",
|
|
53
|
+
"jest": "^29.6.3",
|
|
54
|
+
"prettier": "2.8.8",
|
|
55
|
+
"typescript": "^5.0.4"
|
|
56
|
+
},
|
|
57
|
+
"files": [
|
|
58
|
+
"dist/",
|
|
59
|
+
"src/components/",
|
|
60
|
+
"src/sdk/",
|
|
61
|
+
"src/utils/",
|
|
62
|
+
"types/",
|
|
63
|
+
"services/",
|
|
64
|
+
"hooks/",
|
|
65
|
+
"index.ts",
|
|
66
|
+
"README.md",
|
|
67
|
+
"LICENSE"
|
|
68
|
+
],
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=18"
|
|
71
|
+
}
|
|
72
|
+
}
|