@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 CHANGED
@@ -1,4 +1,4 @@
1
- # @capawesome/capacitor-live-update
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
- We are proud to offer one of the most complete and feature-rich Capacitor plugins for Over-the-Air (OTA) updates. Here are some of the key features:
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
- ## Newsletter
35
+ ## Use Cases
35
36
 
36
- 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/).
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 | Deprecated |
44
- | 6.x.x | 6.x.x | Deprecated |
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/guides/best-practices/#versioned-channels), you can set a default channel directly in your native project by adding a string resource.
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/guides/best-practices/#versioned-channels), you can set a default channel directly in your native project by adding a key to your `Info.plist` file.
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 deleteBundle = async () => {
223
- await LiveUpdate.deleteBundle({ bundleId: 'my-bundle' });
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
- const fetchChannels = async () => {
231
- const result = await LiveUpdate.fetchChannels();
232
- return result.channels;
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 fetchLatestBundle = async () => {
236
- await LiveUpdate.fetchLatestBundle();
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 getCustomId = async () => {
255
- const result = await LiveUpdate.getCustomId();
256
- return result.customId;
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 getNextBundle = async () => {
265
- const result = await LiveUpdate.getNextBundle();
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
- const ready = async () => {
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
- const reload = async () => {
293
- await LiveUpdate.reload();
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/guides/best-practices/#versioned-channels) where the channel is tied to the build version.
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).
@@ -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().versionCode;
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();
@@ -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) {
@@ -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.3.0";
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
- .addWebViewListener(
94
- new WebViewListener() {
95
- @Override
96
- public void onPageLoaded(WebView webView) {
97
- if (implementation != null) {
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
- return (file, [.createIntermediateDirectories])
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
- AF.download(request, to: destination).downloadProgress { progress in
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.3.0"
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.3.0",
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/plugins/live-update/",
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.0.0",
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.6.7",
85
+ "prettier-plugin-java": "2.9.7",
78
86
  "rimraf": "6.1.2",
79
- "rollup": "4.53.3",
87
+ "rollup": "4.62.3",
80
88
  "swiftlint": "2.0.0",
81
89
  "typescript": "5.9.3"
82
90
  },