@capawesome/capacitor-live-update 8.1.1 → 8.2.1-dev.70823ec8.1778138208

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.
Files changed (39) hide show
  1. package/CapawesomeCapacitorLiveUpdate.podspec +15 -0
  2. package/Package.swift +21 -5
  3. package/README.md +147 -3
  4. package/android/build.gradle +7 -0
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +104 -2
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +12 -1
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +39 -1
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetChannelsResponseItem.java +28 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchChannelsOptions.java +38 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchLatestBundleOptions.java +15 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ChannelResult.java +28 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchChannelsResult.java +28 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +30 -0
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicManager.java +232 -0
  15. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicProvider.java +41 -0
  16. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicProviderRegistration.java +46 -0
  17. package/dist/docs.json +151 -3
  18. package/dist/esm/definitions.d.ts +85 -0
  19. package/dist/esm/definitions.js.map +1 -1
  20. package/dist/esm/web.d.ts +2 -1
  21. package/dist/esm/web.js +3 -0
  22. package/dist/esm/web.js.map +1 -1
  23. package/dist/plugin.cjs.js +3 -0
  24. package/dist/plugin.cjs.js.map +1 -1
  25. package/dist/plugin.js +3 -0
  26. package/dist/plugin.js.map +1 -1
  27. package/ios/Plugin/Classes/Options/FetchChannelsOptions.swift +26 -0
  28. package/ios/Plugin/Classes/Options/FetchLatestBundleOptions.swift +12 -0
  29. package/ios/Plugin/Classes/Results/ChannelResult.swift +19 -0
  30. package/ios/Plugin/Classes/Results/FetchChannelsResult.swift +16 -0
  31. package/ios/Plugin/Classes/Results/FetchLatestBundleResult.swift +24 -0
  32. package/ios/Plugin/Enums/CustomError.swift +12 -0
  33. package/ios/Plugin/LiveUpdate.swift +55 -1
  34. package/ios/Plugin/LiveUpdateHttpClient.swift +11 -0
  35. package/ios/Plugin/LiveUpdatePlugin.swift +40 -1
  36. package/ios/Plugin/Protocols/Api/GetChannelsResponseItem.swift +4 -0
  37. package/ios/Plugin/Providers/Ionic/LiveUpdateIonicManager.swift +118 -0
  38. package/ios/Plugin/Providers/Ionic/LiveUpdateIonicProvider.swift +25 -0
  39. package/package.json +1 -3
@@ -17,4 +17,19 @@ Pod::Spec.new do |s|
17
17
  s.dependency 'ZIPFoundation', '~> 0.9.0'
18
18
  s.swift_version = '5.1'
19
19
  s.static_framework = true
20
+ s.default_subspec = 'Default'
21
+
22
+ s.subspec 'Default' do |ss|
23
+ # Default subspec
24
+ end
25
+
26
+ # Opt-in subspec that pulls in the Ionic Live Update Provider SDK and
27
+ # compiles in the provider/manager classes guarded by
28
+ # `#if CAPAWESOME_INCLUDE_IONIC_PROVIDER`.
29
+ s.subspec 'IonicProvider' do |ss|
30
+ ss.dependency 'LiveUpdateProvider', '0.1.0-alpha.2'
31
+ ss.pod_target_xcconfig = {
32
+ 'OTHER_SWIFT_FLAGS' => '$(inherited) -DCAPAWESOME_INCLUDE_IONIC_PROVIDER'
33
+ }
34
+ end
20
35
  end
package/Package.swift CHANGED
@@ -1,4 +1,4 @@
1
- // swift-tools-version: 5.9
1
+ // swift-tools-version: 6.1
2
2
  import PackageDescription
3
3
 
4
4
  let package = Package(
@@ -9,10 +9,17 @@ let package = Package(
9
9
  name: "CapawesomeCapacitorLiveUpdate",
10
10
  targets: ["LiveUpdatePlugin"])
11
11
  ],
12
+ traits: [
13
+ .trait(
14
+ name: "IonicProvider",
15
+ description: "Enables registration with the Ionic Live Update Provider SDK."
16
+ )
17
+ ],
12
18
  dependencies: [
13
19
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
14
20
  .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMinor(from: "5.10.2")),
15
- .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.0"))
21
+ .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.0")),
22
+ .package(url: "https://github.com/ionic-team/live-update-provider-sdk.git", exact: "0.1.0-alpha.2")
16
23
  ],
17
24
  targets: [
18
25
  .target(
@@ -21,12 +28,21 @@ let package = Package(
21
28
  .product(name: "Capacitor", package: "capacitor-swift-pm"),
22
29
  .product(name: "Cordova", package: "capacitor-swift-pm"),
23
30
  .product(name: "Alamofire", package: "Alamofire"),
24
- .product(name: "ZIPFoundation", package: "ZIPFoundation")
31
+ .product(name: "ZIPFoundation", package: "ZIPFoundation"),
32
+ .product(
33
+ name: "LiveUpdateProvider",
34
+ package: "live-update-provider-sdk",
35
+ condition: .when(traits: ["IonicProvider"])
36
+ )
25
37
  ],
26
- path: "ios/Plugin"),
38
+ path: "ios/Plugin",
39
+ swiftSettings: [
40
+ .define("CAPAWESOME_INCLUDE_IONIC_PROVIDER", .when(traits: ["IonicProvider"]))
41
+ ]),
27
42
  .testTarget(
28
43
  name: "LiveUpdatePluginTests",
29
44
  dependencies: ["LiveUpdatePlugin"],
30
45
  path: "ios/PluginTests")
31
- ]
46
+ ],
47
+ swiftLanguageModes: [.v5]
32
48
  )
package/README.md CHANGED
@@ -21,19 +21,29 @@ We are proud to offer one of the most complete and feature-rich Capacitor plugin
21
21
  - 🛟 **Rollback**: Reset the app to the default bundle if an incompatible bundle has been set.
22
22
  - 🚀 **Rollout**: Gradually roll out new bundles to gather valuable feedback.
23
23
  - 🔁 **Delta Updates**: Make your updates faster by only downloading changed files.
24
+ - ⚙️ **Runtime Configuration**: Update plugin configuration at runtime without rebuilding the app.
25
+ - 📡 **Update Lifecycle Events**: Track download progress, react to bundle changes, and detect reloads with auto-blocking of rolled back bundles.
26
+ - 🏠 **Self-Hosted Bundles**: Download bundles from any URL, no Capawesome Cloud dependency required.
27
+ - 🏷️ **Custom Properties**: Associate custom key-value metadata with bundles via Capawesome Cloud.
28
+ - 🌀 **Ionic Portals Support**: Power live updates for [Ionic Portals](https://ionic.io/docs/portals) via the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk).
29
+ - 🧩 **Federated Capacitor Support**: Power live updates for [Federated Capacitor](https://ionic.io/docs/portals/for-capacitor/overview) shells via the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk).
24
30
  - 🔒 **Security**: Verify the authenticity and integrity of the bundle using a public key.
25
31
  - ⚔️ **Battle-Tested**: Used in more than 1,000 projects to update apps on more than 20,000,000 devices.
26
32
  - 🌐 **Open Source**: Licensed under the MIT License.
27
33
 
28
34
  Missing a feature? Just [open an issue](https://github.com/capawesome-team/capacitor-plugins/issues) and we'll take a look!
29
35
 
36
+ ## Newsletter
37
+
38
+ 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/).
39
+
30
40
  ## Compatibility
31
41
 
32
42
  | Plugin Version | Capacitor Version | Status |
33
43
  | -------------- | ----------------- | -------------- |
34
44
  | 8.x.x | >=8.x.x | Active support |
35
45
  | 7.x.x | 7.x.x | Maintenance |
36
- | 6.x.x | 6.x.x | Deprecated |
46
+ | 6.x.x | 6.x.x | Maintenance |
37
47
  | 5.x.x | 5.x.x | Deprecated |
38
48
 
39
49
  ## Guides
@@ -44,6 +54,21 @@ Missing a feature? Just [open an issue](https://github.com/capawesome-team/capac
44
54
 
45
55
  ## Installation
46
56
 
57
+ You can use our **AI-Assisted Setup** to install the plugin.
58
+ Add the [Capawesome Skills](https://github.com/capawesome-team/skills) to your AI tool using the following command:
59
+
60
+ ```bash
61
+ npx skills add capawesome-team/skills --skill capacitor-plugins
62
+ ```
63
+
64
+ Then use the following prompt:
65
+
66
+ ```
67
+ Use the `capacitor-plugins` skill from `capawesome-team/skills` to install the `@capawesome/capacitor-live-update` plugin in my project.
68
+ ```
69
+
70
+ If you prefer **Manual Setup**, install the plugin by running the following commands and follow the platform-specific instructions below:
71
+
47
72
  ```bash
48
73
  npm install @capawesome/capacitor-live-update
49
74
  npx cap sync
@@ -51,6 +76,21 @@ npx cap sync
51
76
 
52
77
  ### Android
53
78
 
79
+ #### Channel
80
+
81
+ If you are using [Versioned Channels](https://capawesome.io/cloud/live-updates/guides/best-practices/#versioned-channels), you can set a default channel directly in your native project by adding a string resource.
82
+ This allows you to tie the channel to the version code at build time.
83
+
84
+ Add the following to your app's `build.gradle` file:
85
+
86
+ ```groovy
87
+ android {
88
+ defaultConfig {
89
+ resValue "string", "capawesome_live_update_default_channel", "production-" + defaultConfig.versionCode
90
+ }
91
+ }
92
+ ```
93
+
54
94
  #### Variables
55
95
 
56
96
  If needed, you can define the following project variable in your app’s `variables.gradle` file to change the default version of the dependency:
@@ -62,6 +102,18 @@ This can be useful if you encounter dependency conflicts with other plugins in y
62
102
 
63
103
  ### iOS
64
104
 
105
+ #### Channel
106
+
107
+ If you are using [Versioned Channels](https://capawesome.io/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.
108
+ This allows you to tie the channel to the build version at build time.
109
+
110
+ Add the following to your `Info.plist` file:
111
+
112
+ ```xml
113
+ <key>CapawesomeLiveUpdateDefaultChannel</key>
114
+ <string>production-$(CURRENT_PROJECT_VERSION)</string>
115
+ ```
116
+
65
117
  #### Privacy manifest
66
118
 
67
119
  Add the `NSPrivacyAccessedAPICategoryUserDefaults` dictionary key to your [Privacy Manifest](https://capacitorjs.com/docs/ios/privacy-manifest) (usually `ios/App/PrivacyInfo.xcprivacy`):
@@ -89,6 +141,10 @@ Add the `NSPrivacyAccessedAPICategoryUserDefaults` dictionary key to your [Priva
89
141
 
90
142
  We recommend to declare [`CA92.1`](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401) as the reason for accessing the [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults) API.
91
143
 
144
+ ## Ionic Live Update Provider SDK integration
145
+
146
+ This plugin can register itself as a provider for the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk), so that **Ionic Portals** and **Federated Capacitor** apps can use Capawesome Cloud to deliver live updates. See [the full setup guide](https://github.com/capawesome-team/capacitor-plugins/blob/main/packages/live-update/docs/ionic-live-update-provider-sdk-integration.md) for details.
147
+
92
148
  ## Configuration
93
149
 
94
150
  <docgen-config>
@@ -100,7 +156,7 @@ We recommend to declare [`CA92.1`](https://developer.apple.com/documentation/bun
100
156
  | **`autoBlockRolledBackBundles`** | <code>boolean</code> | Whether or not to automatically block bundles that have been rolled back. When enabled, the plugin will automatically block bundles that caused a rollback (up to 100 bundles). When the limit is reached, the oldest blocked bundle is unblocked. Blocked bundles will be skipped in future sync operations. **Attention**: This option has no effect if `readyTimeout` is set to `0`. Only available on Android and iOS. | <code>false</code> | 7.3.0 |
101
157
  | **`autoDeleteBundles`** | <code>boolean</code> | Whether or not to automatically delete unused bundles. When enabled, the plugin will automatically delete unused bundles after calling `ready()`. | <code>false</code> | 5.0.0 |
102
158
  | **`autoUpdateStrategy`** | <code>'none' \| 'background'</code> | The auto-update strategy for live updates. - `none`: Live updates will not be applied automatically. - `background`: Live updates will be automatically downloaded and applied in the background at app startup and when the app resumes (if the last check was more than 15 minutes ago). Only available on Android and iOS. | <code>'none'</code> | 7.3.0 |
103
- | **`defaultChannel`** | <code>string</code> | The default channel of the app. | | 6.3.0 |
159
+ | **`defaultChannel`** | <code>string</code> | The default channel of the app. This can be overridden by `setChannel()`, the `channel` parameter of `sync()`, or the native channel configuration (`CapawesomeLiveUpdateDefaultChannel` in `Info.plist` on iOS or `capawesome_live_update_default_channel` in `strings.xml` on Android). | | 6.3.0 |
104
160
  | **`httpTimeout`** | <code>number</code> | The timeout in milliseconds for HTTP requests. | <code>60000</code> | 6.4.0 |
105
161
  | **`publicKey`** | <code>string</code> | The public key to verify the integrity of the bundle. The public key must be a PEM-encoded RSA public key. | | 6.1.0 |
106
162
  | **`readyTimeout`** | <code>number</code> | The timeout in milliseconds to wait for the app to be ready before resetting to the default bundle. It is strongly **recommended** to configure this option (e.g. `10000` ms) so that the plugin can roll back to the default bundle in case of problems. If configured, the plugin will wait for the app to call the `ready()` method before resetting to the default bundle. Set to `0` to disable the timeout. | <code>0</code> | 5.0.0 |
@@ -169,6 +225,11 @@ const downloadBundle = async () => {
169
225
  await LiveUpdate.downloadBundle({ url: 'https://example.com/1.0.0.zip', bundleId: '1.0.0' });
170
226
  };
171
227
 
228
+ const fetchChannels = async () => {
229
+ const result = await LiveUpdate.fetchChannels();
230
+ return result.channels;
231
+ };
232
+
172
233
  const fetchLatestBundle = async () => {
173
234
  await LiveUpdate.fetchLatestBundle();
174
235
  };
@@ -273,6 +334,7 @@ const isNewBundleAvailable = async () => {
273
334
  * [`clearBlockedBundles()`](#clearblockedbundles)
274
335
  * [`deleteBundle(...)`](#deletebundle)
275
336
  * [`downloadBundle(...)`](#downloadbundle)
337
+ * [`fetchChannels(...)`](#fetchchannels)
276
338
  * [`fetchLatestBundle(...)`](#fetchlatestbundle)
277
339
  * [`getBlockedBundles()`](#getblockedbundles)
278
340
  * [`getBundles()`](#getbundles)
@@ -363,6 +425,35 @@ Only available on Android and iOS.
363
425
  --------------------
364
426
 
365
427
 
428
+ ### fetchChannels(...)
429
+
430
+ ```typescript
431
+ fetchChannels(options?: FetchChannelsOptions | undefined) => Promise<FetchChannelsResult>
432
+ ```
433
+
434
+ Fetch channels from [Capawesome Cloud](https://capawesome.io/cloud/).
435
+
436
+ This is primarily intended for development and QA purposes.
437
+ It allows you to retrieve a list of available channels so you can
438
+ dynamically switch between them using `setChannel(...)`.
439
+
440
+ **Attention**: Only works for apps with public channels enabled.
441
+ If channels are private, they can still be set using `setChannel(...)`
442
+ but won't be returned by this method.
443
+
444
+ Only available on Android and iOS.
445
+
446
+ | Param | Type |
447
+ | ------------- | --------------------------------------------------------------------- |
448
+ | **`options`** | <code><a href="#fetchchannelsoptions">FetchChannelsOptions</a></code> |
449
+
450
+ **Returns:** <code>Promise&lt;<a href="#fetchchannelsresult">FetchChannelsResult</a>&gt;</code>
451
+
452
+ **Since:** 8.2.0
453
+
454
+ --------------------
455
+
456
+
366
457
  ### fetchLatestBundle(...)
367
458
 
368
459
  ```typescript
@@ -429,6 +520,15 @@ getChannel() => Promise<GetChannelResult>
429
520
 
430
521
  Get the channel that is used for the update.
431
522
 
523
+ The channel is resolved in the following order (highest priority first):
524
+ 1. `setChannel()` (SharedPreferences on Android / UserDefaults on iOS)
525
+ 2. Native config (`CapawesomeLiveUpdateDefaultChannel` in `Info.plist` on iOS or
526
+ `capawesome_live_update_default_channel` in `strings.xml` on Android)
527
+ 3. Capacitor config `defaultChannel`
528
+
529
+ **Note**: The `channel` parameter of `sync()` takes the highest priority
530
+ but is not persisted and therefore not returned by this method.
531
+
432
532
  Only available on Android and iOS.
433
533
 
434
534
  **Returns:** <code>Promise&lt;<a href="#getchannelresult">GetChannelResult</a>&gt;</code>
@@ -891,12 +991,36 @@ Remove all listeners for this plugin.
891
991
  | **`url`** | <code>string</code> | The URL of the bundle to download. For the `zip` artifact type, the URL must point to a ZIP file. For the `manifest` artifact type, the URL serves as the base URL to download the individual files. For example, if the URL is `https://example.com/download`, the plugin will download the file with the href `index.html` from `https://example.com/download?href=index.html`. To **verify the integrity** of the file, the server should return a `X-Checksum` header with the SHA-256 hash in hex format. To **verify the signature** of the file, the server should return a `X-Signature` header with the signed SHA-256 hash in base64 format. | | 5.0.0 |
892
992
 
893
993
 
994
+ #### FetchChannelsResult
995
+
996
+ | Prop | Type | Description | Since |
997
+ | -------------- | ---------------------- | --------------------- | ----- |
998
+ | **`channels`** | <code>Channel[]</code> | The list of channels. | 8.2.0 |
999
+
1000
+
1001
+ #### Channel
1002
+
1003
+ | Prop | Type | Description | Since |
1004
+ | ---------- | ------------------- | ------------------------------------- | ----- |
1005
+ | **`id`** | <code>string</code> | The unique identifier of the channel. | 8.2.0 |
1006
+ | **`name`** | <code>string</code> | The name of the channel. | 8.2.0 |
1007
+
1008
+
1009
+ #### FetchChannelsOptions
1010
+
1011
+ | Prop | Type | Description | Default | Since |
1012
+ | ------------ | ------------------- | ----------------------------------------- | --------------- | ----- |
1013
+ | **`limit`** | <code>number</code> | The maximum number of channels to return. | <code>50</code> | 8.2.0 |
1014
+ | **`offset`** | <code>number</code> | The number of channels to skip. | <code>0</code> | 8.2.0 |
1015
+ | **`query`** | <code>string</code> | The query to filter channels by name. | | 8.2.0 |
1016
+
1017
+
894
1018
  #### FetchLatestBundleResult
895
1019
 
896
1020
  | Prop | Type | Description | Since |
897
1021
  | ---------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
898
1022
  | **`artifactType`** | <code>'manifest' \| 'zip'</code> | The artifact type of the bundle. | 6.7.0 |
899
- | **`bundleId`** | <code>string \| null</code> | The unique identifier of the latest bundle. If `null`, no bundle is available. | 6.6.0 |
1023
+ | **`bundleId`** | <code>string \| null</code> | The unique identifier of the latest bundle. On Capawesome Cloud, this is the ID of the app build artifact. If `null`, no bundle is available. | 6.6.0 |
900
1024
  | **`checksum`** | <code>string</code> | The checksum of the latest bundle if the bundle is self-hosted. If the bundle is hosted on Capawesome Cloud, the checksum will be returned as response header when downloading the bundle. | 7.1.0 |
901
1025
  | **`customProperties`** | <code>{ [key: string]: string; }</code> | Custom properties that are associated with the latest bundle. | 7.0.0 |
902
1026
  | **`downloadUrl`** | <code>string</code> | The URL of the latest bundle to download. Pass this URL to the `downloadBundle(...)` method to download the bundle. | 6.7.0 |
@@ -1129,6 +1253,26 @@ For this reason, you must be careful to [restrict live updates to compatible nat
1129
1253
 
1130
1254
  ## FAQ
1131
1255
 
1256
+ ### What is a bundle?
1257
+
1258
+ A bundle is a set of web assets (HTML, CSS, JavaScript, etc.) that make up your app's user interface.
1259
+ The plugin manages two types of bundles: the **built-in bundle** that ships with the native app binary, and **live update bundles** that are downloaded at runtime to update the app without a native release.
1260
+ Each live update bundle has a unique **bundle ID** returned by the [`fetchLatestBundle(...)`](#fetchlatestbundle) method.
1261
+ On Capawesome Cloud, the bundle ID corresponds to the ID of the app build artifact.
1262
+
1263
+ ### How do I set a channel?
1264
+
1265
+ There are four ways to set a channel, listed from lowest to highest priority:
1266
+
1267
+ 1. **Capacitor config**: Set the `defaultChannel` property in the [plugin configuration](#configuration). This is the simplest way to set a static default channel.
1268
+ 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/cloud/live-updates/guides/best-practices/#versioned-channels) where the channel is tied to the build version.
1269
+ 3. **[`setChannel(...)`](#setchannel)**: Set the channel at runtime. The value is persisted across app restarts.
1270
+ 4. **[`sync(...)`](#sync)**: Pass a `channel` option to override the channel for a single sync call. This does **not** persist the channel.
1271
+
1272
+ Each method overrides the ones above it. You can check the currently resolved channel by calling [`getChannel()`](#getchannel).
1273
+
1274
+ Additionally, Capawesome Cloud supports [forced channel assignments](https://capawesome.io/blog/capawesome-cloud-forced-channel-assignments/) which allow you to override the channel for a specific device without any app code changes. Please note that forced channel assignments have a higher priority than all of the above methods and should be used with caution.
1275
+
1132
1276
  ### Why can't I see my changes during development?
1133
1277
 
1134
1278
  As soon as you have installed a live update, the app will use the live update bundle and no longer the default bundle.
@@ -5,6 +5,8 @@ ext {
5
5
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
6
6
  okhttp3Version = project.hasProperty('okhttp3Version') ? rootProject.ext.okhttp3Version : '5.3.2'
7
7
  zip4jVersion = project.hasProperty('zip4jVersion') ? rootProject.ext.zip4jVersion : '2.11.5'
8
+ capawesomeCapacitorLiveUpdateIncludeIonicProvider = project.hasProperty('capawesomeCapacitorLiveUpdateIncludeIonicProvider') ? rootProject.ext.capawesomeCapacitorLiveUpdateIncludeIonicProvider : false
9
+ ionicLiveUpdateProviderVersion = project.hasProperty('ionicLiveUpdateProviderVersion') ? rootProject.ext.ionicLiveUpdateProviderVersion : '0.1.0-alpha.2'
8
10
  }
9
11
 
10
12
  buildscript {
@@ -56,6 +58,11 @@ dependencies {
56
58
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
57
59
  implementation "net.lingala.zip4j:zip4j:$zip4jVersion"
58
60
  implementation "com.squareup.okhttp3:okhttp:$okhttp3Version"
61
+ if (capawesomeCapacitorLiveUpdateIncludeIonicProvider) {
62
+ implementation "io.ionic:liveupdateprovider:$ionicLiveUpdateProviderVersion"
63
+ } else {
64
+ compileOnly "io.ionic:liveupdateprovider:$ionicLiveUpdateProviderVersion"
65
+ }
59
66
  testImplementation "junit:junit:$junitVersion"
60
67
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
61
68
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -15,17 +15,21 @@ import com.getcapacitor.Logger;
15
15
  import com.getcapacitor.plugin.WebView;
16
16
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.Manifest;
17
17
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.ManifestItem;
18
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetChannelsResponseItem;
18
19
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
19
20
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.DownloadBundleProgressEvent;
20
21
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.NextBundleSetEvent;
21
22
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundleOptions;
22
23
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
24
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchChannelsOptions;
23
25
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
24
26
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
25
27
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetConfigOptions;
26
28
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
27
29
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetNextBundleOptions;
28
30
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SyncOptions;
31
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.ChannelResult;
32
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchChannelsResult;
29
33
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
30
34
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBlockedBundlesResult;
31
35
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBundlesResult;
@@ -65,6 +69,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
65
69
  import java.util.concurrent.atomic.AtomicLong;
66
70
  import java.util.concurrent.atomic.AtomicReference;
67
71
  import net.lingala.zip4j.ZipFile;
72
+ import net.lingala.zip4j.model.FileHeader;
68
73
  import okhttp3.Call;
69
74
  import okhttp3.HttpUrl;
70
75
  import okhttp3.Response;
@@ -115,6 +120,9 @@ public class LiveUpdate {
115
120
  // Check version and reset config if version changed
116
121
  checkAndResetConfigIfVersionChanged();
117
122
 
123
+ // Set the device ID on the HTTP client (after any potential config reset)
124
+ this.httpClient.setDeviceId(getDeviceId());
125
+
118
126
  // Start the rollback timer to rollback to the default bundle
119
127
  // if the app is not ready after a certain time
120
128
  startRollbackTimer();
@@ -159,6 +167,63 @@ public class LiveUpdate {
159
167
  }
160
168
  }
161
169
 
170
+ public void fetchChannels(@NonNull FetchChannelsOptions options, @NonNull NonEmptyCallback<FetchChannelsResult> callback) {
171
+ try {
172
+ HttpUrl.Builder urlBuilder = new HttpUrl.Builder()
173
+ .scheme("https")
174
+ .host(config.getServerDomain())
175
+ .addPathSegment("v1")
176
+ .addPathSegment("apps")
177
+ .addPathSegment(getAppId())
178
+ .addPathSegment("channels");
179
+ if (options.getLimit() != null) {
180
+ urlBuilder.addQueryParameter("limit", String.valueOf(options.getLimit()));
181
+ }
182
+ if (options.getOffset() != null) {
183
+ urlBuilder.addQueryParameter("offset", String.valueOf(options.getOffset()));
184
+ }
185
+ if (options.getQuery() != null) {
186
+ urlBuilder.addQueryParameter("query", options.getQuery());
187
+ }
188
+ String url = urlBuilder.build().toString();
189
+
190
+ httpClient.enqueue(
191
+ url,
192
+ new NonEmptyCallback<Response>() {
193
+ @Override
194
+ public void success(@NonNull Response response) {
195
+ try {
196
+ String responseBodyString = response.body().string();
197
+ if (response.isSuccessful()) {
198
+ JSONArray responseJsonArray = new JSONArray(responseBodyString);
199
+ ChannelResult[] channels = new ChannelResult[responseJsonArray.length()];
200
+ for (int i = 0; i < responseJsonArray.length(); i++) {
201
+ GetChannelsResponseItem item = new GetChannelsResponseItem(responseJsonArray.getJSONObject(i));
202
+ channels[i] = new ChannelResult(item.getId(), item.getName());
203
+ }
204
+ FetchChannelsResult result = new FetchChannelsResult(channels);
205
+ callback.success(result);
206
+ } else if (response.code() == 401) {
207
+ callback.error(new Exception("Unauthorized. Channel Discovery may not be enabled for this app."));
208
+ } else {
209
+ callback.error(new Exception(responseBodyString));
210
+ }
211
+ } catch (Exception e) {
212
+ callback.error(e);
213
+ }
214
+ }
215
+
216
+ @Override
217
+ public void error(@NonNull Exception exception) {
218
+ callback.error(exception);
219
+ }
220
+ }
221
+ );
222
+ } catch (Exception e) {
223
+ callback.error(e);
224
+ }
225
+ }
226
+
162
227
  public void fetchLatestBundle(@NonNull FetchLatestBundleOptions options, @NonNull NonEmptyCallback<FetchLatestBundleResult> callback) {
163
228
  fetchLatestBundleInternal(
164
229
  options,
@@ -233,6 +298,17 @@ public class LiveUpdate {
233
298
  callback.success(result);
234
299
  }
235
300
 
301
+ /**
302
+ * @return The on-disk directory of a downloaded bundle, or `null` if the bundle does not exist.
303
+ */
304
+ @Nullable
305
+ public File getBundleDirectory(@NonNull String bundleId) {
306
+ if (!hasBundleById(bundleId)) {
307
+ return null;
308
+ }
309
+ return buildBundleDirectoryFor(bundleId);
310
+ }
311
+
236
312
  public void getCustomId(@NonNull NonEmptyCallback callback) {
237
313
  String customId = preferences.getCustomId();
238
314
  GetCustomIdResult result = new GetCustomIdResult(customId);
@@ -907,13 +983,17 @@ public class LiveUpdate {
907
983
  @NonNull NonEmptyCallback<GetLatestBundleResponse> callback
908
984
  ) {
909
985
  try {
986
+ String appId = options.getAppId() == null ? getAppId() : options.getAppId();
987
+ if (appId == null || appId.isEmpty()) {
988
+ throw new Exception(LiveUpdatePlugin.ERROR_APP_ID_MISSING);
989
+ }
910
990
  String channel = options.getChannel() == null ? getChannel() : options.getChannel();
911
991
  String url = new HttpUrl.Builder()
912
992
  .scheme("https")
913
993
  .host(config.getServerDomain())
914
994
  .addPathSegment("v1")
915
995
  .addPathSegment("apps")
916
- .addPathSegment(getAppId())
996
+ .addPathSegment(appId)
917
997
  .addPathSegment("bundles")
918
998
  .addPathSegment("latest")
919
999
  .addQueryParameter("appVersionCode", getVersionCodeAsString())
@@ -1015,12 +1095,28 @@ public class LiveUpdate {
1015
1095
  if (config.getDefaultChannel() != null) {
1016
1096
  channel = config.getDefaultChannel();
1017
1097
  }
1098
+ String nativeChannel = getNativeChannel();
1099
+ if (nativeChannel != null) {
1100
+ channel = nativeChannel;
1101
+ }
1018
1102
  if (preferences.getChannel() != null) {
1019
1103
  channel = preferences.getChannel();
1020
1104
  }
1021
1105
  return channel;
1022
1106
  }
1023
1107
 
1108
+ @Nullable
1109
+ private String getNativeChannel() {
1110
+ int resId = plugin
1111
+ .getContext()
1112
+ .getResources()
1113
+ .getIdentifier("capawesome_live_update_default_channel", "string", plugin.getContext().getPackageName());
1114
+ if (resId == 0) {
1115
+ return null;
1116
+ }
1117
+ return plugin.getContext().getResources().getString(resId);
1118
+ }
1119
+
1024
1120
  /**
1025
1121
  * @return The current bundle ID or `null` if the default bundle is in use.
1026
1122
  */
@@ -1352,7 +1448,13 @@ public class LiveUpdate {
1352
1448
  private File unzipFile(@NonNull File zipFile) throws IOException {
1353
1449
  File destination = buildTemporaryDirectory();
1354
1450
  String destinationPath = destination.getPath();
1355
- new ZipFile(zipFile).extractAll(destinationPath);
1451
+ ZipFile zip = new ZipFile(zipFile);
1452
+ // Clear stored Unix permissions to prevent EACCES errors on newer Android versions
1453
+ // where restrictive directory permissions from the zip block file creation.
1454
+ for (FileHeader fileHeader : zip.getFileHeaders()) {
1455
+ fileHeader.setExternalFileAttributes(null);
1456
+ }
1457
+ zip.extractAll(destinationPath);
1356
1458
  return destination;
1357
1459
  }
1358
1460
 
@@ -25,6 +25,9 @@ public class LiveUpdateHttpClient {
25
25
  @NonNull
26
26
  private final LiveUpdateConfig config;
27
27
 
28
+ @Nullable
29
+ private String deviceId;
30
+
28
31
  @NonNull
29
32
  private final OkHttpClient okHttpClient;
30
33
 
@@ -62,8 +65,16 @@ public class LiveUpdateHttpClient {
62
65
  .build();
63
66
  }
64
67
 
68
+ public void setDeviceId(@NonNull String deviceId) {
69
+ this.deviceId = deviceId;
70
+ }
71
+
65
72
  public Call enqueue(String url, NonEmptyCallback<Response> callback) {
66
- Request request = new Request.Builder().url(url).build();
73
+ Request.Builder builder = new Request.Builder().url(url);
74
+ if (deviceId != null) {
75
+ builder.addHeader("X-Capawesome-Device-Id", deviceId);
76
+ }
77
+ Request request = builder.build();
67
78
 
68
79
  Call call = okHttpClient.newCall(request);
69
80
  call.enqueue(
@@ -14,12 +14,14 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.DownloadBundl
14
14
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.NextBundleSetEvent;
15
15
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DeleteBundleOptions;
16
16
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
17
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchChannelsOptions;
17
18
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
18
19
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetChannelOptions;
19
20
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetConfigOptions;
20
21
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetCustomIdOptions;
21
22
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SetNextBundleOptions;
22
23
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.SyncOptions;
24
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchChannelsResult;
23
25
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
24
26
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetBlockedBundlesResult;
25
27
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetConfigResult;
@@ -30,14 +32,16 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingRes
30
32
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
31
33
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
32
34
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
35
+ import io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic.LiveUpdateIonicProviderRegistration;
33
36
 
34
37
  @CapacitorPlugin(name = "LiveUpdate")
35
38
  public class LiveUpdatePlugin extends Plugin {
36
39
 
37
40
  public static final String TAG = "LiveUpdate";
38
- public static final String VERSION = "8.1.0";
41
+ public static final String VERSION = "8.2.0";
39
42
  public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
40
43
  public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
44
+ public static final String ERROR_BUNDLE_DIRECTORY_NOT_FOUND = "Bundle directory could not be resolved.";
41
45
  public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
42
46
  public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
43
47
  public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
@@ -46,7 +50,9 @@ public class LiveUpdatePlugin extends Plugin {
46
50
  public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
47
51
  public static final String ERROR_CUSTOM_ID_MISSING = "customId must be provided.";
48
52
  public static final String ERROR_DOWNLOAD_FAILED = "Bundle could not be downloaded.";
53
+ public static final String ERROR_DOWNLOAD_URL_MISSING = "Bundle does not have a valid download URL.";
49
54
  public static final String ERROR_HTTP_TIMEOUT = "Request timed out.";
55
+ public static final String ERROR_MANAGER_KEY_MISSING = "managerKey must be provided.";
50
56
  public static final String ERROR_URL_MISSING = "url must be provided.";
51
57
  public static final String ERROR_SIGNATURE_VERIFICATION_FAILED = "Signature verification failed.";
52
58
  public static final String ERROR_PUBLIC_KEY_INVALID = "Invalid public key.";
@@ -69,6 +75,10 @@ public class LiveUpdatePlugin extends Plugin {
69
75
  try {
70
76
  config = getLiveUpdateConfig();
71
77
  implementation = new LiveUpdate(config, this);
78
+ // Register the Ionic Live Update Provider when the optional SDK is on the classpath.
79
+ if (LiveUpdateIonicProviderRegistration.isAvailable()) {
80
+ LiveUpdateIonicProviderRegistration.tryRegister(implementation);
81
+ }
72
82
  } catch (Exception exception) {
73
83
  Logger.error(TAG, exception.getMessage(), exception);
74
84
  }
@@ -178,6 +188,34 @@ public class LiveUpdatePlugin extends Plugin {
178
188
  }
179
189
  }
180
190
 
191
+ @PluginMethod
192
+ public void fetchChannels(PluginCall call) {
193
+ try {
194
+ String appId = config.getAppId();
195
+ if (appId == null || appId.isEmpty()) {
196
+ call.reject(ERROR_APP_ID_MISSING);
197
+ return;
198
+ }
199
+
200
+ FetchChannelsOptions options = new FetchChannelsOptions(call);
201
+ NonEmptyCallback<FetchChannelsResult> callback = new NonEmptyCallback<>() {
202
+ @Override
203
+ public void success(FetchChannelsResult result) {
204
+ resolveCall(call, result.toJSObject());
205
+ }
206
+
207
+ @Override
208
+ public void error(Exception exception) {
209
+ rejectCall(call, exception);
210
+ }
211
+ };
212
+
213
+ implementation.fetchChannels(options, callback);
214
+ } catch (Exception exception) {
215
+ rejectCall(call, exception);
216
+ }
217
+ }
218
+
181
219
  @PluginMethod
182
220
  public void fetchLatestBundle(PluginCall call) {
183
221
  try {