@capawesome/capacitor-live-update 8.3.0 → 8.4.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/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,20 +28,27 @@ 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
@@ -49,6 +56,7 @@ Stay up to date with the latest news and updates about the Capawesome, Capacitor
49
56
  - [Getting Started with Capawesome Cloud Live Updates](https://capawesome.io/docs/cloud/live-updates/setup/)
50
57
  - [Migrating from Ionic Appflow to Capawesome Cloud](https://capawesome.io/blog/migrating-from-ionic-appflow-to-capawesome-cloud/)
51
58
  - [Migrating from App Center to Capawesome Cloud](https://capawesome.io/blog/migrating-from-app-center-to-capawesome-cloud/)
59
+ - [Live Updates for Nuxt Capacitor Apps](https://capawesome.io/blog/live-updates-for-nuxt-capacitor/)
52
60
 
53
61
  ## Installation
54
62
 
@@ -76,7 +84,7 @@ npx cap sync
76
84
 
77
85
  #### Channel
78
86
 
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.
87
+ 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
88
  This allows you to tie the channel to the version code at build time.
81
89
 
82
90
  Add the following to your app's `build.gradle` file:
@@ -93,7 +101,7 @@ android {
93
101
 
94
102
  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
103
 
96
- - `$okhttp3Version` version of `com.squareup.okhttp3:okhttp` (default: `5.3.2`)
104
+ - `$okhttp3Version` version of `com.squareup.okhttp3:okhttp` and `com.squareup.okhttp3:okhttp-brotli` (default: `5.3.2`)
97
105
  - `$zip4jVersion` version of `net.lingala.zip4j:zip4j` (default: `2.11.5`)
98
106
 
99
107
  This can be useful if you encounter dependency conflicts with other plugins in your project.
@@ -102,7 +110,7 @@ This can be useful if you encounter dependency conflicts with other plugins in y
102
110
 
103
111
  #### Channel
104
112
 
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.
113
+ 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
114
  This allows you to tie the channel to the build version at build time.
107
115
 
108
116
  Add the following to your `Info.plist` file:
@@ -216,56 +224,182 @@ A working example can be found here: [capawesome-team/capacitor-live-update-demo
216
224
 
217
225
  ## Usage
218
226
 
227
+ 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.
228
+
229
+ ### Sync with Capawesome Cloud
230
+
231
+ 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:
232
+
219
233
  ```typescript
220
234
  import { LiveUpdate } from '@capawesome/capacitor-live-update';
221
235
 
222
- const deleteBundle = async () => {
223
- await LiveUpdate.deleteBundle({ bundleId: 'my-bundle' });
236
+ const sync = async () => {
237
+ const result = await LiveUpdate.sync({
238
+ channel: 'production-5',
239
+ });
240
+ return result.nextBundleId;
241
+ };
242
+ ```
243
+
244
+ ### Notify the plugin that the app is ready
245
+
246
+ 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:
247
+
248
+ ```typescript
249
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
250
+
251
+ const ready = async () => {
252
+ const result = await LiveUpdate.ready();
253
+ if (result.currentBundleId) {
254
+ console.log(`The app is now using the bundle with the identifier ${result.currentBundleId}.`);
255
+ }
256
+ if (result.previousBundleId) {
257
+ console.log(`The app was using the bundle with the identifier ${result.previousBundleId}.`);
258
+ }
259
+ if (result.rollback) {
260
+ console.log('The app was reset to the default bundle.');
261
+ }
262
+ };
263
+ ```
264
+
265
+ ### Check if a new bundle is available
266
+
267
+ Fetch the latest bundle from Capawesome Cloud and compare it with the bundle that is currently in use:
268
+
269
+ ```typescript
270
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
271
+
272
+ const fetchLatestBundle = async () => {
273
+ await LiveUpdate.fetchLatestBundle();
274
+ };
275
+
276
+ const isNewBundleAvailable = async () => {
277
+ const { bundleId: latestBundleId } = await LiveUpdate.fetchLatestBundle({
278
+ channel: 'production-5',
279
+ });
280
+ if (latestBundleId) {
281
+ const { bundleId: currentBundleId } = await LiveUpdate.getCurrentBundle();
282
+ return latestBundleId !== currentBundleId;
283
+ } else {
284
+ return false;
285
+ }
224
286
  };
287
+ ```
288
+
289
+ ### Download a bundle from your own server
290
+
291
+ Download a self-hosted bundle from any URL, without any Capawesome Cloud dependency:
292
+
293
+ ```typescript
294
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
225
295
 
226
296
  const downloadBundle = async () => {
227
297
  await LiveUpdate.downloadBundle({ url: 'https://example.com/1.0.0.zip', bundleId: '1.0.0' });
228
298
  };
299
+ ```
229
300
 
230
- const fetchChannels = async () => {
231
- const result = await LiveUpdate.fetchChannels();
232
- return result.channels;
301
+ ### Switch to a downloaded bundle
302
+
303
+ Set the next bundle to use for the app. Call `reload()` or restart the app to apply the changes:
304
+
305
+ ```typescript
306
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
307
+
308
+ const setNextBundle = async () => {
309
+ await LiveUpdate.setNextBundle({ bundleId: '7f0b9bf2-dff6-4be2-bcac-b068cc5ea756' });
233
310
  };
234
311
 
235
- const fetchLatestBundle = async () => {
236
- await LiveUpdate.fetchLatestBundle();
312
+ const reload = async () => {
313
+ await LiveUpdate.reload();
237
314
  };
315
+ ```
316
+
317
+ ### Manage downloaded bundles
318
+
319
+ Get the identifiers of all bundles that have been downloaded and delete bundles that are no longer needed:
320
+
321
+ ```typescript
322
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
238
323
 
239
324
  const getBundles = async () => {
240
325
  const result = await LiveUpdate.getBundles();
241
326
  return result.bundleIds;
242
327
  };
243
328
 
329
+ const deleteBundle = async () => {
330
+ await LiveUpdate.deleteBundle({ bundleId: 'my-bundle' });
331
+ };
332
+ ```
333
+
334
+ ### Work with channels
335
+
336
+ 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:
337
+
338
+ ```typescript
339
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
340
+
341
+ const setChannel = async () => {
342
+ await LiveUpdate.setChannel({ channel: 'production-5' });
343
+ };
344
+
244
345
  const getChannel = async () => {
245
346
  const result = await LiveUpdate.getChannel();
246
347
  return result.channel;
247
348
  };
248
349
 
350
+ const fetchChannels = async () => {
351
+ const result = await LiveUpdate.fetchChannels();
352
+ return result.channels;
353
+ };
354
+ ```
355
+
356
+ ### Inspect the current and next bundle
357
+
358
+ 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:
359
+
360
+ ```typescript
361
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
362
+
249
363
  const getCurrentBundle = async () => {
250
364
  const result = await LiveUpdate.getCurrentBundle();
251
365
  return result.bundleId;
252
366
  };
253
367
 
254
- const getCustomId = async () => {
255
- const result = await LiveUpdate.getCustomId();
256
- return result.customId;
368
+ const getNextBundle = async () => {
369
+ const result = await LiveUpdate.getNextBundle();
370
+ return result.bundleId;
257
371
  };
372
+ ```
373
+
374
+ ### Identify devices
375
+
376
+ Read the unique device identifier and manage a custom identifier for the device:
377
+
378
+ ```typescript
379
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
258
380
 
259
381
  const getDeviceId = async () => {
260
382
  const result = await LiveUpdate.getDeviceId();
261
383
  return result.deviceId;
262
384
  };
263
385
 
264
- const getNextBundle = async () => {
265
- const result = await LiveUpdate.getNextBundle();
266
- return result.bundleId;
386
+ const setCustomId = async () => {
387
+ await LiveUpdate.setCustomId({ customId: 'my-custom-id' });
267
388
  };
268
389
 
390
+ const getCustomId = async () => {
391
+ const result = await LiveUpdate.getCustomId();
392
+ return result.customId;
393
+ };
394
+ ```
395
+
396
+ ### Read the app version
397
+
398
+ Get the native version code and version name of the app, for example to restrict live updates to compatible native versions:
399
+
400
+ ```typescript
401
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
402
+
269
403
  const getVersionCode = async () => {
270
404
  const result = await LiveUpdate.getVersionCode();
271
405
  return result.versionCode;
@@ -275,58 +409,18 @@ const getVersionName = async () => {
275
409
  const result = await LiveUpdate.getVersionName();
276
410
  return result.versionName;
277
411
  };
412
+ ```
278
413
 
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
- };
414
+ ### Reset to the default bundle
291
415
 
292
- const reload = async () => {
293
- await LiveUpdate.reload();
294
- };
416
+ Reset the app to the bundle that shipped with the native app binary. Call `reload()` or restart the app to apply the changes:
417
+
418
+ ```typescript
419
+ import { LiveUpdate } from '@capawesome/capacitor-live-update';
295
420
 
296
421
  const reset = async () => {
297
422
  await LiveUpdate.reset();
298
423
  };
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
424
  ```
331
425
 
332
426
  ## API
@@ -1268,7 +1362,7 @@ On Capawesome Cloud, the bundle ID corresponds to the ID of the app build artifa
1268
1362
  There are four ways to set a channel, listed from lowest to highest priority:
1269
1363
 
1270
1364
  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.
1365
+ 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
1366
  3. **[`setChannel(...)`](#setchannel)**: Set the channel at runtime. The value is persisted across app restarts.
1273
1367
  4. **[`sync(...)`](#sync)**: Pass a `channel` option to override the channel for a single sync call. This does **not** persist the channel.
1274
1368
 
@@ -1288,6 +1382,27 @@ You then have three options to get back to the default bundle:
1288
1382
 
1289
1383
  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
1384
 
1385
+ ### Do I have to use Capawesome Cloud?
1386
+
1387
+ 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.
1388
+
1389
+ ### What changes can I ship with a live update?
1390
+
1391
+ 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.
1392
+
1393
+ ### How does the rollback mechanism work?
1394
+
1395
+ 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.
1396
+
1397
+ ## Related Plugins
1398
+
1399
+ - [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.
1400
+ - [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.
1401
+
1402
+ ## Newsletter
1403
+
1404
+ 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/).
1405
+
1291
1406
  ## Changelog
1292
1407
 
1293
1408
  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) {
@@ -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 {
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.0",
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",
@@ -68,15 +75,15 @@
68
75
  },
69
76
  "devDependencies": {
70
77
  "@capacitor/android": "8.0.0",
71
- "@capacitor/cli": "8.0.0",
78
+ "@capacitor/cli": "8.4.2",
72
79
  "@capacitor/core": "8.0.0",
73
80
  "@capacitor/docgen": "0.3.1",
74
81
  "@capacitor/ios": "8.0.0",
75
82
  "@ionic/eslint-config": "0.4.0",
76
83
  "eslint": "8.57.0",
77
- "prettier-plugin-java": "2.6.7",
84
+ "prettier-plugin-java": "2.9.7",
78
85
  "rimraf": "6.1.2",
79
- "rollup": "4.53.3",
86
+ "rollup": "4.62.3",
80
87
  "swiftlint": "2.0.0",
81
88
  "typescript": "5.9.3"
82
89
  },