@capawesome/capacitor-live-update 8.2.0 → 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 (22) hide show
  1. package/CapawesomeCapacitorLiveUpdate.podspec +15 -0
  2. package/Package.swift +21 -5
  3. package/README.md +37 -4
  4. package/android/build.gradle +7 -0
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +24 -2
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +8 -0
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchLatestBundleOptions.java +15 -0
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +30 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicManager.java +232 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicProvider.java +41 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicProviderRegistration.java +46 -0
  12. package/dist/docs.json +1 -1
  13. package/dist/esm/definitions.d.ts +2 -0
  14. package/dist/esm/definitions.js.map +1 -1
  15. package/ios/Plugin/Classes/Options/FetchLatestBundleOptions.swift +12 -0
  16. package/ios/Plugin/Classes/Results/FetchLatestBundleResult.swift +24 -0
  17. package/ios/Plugin/Enums/CustomError.swift +9 -0
  18. package/ios/Plugin/LiveUpdate.swift +12 -1
  19. package/ios/Plugin/LiveUpdatePlugin.swift +19 -0
  20. package/ios/Plugin/Providers/Ionic/LiveUpdateIonicManager.swift +118 -0
  21. package/ios/Plugin/Providers/Ionic/LiveUpdateIonicProvider.swift +25 -0
  22. package/package.json +1 -1
@@ -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
@@ -25,19 +25,25 @@ We are proud to offer one of the most complete and feature-rich Capacitor plugin
25
25
  - 📡 **Update Lifecycle Events**: Track download progress, react to bundle changes, and detect reloads with auto-blocking of rolled back bundles.
26
26
  - 🏠 **Self-Hosted Bundles**: Download bundles from any URL, no Capawesome Cloud dependency required.
27
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).
28
30
  - 🔒 **Security**: Verify the authenticity and integrity of the bundle using a public key.
29
31
  - ⚔️ **Battle-Tested**: Used in more than 1,000 projects to update apps on more than 20,000,000 devices.
30
32
  - 🌐 **Open Source**: Licensed under the MIT License.
31
33
 
32
34
  Missing a feature? Just [open an issue](https://github.com/capawesome-team/capacitor-plugins/issues) and we'll take a look!
33
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
+
34
40
  ## Compatibility
35
41
 
36
42
  | Plugin Version | Capacitor Version | Status |
37
43
  | -------------- | ----------------- | -------------- |
38
44
  | 8.x.x | >=8.x.x | Active support |
39
45
  | 7.x.x | 7.x.x | Maintenance |
40
- | 6.x.x | 6.x.x | Deprecated |
46
+ | 6.x.x | 6.x.x | Maintenance |
41
47
  | 5.x.x | 5.x.x | Deprecated |
42
48
 
43
49
  ## Guides
@@ -48,6 +54,21 @@ Missing a feature? Just [open an issue](https://github.com/capawesome-team/capac
48
54
 
49
55
  ## Installation
50
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
+
51
72
  ```bash
52
73
  npm install @capawesome/capacitor-live-update
53
74
  npx cap sync
@@ -120,6 +141,10 @@ Add the `NSPrivacyAccessedAPICategoryUserDefaults` dictionary key to your [Priva
120
141
 
121
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.
122
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
+
123
148
  ## Configuration
124
149
 
125
150
  <docgen-config>
@@ -995,7 +1020,7 @@ Remove all listeners for this plugin.
995
1020
  | Prop | Type | Description | Since |
996
1021
  | ---------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
997
1022
  | **`artifactType`** | <code>'manifest' \| 'zip'</code> | The artifact type of the bundle. | 6.7.0 |
998
- | **`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 |
999
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 |
1000
1025
  | **`customProperties`** | <code>{ [key: string]: string; }</code> | Custom properties that are associated with the latest bundle. | 7.0.0 |
1001
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 |
@@ -1228,6 +1253,13 @@ For this reason, you must be careful to [restrict live updates to compatible nat
1228
1253
 
1229
1254
  ## FAQ
1230
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
+
1231
1263
  ### How do I set a channel?
1232
1264
 
1233
1265
  There are four ways to set a channel, listed from lowest to highest priority:
@@ -1237,8 +1269,9 @@ There are four ways to set a channel, listed from lowest to highest priority:
1237
1269
  3. **[`setChannel(...)`](#setchannel)**: Set the channel at runtime. The value is persisted across app restarts.
1238
1270
  4. **[`sync(...)`](#sync)**: Pass a `channel` option to override the channel for a single sync call. This does **not** persist the channel.
1239
1271
 
1240
- Each method overrides the ones above it.
1241
- You can check the currently resolved channel by calling [`getChannel()`](#getchannel).
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.
1242
1275
 
1243
1276
  ### Why can't I see my changes during development?
1244
1277
 
@@ -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"
@@ -69,6 +69,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
69
69
  import java.util.concurrent.atomic.AtomicLong;
70
70
  import java.util.concurrent.atomic.AtomicReference;
71
71
  import net.lingala.zip4j.ZipFile;
72
+ import net.lingala.zip4j.model.FileHeader;
72
73
  import okhttp3.Call;
73
74
  import okhttp3.HttpUrl;
74
75
  import okhttp3.Response;
@@ -297,6 +298,17 @@ public class LiveUpdate {
297
298
  callback.success(result);
298
299
  }
299
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
+
300
312
  public void getCustomId(@NonNull NonEmptyCallback callback) {
301
313
  String customId = preferences.getCustomId();
302
314
  GetCustomIdResult result = new GetCustomIdResult(customId);
@@ -971,13 +983,17 @@ public class LiveUpdate {
971
983
  @NonNull NonEmptyCallback<GetLatestBundleResponse> callback
972
984
  ) {
973
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
+ }
974
990
  String channel = options.getChannel() == null ? getChannel() : options.getChannel();
975
991
  String url = new HttpUrl.Builder()
976
992
  .scheme("https")
977
993
  .host(config.getServerDomain())
978
994
  .addPathSegment("v1")
979
995
  .addPathSegment("apps")
980
- .addPathSegment(getAppId())
996
+ .addPathSegment(appId)
981
997
  .addPathSegment("bundles")
982
998
  .addPathSegment("latest")
983
999
  .addQueryParameter("appVersionCode", getVersionCodeAsString())
@@ -1432,7 +1448,13 @@ public class LiveUpdate {
1432
1448
  private File unzipFile(@NonNull File zipFile) throws IOException {
1433
1449
  File destination = buildTemporaryDirectory();
1434
1450
  String destinationPath = destination.getPath();
1435
- 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);
1436
1458
  return destination;
1437
1459
  }
1438
1460
 
@@ -32,6 +32,7 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingRes
32
32
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
33
33
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
34
34
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
35
+ import io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic.LiveUpdateIonicProviderRegistration;
35
36
 
36
37
  @CapacitorPlugin(name = "LiveUpdate")
37
38
  public class LiveUpdatePlugin extends Plugin {
@@ -40,6 +41,7 @@ public class LiveUpdatePlugin extends Plugin {
40
41
  public static final String VERSION = "8.2.0";
41
42
  public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
42
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.";
43
45
  public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
44
46
  public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
45
47
  public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
@@ -48,7 +50,9 @@ public class LiveUpdatePlugin extends Plugin {
48
50
  public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
49
51
  public static final String ERROR_CUSTOM_ID_MISSING = "customId must be provided.";
50
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.";
51
54
  public static final String ERROR_HTTP_TIMEOUT = "Request timed out.";
55
+ public static final String ERROR_MANAGER_KEY_MISSING = "managerKey must be provided.";
52
56
  public static final String ERROR_URL_MISSING = "url must be provided.";
53
57
  public static final String ERROR_SIGNATURE_VERIFICATION_FAILED = "Signature verification failed.";
54
58
  public static final String ERROR_PUBLIC_KEY_INVALID = "Invalid public key.";
@@ -71,6 +75,10 @@ public class LiveUpdatePlugin extends Plugin {
71
75
  try {
72
76
  config = getLiveUpdateConfig();
73
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
+ }
74
82
  } catch (Exception exception) {
75
83
  Logger.error(TAG, exception.getMessage(), exception);
76
84
  }
@@ -6,17 +6,32 @@ import com.getcapacitor.PluginCall;
6
6
 
7
7
  public class FetchLatestBundleOptions {
8
8
 
9
+ @Nullable
10
+ private final String appId;
11
+
9
12
  @Nullable
10
13
  private final String channel;
11
14
 
12
15
  public FetchLatestBundleOptions(@NonNull PluginCall call) {
16
+ this.appId = null;
13
17
  this.channel = call.getString("channel", null);
14
18
  }
15
19
 
16
20
  public FetchLatestBundleOptions(@Nullable String channel) {
21
+ this.appId = null;
22
+ this.channel = channel;
23
+ }
24
+
25
+ public FetchLatestBundleOptions(@Nullable String appId, @Nullable String channel) {
26
+ this.appId = appId;
17
27
  this.channel = channel;
18
28
  }
19
29
 
30
+ @Nullable
31
+ public String getAppId() {
32
+ return appId;
33
+ }
34
+
20
35
  @Nullable
21
36
  public String getChannel() {
22
37
  return channel;
@@ -43,6 +43,36 @@ public class FetchLatestBundleResult implements Result {
43
43
  this.signature = signature;
44
44
  }
45
45
 
46
+ @Nullable
47
+ public ArtifactType getArtifactType() {
48
+ return artifactType;
49
+ }
50
+
51
+ @Nullable
52
+ public String getBundleId() {
53
+ return bundleId;
54
+ }
55
+
56
+ @Nullable
57
+ public String getChecksum() {
58
+ return checksum;
59
+ }
60
+
61
+ @Nullable
62
+ public JSONObject getCustomProperties() {
63
+ return customProperties;
64
+ }
65
+
66
+ @Nullable
67
+ public String getDownloadUrl() {
68
+ return downloadUrl;
69
+ }
70
+
71
+ @Nullable
72
+ public String getSignature() {
73
+ return signature;
74
+ }
75
+
46
76
  @NonNull
47
77
  public JSObject toJSObject() {
48
78
  JSObject result = new JSObject();
@@ -0,0 +1,232 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic;
2
+
3
+ import android.content.Context;
4
+ import android.content.SharedPreferences;
5
+ import androidx.annotation.NonNull;
6
+ import androidx.annotation.Nullable;
7
+ import com.getcapacitor.Logger;
8
+ import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdate;
9
+ import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdatePlugin;
10
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions;
11
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions;
12
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult;
13
+ import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
14
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
15
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
16
+ import io.ionic.liveupdateprovider.FederatedCapacitorSyncResult;
17
+ import io.ionic.liveupdateprovider.LiveUpdateProviderError;
18
+ import io.ionic.liveupdateprovider.LiveUpdateProviderManager;
19
+ import io.ionic.liveupdateprovider.LiveUpdateProviderSyncCallback;
20
+ import java.io.File;
21
+ import java.util.HashMap;
22
+ import java.util.Iterator;
23
+ import java.util.Map;
24
+ import org.json.JSONObject;
25
+
26
+ /**
27
+ * Ionic Live Update Provider manager backed by the Capawesome live update plugin.
28
+ *
29
+ * Each manager instance is scoped by a {@code managerKey} so that multiple shells (Portals or
30
+ * Federated Capacitor apps) can persist their own active bundle without colliding with each
31
+ * other or with the standalone plugin's {@code currentBundleId} / {@code nextBundleId} state.
32
+ *
33
+ * Provider config keys (V1):
34
+ * <ul>
35
+ * <li>{@code managerKey} (required) — scopes per-manager persisted state</li>
36
+ * <li>{@code appId} (optional) — Capawesome Cloud app UUID; falls back to plugin config</li>
37
+ * <li>{@code channel} (optional) — channel to sync; falls back to plugin config</li>
38
+ * </ul>
39
+ */
40
+ public class LiveUpdateIonicManager implements LiveUpdateProviderManager {
41
+
42
+ public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdateIonicProvider";
43
+ public static final String PREF_PREFIX_LAST_SYNCED_BUNDLE_ID = "lastSyncedBundleId.";
44
+
45
+ @Nullable
46
+ private final String appId;
47
+
48
+ @Nullable
49
+ private final String channel;
50
+
51
+ @Nullable
52
+ private File latestAppDirectory;
53
+
54
+ @NonNull
55
+ private final LiveUpdate liveUpdate;
56
+
57
+ @NonNull
58
+ private final String managerKey;
59
+
60
+ @NonNull
61
+ private final SharedPreferences sharedPreferences;
62
+
63
+ public LiveUpdateIonicManager(@NonNull Context context, @NonNull Map<String, ?> config, @NonNull LiveUpdate liveUpdate)
64
+ throws LiveUpdateProviderError.InvalidConfiguration {
65
+ Object managerKeyValue = config.get("managerKey");
66
+ if (!(managerKeyValue instanceof String) || ((String) managerKeyValue).isEmpty()) {
67
+ throw new LiveUpdateProviderError.InvalidConfiguration(LiveUpdatePlugin.ERROR_MANAGER_KEY_MISSING, null);
68
+ }
69
+ this.managerKey = (String) managerKeyValue;
70
+
71
+ Object appIdValue = config.get("appId");
72
+ this.appId = appIdValue instanceof String ? (String) appIdValue : null;
73
+
74
+ Object channelValue = config.get("channel");
75
+ this.channel = channelValue instanceof String ? (String) channelValue : null;
76
+
77
+ this.liveUpdate = liveUpdate;
78
+ this.sharedPreferences = context.getApplicationContext().getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
79
+
80
+ // Restore the last synced bundle directory, if any.
81
+ String persistedBundleId = sharedPreferences.getString(getLastSyncedBundleIdKey(), null);
82
+ if (persistedBundleId != null) {
83
+ this.latestAppDirectory = liveUpdate.getBundleDirectory(persistedBundleId);
84
+ }
85
+ }
86
+
87
+ @Nullable
88
+ @Override
89
+ public File getLatestAppDirectory() {
90
+ return latestAppDirectory;
91
+ }
92
+
93
+ @Override
94
+ public void sync(@Nullable LiveUpdateProviderSyncCallback callback) {
95
+ try {
96
+ FetchLatestBundleOptions fetchOptions = new FetchLatestBundleOptions(appId, channel);
97
+ liveUpdate.fetchLatestBundle(
98
+ fetchOptions,
99
+ new NonEmptyCallback<FetchLatestBundleResult>() {
100
+ @Override
101
+ public void success(@NonNull FetchLatestBundleResult result) {
102
+ handleFetchLatestBundleSuccess(result, callback);
103
+ }
104
+
105
+ @Override
106
+ public void error(@NonNull Exception exception) {
107
+ notifyFailure(callback, exception);
108
+ }
109
+ }
110
+ );
111
+ } catch (Exception exception) {
112
+ notifyFailure(callback, exception);
113
+ }
114
+ }
115
+
116
+ private void handleFetchLatestBundleSuccess(
117
+ @NonNull FetchLatestBundleResult result,
118
+ @Nullable LiveUpdateProviderSyncCallback callback
119
+ ) {
120
+ try {
121
+ String bundleId = result.getBundleId();
122
+ if (bundleId == null) {
123
+ // No update available; report success without changing state.
124
+ notifySuccess(callback, null, null);
125
+ return;
126
+ }
127
+
128
+ // Bundle already on disk — just point latestAppDirectory at it and persist.
129
+ File existingDirectory = liveUpdate.getBundleDirectory(bundleId);
130
+ if (existingDirectory != null) {
131
+ applySyncedBundle(bundleId, existingDirectory);
132
+ notifySuccess(callback, bundleId, result.getCustomProperties());
133
+ return;
134
+ }
135
+
136
+ // Otherwise, download and then apply.
137
+ String downloadUrl = result.getDownloadUrl();
138
+ if (downloadUrl == null || downloadUrl.isEmpty()) {
139
+ notifyFailure(callback, new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_URL_MISSING));
140
+ return;
141
+ }
142
+ ArtifactType artifactType = result.getArtifactType() == null ? ArtifactType.ZIP : result.getArtifactType();
143
+ String artifactTypeString = artifactType == ArtifactType.MANIFEST ? "manifest" : "zip";
144
+ DownloadBundleOptions downloadOptions = new DownloadBundleOptions(
145
+ artifactTypeString,
146
+ bundleId,
147
+ result.getChecksum(),
148
+ result.getSignature(),
149
+ downloadUrl
150
+ );
151
+ liveUpdate.downloadBundle(
152
+ downloadOptions,
153
+ new EmptyCallback() {
154
+ @Override
155
+ public void success() {
156
+ File downloadedDirectory = liveUpdate.getBundleDirectory(bundleId);
157
+ if (downloadedDirectory == null) {
158
+ notifyFailure(callback, new Exception(LiveUpdatePlugin.ERROR_BUNDLE_DIRECTORY_NOT_FOUND));
159
+ return;
160
+ }
161
+ applySyncedBundle(bundleId, downloadedDirectory);
162
+ notifySuccess(callback, bundleId, result.getCustomProperties());
163
+ }
164
+
165
+ @Override
166
+ public void error(@NonNull Exception exception) {
167
+ notifyFailure(callback, exception);
168
+ }
169
+ }
170
+ );
171
+ } catch (Exception exception) {
172
+ notifyFailure(callback, exception);
173
+ }
174
+ }
175
+
176
+ private void applySyncedBundle(@NonNull String bundleId, @NonNull File directory) {
177
+ latestAppDirectory = directory;
178
+ sharedPreferences.edit().putString(getLastSyncedBundleIdKey(), bundleId).apply();
179
+ }
180
+
181
+ private void notifySuccess(
182
+ @Nullable LiveUpdateProviderSyncCallback callback,
183
+ @Nullable String bundleId,
184
+ @Nullable JSONObject customProperties
185
+ ) {
186
+ if (callback == null) {
187
+ return;
188
+ }
189
+ Map<String, Object> metadata = new HashMap<>();
190
+ if (bundleId != null) {
191
+ metadata.put("bundleId", bundleId);
192
+ }
193
+ if (channel != null) {
194
+ metadata.put("channel", channel);
195
+ }
196
+ if (customProperties != null) {
197
+ Map<String, Object> customPropertiesMap = jsonObjectToMap(customProperties);
198
+ if (!customPropertiesMap.isEmpty()) {
199
+ metadata.put("customProperties", customPropertiesMap);
200
+ }
201
+ }
202
+ callback.onSuccess(new FederatedCapacitorSyncResult(metadata));
203
+ }
204
+
205
+ private void notifyFailure(@Nullable LiveUpdateProviderSyncCallback callback, @NonNull Exception exception) {
206
+ Logger.error(LiveUpdatePlugin.TAG, "Ionic provider sync failed: " + exception.getMessage(), exception);
207
+ if (callback == null) {
208
+ return;
209
+ }
210
+ String message = exception.getMessage() == null ? LiveUpdatePlugin.ERROR_UNKNOWN_ERROR : exception.getMessage();
211
+ callback.onFailure(new LiveUpdateProviderError.SyncFailed(message, exception));
212
+ }
213
+
214
+ @NonNull
215
+ private String getLastSyncedBundleIdKey() {
216
+ return PREF_PREFIX_LAST_SYNCED_BUNDLE_ID + managerKey;
217
+ }
218
+
219
+ @NonNull
220
+ private static Map<String, Object> jsonObjectToMap(@NonNull JSONObject jsonObject) {
221
+ Map<String, Object> map = new HashMap<>();
222
+ Iterator<String> keys = jsonObject.keys();
223
+ while (keys.hasNext()) {
224
+ String key = keys.next();
225
+ Object value = jsonObject.opt(key);
226
+ if (value != null && value != JSONObject.NULL) {
227
+ map.put(key, value);
228
+ }
229
+ }
230
+ return map;
231
+ }
232
+ }
@@ -0,0 +1,41 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic;
2
+
3
+ import android.content.Context;
4
+ import androidx.annotation.NonNull;
5
+ import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdate;
6
+ import io.ionic.liveupdateprovider.LiveUpdateProvider;
7
+ import io.ionic.liveupdateprovider.LiveUpdateProviderError;
8
+ import io.ionic.liveupdateprovider.LiveUpdateProviderManager;
9
+ import java.util.Map;
10
+
11
+ /**
12
+ * Ionic Live Update Provider implementation backed by the Capawesome live update plugin.
13
+ *
14
+ * Registered with {@link io.ionic.liveupdateprovider.LiveUpdateProviderRegistry} when the
15
+ * Capacitor plugin loads, so Federated Capacitor can resolve provider id {@code "capawesome"}
16
+ * and create managers from the per-shell configuration.
17
+ */
18
+ public class LiveUpdateIonicProvider implements LiveUpdateProvider {
19
+
20
+ public static final String ID = "capawesome";
21
+
22
+ @NonNull
23
+ private final LiveUpdate liveUpdate;
24
+
25
+ public LiveUpdateIonicProvider(@NonNull LiveUpdate liveUpdate) {
26
+ this.liveUpdate = liveUpdate;
27
+ }
28
+
29
+ @NonNull
30
+ @Override
31
+ public String getId() {
32
+ return ID;
33
+ }
34
+
35
+ @NonNull
36
+ @Override
37
+ public LiveUpdateProviderManager createManager(@NonNull Context context, @NonNull Map<String, ?> config)
38
+ throws LiveUpdateProviderError.InvalidConfiguration {
39
+ return new LiveUpdateIonicManager(context, config, liveUpdate);
40
+ }
41
+ }
@@ -0,0 +1,46 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.getcapacitor.Logger;
5
+ import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdate;
6
+ import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdatePlugin;
7
+ import io.ionic.liveupdateprovider.LiveUpdateProviderRegistry;
8
+
9
+ /**
10
+ * Helper that registers the Capawesome live update provider with the Ionic
11
+ * {@link LiveUpdateProviderRegistry}, but only when the optional
12
+ * {@code io.ionic:liveupdateprovider} dependency is present at runtime.
13
+ *
14
+ * The Gradle build wires the SDK as either {@code implementation} (when the consumer opts in
15
+ * via {@code capawesomeCapacitorLiveUpdateIncludeIonicProvider = true}) or {@code compileOnly}
16
+ * (the default). When opted out, the SDK classes are stripped from the consumer APK and the
17
+ * reflection check below short-circuits registration.
18
+ */
19
+ public final class LiveUpdateIonicProviderRegistration {
20
+
21
+ private LiveUpdateIonicProviderRegistration() {
22
+ // Static utility — no instances.
23
+ }
24
+
25
+ public static boolean isAvailable() {
26
+ try {
27
+ Class.forName("io.ionic.liveupdateprovider.LiveUpdateProviderRegistry");
28
+ return true;
29
+ } catch (ClassNotFoundException exception) {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ public static void tryRegister(@NonNull LiveUpdate liveUpdate) {
35
+ try {
36
+ if (LiveUpdateProviderRegistry.resolve(LiveUpdateIonicProvider.ID) != null) {
37
+ Logger.debug(LiveUpdatePlugin.TAG, "Ionic provider '" + LiveUpdateIonicProvider.ID + "' already registered.");
38
+ return;
39
+ }
40
+ LiveUpdateProviderRegistry.register(new LiveUpdateIonicProvider(liveUpdate));
41
+ Logger.debug(LiveUpdatePlugin.TAG, "Registered Ionic provider '" + LiveUpdateIonicProvider.ID + "'.");
42
+ } catch (Exception exception) {
43
+ Logger.error(LiveUpdatePlugin.TAG, "Failed to register Ionic provider: " + exception.getMessage(), exception);
44
+ }
45
+ }
46
+ }
package/dist/docs.json CHANGED
@@ -877,7 +877,7 @@
877
877
  "name": "since"
878
878
  }
879
879
  ],
880
- "docs": "The unique identifier of the latest bundle.\n\nIf `null`, no bundle is available.",
880
+ "docs": "The unique identifier of the latest bundle.\n\nOn Capawesome Cloud, this is the ID of the app build artifact.\n\nIf `null`, no bundle is available.",
881
881
  "complexTypes": [],
882
882
  "type": "string | null"
883
883
  },
@@ -556,6 +556,8 @@ export interface FetchLatestBundleResult {
556
556
  /**
557
557
  * The unique identifier of the latest bundle.
558
558
  *
559
+ * On Capawesome Cloud, this is the ID of the app build artifact.
560
+ *
559
561
  * If `null`, no bundle is available.
560
562
  *
561
563
  * @since 6.6.0
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,wCAAwC","sourcesContent":["/// <reference types=\"@capacitor/cli\" />\n\nimport type { PluginListenerHandle } from '@capacitor/core';\n\ndeclare module '@capacitor/cli' {\n export interface PluginsConfig {\n LiveUpdate?: {\n /**\n * The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * This is **NOT** the same as the app identifier (e.g. `com.example.app`).\n * This is a unique identifier generated by Capawesome Cloud (e.g. `6e351b4f-69a7-415e-a057-4567df7ffe94`).\n *\n * @since 5.0.0\n * @example '6e351b4f-69a7-415e-a057-4567df7ffe94'\n */\n appId?: string;\n /**\n * Whether or not to automatically block bundles that have been rolled back.\n *\n * When enabled, the plugin will automatically block bundles that caused a rollback\n * (up to 100 bundles). When the limit is reached, the oldest blocked bundle is unblocked.\n * Blocked bundles will be skipped in future sync operations.\n *\n * **Attention**: This option has no effect if `readyTimeout` is set to `0`.\n *\n * Only available on Android and iOS.\n *\n * @since 7.3.0\n * @default false\n */\n autoBlockRolledBackBundles?: boolean;\n /**\n * Whether or not to automatically delete unused bundles.\n *\n * When enabled, the plugin will automatically delete unused bundles after calling `ready()`.\n *\n * @since 5.0.0\n * @default false\n */\n autoDeleteBundles?: boolean;\n /**\n * The auto-update strategy for live updates.\n *\n * - `none`: Live updates will not be applied automatically.\n * - `background`: Live updates will be automatically downloaded\n * and applied in the background at app startup and when the app resumes\n * (if the last check was more than 15 minutes ago).\n *\n * Only available on Android and iOS.\n *\n * @since 7.3.0\n * @default 'none'\n * @example 'background'\n */\n autoUpdateStrategy?: 'none' | 'background';\n /**\n * The default channel of the app.\n *\n * This can be overridden by `setChannel()`, the `channel` parameter of `sync()`,\n * or the native channel configuration\n * (`CapawesomeLiveUpdateDefaultChannel` in `Info.plist` on iOS or `capawesome_live_update_default_channel`\n * in `strings.xml` on Android).\n *\n * @since 6.3.0\n * @example 'production'\n */\n defaultChannel?: string;\n /**\n * The timeout in milliseconds for HTTP requests.\n *\n * @since 6.4.0\n * @default 60000\n */\n httpTimeout?: number;\n /**\n * The public key to verify the integrity of the bundle.\n *\n * The public key must be a PEM-encoded RSA public key.\n *\n * @since 6.1.0\n * @example '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----'\n */\n publicKey?: string;\n /**\n * The timeout in milliseconds to wait for the app to be ready\n * before resetting to the default bundle.\n *\n * It is strongly **recommended** to configure this option (e.g. `10000` ms)\n * so that the plugin can roll back to the default bundle in case of problems.\n * If configured, the plugin will wait for the app to call the `ready()`\n * method before resetting to the default bundle.\n *\n * Set to `0` to disable the timeout.\n *\n * @since 5.0.0\n * @default 0\n * @example 10000\n */\n readyTimeout?: number;\n /**\n * The API domain of the [Capawesome Cloud](https://cloud.capawesome.io) server **without** scheme or path.\n *\n * @since 7.0.0\n * @default 'api.cloud.capawesome.io'\n * @example 'api.cloud.capawesome.eu'\n */\n serverDomain?: string;\n };\n }\n}\n\nexport interface LiveUpdatePlugin {\n /**\n * Clear all blocked bundles from the blocked list.\n *\n * This removes all bundle identifiers that were automatically blocked\n * due to rollbacks when `autoBlockRolledBackBundles` is enabled.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n clearBlockedBundles(): Promise<void>;\n /**\n * Delete a bundle from the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n deleteBundle(options: DeleteBundleOptions): Promise<void>;\n /**\n * Download a bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n downloadBundle(options: DownloadBundleOptions): Promise<void>;\n /**\n * Fetch channels from [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * This is primarily intended for development and QA purposes.\n * It allows you to retrieve a list of available channels so you can\n * dynamically switch between them using `setChannel(...)`.\n *\n * **Attention**: Only works for apps with public channels enabled.\n * If channels are private, they can still be set using `setChannel(...)`\n * but won't be returned by this method.\n *\n * Only available on Android and iOS.\n *\n * @since 8.2.0\n */\n fetchChannels(options?: FetchChannelsOptions): Promise<FetchChannelsResult>;\n /**\n * Fetch the latest bundle using the [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * Only available on Android and iOS.\n *\n * @since 6.6.0\n */\n fetchLatestBundle(\n options?: FetchLatestBundleOptions,\n ): Promise<FetchLatestBundleResult>;\n /**\n * Get all blocked bundle identifiers.\n *\n * Returns the list of bundle identifiers that were automatically blocked\n * due to rollbacks when `autoBlockRolledBackBundles` is enabled.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n getBlockedBundles(): Promise<GetBlockedBundlesResult>;\n /**\n * Get all identifiers of bundles that have been downloaded.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n * @deprecated Use `getDownloadedBundles()` instead.\n */\n getBundles(): Promise<GetBundlesResult>;\n /**\n * Get the channel that is used for the update.\n *\n * The channel is resolved in the following order (highest priority first):\n * 1. `setChannel()` (SharedPreferences on Android / UserDefaults on iOS)\n * 2. Native config (`CapawesomeLiveUpdateDefaultChannel` in `Info.plist` on iOS or\n * `capawesome_live_update_default_channel` in `strings.xml` on Android)\n * 3. Capacitor config `defaultChannel`\n *\n * **Note**: The `channel` parameter of `sync()` takes the highest priority\n * but is not persisted and therefore not returned by this method.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getChannel(): Promise<GetChannelResult>;\n /**\n * Get the runtime configuration.\n *\n * Returns the current plugin configuration including any runtime\n * overrides set via `setConfig()`.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n getConfig(): Promise<GetConfigResult>;\n /**\n * Get all identifiers of bundles that have been downloaded.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n getDownloadedBundles(): Promise<GetDownloadedBundlesResult>;\n /**\n * Get the bundle identifier of the current bundle.\n * The current bundle is the bundle that is currently used by the app.\n *\n * Only available on Android and iOS.\n *\n * @since 6.7.0\n */\n getCurrentBundle(): Promise<GetCurrentBundleResult>;\n /**\n * Get the custom identifier of the device.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getCustomId(): Promise<GetCustomIdResult>;\n /**\n * Get the unique device identifier.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getDeviceId(): Promise<GetDeviceIdResult>;\n /**\n * Check whether a sync operation is currently in progress.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n isSyncing(): Promise<IsSyncingResult>;\n /**\n * Get the bundle identifier of the next bundle.\n * The next bundle is the bundle that will be used after calling `reload()`\n * or restarting the app.\n *\n * Only available on Android and iOS.\n *\n * @since 6.7.0\n */\n getNextBundle(): Promise<GetNextBundleResult>;\n /**\n * Get the version code of the app.\n *\n * On **Android**, this is the `versionCode` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleVersion` from the `Info.plist` file.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionCode(): Promise<GetVersionCodeResult>;\n /**\n * Get the version name of the app.\n *\n * On **Android**, this is the `versionName` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleShortVersionString` from the `Info.plist` file.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionName(): Promise<GetVersionNameResult>;\n /**\n * Notify the plugin that the app is ready to use and no rollback is needed.\n *\n * **Attention**: This method should be called as soon as the app is ready to use\n * to prevent the app from being reset to the default bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n ready(): Promise<ReadyResult>;\n /**\n * Reload the app to apply the new bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reload(): Promise<void>;\n /**\n * Reset the app to the default bundle.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reset(): Promise<void>;\n /**\n * Reset the runtime configuration to the values from the Capacitor config file.\n *\n * This clears any runtime configuration set via `setConfig()`.\n * The changes take effect immediately.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n resetConfig(): Promise<void>;\n /**\n * Set the channel to use for the update.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setChannel(options: SetChannelOptions): Promise<void>;\n /**\n * Set the runtime configuration.\n *\n * This allows updating plugin configuration options at runtime.\n * The changes are persisted across app restarts and take effect immediately.\n *\n * **Important:** Runtime configuration is automatically reset to default values\n * whenever the native app is updated to a new version. This ensures that\n * configuration from previous versions doesn't persist after an app update.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n setConfig(options: SetConfigOptions): Promise<void>;\n /**\n * Set the custom identifier of the device.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setCustomId(options: SetCustomIdOptions): Promise<void>;\n /**\n * Set the next bundle to use for the app.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 6.7.0\n */\n setNextBundle(options: SetNextBundleOptions): Promise<void>;\n /**\n * Automatically download and set the latest bundle for the app using the [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n sync(options?: SyncOptions): Promise<SyncResult>;\n /**\n * Listen for the download progress of a bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'downloadBundleProgress',\n listenerFunc: DownloadBundleProgressListener,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for when a bundle is set as the next bundle.\n *\n * This event is triggered whenever a bundle is set to be used on the next app restart,\n * either through automatic updates or manual calls to `setNextBundle()`.\n *\n * Only available on Android and iOS.\n *\n * @since 7.3.0\n */\n addListener(\n eventName: 'nextBundleSet',\n listenerFunc: NextBundleSetListener,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for when the app is reloaded.\n *\n * This event is triggered after the `reload()` method is called\n * and the app has been reloaded.\n *\n * **Note:** To verify whether an update was successfully applied after a reload,\n * use the `ready()` method instead. The `ready()` method provides detailed information\n * about the current bundle, previous bundle, and whether a rollback occurred.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n addListener(\n eventName: 'reloaded',\n listenerFunc: ReloadedListener,\n ): Promise<PluginListenerHandle>;\n /**\n * Remove all listeners for this plugin.\n *\n * @since 7.2.0\n */\n removeAllListeners(): Promise<void>;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface DeleteBundleOptions {\n /**\n * The unique identifier of the bundle to delete.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\nexport interface DownloadBundleOptions {\n /**\n * The artifact type of the bundle.\n *\n * @since 6.6.0\n * @default 'zip'\n * @example 'manifest'\n */\n artifactType?: 'manifest' | 'zip';\n /**\n * The unique identifier of the bundle.\n *\n * **Attention**: The value `public` is reserved and cannot be used as a bundle identifier.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n /**\n * The checksum of the self-hosted bundle as a SHA-256 hash\n * in hex format to verify the integrity of the bundle.\n *\n * **Attention**: Only supported for the `zip` artifact type.\n *\n * @since 7.1.0\n */\n checksum?: string;\n /**\n * The signature of the self-hosted bundle as a signed SHA-256 hash\n * in base64 format to verify the integrity of the bundle.\n *\n * **Attention**: Only supported for the `zip` artifact type.\n *\n * @since 7.1.0\n */\n signature?: string;\n /**\n * The URL of the bundle to download.\n *\n * For the `zip` artifact type, the URL must point to a ZIP file.\n * For the `manifest` artifact type, the URL serves as the base URL\n * to download the individual files. For example, if the URL is\n * `https://example.com/download`, the plugin will download the file\n * with the href `index.html` from `https://example.com/download?href=index.html`.\n *\n * To **verify the integrity** of the file, the server should return\n * a `X-Checksum` header with the SHA-256 hash in hex format.\n *\n * To **verify the signature** of the file, the server should return\n * a `X-Signature` header with the signed SHA-256 hash in base64 format.\n *\n * @since 5.0.0\n * @example 'https://example.com/bundle.zip'\n */\n url: string;\n}\n\n/**\n * @since 8.2.0\n */\nexport interface FetchChannelsOptions {\n /**\n * The maximum number of channels to return.\n *\n * @since 8.2.0\n * @default 50\n */\n limit?: number;\n /**\n * The number of channels to skip.\n *\n * @since 8.2.0\n * @default 0\n */\n offset?: number;\n /**\n * The query to filter channels by name.\n *\n * @since 8.2.0\n */\n query?: string;\n}\n\n/**\n * @since 8.2.0\n */\nexport interface FetchChannelsResult {\n /**\n * The list of channels.\n *\n * @since 8.2.0\n */\n channels: Channel[];\n}\n\n/**\n * @since 8.2.0\n */\nexport interface Channel {\n /**\n * The unique identifier of the channel.\n *\n * @since 8.2.0\n */\n id: string;\n /**\n * The name of the channel.\n *\n * @since 8.2.0\n */\n name: string;\n}\n\n/**\n * @since 6.7.0\n */\nexport interface FetchLatestBundleOptions {\n /**\n * The name of the channel where the latest bundle is fetched from.\n *\n * @since 6.7.0\n */\n channel?: string;\n}\n\n/**\n * @since 6.6.0\n */\nexport interface FetchLatestBundleResult {\n /**\n * The artifact type of the bundle.\n *\n * @since 6.7.0\n */\n artifactType?: 'manifest' | 'zip';\n /**\n * The unique identifier of the latest bundle.\n *\n * If `null`, no bundle is available.\n *\n * @since 6.6.0\n */\n bundleId: string | null;\n /**\n * The checksum of the latest bundle if the bundle is self-hosted.\n *\n * If the bundle is hosted on Capawesome Cloud, the checksum will be\n * returned as response header when downloading the bundle.\n *\n * @since 7.1.0\n */\n checksum?: string;\n /**\n * Custom properties that are associated with the latest bundle.\n *\n * @since 7.0.0\n * @example { \"key\": \"value\" }\n */\n customProperties?: { [key: string]: string };\n /**\n * The URL of the latest bundle to download.\n * Pass this URL to the `downloadBundle(...)` method to download the bundle.\n *\n * @since 6.7.0\n */\n downloadUrl?: string;\n /**\n * The signature of the latest bundle if the bundle is self-hosted.\n *\n * If the bundle is hosted on Capawesome Cloud, the signature will be\n * returned as response header when downloading the bundle.\n *\n * @since 7.1.0\n */\n signature?: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundleResult {\n /**\n * The unique identifier of the active bundle.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string | null;\n}\n\n/**\n * @since 7.4.0\n */\nexport interface GetBlockedBundlesResult {\n /**\n * An array of unique identifiers of all blocked bundles.\n *\n * @since 7.4.0\n */\n bundleIds: string[];\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundlesResult {\n /**\n * An array of unique identifiers of all available bundles.\n *\n * @since 5.0.0\n */\n bundleIds: string[];\n}\n\n/**\n * @since 7.4.0\n */\nexport interface GetDownloadedBundlesResult {\n /**\n * An array of unique identifiers of all downloaded bundles.\n *\n * @since 7.4.0\n */\n bundleIds: string[];\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetChannelResult {\n /**\n * The channel name.\n *\n * If `null`, the app is using the default channel.\n *\n * @since 5.0.0\n * @example 'production'\n */\n channel: string | null;\n}\n\n/**\n * @since 7.4.0\n */\nexport interface GetConfigResult {\n /**\n * The app ID used to identify the app.\n *\n * If `null`, no app ID is configured.\n *\n * @since 7.4.0\n * @example '6e351b4f-69a7-415e-a057-4567df7ffe94'\n */\n appId: string | null;\n /**\n * The auto-update strategy for live updates.\n *\n * @since 7.4.0\n * @example 'background'\n */\n autoUpdateStrategy: 'none' | 'background';\n}\n\n/**\n * @since 6.7.0\n */\nexport interface GetCurrentBundleResult {\n /**\n * The unique identifier of the current bundle.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 6.7.0\n */\n bundleId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetDeviceIdResult {\n /**\n * The unique identifier of the device.\n *\n * On iOS, [`identifierForVendor`](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) is used.\n * The value of this property is the same for apps that come from the same vendor running on the same device.\n *\n * @since 5.0.0\n *\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n deviceId: string;\n}\n\n/**\n * @since 7.4.0\n */\nexport interface IsSyncingResult {\n /**\n * Whether a sync operation is currently in progress.\n *\n * @since 7.4.0\n */\n syncing: boolean;\n}\n\n/**\n * @since 6.7.0\n */\nexport interface GetNextBundleResult {\n /**\n * The unique identifier of the next bundle.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 6.7.0\n */\n bundleId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionCodeResult {\n /**\n * The version code of the app.\n *\n * On **Android**, this is the `versionCode` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleVersion` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1\"\n */\n versionCode: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionNameResult {\n /**\n * The version name of the app.\n *\n * On **Android**, this is the `versionName` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleShortVersionString` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1.0.0\"\n */\n versionName: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetCustomIdResult {\n /**\n * The custom identifier of the device.\n *\n * If `null`, no custom identifier is set.\n *\n * @since 5.0.0\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n customId: string | null;\n}\n\n/**\n * @since 7.0.0\n */\nexport interface ReadyResult {\n /**\n * The identifier of the previous bundle used.\n *\n * If `null`, the default bundle was used.\n *\n * @since 7.0.0\n */\n previousBundleId: string | null;\n /**\n * The identifier of the current bundle used.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 7.0.0\n */\n currentBundleId: string | null;\n /**\n * Whether or not the app was reset to the default bundle.\n *\n * @since 7.0.0\n */\n rollback: boolean;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetBundleOptions {\n /**\n * The unique identifier of the bundle to use.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetChannelOptions {\n /**\n * The channel name.\n *\n * Set `null` to remove the channel.\n *\n * @since 5.0.0\n */\n channel: string | null;\n}\n\n/**\n * @since 7.4.0\n */\nexport interface SetConfigOptions {\n /**\n * The app ID used to identify the app.\n *\n * Set `null` to reset to the value from the Capacitor config file.\n *\n * @since 7.4.0\n * @example '6e351b4f-69a7-415e-a057-4567df7ffe94'\n */\n appId?: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetCustomIdOptions {\n /**\n * The custom identifier of the device.\n *\n * Set `null` to remove the custom identifier.\n *\n * @since 5.0.0\n */\n customId: string | null;\n}\n\n/**\n * @since 6.7.0\n */\nexport interface SetNextBundleOptions {\n /**\n * The unique identifier of the bundle to use.\n *\n * Set `null` to use the default bundle (same as calling `reset()`).\n *\n * @since 6.7.0\n * @example '1.0.0'\n */\n bundleId: string | null;\n}\n\n/**\n * @since 6.7.0\n */\nexport interface SyncOptions {\n /**\n * The name of the channel where the latest bundle is fetched from.\n *\n * @since 6.7.0\n */\n channel?: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SyncResult {\n /**\n * The identifier of the next bundle to use.\n *\n * If `null`, the app is up-to-date and no new bundle is available.\n *\n * @since 5.0.0\n */\n nextBundleId: string | null;\n}\n\n/**\n * Listener for the download progress of a bundle.\n *\n * @since 7.0.0\n */\nexport type DownloadBundleProgressListener = (\n event: DownloadBundleProgressEvent,\n) => void;\n\n/**\n * Event that is triggered when the download progress of a bundle changes.\n *\n * @since 7.0.0\n */\nexport interface DownloadBundleProgressEvent {\n /**\n * The unique identifier of the bundle that is being downloaded.\n *\n * @since 7.0.0\n */\n bundleId: string;\n /**\n * The number of bytes that have been downloaded.\n *\n * @since 7.0.0\n */\n downloadedBytes: number;\n /**\n * The progress of the download in percent as a value between `0` and `1`.\n *\n * @since 7.0.0\n * @example 0.5\n */\n progress: number;\n /**\n * The total number of bytes to download.\n *\n * @since 7.0.0\n */\n totalBytes: number;\n}\n\n/**\n * Listener for when a bundle is set as the next bundle.\n *\n * @since 7.3.0\n */\nexport type NextBundleSetListener = (event: NextBundleSetEvent) => void;\n\n/**\n * Event that is triggered when a bundle is set as the next bundle.\n *\n * @since 7.3.0\n */\nexport interface NextBundleSetEvent {\n /**\n * The unique identifier of the bundle that is set as the next bundle.\n *\n * If `null`, the default bundle will be used.\n *\n * @since 7.3.0\n * @example '1.0.0'\n */\n bundleId: string | null;\n}\n\n/**\n * Listener for when the app is reloaded.\n *\n * @since 7.4.0\n */\nexport type ReloadedListener = () => void;\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,wCAAwC","sourcesContent":["/// <reference types=\"@capacitor/cli\" />\n\nimport type { PluginListenerHandle } from '@capacitor/core';\n\ndeclare module '@capacitor/cli' {\n export interface PluginsConfig {\n LiveUpdate?: {\n /**\n * The app ID is used to identify the app when using [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * This is **NOT** the same as the app identifier (e.g. `com.example.app`).\n * This is a unique identifier generated by Capawesome Cloud (e.g. `6e351b4f-69a7-415e-a057-4567df7ffe94`).\n *\n * @since 5.0.0\n * @example '6e351b4f-69a7-415e-a057-4567df7ffe94'\n */\n appId?: string;\n /**\n * Whether or not to automatically block bundles that have been rolled back.\n *\n * When enabled, the plugin will automatically block bundles that caused a rollback\n * (up to 100 bundles). When the limit is reached, the oldest blocked bundle is unblocked.\n * Blocked bundles will be skipped in future sync operations.\n *\n * **Attention**: This option has no effect if `readyTimeout` is set to `0`.\n *\n * Only available on Android and iOS.\n *\n * @since 7.3.0\n * @default false\n */\n autoBlockRolledBackBundles?: boolean;\n /**\n * Whether or not to automatically delete unused bundles.\n *\n * When enabled, the plugin will automatically delete unused bundles after calling `ready()`.\n *\n * @since 5.0.0\n * @default false\n */\n autoDeleteBundles?: boolean;\n /**\n * The auto-update strategy for live updates.\n *\n * - `none`: Live updates will not be applied automatically.\n * - `background`: Live updates will be automatically downloaded\n * and applied in the background at app startup and when the app resumes\n * (if the last check was more than 15 minutes ago).\n *\n * Only available on Android and iOS.\n *\n * @since 7.3.0\n * @default 'none'\n * @example 'background'\n */\n autoUpdateStrategy?: 'none' | 'background';\n /**\n * The default channel of the app.\n *\n * This can be overridden by `setChannel()`, the `channel` parameter of `sync()`,\n * or the native channel configuration\n * (`CapawesomeLiveUpdateDefaultChannel` in `Info.plist` on iOS or `capawesome_live_update_default_channel`\n * in `strings.xml` on Android).\n *\n * @since 6.3.0\n * @example 'production'\n */\n defaultChannel?: string;\n /**\n * The timeout in milliseconds for HTTP requests.\n *\n * @since 6.4.0\n * @default 60000\n */\n httpTimeout?: number;\n /**\n * The public key to verify the integrity of the bundle.\n *\n * The public key must be a PEM-encoded RSA public key.\n *\n * @since 6.1.0\n * @example '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDodf1SD0OOn6hIlDuKBza0Ed0OqtwyVJwiyjmE9BJaZ7y8ZUfcF+SKmd0l2cDPM45XIg2tAFux5n29uoKyHwSt+6tCi5CJA5Z1/1eZruRRqABLonV77KS3HUtvOgqRLDnKSV89dYZkM++NwmzOPgIF422mvc+VukcVOBfc8/AHQIDAQAB-----END PUBLIC KEY-----'\n */\n publicKey?: string;\n /**\n * The timeout in milliseconds to wait for the app to be ready\n * before resetting to the default bundle.\n *\n * It is strongly **recommended** to configure this option (e.g. `10000` ms)\n * so that the plugin can roll back to the default bundle in case of problems.\n * If configured, the plugin will wait for the app to call the `ready()`\n * method before resetting to the default bundle.\n *\n * Set to `0` to disable the timeout.\n *\n * @since 5.0.0\n * @default 0\n * @example 10000\n */\n readyTimeout?: number;\n /**\n * The API domain of the [Capawesome Cloud](https://cloud.capawesome.io) server **without** scheme or path.\n *\n * @since 7.0.0\n * @default 'api.cloud.capawesome.io'\n * @example 'api.cloud.capawesome.eu'\n */\n serverDomain?: string;\n };\n }\n}\n\nexport interface LiveUpdatePlugin {\n /**\n * Clear all blocked bundles from the blocked list.\n *\n * This removes all bundle identifiers that were automatically blocked\n * due to rollbacks when `autoBlockRolledBackBundles` is enabled.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n clearBlockedBundles(): Promise<void>;\n /**\n * Delete a bundle from the app.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n deleteBundle(options: DeleteBundleOptions): Promise<void>;\n /**\n * Download a bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n downloadBundle(options: DownloadBundleOptions): Promise<void>;\n /**\n * Fetch channels from [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * This is primarily intended for development and QA purposes.\n * It allows you to retrieve a list of available channels so you can\n * dynamically switch between them using `setChannel(...)`.\n *\n * **Attention**: Only works for apps with public channels enabled.\n * If channels are private, they can still be set using `setChannel(...)`\n * but won't be returned by this method.\n *\n * Only available on Android and iOS.\n *\n * @since 8.2.0\n */\n fetchChannels(options?: FetchChannelsOptions): Promise<FetchChannelsResult>;\n /**\n * Fetch the latest bundle using the [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * Only available on Android and iOS.\n *\n * @since 6.6.0\n */\n fetchLatestBundle(\n options?: FetchLatestBundleOptions,\n ): Promise<FetchLatestBundleResult>;\n /**\n * Get all blocked bundle identifiers.\n *\n * Returns the list of bundle identifiers that were automatically blocked\n * due to rollbacks when `autoBlockRolledBackBundles` is enabled.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n getBlockedBundles(): Promise<GetBlockedBundlesResult>;\n /**\n * Get all identifiers of bundles that have been downloaded.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n * @deprecated Use `getDownloadedBundles()` instead.\n */\n getBundles(): Promise<GetBundlesResult>;\n /**\n * Get the channel that is used for the update.\n *\n * The channel is resolved in the following order (highest priority first):\n * 1. `setChannel()` (SharedPreferences on Android / UserDefaults on iOS)\n * 2. Native config (`CapawesomeLiveUpdateDefaultChannel` in `Info.plist` on iOS or\n * `capawesome_live_update_default_channel` in `strings.xml` on Android)\n * 3. Capacitor config `defaultChannel`\n *\n * **Note**: The `channel` parameter of `sync()` takes the highest priority\n * but is not persisted and therefore not returned by this method.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getChannel(): Promise<GetChannelResult>;\n /**\n * Get the runtime configuration.\n *\n * Returns the current plugin configuration including any runtime\n * overrides set via `setConfig()`.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n getConfig(): Promise<GetConfigResult>;\n /**\n * Get all identifiers of bundles that have been downloaded.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n getDownloadedBundles(): Promise<GetDownloadedBundlesResult>;\n /**\n * Get the bundle identifier of the current bundle.\n * The current bundle is the bundle that is currently used by the app.\n *\n * Only available on Android and iOS.\n *\n * @since 6.7.0\n */\n getCurrentBundle(): Promise<GetCurrentBundleResult>;\n /**\n * Get the custom identifier of the device.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getCustomId(): Promise<GetCustomIdResult>;\n /**\n * Get the unique device identifier.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getDeviceId(): Promise<GetDeviceIdResult>;\n /**\n * Check whether a sync operation is currently in progress.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n isSyncing(): Promise<IsSyncingResult>;\n /**\n * Get the bundle identifier of the next bundle.\n * The next bundle is the bundle that will be used after calling `reload()`\n * or restarting the app.\n *\n * Only available on Android and iOS.\n *\n * @since 6.7.0\n */\n getNextBundle(): Promise<GetNextBundleResult>;\n /**\n * Get the version code of the app.\n *\n * On **Android**, this is the `versionCode` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleVersion` from the `Info.plist` file.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionCode(): Promise<GetVersionCodeResult>;\n /**\n * Get the version name of the app.\n *\n * On **Android**, this is the `versionName` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleShortVersionString` from the `Info.plist` file.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n getVersionName(): Promise<GetVersionNameResult>;\n /**\n * Notify the plugin that the app is ready to use and no rollback is needed.\n *\n * **Attention**: This method should be called as soon as the app is ready to use\n * to prevent the app from being reset to the default bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n ready(): Promise<ReadyResult>;\n /**\n * Reload the app to apply the new bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reload(): Promise<void>;\n /**\n * Reset the app to the default bundle.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n reset(): Promise<void>;\n /**\n * Reset the runtime configuration to the values from the Capacitor config file.\n *\n * This clears any runtime configuration set via `setConfig()`.\n * The changes take effect immediately.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n resetConfig(): Promise<void>;\n /**\n * Set the channel to use for the update.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setChannel(options: SetChannelOptions): Promise<void>;\n /**\n * Set the runtime configuration.\n *\n * This allows updating plugin configuration options at runtime.\n * The changes are persisted across app restarts and take effect immediately.\n *\n * **Important:** Runtime configuration is automatically reset to default values\n * whenever the native app is updated to a new version. This ensures that\n * configuration from previous versions doesn't persist after an app update.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n setConfig(options: SetConfigOptions): Promise<void>;\n /**\n * Set the custom identifier of the device.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n setCustomId(options: SetCustomIdOptions): Promise<void>;\n /**\n * Set the next bundle to use for the app.\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 6.7.0\n */\n setNextBundle(options: SetNextBundleOptions): Promise<void>;\n /**\n * Automatically download and set the latest bundle for the app using the [Capawesome Cloud](https://capawesome.io/cloud/).\n *\n * Call `reload()` or restart the app to apply the changes.\n *\n * Only available on Android and iOS.\n *\n * @since 5.0.0\n */\n sync(options?: SyncOptions): Promise<SyncResult>;\n /**\n * Listen for the download progress of a bundle.\n *\n * Only available on Android and iOS.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'downloadBundleProgress',\n listenerFunc: DownloadBundleProgressListener,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for when a bundle is set as the next bundle.\n *\n * This event is triggered whenever a bundle is set to be used on the next app restart,\n * either through automatic updates or manual calls to `setNextBundle()`.\n *\n * Only available on Android and iOS.\n *\n * @since 7.3.0\n */\n addListener(\n eventName: 'nextBundleSet',\n listenerFunc: NextBundleSetListener,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for when the app is reloaded.\n *\n * This event is triggered after the `reload()` method is called\n * and the app has been reloaded.\n *\n * **Note:** To verify whether an update was successfully applied after a reload,\n * use the `ready()` method instead. The `ready()` method provides detailed information\n * about the current bundle, previous bundle, and whether a rollback occurred.\n *\n * Only available on Android and iOS.\n *\n * @since 7.4.0\n */\n addListener(\n eventName: 'reloaded',\n listenerFunc: ReloadedListener,\n ): Promise<PluginListenerHandle>;\n /**\n * Remove all listeners for this plugin.\n *\n * @since 7.2.0\n */\n removeAllListeners(): Promise<void>;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface DeleteBundleOptions {\n /**\n * The unique identifier of the bundle to delete.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\nexport interface DownloadBundleOptions {\n /**\n * The artifact type of the bundle.\n *\n * @since 6.6.0\n * @default 'zip'\n * @example 'manifest'\n */\n artifactType?: 'manifest' | 'zip';\n /**\n * The unique identifier of the bundle.\n *\n * **Attention**: The value `public` is reserved and cannot be used as a bundle identifier.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n /**\n * The checksum of the self-hosted bundle as a SHA-256 hash\n * in hex format to verify the integrity of the bundle.\n *\n * **Attention**: Only supported for the `zip` artifact type.\n *\n * @since 7.1.0\n */\n checksum?: string;\n /**\n * The signature of the self-hosted bundle as a signed SHA-256 hash\n * in base64 format to verify the integrity of the bundle.\n *\n * **Attention**: Only supported for the `zip` artifact type.\n *\n * @since 7.1.0\n */\n signature?: string;\n /**\n * The URL of the bundle to download.\n *\n * For the `zip` artifact type, the URL must point to a ZIP file.\n * For the `manifest` artifact type, the URL serves as the base URL\n * to download the individual files. For example, if the URL is\n * `https://example.com/download`, the plugin will download the file\n * with the href `index.html` from `https://example.com/download?href=index.html`.\n *\n * To **verify the integrity** of the file, the server should return\n * a `X-Checksum` header with the SHA-256 hash in hex format.\n *\n * To **verify the signature** of the file, the server should return\n * a `X-Signature` header with the signed SHA-256 hash in base64 format.\n *\n * @since 5.0.0\n * @example 'https://example.com/bundle.zip'\n */\n url: string;\n}\n\n/**\n * @since 8.2.0\n */\nexport interface FetchChannelsOptions {\n /**\n * The maximum number of channels to return.\n *\n * @since 8.2.0\n * @default 50\n */\n limit?: number;\n /**\n * The number of channels to skip.\n *\n * @since 8.2.0\n * @default 0\n */\n offset?: number;\n /**\n * The query to filter channels by name.\n *\n * @since 8.2.0\n */\n query?: string;\n}\n\n/**\n * @since 8.2.0\n */\nexport interface FetchChannelsResult {\n /**\n * The list of channels.\n *\n * @since 8.2.0\n */\n channels: Channel[];\n}\n\n/**\n * @since 8.2.0\n */\nexport interface Channel {\n /**\n * The unique identifier of the channel.\n *\n * @since 8.2.0\n */\n id: string;\n /**\n * The name of the channel.\n *\n * @since 8.2.0\n */\n name: string;\n}\n\n/**\n * @since 6.7.0\n */\nexport interface FetchLatestBundleOptions {\n /**\n * The name of the channel where the latest bundle is fetched from.\n *\n * @since 6.7.0\n */\n channel?: string;\n}\n\n/**\n * @since 6.6.0\n */\nexport interface FetchLatestBundleResult {\n /**\n * The artifact type of the bundle.\n *\n * @since 6.7.0\n */\n artifactType?: 'manifest' | 'zip';\n /**\n * The unique identifier of the latest bundle.\n *\n * On Capawesome Cloud, this is the ID of the app build artifact.\n *\n * If `null`, no bundle is available.\n *\n * @since 6.6.0\n */\n bundleId: string | null;\n /**\n * The checksum of the latest bundle if the bundle is self-hosted.\n *\n * If the bundle is hosted on Capawesome Cloud, the checksum will be\n * returned as response header when downloading the bundle.\n *\n * @since 7.1.0\n */\n checksum?: string;\n /**\n * Custom properties that are associated with the latest bundle.\n *\n * @since 7.0.0\n * @example { \"key\": \"value\" }\n */\n customProperties?: { [key: string]: string };\n /**\n * The URL of the latest bundle to download.\n * Pass this URL to the `downloadBundle(...)` method to download the bundle.\n *\n * @since 6.7.0\n */\n downloadUrl?: string;\n /**\n * The signature of the latest bundle if the bundle is self-hosted.\n *\n * If the bundle is hosted on Capawesome Cloud, the signature will be\n * returned as response header when downloading the bundle.\n *\n * @since 7.1.0\n */\n signature?: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundleResult {\n /**\n * The unique identifier of the active bundle.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string | null;\n}\n\n/**\n * @since 7.4.0\n */\nexport interface GetBlockedBundlesResult {\n /**\n * An array of unique identifiers of all blocked bundles.\n *\n * @since 7.4.0\n */\n bundleIds: string[];\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetBundlesResult {\n /**\n * An array of unique identifiers of all available bundles.\n *\n * @since 5.0.0\n */\n bundleIds: string[];\n}\n\n/**\n * @since 7.4.0\n */\nexport interface GetDownloadedBundlesResult {\n /**\n * An array of unique identifiers of all downloaded bundles.\n *\n * @since 7.4.0\n */\n bundleIds: string[];\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetChannelResult {\n /**\n * The channel name.\n *\n * If `null`, the app is using the default channel.\n *\n * @since 5.0.0\n * @example 'production'\n */\n channel: string | null;\n}\n\n/**\n * @since 7.4.0\n */\nexport interface GetConfigResult {\n /**\n * The app ID used to identify the app.\n *\n * If `null`, no app ID is configured.\n *\n * @since 7.4.0\n * @example '6e351b4f-69a7-415e-a057-4567df7ffe94'\n */\n appId: string | null;\n /**\n * The auto-update strategy for live updates.\n *\n * @since 7.4.0\n * @example 'background'\n */\n autoUpdateStrategy: 'none' | 'background';\n}\n\n/**\n * @since 6.7.0\n */\nexport interface GetCurrentBundleResult {\n /**\n * The unique identifier of the current bundle.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 6.7.0\n */\n bundleId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetDeviceIdResult {\n /**\n * The unique identifier of the device.\n *\n * On iOS, [`identifierForVendor`](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) is used.\n * The value of this property is the same for apps that come from the same vendor running on the same device.\n *\n * @since 5.0.0\n *\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n deviceId: string;\n}\n\n/**\n * @since 7.4.0\n */\nexport interface IsSyncingResult {\n /**\n * Whether a sync operation is currently in progress.\n *\n * @since 7.4.0\n */\n syncing: boolean;\n}\n\n/**\n * @since 6.7.0\n */\nexport interface GetNextBundleResult {\n /**\n * The unique identifier of the next bundle.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 6.7.0\n */\n bundleId: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionCodeResult {\n /**\n * The version code of the app.\n *\n * On **Android**, this is the `versionCode` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleVersion` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1\"\n */\n versionCode: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetVersionNameResult {\n /**\n * The version name of the app.\n *\n * On **Android**, this is the `versionName` from the `android/app/build.gradle` file.\n * On **iOS**, this is the `CFBundleShortVersionString` from the `Info.plist` file.\n *\n * @since 5.0.0\n * @example \"1.0.0\"\n */\n versionName: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface GetCustomIdResult {\n /**\n * The custom identifier of the device.\n *\n * If `null`, no custom identifier is set.\n *\n * @since 5.0.0\n * @example '50d2a548-80b7-4dad-adc7-97c0e79d8a89'\n */\n customId: string | null;\n}\n\n/**\n * @since 7.0.0\n */\nexport interface ReadyResult {\n /**\n * The identifier of the previous bundle used.\n *\n * If `null`, the default bundle was used.\n *\n * @since 7.0.0\n */\n previousBundleId: string | null;\n /**\n * The identifier of the current bundle used.\n *\n * If `null`, the default bundle is being used.\n *\n * @since 7.0.0\n */\n currentBundleId: string | null;\n /**\n * Whether or not the app was reset to the default bundle.\n *\n * @since 7.0.0\n */\n rollback: boolean;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetBundleOptions {\n /**\n * The unique identifier of the bundle to use.\n *\n * @since 5.0.0\n * @example '1.0.0'\n */\n bundleId: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetChannelOptions {\n /**\n * The channel name.\n *\n * Set `null` to remove the channel.\n *\n * @since 5.0.0\n */\n channel: string | null;\n}\n\n/**\n * @since 7.4.0\n */\nexport interface SetConfigOptions {\n /**\n * The app ID used to identify the app.\n *\n * Set `null` to reset to the value from the Capacitor config file.\n *\n * @since 7.4.0\n * @example '6e351b4f-69a7-415e-a057-4567df7ffe94'\n */\n appId?: string | null;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SetCustomIdOptions {\n /**\n * The custom identifier of the device.\n *\n * Set `null` to remove the custom identifier.\n *\n * @since 5.0.0\n */\n customId: string | null;\n}\n\n/**\n * @since 6.7.0\n */\nexport interface SetNextBundleOptions {\n /**\n * The unique identifier of the bundle to use.\n *\n * Set `null` to use the default bundle (same as calling `reset()`).\n *\n * @since 6.7.0\n * @example '1.0.0'\n */\n bundleId: string | null;\n}\n\n/**\n * @since 6.7.0\n */\nexport interface SyncOptions {\n /**\n * The name of the channel where the latest bundle is fetched from.\n *\n * @since 6.7.0\n */\n channel?: string;\n}\n\n/**\n * @since 5.0.0\n */\nexport interface SyncResult {\n /**\n * The identifier of the next bundle to use.\n *\n * If `null`, the app is up-to-date and no new bundle is available.\n *\n * @since 5.0.0\n */\n nextBundleId: string | null;\n}\n\n/**\n * Listener for the download progress of a bundle.\n *\n * @since 7.0.0\n */\nexport type DownloadBundleProgressListener = (\n event: DownloadBundleProgressEvent,\n) => void;\n\n/**\n * Event that is triggered when the download progress of a bundle changes.\n *\n * @since 7.0.0\n */\nexport interface DownloadBundleProgressEvent {\n /**\n * The unique identifier of the bundle that is being downloaded.\n *\n * @since 7.0.0\n */\n bundleId: string;\n /**\n * The number of bytes that have been downloaded.\n *\n * @since 7.0.0\n */\n downloadedBytes: number;\n /**\n * The progress of the download in percent as a value between `0` and `1`.\n *\n * @since 7.0.0\n * @example 0.5\n */\n progress: number;\n /**\n * The total number of bytes to download.\n *\n * @since 7.0.0\n */\n totalBytes: number;\n}\n\n/**\n * Listener for when a bundle is set as the next bundle.\n *\n * @since 7.3.0\n */\nexport type NextBundleSetListener = (event: NextBundleSetEvent) => void;\n\n/**\n * Event that is triggered when a bundle is set as the next bundle.\n *\n * @since 7.3.0\n */\nexport interface NextBundleSetEvent {\n /**\n * The unique identifier of the bundle that is set as the next bundle.\n *\n * If `null`, the default bundle will be used.\n *\n * @since 7.3.0\n * @example '1.0.0'\n */\n bundleId: string | null;\n}\n\n/**\n * Listener for when the app is reloaded.\n *\n * @since 7.4.0\n */\nexport type ReloadedListener = () => void;\n"]}
@@ -2,16 +2,28 @@ import Foundation
2
2
  import Capacitor
3
3
 
4
4
  @objc public class FetchLatestBundleOptions: NSObject {
5
+ private var appId: String?
5
6
  private var channel: String?
6
7
 
7
8
  init(_ call: CAPPluginCall) {
9
+ self.appId = nil
8
10
  self.channel = call.getString("channel")
9
11
  }
10
12
 
11
13
  init(channel: String?) {
14
+ self.appId = nil
12
15
  self.channel = channel
13
16
  }
14
17
 
18
+ init(appId: String?, channel: String?) {
19
+ self.appId = appId
20
+ self.channel = channel
21
+ }
22
+
23
+ func getAppId() -> String? {
24
+ return appId
25
+ }
26
+
15
27
  func getChannel() -> String? {
16
28
  return channel
17
29
  }
@@ -18,6 +18,30 @@ import Capacitor
18
18
  self.signature = signature
19
19
  }
20
20
 
21
+ public func getArtifactType() -> String? {
22
+ return artifactType?.rawValue
23
+ }
24
+
25
+ public func getBundleId() -> String? {
26
+ return bundleId
27
+ }
28
+
29
+ public func getChecksum() -> String? {
30
+ return checksum
31
+ }
32
+
33
+ public func getCustomProperties() -> [String: Any]? {
34
+ return customProperties
35
+ }
36
+
37
+ public func getDownloadUrl() -> String? {
38
+ return downloadUrl
39
+ }
40
+
41
+ public func getSignature() -> String? {
42
+ return signature
43
+ }
44
+
21
45
  public func toJSObject() -> AnyObject {
22
46
  var result = JSObject()
23
47
  if artifactType == .manifest {
@@ -3,6 +3,7 @@ import Foundation
3
3
  public enum CustomError: Error {
4
4
  case appIdMissing
5
5
  case bundleAlreadyExists
6
+ case bundleDirectoryNotFound
6
7
  case bundleIdMissing
7
8
  case bundleIndexHtmlMissing
8
9
  case bundleNotFound
@@ -12,7 +13,9 @@ public enum CustomError: Error {
12
13
  case checksumMismatch
13
14
  case customIdMissing
14
15
  case downloadFailed
16
+ case downloadUrlMissing
15
17
  case httpTimeout
18
+ case managerKeyMissing
16
19
  case signatureMissing
17
20
  case signatureVerificationFailed
18
21
  case syncInProgress
@@ -27,6 +30,8 @@ extension CustomError: LocalizedError {
27
30
  return NSLocalizedString("appId must be configured.", comment: "appIdMissing")
28
31
  case .bundleAlreadyExists:
29
32
  return NSLocalizedString("bundle already exists.", comment: "bundleAlreadyExists")
33
+ case .bundleDirectoryNotFound:
34
+ return NSLocalizedString("Bundle directory could not be resolved.", comment: "bundleDirectoryNotFound")
30
35
  case .bundleIdMissing:
31
36
  return NSLocalizedString("bundleId must be provided.", comment: "bundleIdMissing")
32
37
  case .bundleIndexHtmlMissing:
@@ -45,8 +50,12 @@ extension CustomError: LocalizedError {
45
50
  return NSLocalizedString("customId must be provided.", comment: "customIdMissing")
46
51
  case .downloadFailed:
47
52
  return NSLocalizedString("Bundle could not be downloaded.", comment: "downloadFailed")
53
+ case .downloadUrlMissing:
54
+ return NSLocalizedString("Bundle does not have a valid download URL.", comment: "downloadUrlMissing")
48
55
  case .httpTimeout:
49
56
  return NSLocalizedString("Request timed out.", comment: "httpTimeout")
57
+ case .managerKeyMissing:
58
+ return NSLocalizedString("managerKey must be provided.", comment: "managerKeyMissing")
50
59
  case .signatureMissing:
51
60
  return NSLocalizedString("Bundle does not contain a signature.", comment: "signatureMissing")
52
61
  case .signatureVerificationFailed:
@@ -161,6 +161,14 @@ import CommonCrypto
161
161
  completion(result, nil)
162
162
  }
163
163
 
164
+ /// - Returns: The on-disk URL of a downloaded bundle, or `nil` if the bundle does not exist.
165
+ public func getBundleDirectory(bundleId: String) -> URL? {
166
+ guard hasBundleById(bundleId) else {
167
+ return nil
168
+ }
169
+ return buildBundleURLFor(bundleId: bundleId)
170
+ }
171
+
164
172
  @objc public func getCustomId(completion: @escaping (Result?, Error?) -> Void) {
165
173
  let customId = preferences.getCustomId()
166
174
 
@@ -580,6 +588,9 @@ import CommonCrypto
580
588
  }
581
589
 
582
590
  private func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> GetLatestBundleResponse? {
591
+ guard let appId = options.getAppId() ?? getAppId(), !appId.isEmpty else {
592
+ throw CustomError.appIdMissing
593
+ }
583
594
  let channel = options.getChannel() ?? getChannel()
584
595
  var parameters = [String: String]()
585
596
  parameters["appVersionCode"] = getVersionCode()
@@ -591,7 +602,7 @@ import CommonCrypto
591
602
  parameters["osVersion"] = await UIDevice.current.systemVersion
592
603
  parameters["platform"] = "1"
593
604
  parameters["pluginVersion"] = LiveUpdatePlugin.version
594
- var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(getAppId() ?? "")/bundles/latest")!
605
+ var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(appId)/bundles/latest")!
595
606
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
596
607
  let url = try urlComponents.asURL()
597
608
  CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
@@ -1,5 +1,8 @@
1
1
  import Foundation
2
2
  import Capacitor
3
+ #if CAPAWESOME_INCLUDE_IONIC_PROVIDER
4
+ import LiveUpdateProvider
5
+ #endif
3
6
 
4
7
  /**
5
8
  * Please read the Capacitor iOS Plugin Development Guide
@@ -56,6 +59,22 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
56
59
  // Notify implementation about load
57
60
  implementation?.handleLoad()
58
61
 
62
+ // Register the Ionic Live Update Provider when the optional SDK is linked.
63
+ #if CAPAWESOME_INCLUDE_IONIC_PROVIDER
64
+ if let implementation = implementation {
65
+ do {
66
+ if LiveUpdateProviderRegistry.shared.resolve(LiveUpdateIonicProvider.id) == nil {
67
+ try LiveUpdateProviderRegistry.shared.register(
68
+ LiveUpdateIonicProvider(liveUpdate: implementation)
69
+ )
70
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] Registered Ionic provider '\(LiveUpdateIonicProvider.id)'.")
71
+ }
72
+ } catch {
73
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] Failed to register Ionic provider: ", error.localizedDescription)
74
+ }
75
+ }
76
+ #endif
77
+
59
78
  // Register for app resume notifications
60
79
  NotificationCenter.default.addObserver(
61
80
  self,
@@ -0,0 +1,118 @@
1
+ #if CAPAWESOME_INCLUDE_IONIC_PROVIDER
2
+ import Foundation
3
+ import LiveUpdateProvider
4
+
5
+ /// Ionic Live Update Provider manager backed by the Capawesome live update plugin.
6
+ ///
7
+ /// Each manager instance is scoped by a `managerKey` so that multiple shells (Portals or
8
+ /// Federated Capacitor apps) can persist their own active bundle without colliding with
9
+ /// each other or with the standalone plugin's `currentBundleId` / `nextBundleId` state.
10
+ ///
11
+ /// Provider config keys (V1):
12
+ /// - `managerKey` (required) — scopes per-manager persisted state
13
+ /// - `appId` (optional) — Capawesome Cloud app UUID; falls back to plugin config
14
+ /// - `channel` (optional) — channel to sync; falls back to plugin config
15
+ public final class LiveUpdateIonicManager: LiveUpdateManaging {
16
+ private static let userDefaultsPrefix = "CapawesomeLiveUpdateIonicProvider.lastSyncedBundleId."
17
+
18
+ private let appId: String?
19
+ private let channel: String?
20
+ private let liveUpdate: LiveUpdate
21
+ private let managerKey: String
22
+
23
+ public private(set) var latestAppDirectory: URL?
24
+
25
+ public init(config: [String: Any], liveUpdate: LiveUpdate) throws {
26
+ guard let managerKey = config["managerKey"] as? String, !managerKey.isEmpty else {
27
+ throw LiveUpdateProviderError.invalidConfiguration(
28
+ CustomError.managerKeyMissing.localizedDescription,
29
+ underlyingError: nil
30
+ )
31
+ }
32
+ self.managerKey = managerKey
33
+ self.appId = config["appId"] as? String
34
+ self.channel = config["channel"] as? String
35
+ self.liveUpdate = liveUpdate
36
+
37
+ // Restore the last synced bundle directory, if any.
38
+ if let persistedBundleId = UserDefaults.standard.string(forKey: lastSyncedBundleIdKey()) {
39
+ self.latestAppDirectory = liveUpdate.getBundleDirectory(bundleId: persistedBundleId)
40
+ }
41
+ }
42
+
43
+ public func sync() async throws -> any LiveUpdateProvider.SyncResult {
44
+ do {
45
+ let fetchOptions = FetchLatestBundleOptions(appId: appId, channel: channel)
46
+ let result = try await liveUpdate.fetchLatestBundle(fetchOptions)
47
+
48
+ guard let bundleId = result.getBundleId() else {
49
+ // No update available; report success without changing state.
50
+ return DefaultFederatedCapacitorSyncResult(metadata: buildMetadata(bundleId: nil, customProperties: nil))
51
+ }
52
+
53
+ // Bundle already on disk — just point latestAppDirectory at it and persist.
54
+ if let existingDirectory = liveUpdate.getBundleDirectory(bundleId: bundleId) {
55
+ applySyncedBundle(bundleId: bundleId, directory: existingDirectory)
56
+ return DefaultFederatedCapacitorSyncResult(
57
+ metadata: buildMetadata(bundleId: bundleId, customProperties: result.getCustomProperties())
58
+ )
59
+ }
60
+
61
+ // Otherwise, download and then apply.
62
+ guard let downloadUrl = result.getDownloadUrl(), !downloadUrl.isEmpty else {
63
+ throw LiveUpdateProviderError.syncFailed(
64
+ CustomError.downloadUrlMissing.localizedDescription,
65
+ underlyingError: nil
66
+ )
67
+ }
68
+ let artifactType = result.getArtifactType() ?? "zip"
69
+ let downloadOptions = DownloadBundleOptions(
70
+ artifactType: artifactType,
71
+ bundleId: bundleId,
72
+ checksum: result.getChecksum(),
73
+ signature: result.getSignature(),
74
+ url: downloadUrl
75
+ )
76
+ try await liveUpdate.downloadBundle(downloadOptions)
77
+
78
+ guard let downloadedDirectory = liveUpdate.getBundleDirectory(bundleId: bundleId) else {
79
+ throw LiveUpdateProviderError.syncFailed(
80
+ CustomError.bundleDirectoryNotFound.localizedDescription,
81
+ underlyingError: nil
82
+ )
83
+ }
84
+ applySyncedBundle(bundleId: bundleId, directory: downloadedDirectory)
85
+ return DefaultFederatedCapacitorSyncResult(
86
+ metadata: buildMetadata(bundleId: bundleId, customProperties: result.getCustomProperties())
87
+ )
88
+ } catch let error as LiveUpdateProviderError {
89
+ throw error
90
+ } catch {
91
+ throw LiveUpdateProviderError.syncFailed(error.localizedDescription, underlyingError: error)
92
+ }
93
+ }
94
+
95
+ private func applySyncedBundle(bundleId: String, directory: URL) {
96
+ latestAppDirectory = directory
97
+ UserDefaults.standard.set(bundleId, forKey: lastSyncedBundleIdKey())
98
+ }
99
+
100
+ private func buildMetadata(bundleId: String?, customProperties: [String: Any]?) -> [String: Any]? {
101
+ var metadata: [String: Any] = [:]
102
+ if let bundleId = bundleId {
103
+ metadata["bundleId"] = bundleId
104
+ }
105
+ if let channel = channel {
106
+ metadata["channel"] = channel
107
+ }
108
+ if let customProperties = customProperties, !customProperties.isEmpty {
109
+ metadata["customProperties"] = customProperties
110
+ }
111
+ return metadata.isEmpty ? nil : metadata
112
+ }
113
+
114
+ private func lastSyncedBundleIdKey() -> String {
115
+ return Self.userDefaultsPrefix + managerKey
116
+ }
117
+ }
118
+ #endif
@@ -0,0 +1,25 @@
1
+ #if CAPAWESOME_INCLUDE_IONIC_PROVIDER
2
+ import Foundation
3
+ import LiveUpdateProvider
4
+
5
+ /// Ionic Live Update Provider implementation backed by the Capawesome live update plugin.
6
+ ///
7
+ /// Registered with `LiveUpdateProviderRegistry.shared` when the Capacitor plugin loads, so
8
+ /// Federated Capacitor can resolve provider id `"capawesome"` and create managers from the
9
+ /// per-shell configuration.
10
+ public struct LiveUpdateIonicProvider: LiveUpdateProviding {
11
+ public static let id = "capawesome"
12
+
13
+ public let id: String = LiveUpdateIonicProvider.id
14
+
15
+ private let liveUpdate: LiveUpdate
16
+
17
+ public init(liveUpdate: LiveUpdate) {
18
+ self.liveUpdate = liveUpdate
19
+ }
20
+
21
+ public func createManager(config: [String: Any]) throws -> any LiveUpdateManaging {
22
+ return try LiveUpdateIonicManager(config: config, liveUpdate: liveUpdate)
23
+ }
24
+ }
25
+ #endif
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/capacitor-live-update",
3
- "version": "8.2.0",
3
+ "version": "8.2.1-dev.70823ec8.1778138208",
4
4
  "description": "Capacitor plugin to update your app remotely in real-time.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",