@capawesome/capacitor-live-update 8.3.0 → 8.4.1
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/README.md +188 -69
- package/android/build.gradle +1 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +6 -4
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +3 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +9 -10
- package/ios/Plugin/LiveUpdate.swift +2 -1
- package/ios/Plugin/LiveUpdateHttpClient.swift +6 -1
- package/ios/Plugin/LiveUpdatePlugin.swift +1 -1
- package/package.json +16 -8
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Capacitor Live Update Plugin
|
|
2
2
|
|
|
3
3
|
Capacitor plugin that allows you to update your app remotely in real-time without requiring users to download a new version from the app store, known as Over-the-Air (OTA) updates.
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ Capacitor plugin that allows you to update your app remotely in real-time withou
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
The Capacitor Live Update plugin is one of the most feature-rich Over-the-Air (OTA) update solutions for Capacitor apps. Here are some of the key features:
|
|
14
14
|
|
|
15
15
|
- 🔋 Supports **Android and iOS**
|
|
16
16
|
- ⚡️ **Capacitor 6/7/8** support
|
|
@@ -28,27 +28,39 @@ We are proud to offer one of the most complete and feature-rich Capacitor plugin
|
|
|
28
28
|
- 🔒 **Security**: Verify the authenticity and integrity of the bundle using a public key.
|
|
29
29
|
- ⚔️ **Battle-Tested**: Used in more than 1,000 projects to update apps on more than 20,000,000 devices.
|
|
30
30
|
- 🌐 **Open Source**: Licensed under the MIT License.
|
|
31
|
+
- 🤝 **Compatibility**: Works alongside the [App Update](https://capawesome.io/docs/sdks/capacitor/app-update/) and [Firebase Remote Config](https://capawesome.io/docs/sdks/capacitor/firebase/remote-config/) plugins.
|
|
31
32
|
|
|
32
33
|
Missing a feature? Just [open an issue](https://github.com/capawesome-team/capacitor-plugins/issues) and we'll take a look!
|
|
33
34
|
|
|
34
|
-
##
|
|
35
|
+
## Use Cases
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
The Live Update plugin is typically used to ship changes to users without going through the app stores, for example:
|
|
38
|
+
|
|
39
|
+
- **Instant bug fixes**: Deliver critical fixes to your users in real-time without waiting for app store reviews.
|
|
40
|
+
- **Fast feature delivery**: Ship new web-based features as soon as they are ready by publishing a new bundle.
|
|
41
|
+
- **Staged rollouts**: Gradually roll out new bundles to a subset of devices to gather feedback before a full release.
|
|
42
|
+
- **Testing and QA channels**: Distribute preview versions to testers via dedicated channels before promoting them to production.
|
|
43
|
+
- **Safe releases**: Automatically roll back to the default bundle if a faulty update prevents the app from becoming ready.
|
|
37
44
|
|
|
38
45
|
## Compatibility
|
|
39
46
|
|
|
40
47
|
| Plugin Version | Capacitor Version | Status |
|
|
41
48
|
| -------------- | ----------------- | -------------- |
|
|
42
49
|
| 8.x.x | >=8.x.x | Active support |
|
|
43
|
-
| 7.x.x | 7.x.x |
|
|
44
|
-
| 6.x.x | 6.x.x |
|
|
50
|
+
| 7.x.x | 7.x.x | Maintenance |
|
|
51
|
+
| 6.x.x | 6.x.x | Maintenance |
|
|
45
52
|
| 5.x.x | 5.x.x | Deprecated |
|
|
46
53
|
|
|
47
54
|
## Guides
|
|
48
55
|
|
|
49
56
|
- [Getting Started with Capawesome Cloud Live Updates](https://capawesome.io/docs/cloud/live-updates/setup/)
|
|
57
|
+
- [Capacitor Live Updates: The Complete OTA Guide](https://capawesome.io/blog/capacitor-live-updates-guide/)
|
|
58
|
+
- [Best OTA Update Tools for Capacitor (2026)](https://capawesome.io/blog/best-ota-update-tools-for-capacitor/)
|
|
59
|
+
- [Capacitor Live Updates: Signing vs Encryption](https://capawesome.io/blog/capacitor-live-updates-end-to-end-encryption/)
|
|
60
|
+
- [Capacitor Live Updates: Reliability & Uptime](https://capawesome.io/blog/capacitor-live-updates-reliability-and-uptime/)
|
|
50
61
|
- [Migrating from Ionic Appflow to Capawesome Cloud](https://capawesome.io/blog/migrating-from-ionic-appflow-to-capawesome-cloud/)
|
|
51
62
|
- [Migrating from App Center to Capawesome Cloud](https://capawesome.io/blog/migrating-from-app-center-to-capawesome-cloud/)
|
|
63
|
+
- [Live Updates for Nuxt Capacitor Apps](https://capawesome.io/blog/live-updates-for-nuxt-capacitor/)
|
|
52
64
|
|
|
53
65
|
## Installation
|
|
54
66
|
|
|
@@ -76,7 +88,7 @@ npx cap sync
|
|
|
76
88
|
|
|
77
89
|
#### Channel
|
|
78
90
|
|
|
79
|
-
If you are using [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/
|
|
91
|
+
If you are using [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/channels/#versioned-channels), you can set a default channel directly in your native project by adding a string resource.
|
|
80
92
|
This allows you to tie the channel to the version code at build time.
|
|
81
93
|
|
|
82
94
|
Add the following to your app's `build.gradle` file:
|
|
@@ -93,7 +105,7 @@ android {
|
|
|
93
105
|
|
|
94
106
|
If needed, you can define the following project variable in your app’s `variables.gradle` file to change the default version of the dependency:
|
|
95
107
|
|
|
96
|
-
- `$okhttp3Version` version of `com.squareup.okhttp3:okhttp` (default: `5.3.2`)
|
|
108
|
+
- `$okhttp3Version` version of `com.squareup.okhttp3:okhttp` and `com.squareup.okhttp3:okhttp-brotli` (default: `5.3.2`)
|
|
97
109
|
- `$zip4jVersion` version of `net.lingala.zip4j:zip4j` (default: `2.11.5`)
|
|
98
110
|
|
|
99
111
|
This can be useful if you encounter dependency conflicts with other plugins in your project.
|
|
@@ -102,7 +114,7 @@ This can be useful if you encounter dependency conflicts with other plugins in y
|
|
|
102
114
|
|
|
103
115
|
#### Channel
|
|
104
116
|
|
|
105
|
-
If you are using [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/
|
|
117
|
+
If you are using [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/channels/#versioned-channels), you can set a default channel directly in your native project by adding a key to your `Info.plist` file.
|
|
106
118
|
This allows you to tie the channel to the build version at build time.
|
|
107
119
|
|
|
108
120
|
Add the following to your `Info.plist` file:
|
|
@@ -216,56 +228,182 @@ A working example can be found here: [capawesome-team/capacitor-live-update-demo
|
|
|
216
228
|
|
|
217
229
|
## Usage
|
|
218
230
|
|
|
231
|
+
All methods are only available on Android and iOS. The following examples show how to sync with Capawesome Cloud, mark the app as ready, check for and download new bundles, switch between and manage bundles, work with channels, inspect the current and next bundle, identify devices, read the app version, and reset to the default bundle.
|
|
232
|
+
|
|
233
|
+
### Sync with Capawesome Cloud
|
|
234
|
+
|
|
235
|
+
Automatically download and set the latest bundle for the app using [Capawesome Cloud](https://cloud.capawesome.io/). Call `reload()` or restart the app to apply the changes. The `channel` option overrides the channel for this single sync call without persisting it:
|
|
236
|
+
|
|
219
237
|
```typescript
|
|
220
238
|
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
221
239
|
|
|
222
|
-
const
|
|
223
|
-
await LiveUpdate.
|
|
240
|
+
const sync = async () => {
|
|
241
|
+
const result = await LiveUpdate.sync({
|
|
242
|
+
channel: 'production-5',
|
|
243
|
+
});
|
|
244
|
+
return result.nextBundleId;
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Notify the plugin that the app is ready
|
|
249
|
+
|
|
250
|
+
Call `ready()` as soon as the app is ready to use so that the plugin knows no rollback is needed. If the `readyTimeout` configuration option is set and this method is not called in time, the plugin resets the app to the default bundle:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
254
|
+
|
|
255
|
+
const ready = async () => {
|
|
256
|
+
const result = await LiveUpdate.ready();
|
|
257
|
+
if (result.currentBundleId) {
|
|
258
|
+
console.log(`The app is now using the bundle with the identifier ${result.currentBundleId}.`);
|
|
259
|
+
}
|
|
260
|
+
if (result.previousBundleId) {
|
|
261
|
+
console.log(`The app was using the bundle with the identifier ${result.previousBundleId}.`);
|
|
262
|
+
}
|
|
263
|
+
if (result.rollback) {
|
|
264
|
+
console.log('The app was reset to the default bundle.');
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Check if a new bundle is available
|
|
270
|
+
|
|
271
|
+
Fetch the latest bundle from Capawesome Cloud and compare it with the bundle that is currently in use:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
275
|
+
|
|
276
|
+
const fetchLatestBundle = async () => {
|
|
277
|
+
await LiveUpdate.fetchLatestBundle();
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const isNewBundleAvailable = async () => {
|
|
281
|
+
const { bundleId: latestBundleId } = await LiveUpdate.fetchLatestBundle({
|
|
282
|
+
channel: 'production-5',
|
|
283
|
+
});
|
|
284
|
+
if (latestBundleId) {
|
|
285
|
+
const { bundleId: currentBundleId } = await LiveUpdate.getCurrentBundle();
|
|
286
|
+
return latestBundleId !== currentBundleId;
|
|
287
|
+
} else {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
224
290
|
};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Download a bundle from your own server
|
|
294
|
+
|
|
295
|
+
Download a self-hosted bundle from any URL, without any Capawesome Cloud dependency:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
225
299
|
|
|
226
300
|
const downloadBundle = async () => {
|
|
227
301
|
await LiveUpdate.downloadBundle({ url: 'https://example.com/1.0.0.zip', bundleId: '1.0.0' });
|
|
228
302
|
};
|
|
303
|
+
```
|
|
229
304
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
305
|
+
### Switch to a downloaded bundle
|
|
306
|
+
|
|
307
|
+
Set the next bundle to use for the app. Call `reload()` or restart the app to apply the changes:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
311
|
+
|
|
312
|
+
const setNextBundle = async () => {
|
|
313
|
+
await LiveUpdate.setNextBundle({ bundleId: '7f0b9bf2-dff6-4be2-bcac-b068cc5ea756' });
|
|
233
314
|
};
|
|
234
315
|
|
|
235
|
-
const
|
|
236
|
-
await LiveUpdate.
|
|
316
|
+
const reload = async () => {
|
|
317
|
+
await LiveUpdate.reload();
|
|
237
318
|
};
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Manage downloaded bundles
|
|
322
|
+
|
|
323
|
+
Get the identifiers of all bundles that have been downloaded and delete bundles that are no longer needed:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
238
327
|
|
|
239
328
|
const getBundles = async () => {
|
|
240
329
|
const result = await LiveUpdate.getBundles();
|
|
241
330
|
return result.bundleIds;
|
|
242
331
|
};
|
|
243
332
|
|
|
333
|
+
const deleteBundle = async () => {
|
|
334
|
+
await LiveUpdate.deleteBundle({ bundleId: 'my-bundle' });
|
|
335
|
+
};
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Work with channels
|
|
339
|
+
|
|
340
|
+
Set the channel to use for updates, read the currently resolved channel, and fetch the available channels from Capawesome Cloud. The channel set via `setChannel(...)` is persisted across app restarts. Note that `fetchChannels(...)` only works for apps with public channels enabled:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
344
|
+
|
|
345
|
+
const setChannel = async () => {
|
|
346
|
+
await LiveUpdate.setChannel({ channel: 'production-5' });
|
|
347
|
+
};
|
|
348
|
+
|
|
244
349
|
const getChannel = async () => {
|
|
245
350
|
const result = await LiveUpdate.getChannel();
|
|
246
351
|
return result.channel;
|
|
247
352
|
};
|
|
248
353
|
|
|
354
|
+
const fetchChannels = async () => {
|
|
355
|
+
const result = await LiveUpdate.fetchChannels();
|
|
356
|
+
return result.channels;
|
|
357
|
+
};
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Inspect the current and next bundle
|
|
361
|
+
|
|
362
|
+
Get the identifier of the bundle that is currently used by the app and of the bundle that will be used after the next reload or app restart:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
366
|
+
|
|
249
367
|
const getCurrentBundle = async () => {
|
|
250
368
|
const result = await LiveUpdate.getCurrentBundle();
|
|
251
369
|
return result.bundleId;
|
|
252
370
|
};
|
|
253
371
|
|
|
254
|
-
const
|
|
255
|
-
const result = await LiveUpdate.
|
|
256
|
-
return result.
|
|
372
|
+
const getNextBundle = async () => {
|
|
373
|
+
const result = await LiveUpdate.getNextBundle();
|
|
374
|
+
return result.bundleId;
|
|
257
375
|
};
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Identify devices
|
|
379
|
+
|
|
380
|
+
Read the unique device identifier and manage a custom identifier for the device:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
258
384
|
|
|
259
385
|
const getDeviceId = async () => {
|
|
260
386
|
const result = await LiveUpdate.getDeviceId();
|
|
261
387
|
return result.deviceId;
|
|
262
388
|
};
|
|
263
389
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
return result.bundleId;
|
|
390
|
+
const setCustomId = async () => {
|
|
391
|
+
await LiveUpdate.setCustomId({ customId: 'my-custom-id' });
|
|
267
392
|
};
|
|
268
393
|
|
|
394
|
+
const getCustomId = async () => {
|
|
395
|
+
const result = await LiveUpdate.getCustomId();
|
|
396
|
+
return result.customId;
|
|
397
|
+
};
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Read the app version
|
|
401
|
+
|
|
402
|
+
Get the native version code and version name of the app, for example to restrict live updates to compatible native versions:
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
406
|
+
|
|
269
407
|
const getVersionCode = async () => {
|
|
270
408
|
const result = await LiveUpdate.getVersionCode();
|
|
271
409
|
return result.versionCode;
|
|
@@ -275,58 +413,18 @@ const getVersionName = async () => {
|
|
|
275
413
|
const result = await LiveUpdate.getVersionName();
|
|
276
414
|
return result.versionName;
|
|
277
415
|
};
|
|
416
|
+
```
|
|
278
417
|
|
|
279
|
-
|
|
280
|
-
const result = await LiveUpdate.ready();
|
|
281
|
-
if (result.currentBundleId) {
|
|
282
|
-
console.log(`The app is now using the bundle with the identifier ${result.currentBundleId}.`);
|
|
283
|
-
}
|
|
284
|
-
if (result.previousBundleId) {
|
|
285
|
-
console.log(`The app was using the bundle with the identifier ${result.previousBundleId}.`);
|
|
286
|
-
}
|
|
287
|
-
if (result.rollback) {
|
|
288
|
-
console.log('The app was reset to the default bundle.');
|
|
289
|
-
}
|
|
290
|
-
};
|
|
418
|
+
### Reset to the default bundle
|
|
291
419
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
420
|
+
Reset the app to the bundle that shipped with the native app binary. Call `reload()` or restart the app to apply the changes:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
import { LiveUpdate } from '@capawesome/capacitor-live-update';
|
|
295
424
|
|
|
296
425
|
const reset = async () => {
|
|
297
426
|
await LiveUpdate.reset();
|
|
298
427
|
};
|
|
299
|
-
|
|
300
|
-
const setChannel = async () => {
|
|
301
|
-
await LiveUpdate.setChannel({ channel: 'production-5' });
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
const setCustomId = async () => {
|
|
305
|
-
await LiveUpdate.setCustomId({ customId: 'my-custom-id' });
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const setNextBundle = async () => {
|
|
309
|
-
await LiveUpdate.setNextBundle({ bundleId: '7f0b9bf2-dff6-4be2-bcac-b068cc5ea756' });
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
const sync = async () => {
|
|
313
|
-
const result = await LiveUpdate.sync({
|
|
314
|
-
channel: 'production-5',
|
|
315
|
-
});
|
|
316
|
-
return result.nextBundleId;
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const isNewBundleAvailable = async () => {
|
|
320
|
-
const { bundleId: latestBundleId } = await LiveUpdate.fetchLatestBundle({
|
|
321
|
-
channel: 'production-5',
|
|
322
|
-
});
|
|
323
|
-
if (latestBundleId) {
|
|
324
|
-
const { bundleId: currentBundleId } = await LiveUpdate.getCurrentBundle();
|
|
325
|
-
return latestBundleId !== currentBundleId;
|
|
326
|
-
} else {
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
428
|
```
|
|
331
429
|
|
|
332
430
|
## API
|
|
@@ -1268,7 +1366,7 @@ On Capawesome Cloud, the bundle ID corresponds to the ID of the app build artifa
|
|
|
1268
1366
|
There are four ways to set a channel, listed from lowest to highest priority:
|
|
1269
1367
|
|
|
1270
1368
|
1. **Capacitor config**: Set the `defaultChannel` property in the [plugin configuration](#configuration). This is the simplest way to set a static default channel.
|
|
1271
|
-
2. **Native config**: Set `CapawesomeLiveUpdateDefaultChannel` in `Info.plist` (iOS) or `capawesome_live_update_default_channel` in `strings.xml` (Android). This is useful for [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/
|
|
1369
|
+
2. **Native config**: Set `CapawesomeLiveUpdateDefaultChannel` in `Info.plist` (iOS) or `capawesome_live_update_default_channel` in `strings.xml` (Android). This is useful for [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/channels/#versioned-channels) where the channel is tied to the build version.
|
|
1272
1370
|
3. **[`setChannel(...)`](#setchannel)**: Set the channel at runtime. The value is persisted across app restarts.
|
|
1273
1371
|
4. **[`sync(...)`](#sync)**: Pass a `channel` option to override the channel for a single sync call. This does **not** persist the channel.
|
|
1274
1372
|
|
|
@@ -1288,6 +1386,27 @@ You then have three options to get back to the default bundle:
|
|
|
1288
1386
|
|
|
1289
1387
|
However, this is only a problem during development. It is not a problem in production, as Capacitor automatically resets to the default bundle after a native update.
|
|
1290
1388
|
|
|
1389
|
+
### Do I have to use Capawesome Cloud?
|
|
1390
|
+
|
|
1391
|
+
No, the plugin also supports self-hosted bundles. You can download a bundle from any URL using the [`downloadBundle(...)`](#downloadbundle) method and apply it with [`setNextBundle(...)`](#setnextbundle), without any Capawesome Cloud dependency. Capawesome Cloud is only required for the convenience features such as [`sync(...)`](#sync), channels, and rollouts.
|
|
1392
|
+
|
|
1393
|
+
### What changes can I ship with a live update?
|
|
1394
|
+
|
|
1395
|
+
Live updates are only supported for [binary-compatible changes](https://capawesome.io/docs/cloud/live-updates/faq/#what-are-binary-compatible-changes) such as HTML, CSS, and JavaScript. If you change native code, for example by adding a new plugin, you need to resubmit your app to the app stores. You should therefore [restrict live updates to compatible native versions](https://capawesome.io/blog/how-to-restrict-capacitor-live-updates-to-native-versions/) of your app, as described in the [Limitations](#limitations) section.
|
|
1396
|
+
|
|
1397
|
+
### How does the rollback mechanism work?
|
|
1398
|
+
|
|
1399
|
+
If the `readyTimeout` configuration option is set, the plugin waits for the app to call the [`ready()`](#ready) method after a new bundle has been applied. If the method is not called within the configured timeout, the plugin assumes the bundle is faulty and resets the app to the default bundle. It is strongly recommended to configure `readyTimeout` (e.g. `10000` ms). Additionally, with the `autoBlockRolledBackBundles` option enabled, bundles that caused a rollback are automatically blocked and skipped in future sync operations.
|
|
1400
|
+
|
|
1401
|
+
## Related Plugins
|
|
1402
|
+
|
|
1403
|
+
- [App Update](https://capawesome.io/docs/sdks/capacitor/app-update/): Assist users with native app updates, which are required for changes that are not binary-compatible.
|
|
1404
|
+
- [Firebase Remote Config](https://capawesome.io/docs/sdks/capacitor/firebase/remote-config/): Change the behavior and appearance of your app remotely without publishing an update.
|
|
1405
|
+
|
|
1406
|
+
## Newsletter
|
|
1407
|
+
|
|
1408
|
+
Stay up to date with the latest news and updates about the Capawesome, Capacitor, and Ionic ecosystem by subscribing to our [Capawesome Newsletter](https://cloud.capawesome.io/newsletter/).
|
|
1409
|
+
|
|
1291
1410
|
## Changelog
|
|
1292
1411
|
|
|
1293
1412
|
See [CHANGELOG.md](https://github.com/capawesome-team/capacitor-plugins/blob/main/packages/live-update/CHANGELOG.md).
|
package/android/build.gradle
CHANGED
|
@@ -56,6 +56,7 @@ dependencies {
|
|
|
56
56
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
57
57
|
implementation "net.lingala.zip4j:zip4j:$zip4jVersion"
|
|
58
58
|
implementation "com.squareup.okhttp3:okhttp:$okhttp3Version"
|
|
59
|
+
implementation "com.squareup.okhttp3:okhttp-brotli:$okhttp3Version"
|
|
59
60
|
testImplementation "junit:junit:$junitVersion"
|
|
60
61
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
61
62
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
@@ -7,9 +7,11 @@ import android.content.pm.PackageManager;
|
|
|
7
7
|
import android.content.res.AssetManager;
|
|
8
8
|
import android.os.Build;
|
|
9
9
|
import android.os.Handler;
|
|
10
|
+
import android.os.Looper;
|
|
10
11
|
import android.util.Base64;
|
|
11
12
|
import androidx.annotation.NonNull;
|
|
12
13
|
import androidx.annotation.Nullable;
|
|
14
|
+
import androidx.core.content.pm.PackageInfoCompat;
|
|
13
15
|
import com.getcapacitor.Bridge;
|
|
14
16
|
import com.getcapacitor.Logger;
|
|
15
17
|
import com.getcapacitor.plugin.WebView;
|
|
@@ -102,7 +104,7 @@ public class LiveUpdate {
|
|
|
102
104
|
private final SharedPreferences.Editor webViewSettingsEditor;
|
|
103
105
|
|
|
104
106
|
private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
|
|
105
|
-
private final Handler rollbackHandler = new Handler();
|
|
107
|
+
private final Handler rollbackHandler = new Handler(Looper.getMainLooper());
|
|
106
108
|
private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
|
|
107
109
|
|
|
108
110
|
private boolean initialPageLoaded = false;
|
|
@@ -1050,7 +1052,7 @@ public class LiveUpdate {
|
|
|
1050
1052
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
1051
1053
|
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
1052
1054
|
Buffer buffer = new Buffer();
|
|
1053
|
-
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
1055
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1; ) {
|
|
1054
1056
|
digest.update(buffer.readByteArray());
|
|
1055
1057
|
}
|
|
1056
1058
|
source.close();
|
|
@@ -1173,7 +1175,7 @@ public class LiveUpdate {
|
|
|
1173
1175
|
}
|
|
1174
1176
|
|
|
1175
1177
|
private int getVersionCodeAsInt() throws PackageManager.NameNotFoundException {
|
|
1176
|
-
return getPackageInfo()
|
|
1178
|
+
return (int) PackageInfoCompat.getLongVersionCode(getPackageInfo());
|
|
1177
1179
|
}
|
|
1178
1180
|
|
|
1179
1181
|
private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
|
|
@@ -1488,7 +1490,7 @@ public class LiveUpdate {
|
|
|
1488
1490
|
sig.initVerify(key);
|
|
1489
1491
|
BufferedSource source = Okio.buffer(Okio.source(file));
|
|
1490
1492
|
Buffer buffer = new Buffer();
|
|
1491
|
-
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
|
|
1493
|
+
for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1; ) {
|
|
1492
1494
|
sig.update(buffer.readByteArray());
|
|
1493
1495
|
}
|
|
1494
1496
|
source.close();
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java
CHANGED
|
@@ -15,6 +15,7 @@ import okhttp3.OkHttpClient;
|
|
|
15
15
|
import okhttp3.Request;
|
|
16
16
|
import okhttp3.Response;
|
|
17
17
|
import okhttp3.ResponseBody;
|
|
18
|
+
import okhttp3.brotli.BrotliInterceptor;
|
|
18
19
|
import okio.Buffer;
|
|
19
20
|
import okio.BufferedSink;
|
|
20
21
|
import okio.BufferedSource;
|
|
@@ -58,6 +59,7 @@ public class LiveUpdateHttpClient {
|
|
|
58
59
|
dispatcher.setMaxRequestsPerHost(30);
|
|
59
60
|
|
|
60
61
|
this.okHttpClient = new OkHttpClient.Builder()
|
|
62
|
+
.addInterceptor(BrotliInterceptor.INSTANCE)
|
|
61
63
|
.dispatcher(dispatcher)
|
|
62
64
|
.connectTimeout(httpTimeout, TimeUnit.MILLISECONDS)
|
|
63
65
|
.readTimeout(httpTimeout, TimeUnit.MILLISECONDS)
|
|
@@ -101,7 +103,7 @@ public class LiveUpdateHttpClient {
|
|
|
101
103
|
Buffer sinkBuffer = sink.getBuffer();
|
|
102
104
|
long totalBytesRead = 0;
|
|
103
105
|
int bufferSize = 8 * 1024;
|
|
104
|
-
for (long bytesRead; (bytesRead = source.read(sinkBuffer, bufferSize)) != -1;) {
|
|
106
|
+
for (long bytesRead; (bytesRead = source.read(sinkBuffer, bufferSize)) != -1; ) {
|
|
105
107
|
sink.emit();
|
|
106
108
|
totalBytesRead += bytesRead;
|
|
107
109
|
if (callback != null) {
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java
CHANGED
|
@@ -37,7 +37,7 @@ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
|
|
|
37
37
|
public class LiveUpdatePlugin extends Plugin {
|
|
38
38
|
|
|
39
39
|
public static final String TAG = "LiveUpdate";
|
|
40
|
-
public static final String VERSION = "8.
|
|
40
|
+
public static final String VERSION = "8.4.1";
|
|
41
41
|
public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
|
|
42
42
|
public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
|
|
43
43
|
public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
|
|
@@ -89,17 +89,16 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
89
89
|
// or constructor, it MUST be done here in onResume().
|
|
90
90
|
if (!webViewListenerRegistered) {
|
|
91
91
|
webViewListenerRegistered = true;
|
|
92
|
-
getBridge()
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
implementation.handleOnPageLoaded();
|
|
99
|
-
}
|
|
92
|
+
getBridge().addWebViewListener(
|
|
93
|
+
new WebViewListener() {
|
|
94
|
+
@Override
|
|
95
|
+
public void onPageLoaded(WebView webView) {
|
|
96
|
+
if (implementation != null) {
|
|
97
|
+
implementation.handleOnPageLoaded();
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
|
-
|
|
100
|
+
}
|
|
101
|
+
);
|
|
103
102
|
}
|
|
104
103
|
} catch (Exception exception) {
|
|
105
104
|
Logger.error(TAG, exception.getMessage(), exception);
|
|
@@ -463,7 +463,8 @@ import CommonCrypto
|
|
|
463
463
|
|
|
464
464
|
private func downloadAndVerifyFile(url: String, file: URL, checksum: String?, signature: String?, callback: ((Progress) -> Void)?) async throws {
|
|
465
465
|
let destination: DownloadRequest.Destination = { _, _ in
|
|
466
|
-
|
|
466
|
+
// `removePreviousFile` ensures that a leftover file from a failed attempt does not fail the download
|
|
467
|
+
return (file, [.createIntermediateDirectories, .removePreviousFile])
|
|
467
468
|
}
|
|
468
469
|
let urlComponents = URLComponents(string: url)!
|
|
469
470
|
let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
|
|
@@ -27,13 +27,16 @@ public class LiveUpdateHttpClient: NSObject {
|
|
|
27
27
|
|
|
28
28
|
public func download(url: URL, destination: @escaping DownloadRequest.Destination, callback: ((Progress) -> Void)?) async throws -> AFDownloadResponse<Data> {
|
|
29
29
|
var request = URLRequest(url: url)
|
|
30
|
+
// Ignore the URL cache so that a cached response is never replayed for a deterministic request URL
|
|
31
|
+
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
30
32
|
request.httpMethod = HTTPMethod.get.rawValue
|
|
31
33
|
request.timeoutInterval = Double(config.httpTimeout) / 1000.0
|
|
32
34
|
if let deviceId = deviceId, !deviceId.isEmpty {
|
|
33
35
|
request.setValue(deviceId, forHTTPHeaderField: "X-Capawesome-Device-Id")
|
|
34
36
|
}
|
|
35
37
|
return try await withCheckedThrowingContinuation { continuation in
|
|
36
|
-
|
|
38
|
+
// `validate()` treats non-2xx responses as errors so that an error body is never saved as a bundle file
|
|
39
|
+
AF.download(request, to: destination).validate().downloadProgress { progress in
|
|
37
40
|
if let callback = callback {
|
|
38
41
|
callback(progress)
|
|
39
42
|
}
|
|
@@ -45,6 +48,8 @@ public class LiveUpdateHttpClient: NSObject {
|
|
|
45
48
|
|
|
46
49
|
public func request<T: Decodable>(url: URL, type: T.Type) async throws -> AFDataResponse<T> {
|
|
47
50
|
var request = URLRequest(url: url)
|
|
51
|
+
// Ignore the URL cache so that a cached response is never replayed for a deterministic request URL
|
|
52
|
+
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
48
53
|
request.httpMethod = HTTPMethod.get.rawValue
|
|
49
54
|
request.timeoutInterval = Double(config.httpTimeout) / 1000.0
|
|
50
55
|
if let deviceId = deviceId, !deviceId.isEmpty {
|
|
@@ -8,7 +8,7 @@ import Capacitor
|
|
|
8
8
|
@objc(LiveUpdatePlugin)
|
|
9
9
|
public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
10
10
|
public static let tag = "LiveUpdate"
|
|
11
|
-
public static let version = "8.
|
|
11
|
+
public static let version = "8.4.1"
|
|
12
12
|
public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
|
|
13
13
|
|
|
14
14
|
public let identifier = "LiveUpdatePlugin"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capawesome/capacitor-live-update",
|
|
3
|
-
"version": "8.
|
|
4
|
-
"description": "Capacitor plugin to update your app remotely in real-time.",
|
|
3
|
+
"version": "8.4.1",
|
|
4
|
+
"description": "Capacitor plugin to update your app remotely in real-time on Android and iOS.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/esm/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"bugs": {
|
|
24
24
|
"url": "https://github.com/capawesome-team/capacitor-plugins/issues"
|
|
25
25
|
},
|
|
26
|
-
"homepage": "https://capawesome.io/docs/
|
|
26
|
+
"homepage": "https://capawesome.io/docs/sdks/capacitor/live-update/",
|
|
27
27
|
"funding": [
|
|
28
28
|
{
|
|
29
29
|
"type": "github",
|
|
@@ -44,7 +44,14 @@
|
|
|
44
44
|
"live update",
|
|
45
45
|
"remote update",
|
|
46
46
|
"ota",
|
|
47
|
-
"capgo"
|
|
47
|
+
"capgo",
|
|
48
|
+
"capacitor-plugin",
|
|
49
|
+
"live updates",
|
|
50
|
+
"ota update",
|
|
51
|
+
"over-the-air updates",
|
|
52
|
+
"code push",
|
|
53
|
+
"hot update",
|
|
54
|
+
"bundle management"
|
|
48
55
|
],
|
|
49
56
|
"scripts": {
|
|
50
57
|
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
@@ -53,7 +60,7 @@
|
|
|
53
60
|
"verify:web": "npm run build",
|
|
54
61
|
"e2e:android": "npm install --prefix example && npm run build && npm --prefix example run e2e:android",
|
|
55
62
|
"e2e:ios": "npm install --prefix example && npm run build && npm --prefix example run e2e:ios",
|
|
56
|
-
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
63
|
+
"lint": "npm run version:check && npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
57
64
|
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
|
|
58
65
|
"eslint": "eslint . --ext ts",
|
|
59
66
|
"prettier": "prettier \"**/*.{css,html,ts,js,java}\"",
|
|
@@ -63,20 +70,21 @@
|
|
|
63
70
|
"clean": "rimraf ./dist",
|
|
64
71
|
"watch": "tsc --watch",
|
|
65
72
|
"version": "node scripts/sync-plugin-version.mjs",
|
|
73
|
+
"version:check": "node scripts/sync-plugin-version.mjs --check",
|
|
66
74
|
"ios:pod:install": "cd ios && pod install --repo-update && cd ..",
|
|
67
75
|
"prepublishOnly": "npm run build"
|
|
68
76
|
},
|
|
69
77
|
"devDependencies": {
|
|
70
78
|
"@capacitor/android": "8.0.0",
|
|
71
|
-
"@capacitor/cli": "8.
|
|
79
|
+
"@capacitor/cli": "8.4.2",
|
|
72
80
|
"@capacitor/core": "8.0.0",
|
|
73
81
|
"@capacitor/docgen": "0.3.1",
|
|
74
82
|
"@capacitor/ios": "8.0.0",
|
|
75
83
|
"@ionic/eslint-config": "0.4.0",
|
|
76
84
|
"eslint": "8.57.0",
|
|
77
|
-
"prettier-plugin-java": "2.
|
|
85
|
+
"prettier-plugin-java": "2.9.7",
|
|
78
86
|
"rimraf": "6.1.2",
|
|
79
|
-
"rollup": "4.
|
|
87
|
+
"rollup": "4.62.3",
|
|
80
88
|
"swiftlint": "2.0.0",
|
|
81
89
|
"typescript": "5.9.3"
|
|
82
90
|
},
|