@capawesome/capacitor-live-update 8.3.0-dev.f9cc1d15.1783929784 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,19 +17,4 @@ 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', '~> 1.0.0'
31
- ss.pod_target_xcconfig = {
32
- 'OTHER_SWIFT_FLAGS' => '$(inherited) -DCAPAWESOME_INCLUDE_IONIC_PROVIDER'
33
- }
34
- end
35
20
  end
package/Package.swift CHANGED
@@ -1,4 +1,4 @@
1
- // swift-tools-version: 6.1
1
+ // swift-tools-version: 5.9
2
2
  import PackageDescription
3
3
 
4
4
  let package = Package(
@@ -9,17 +9,10 @@ let package = Package(
9
9
  name: "CapawesomeCapacitorLiveUpdate",
10
10
  targets: ["LiveUpdatePlugin"])
11
11
  ],
12
- traits: [
13
- .trait(
14
- name: "IonicProvider",
15
- description: "Enables the Ionic Live Update Provider SDK integration."
16
- )
17
- ],
18
12
  dependencies: [
19
13
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
20
14
  .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.10.2")),
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", from: "1.0.0")
15
+ .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.0"))
23
16
  ],
24
17
  targets: [
25
18
  .target(
@@ -28,21 +21,12 @@ let package = Package(
28
21
  .product(name: "Capacitor", package: "capacitor-swift-pm"),
29
22
  .product(name: "Cordova", package: "capacitor-swift-pm"),
30
23
  .product(name: "Alamofire", package: "Alamofire"),
31
- .product(name: "ZIPFoundation", package: "ZIPFoundation"),
32
- .product(
33
- name: "LiveUpdateProvider",
34
- package: "live-update-provider-sdk",
35
- condition: .when(traits: ["IonicProvider"])
36
- )
24
+ .product(name: "ZIPFoundation", package: "ZIPFoundation")
37
25
  ],
38
- path: "ios/Plugin",
39
- swiftSettings: [
40
- .define("CAPAWESOME_INCLUDE_IONIC_PROVIDER", .when(traits: ["IonicProvider"]))
41
- ]),
26
+ path: "ios/Plugin"),
42
27
  .testTarget(
43
28
  name: "LiveUpdatePluginTests",
44
29
  dependencies: ["LiveUpdatePlugin"],
45
30
  path: "ios/PluginTests")
46
- ],
47
- swiftLanguageModes: [.v5]
31
+ ]
48
32
  )
package/README.md CHANGED
@@ -25,11 +25,10 @@ The Capacitor Live Update plugin is one of the most feature-rich Over-the-Air (O
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).
30
28
  - 🔒 **Security**: Verify the authenticity and integrity of the bundle using a public key.
31
29
  - ⚔️ **Battle-Tested**: Used in more than 1,000 projects to update apps on more than 20,000,000 devices.
32
30
  - 🌐 **Open Source**: Licensed under the MIT License.
31
+ - 🤝 **Compatibility**: Works alongside the [App Update](https://capawesome.io/docs/sdks/capacitor/app-update/) and [Firebase Remote Config](https://capawesome.io/docs/sdks/capacitor/firebase/remote-config/) plugins.
33
32
 
34
33
  Missing a feature? Just [open an issue](https://github.com/capawesome-team/capacitor-plugins/issues) and we'll take a look!
35
34
 
@@ -57,6 +56,7 @@ The Live Update plugin is typically used to ship changes to users without going
57
56
  - [Getting Started with Capawesome Cloud Live Updates](https://capawesome.io/docs/cloud/live-updates/setup/)
58
57
  - [Migrating from Ionic Appflow to Capawesome Cloud](https://capawesome.io/blog/migrating-from-ionic-appflow-to-capawesome-cloud/)
59
58
  - [Migrating from App Center to Capawesome Cloud](https://capawesome.io/blog/migrating-from-app-center-to-capawesome-cloud/)
59
+ - [Live Updates for Nuxt Capacitor Apps](https://capawesome.io/blog/live-updates-for-nuxt-capacitor/)
60
60
 
61
61
  ## Installation
62
62
 
@@ -84,7 +84,7 @@ npx cap sync
84
84
 
85
85
  #### Channel
86
86
 
87
- If you are using [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/guides/best-practices/#versioned-channels), you can set a default channel directly in your native project by adding a string resource.
87
+ If you are using [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/channels/#versioned-channels), you can set a default channel directly in your native project by adding a string resource.
88
88
  This allows you to tie the channel to the version code at build time.
89
89
 
90
90
  Add the following to your app's `build.gradle` file:
@@ -101,8 +101,7 @@ android {
101
101
 
102
102
  If needed, you can define the following project variable in your app’s `variables.gradle` file to change the default version of the dependency:
103
103
 
104
- - `$ionicLiveUpdateProviderVersion` version of `io.ionic:liveupdateprovider` (default: `1.0.0`)
105
- - `$okhttp3Version` version of `com.squareup.okhttp3:okhttp` (default: `5.3.2`)
104
+ - `$okhttp3Version` version of `com.squareup.okhttp3:okhttp` and `com.squareup.okhttp3:okhttp-brotli` (default: `5.3.2`)
106
105
  - `$zip4jVersion` version of `net.lingala.zip4j:zip4j` (default: `2.11.5`)
107
106
 
108
107
  This can be useful if you encounter dependency conflicts with other plugins in your project.
@@ -111,7 +110,7 @@ This can be useful if you encounter dependency conflicts with other plugins in y
111
110
 
112
111
  #### Channel
113
112
 
114
- If you are using [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/guides/best-practices/#versioned-channels), you can set a default channel directly in your native project by adding a key to your `Info.plist` file.
113
+ If you are using [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/channels/#versioned-channels), you can set a default channel directly in your native project by adding a key to your `Info.plist` file.
115
114
  This allows you to tie the channel to the build version at build time.
116
115
 
117
116
  Add the following to your `Info.plist` file:
@@ -148,10 +147,6 @@ Add the `NSPrivacyAccessedAPICategoryUserDefaults` dictionary key to your [Priva
148
147
 
149
148
  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.
150
149
 
151
- ## Ionic Live Update Provider SDK Integration
152
-
153
- This plugin implements the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk) contract, so that **Ionic Portals** and **Federated Capacitor** apps can use Capawesome Cloud to deliver live updates. Federated Capacitor resolves the provider by the plugin name `LiveUpdate`. 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.
154
-
155
150
  ## Configuration
156
151
 
157
152
  <docgen-config>
@@ -1367,7 +1362,7 @@ On Capawesome Cloud, the bundle ID corresponds to the ID of the app build artifa
1367
1362
  There are four ways to set a channel, listed from lowest to highest priority:
1368
1363
 
1369
1364
  1. **Capacitor config**: Set the `defaultChannel` property in the [plugin configuration](#configuration). This is the simplest way to set a static default channel.
1370
- 2. **Native config**: Set `CapawesomeLiveUpdateDefaultChannel` in `Info.plist` (iOS) or `capawesome_live_update_default_channel` in `strings.xml` (Android). This is useful for [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/guides/best-practices/#versioned-channels) where the channel is tied to the build version.
1365
+ 2. **Native config**: Set `CapawesomeLiveUpdateDefaultChannel` in `Info.plist` (iOS) or `capawesome_live_update_default_channel` in `strings.xml` (Android). This is useful for [Versioned Channels](https://capawesome.io/docs/cloud/live-updates/channels/#versioned-channels) where the channel is tied to the build version.
1371
1366
  3. **[`setChannel(...)`](#setchannel)**: Set the channel at runtime. The value is persisted across app restarts.
1372
1367
  4. **[`sync(...)`](#sync)**: Pass a `channel` option to override the channel for a single sync call. This does **not** persist the channel.
1373
1368
 
@@ -1,5 +1,3 @@
1
- import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2
-
3
1
  ext {
4
2
  junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
5
3
  androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
@@ -7,23 +5,19 @@ ext {
7
5
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
8
6
  okhttp3Version = project.hasProperty('okhttp3Version') ? rootProject.ext.okhttp3Version : '5.3.2'
9
7
  zip4jVersion = project.hasProperty('zip4jVersion') ? rootProject.ext.zip4jVersion : '2.11.5'
10
- ionicLiveUpdateProviderVersion = project.hasProperty('ionicLiveUpdateProviderVersion') ? rootProject.ext.ionicLiveUpdateProviderVersion : '1.0.0'
11
8
  }
12
9
 
13
10
  buildscript {
14
- ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '2.2.20'
15
11
  repositories {
16
12
  google()
17
13
  mavenCentral()
18
14
  }
19
15
  dependencies {
20
16
  classpath 'com.android.tools.build:gradle:8.13.0'
21
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
22
17
  }
23
18
  }
24
19
 
25
20
  apply plugin: 'com.android.library'
26
- apply plugin: 'org.jetbrains.kotlin.android'
27
21
 
28
22
  android {
29
23
  namespace = "io.capawesome.capacitorjs.plugins.liveupdate"
@@ -34,7 +28,6 @@ android {
34
28
  versionCode 1
35
29
  versionName "1.0"
36
30
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
37
- consumerProguardFiles 'consumer-rules.pro'
38
31
  }
39
32
  buildTypes {
40
33
  release {
@@ -51,12 +44,6 @@ android {
51
44
  }
52
45
  }
53
46
 
54
- kotlin {
55
- compilerOptions {
56
- jvmTarget = JvmTarget.JVM_21
57
- }
58
- }
59
-
60
47
  repositories {
61
48
  google()
62
49
  mavenCentral()
@@ -69,10 +56,7 @@ dependencies {
69
56
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
70
57
  implementation "net.lingala.zip4j:zip4j:$zip4jVersion"
71
58
  implementation "com.squareup.okhttp3:okhttp:$okhttp3Version"
72
- // The Ionic Live Update Provider SDK is compile-time only: Federated Capacitor
73
- // and Ionic Portals bring it transitively at runtime, and the provider classes
74
- // are only loaded when a host actually calls `createManager`.
75
- compileOnly "io.ionic:liveupdateprovider:$ionicLiveUpdateProviderVersion"
59
+ implementation "com.squareup.okhttp3:okhttp-brotli:$okhttp3Version"
76
60
  testImplementation "junit:junit:$junitVersion"
77
61
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
78
62
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -1,16 +1,17 @@
1
1
  package io.capawesome.capacitorjs.plugins.liveupdate;
2
2
 
3
3
  import android.app.Activity;
4
- import android.content.Context;
5
4
  import android.content.SharedPreferences;
6
5
  import android.content.pm.PackageInfo;
7
6
  import android.content.pm.PackageManager;
8
7
  import android.content.res.AssetManager;
9
8
  import android.os.Build;
10
9
  import android.os.Handler;
10
+ import android.os.Looper;
11
11
  import android.util.Base64;
12
12
  import androidx.annotation.NonNull;
13
13
  import androidx.annotation.Nullable;
14
+ import androidx.core.content.pm.PackageInfoCompat;
14
15
  import com.getcapacitor.Bridge;
15
16
  import com.getcapacitor.Logger;
16
17
  import com.getcapacitor.plugin.WebView;
@@ -88,15 +89,12 @@ public class LiveUpdate {
88
89
  @NonNull
89
90
  private final LiveUpdateConfig config;
90
91
 
91
- @NonNull
92
- private final Context context;
93
-
94
92
  private final String defaultWebAssetDir = Bridge.DEFAULT_WEB_ASSET_DIR;
95
93
 
96
94
  @NonNull
97
95
  private final LiveUpdateHttpClient httpClient;
98
96
 
99
- @Nullable
97
+ @NonNull
100
98
  private final LiveUpdatePlugin plugin;
101
99
 
102
100
  @NonNull
@@ -106,7 +104,7 @@ public class LiveUpdate {
106
104
  private final SharedPreferences.Editor webViewSettingsEditor;
107
105
 
108
106
  private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
109
- private final Handler rollbackHandler = new Handler();
107
+ private final Handler rollbackHandler = new Handler(Looper.getMainLooper());
110
108
  private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
111
109
 
112
110
  private boolean initialPageLoaded = false;
@@ -115,26 +113,11 @@ public class LiveUpdate {
115
113
  private boolean syncInProgress = false;
116
114
 
117
115
  public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
118
- this(config, plugin.getContext(), plugin);
119
- }
120
-
121
- /**
122
- * Creates a headless instance without a Capacitor plugin, e.g. for Ionic Portals hosts
123
- * that construct a provider manager directly. WebView-related operations (server path
124
- * changes, reload) and plugin events are skipped in this mode.
125
- */
126
- public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull Context context) throws PackageManager.NameNotFoundException {
127
- this(config, context, null);
128
- }
129
-
130
- private LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull Context context, @Nullable LiveUpdatePlugin plugin)
131
- throws PackageManager.NameNotFoundException {
132
116
  this.config = config;
133
- this.context = context;
134
117
  this.httpClient = new LiveUpdateHttpClient(config);
135
118
  this.plugin = plugin;
136
- this.preferences = new LiveUpdatePreferences(context);
137
- this.webViewSettingsEditor = context.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
119
+ this.preferences = new LiveUpdatePreferences(plugin.getContext());
120
+ this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
138
121
 
139
122
  // Check version and reset config if version changed
140
123
  checkAndResetConfigIfVersionChanged();
@@ -319,17 +302,6 @@ public class LiveUpdate {
319
302
  callback.success(result);
320
303
  }
321
304
 
322
- /**
323
- * @return The on-disk directory of a downloaded bundle, or `null` if the bundle does not exist.
324
- */
325
- @Nullable
326
- public File getBundleDirectory(@NonNull String bundleId) {
327
- if (!hasBundleById(bundleId)) {
328
- return null;
329
- }
330
- return buildBundleDirectoryFor(bundleId);
331
- }
332
-
333
305
  public void getCustomId(@NonNull NonEmptyCallback callback) {
334
306
  String customId = preferences.getCustomId();
335
307
  GetCustomIdResult result = new GetCustomIdResult(customId);
@@ -579,21 +551,21 @@ public class LiveUpdate {
579
551
  }
580
552
 
581
553
  private File buildBundlesDirectory() {
582
- return new File(context.getFilesDir(), bundlesDirectory);
554
+ return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
583
555
  }
584
556
 
585
557
  private File buildBundleDirectoryFor(@NonNull String bundleId) {
586
- return new File(context.getFilesDir(), bundlesDirectory + "/" + bundleId);
558
+ return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
587
559
  }
588
560
 
589
561
  private File buildTemporaryDirectory() {
590
562
  String fileName = UUID.randomUUID().toString();
591
- return new File(context.getCacheDir(), fileName);
563
+ return new File(plugin.getContext().getCacheDir(), fileName);
592
564
  }
593
565
 
594
566
  private File buildTemporaryZipFile() {
595
567
  String fileName = UUID.randomUUID().toString() + ".zip";
596
- return new File(context.getCacheDir(), fileName);
568
+ return new File(plugin.getContext().getCacheDir(), fileName);
597
569
  }
598
570
 
599
571
  private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
@@ -601,7 +573,7 @@ public class LiveUpdate {
601
573
  String currentBundleId = getCurrentBundleId();
602
574
  if (currentBundleId == null) {
603
575
  // Create the source input stream
604
- AssetManager assets = context.getAssets();
576
+ AssetManager assets = plugin.getContext().getAssets();
605
577
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
606
578
  // Create the destination file
607
579
  File destination = new File(destinationDirectory, href);
@@ -1007,17 +979,13 @@ public class LiveUpdate {
1007
979
  @NonNull NonEmptyCallback<GetLatestBundleResponse> callback
1008
980
  ) {
1009
981
  try {
1010
- String appId = options.getAppId() == null ? getAppId() : options.getAppId();
1011
- if (appId == null || appId.isEmpty()) {
1012
- throw new Exception(LiveUpdatePlugin.ERROR_APP_ID_MISSING);
1013
- }
1014
982
  String channel = options.getChannel() == null ? getChannel() : options.getChannel();
1015
983
  String url = new HttpUrl.Builder()
1016
984
  .scheme("https")
1017
985
  .host(config.getServerDomain())
1018
986
  .addPathSegment("v1")
1019
987
  .addPathSegment("apps")
1020
- .addPathSegment(appId)
988
+ .addPathSegment(getAppId())
1021
989
  .addPathSegment("bundles")
1022
990
  .addPathSegment("latest")
1023
991
  .addQueryParameter("appVersionCode", getVersionCodeAsString())
@@ -1084,7 +1052,7 @@ public class LiveUpdate {
1084
1052
  MessageDigest digest = MessageDigest.getInstance("SHA-256");
1085
1053
  BufferedSource source = Okio.buffer(Okio.source(file));
1086
1054
  Buffer buffer = new Buffer();
1087
- for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
1055
+ for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1; ) {
1088
1056
  digest.update(buffer.readByteArray());
1089
1057
  }
1090
1058
  source.close();
@@ -1131,11 +1099,14 @@ public class LiveUpdate {
1131
1099
 
1132
1100
  @Nullable
1133
1101
  private String getNativeChannel() {
1134
- int resId = context.getResources().getIdentifier("capawesome_live_update_default_channel", "string", context.getPackageName());
1102
+ int resId = plugin
1103
+ .getContext()
1104
+ .getResources()
1105
+ .getIdentifier("capawesome_live_update_default_channel", "string", plugin.getContext().getPackageName());
1135
1106
  if (resId == 0) {
1136
1107
  return null;
1137
1108
  }
1138
- return context.getResources().getString(resId);
1109
+ return plugin.getContext().getResources().getString(resId);
1139
1110
  }
1140
1111
 
1141
1112
  /**
@@ -1154,10 +1125,6 @@ public class LiveUpdate {
1154
1125
  * @return The absolute path to the current bundle directory (`public` for the built-in bundle).
1155
1126
  */
1156
1127
  private String getCurrentCapacitorServerPath() {
1157
- if (plugin == null) {
1158
- // Headless mode: there is no WebView, so the default bundle is always current.
1159
- return defaultWebAssetDir;
1160
- }
1161
1128
  return plugin.getBridge().getServerBasePath();
1162
1129
  }
1163
1130
 
@@ -1188,7 +1155,8 @@ public class LiveUpdate {
1188
1155
  */
1189
1156
  @NonNull
1190
1157
  private String getNextCapacitorServerPath() {
1191
- String path = context
1158
+ String path = plugin
1159
+ .getContext()
1192
1160
  .getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
1193
1161
  .getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
1194
1162
  // Empty path means default path
@@ -1207,7 +1175,7 @@ public class LiveUpdate {
1207
1175
  }
1208
1176
 
1209
1177
  private int getVersionCodeAsInt() throws PackageManager.NameNotFoundException {
1210
- return getPackageInfo().versionCode;
1178
+ return (int) PackageInfoCompat.getLongVersionCode(getPackageInfo());
1211
1179
  }
1212
1180
 
1213
1181
  private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
@@ -1233,7 +1201,7 @@ public class LiveUpdate {
1233
1201
  private Manifest loadCurrentManifest() throws Exception {
1234
1202
  String currentBundleId = getCurrentBundleId();
1235
1203
  if (currentBundleId == null) {
1236
- AssetManager assets = context.getAssets();
1204
+ AssetManager assets = plugin.getContext().getAssets();
1237
1205
  boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
1238
1206
  if (manifestFileExists) {
1239
1207
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
@@ -1265,9 +1233,6 @@ public class LiveUpdate {
1265
1233
  }
1266
1234
 
1267
1235
  private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
1268
- if (plugin == null) {
1269
- return;
1270
- }
1271
1236
  plugin.notifyDownloadBundleProgressListeners(event);
1272
1237
  }
1273
1238
 
@@ -1354,10 +1319,6 @@ public class LiveUpdate {
1354
1319
  }
1355
1320
 
1356
1321
  private void setCurrentCapacitorServerPath(@NonNull String path) {
1357
- if (plugin == null) {
1358
- // Headless mode: there is no WebView to point at the new path.
1359
- return;
1360
- }
1361
1322
  if (path.equals(defaultWebAssetDir)) {
1362
1323
  this.plugin.getBridge().setServerAssetPath(path);
1363
1324
  } else {
@@ -1384,17 +1345,11 @@ public class LiveUpdate {
1384
1345
  }
1385
1346
 
1386
1347
  private void notifyNextBundleSetListeners(@Nullable String bundleId) {
1387
- if (plugin == null) {
1388
- return;
1389
- }
1390
1348
  NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
1391
1349
  plugin.notifyNextBundleSetListeners(event);
1392
1350
  }
1393
1351
 
1394
1352
  private void notifyReloadedListeners() {
1395
- if (plugin == null) {
1396
- return;
1397
- }
1398
1353
  plugin.notifyReloadedListeners();
1399
1354
  }
1400
1355
 
@@ -1535,7 +1490,7 @@ public class LiveUpdate {
1535
1490
  sig.initVerify(key);
1536
1491
  BufferedSource source = Okio.buffer(Okio.source(file));
1537
1492
  Buffer buffer = new Buffer();
1538
- for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1;) {
1493
+ for (long bytesRead; (bytesRead = source.read(buffer, 2048)) != -1; ) {
1539
1494
  sig.update(buffer.readByteArray());
1540
1495
  }
1541
1496
  source.close();
@@ -1547,11 +1502,11 @@ public class LiveUpdate {
1547
1502
  }
1548
1503
 
1549
1504
  private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
1550
- String packageName = this.context.getPackageName();
1505
+ String packageName = this.plugin.getContext().getPackageName();
1551
1506
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1552
- return this.context.getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
1507
+ return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
1553
1508
  } else {
1554
- return this.context.getPackageManager().getPackageInfo(packageName, 0);
1509
+ return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, 0);
1555
1510
  }
1556
1511
  }
1557
1512
  }
@@ -15,6 +15,7 @@ import okhttp3.OkHttpClient;
15
15
  import okhttp3.Request;
16
16
  import okhttp3.Response;
17
17
  import okhttp3.ResponseBody;
18
+ import okhttp3.brotli.BrotliInterceptor;
18
19
  import okio.Buffer;
19
20
  import okio.BufferedSink;
20
21
  import okio.BufferedSource;
@@ -58,6 +59,7 @@ public class LiveUpdateHttpClient {
58
59
  dispatcher.setMaxRequestsPerHost(30);
59
60
 
60
61
  this.okHttpClient = new OkHttpClient.Builder()
62
+ .addInterceptor(BrotliInterceptor.INSTANCE)
61
63
  .dispatcher(dispatcher)
62
64
  .connectTimeout(httpTimeout, TimeUnit.MILLISECONDS)
63
65
  .readTimeout(httpTimeout, TimeUnit.MILLISECONDS)
@@ -101,7 +103,7 @@ public class LiveUpdateHttpClient {
101
103
  Buffer sinkBuffer = sink.getBuffer();
102
104
  long totalBytesRead = 0;
103
105
  int bufferSize = 8 * 1024;
104
- for (long bytesRead; (bytesRead = source.read(sinkBuffer, bufferSize)) != -1;) {
106
+ for (long bytesRead; (bytesRead = source.read(sinkBuffer, bufferSize)) != -1; ) {
105
107
  sink.emit();
106
108
  totalBytesRead += bytesRead;
107
109
  if (callback != null) {
@@ -1,6 +1,5 @@
1
1
  package io.capawesome.capacitorjs.plugins.liveupdate;
2
2
 
3
- import android.content.Context;
4
3
  import android.webkit.WebView;
5
4
  import androidx.annotation.NonNull;
6
5
  import androidx.annotation.Nullable;
@@ -33,10 +32,6 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingRes
33
32
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
34
33
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
35
34
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
36
- import io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic.LiveUpdateIonicManager;
37
- import io.ionic.liveupdateprovider.ProviderError;
38
- import io.ionic.liveupdateprovider.ProviderManager;
39
- import java.util.Map;
40
35
 
41
36
  @CapacitorPlugin(name = "LiveUpdate")
42
37
  public class LiveUpdatePlugin extends Plugin {
@@ -45,7 +40,6 @@ public class LiveUpdatePlugin extends Plugin {
45
40
  public static final String VERSION = "8.3.0";
46
41
  public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
47
42
  public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
48
- public static final String ERROR_BUNDLE_DIRECTORY_NOT_FOUND = "Bundle directory could not be resolved.";
49
43
  public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
50
44
  public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
51
45
  public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
@@ -54,10 +48,7 @@ public class LiveUpdatePlugin extends Plugin {
54
48
  public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
55
49
  public static final String ERROR_CUSTOM_ID_MISSING = "customId must be provided.";
56
50
  public static final String ERROR_DOWNLOAD_FAILED = "Bundle could not be downloaded.";
57
- public static final String ERROR_DOWNLOAD_URL_MISSING = "Bundle does not have a valid download URL.";
58
51
  public static final String ERROR_HTTP_TIMEOUT = "Request timed out.";
59
- public static final String ERROR_MANAGER_KEY_MISSING = "managerKey must be provided.";
60
- public static final String ERROR_PLUGIN_NOT_INITIALIZED = "Plugin is not initialized.";
61
52
  public static final String ERROR_URL_MISSING = "url must be provided.";
62
53
  public static final String ERROR_SIGNATURE_VERIFICATION_FAILED = "Signature verification failed.";
63
54
  public static final String ERROR_PUBLIC_KEY_INVALID = "Invalid public key.";
@@ -85,26 +76,6 @@ public class LiveUpdatePlugin extends Plugin {
85
76
  }
86
77
  }
87
78
 
88
- /**
89
- * Creates a manager for the Ionic Live Update Provider SDK. Invoked natively by
90
- * Federated Capacitor after resolving this plugin by its Capacitor plugin name.
91
- *
92
- * This class deliberately does NOT implement the SDK's `LiveUpdateProvider` interface:
93
- * superinterfaces are resolved eagerly at class-definition time, which would crash the
94
- * plugin when the compileOnly SDK is absent at runtime. Instead, this method matches the
95
- * interface's exact signature and Federated Capacitor invokes it via reflection when the
96
- * cast to `LiveUpdateProvider` fails. Method references are resolved lazily, so this
97
- * class loads fine without the SDK. Do NOT change the signature.
98
- */
99
- @NonNull
100
- public ProviderManager createManager(@NonNull Context context, @NonNull Map<String, ?> configuration)
101
- throws ProviderError.InvalidConfiguration {
102
- if (implementation == null) {
103
- throw new ProviderError.InvalidConfiguration(ERROR_PLUGIN_NOT_INITIALIZED, null);
104
- }
105
- return new LiveUpdateIonicManager(context, configuration, implementation);
106
- }
107
-
108
79
  @Override
109
80
  protected void handleOnResume() {
110
81
  super.handleOnResume();
@@ -118,17 +89,16 @@ public class LiveUpdatePlugin extends Plugin {
118
89
  // or constructor, it MUST be done here in onResume().
119
90
  if (!webViewListenerRegistered) {
120
91
  webViewListenerRegistered = true;
121
- getBridge()
122
- .addWebViewListener(
123
- new WebViewListener() {
124
- @Override
125
- public void onPageLoaded(WebView webView) {
126
- if (implementation != null) {
127
- implementation.handleOnPageLoaded();
128
- }
92
+ getBridge().addWebViewListener(
93
+ new WebViewListener() {
94
+ @Override
95
+ public void onPageLoaded(WebView webView) {
96
+ if (implementation != null) {
97
+ implementation.handleOnPageLoaded();
129
98
  }
130
99
  }
131
- );
100
+ }
101
+ );
132
102
  }
133
103
  } catch (Exception exception) {
134
104
  Logger.error(TAG, exception.getMessage(), exception);
@@ -6,32 +6,17 @@ import com.getcapacitor.PluginCall;
6
6
 
7
7
  public class FetchLatestBundleOptions {
8
8
 
9
- @Nullable
10
- private final String appId;
11
-
12
9
  @Nullable
13
10
  private final String channel;
14
11
 
15
12
  public FetchLatestBundleOptions(@NonNull PluginCall call) {
16
- this.appId = null;
17
13
  this.channel = call.getString("channel", null);
18
14
  }
19
15
 
20
16
  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;
27
17
  this.channel = channel;
28
18
  }
29
19
 
30
- @Nullable
31
- public String getAppId() {
32
- return appId;
33
- }
34
-
35
20
  @Nullable
36
21
  public String getChannel() {
37
22
  return channel;
@@ -48,36 +48,6 @@ public class FetchLatestBundleResult implements Result {
48
48
  this.signature = signature;
49
49
  }
50
50
 
51
- @Nullable
52
- public ArtifactType getArtifactType() {
53
- return artifactType;
54
- }
55
-
56
- @Nullable
57
- public String getBundleId() {
58
- return bundleId;
59
- }
60
-
61
- @Nullable
62
- public String getChecksum() {
63
- return checksum;
64
- }
65
-
66
- @Nullable
67
- public JSONObject getCustomProperties() {
68
- return customProperties;
69
- }
70
-
71
- @Nullable
72
- public String getDownloadUrl() {
73
- return downloadUrl;
74
- }
75
-
76
- @Nullable
77
- public String getSignature() {
78
- return signature;
79
- }
80
-
81
51
  @NonNull
82
52
  public JSObject toJSObject() {
83
53
  JSObject result = new JSObject();
@@ -2,28 +2,16 @@ import Foundation
2
2
  import Capacitor
3
3
 
4
4
  @objc public class FetchLatestBundleOptions: NSObject {
5
- private var appId: String?
6
5
  private var channel: String?
7
6
 
8
7
  init(_ call: CAPPluginCall) {
9
- self.appId = nil
10
8
  self.channel = call.getString("channel")
11
9
  }
12
10
 
13
11
  init(channel: String?) {
14
- self.appId = nil
15
12
  self.channel = channel
16
13
  }
17
14
 
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
-
27
15
  func getChannel() -> String? {
28
16
  return channel
29
17
  }
@@ -20,30 +20,6 @@ import Capacitor
20
20
  self.signature = signature
21
21
  }
22
22
 
23
- public func getArtifactType() -> String? {
24
- return artifactType?.rawValue
25
- }
26
-
27
- public func getBundleId() -> String? {
28
- return bundleId
29
- }
30
-
31
- public func getChecksum() -> String? {
32
- return checksum
33
- }
34
-
35
- public func getCustomProperties() -> [String: Any]? {
36
- return customProperties
37
- }
38
-
39
- public func getDownloadUrl() -> String? {
40
- return downloadUrl
41
- }
42
-
43
- public func getSignature() -> String? {
44
- return signature
45
- }
46
-
47
23
  public func toJSObject() -> AnyObject {
48
24
  var result = JSObject()
49
25
  if artifactType == .manifest {
@@ -3,7 +3,6 @@ import Foundation
3
3
  public enum CustomError: Error {
4
4
  case appIdMissing
5
5
  case bundleAlreadyExists
6
- case bundleDirectoryNotFound
7
6
  case bundleIdMissing
8
7
  case bundleIndexHtmlMissing
9
8
  case bundleNotFound
@@ -13,10 +12,7 @@ public enum CustomError: Error {
13
12
  case checksumMismatch
14
13
  case customIdMissing
15
14
  case downloadFailed
16
- case downloadUrlMissing
17
15
  case httpTimeout
18
- case managerKeyMissing
19
- case pluginNotInitialized
20
16
  case signatureMissing
21
17
  case signatureVerificationFailed
22
18
  case syncInProgress
@@ -31,8 +27,6 @@ extension CustomError: LocalizedError {
31
27
  return NSLocalizedString("appId must be configured.", comment: "appIdMissing")
32
28
  case .bundleAlreadyExists:
33
29
  return NSLocalizedString("bundle already exists.", comment: "bundleAlreadyExists")
34
- case .bundleDirectoryNotFound:
35
- return NSLocalizedString("Bundle directory could not be resolved.", comment: "bundleDirectoryNotFound")
36
30
  case .bundleIdMissing:
37
31
  return NSLocalizedString("bundleId must be provided.", comment: "bundleIdMissing")
38
32
  case .bundleIndexHtmlMissing:
@@ -51,14 +45,8 @@ extension CustomError: LocalizedError {
51
45
  return NSLocalizedString("customId must be provided.", comment: "customIdMissing")
52
46
  case .downloadFailed:
53
47
  return NSLocalizedString("Bundle could not be downloaded.", comment: "downloadFailed")
54
- case .downloadUrlMissing:
55
- return NSLocalizedString("Bundle does not have a valid download URL.", comment: "downloadUrlMissing")
56
48
  case .httpTimeout:
57
49
  return NSLocalizedString("Request timed out.", comment: "httpTimeout")
58
- case .managerKeyMissing:
59
- return NSLocalizedString("managerKey must be provided.", comment: "managerKeyMissing")
60
- case .pluginNotInitialized:
61
- return NSLocalizedString("Plugin is not initialized.", comment: "pluginNotInitialized")
62
50
  case .signatureMissing:
63
51
  return NSLocalizedString("Bundle does not contain a signature.", comment: "signatureMissing")
64
52
  case .signatureVerificationFailed:
@@ -16,7 +16,7 @@ import CommonCrypto
16
16
  private let httpClient: LiveUpdateHttpClient
17
17
  private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
18
18
  private let manifestFileName = "capawesome-live-update-manifest.json" // DO NOT CHANGE!
19
- private let plugin: LiveUpdatePlugin?
19
+ private let plugin: LiveUpdatePlugin
20
20
  private let preferences: LiveUpdatePreferences
21
21
 
22
22
  private var rollbackDispatchWorkItem: DispatchWorkItem?
@@ -24,10 +24,7 @@ import CommonCrypto
24
24
  private var lastAutoUpdateCheckTimestamp: Int64 = 0
25
25
  private var syncInProgress = false
26
26
 
27
- /// - Parameter plugin: The Capacitor plugin, or `nil` for a headless instance
28
- /// (e.g. for Ionic Portals hosts that construct a provider manager directly).
29
- /// In headless mode, WebView-related operations and plugin events are skipped.
30
- init(config: LiveUpdateConfig, plugin: LiveUpdatePlugin? = nil) {
27
+ init(config: LiveUpdateConfig, plugin: LiveUpdatePlugin) {
31
28
  self.config = config
32
29
  self.httpClient = LiveUpdateHttpClient(config: config)
33
30
  self.plugin = plugin
@@ -164,14 +161,6 @@ import CommonCrypto
164
161
  completion(result, nil)
165
162
  }
166
163
 
167
- /// - Returns: The on-disk URL of a downloaded bundle, or `nil` if the bundle does not exist.
168
- public func getBundleDirectory(bundleId: String) -> URL? {
169
- guard hasBundleById(bundleId) else {
170
- return nil
171
- }
172
- return buildBundleURLFor(bundleId: bundleId)
173
- }
174
-
175
164
  @objc public func getCustomId(completion: @escaping (Result?, Error?) -> Void) {
176
165
  let customId = preferences.getCustomId()
177
166
 
@@ -474,7 +463,8 @@ import CommonCrypto
474
463
 
475
464
  private func downloadAndVerifyFile(url: String, file: URL, checksum: String?, signature: String?, callback: ((Progress) -> Void)?) async throws {
476
465
  let destination: DownloadRequest.Destination = { _, _ in
477
- return (file, [.createIntermediateDirectories])
466
+ // `removePreviousFile` ensures that a leftover file from a failed attempt does not fail the download
467
+ return (file, [.createIntermediateDirectories, .removePreviousFile])
478
468
  }
479
469
  let urlComponents = URLComponents(string: url)!
480
470
  let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
@@ -591,9 +581,6 @@ import CommonCrypto
591
581
  }
592
582
 
593
583
  private func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> GetLatestBundleResponse? {
594
- guard let appId = options.getAppId() ?? getAppId(), !appId.isEmpty else {
595
- throw CustomError.appIdMissing
596
- }
597
584
  let channel = options.getChannel() ?? getChannel()
598
585
  var parameters = [String: String]()
599
586
  parameters["appVersionCode"] = getVersionCode()
@@ -605,7 +592,7 @@ import CommonCrypto
605
592
  parameters["osVersion"] = await UIDevice.current.systemVersion
606
593
  parameters["platform"] = "1"
607
594
  parameters["pluginVersion"] = LiveUpdatePlugin.version
608
- var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(appId)/bundles/latest")!
595
+ var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(getAppId() ?? "")/bundles/latest")!
609
596
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
610
597
  let url = try urlComponents.asURL()
611
598
  CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
@@ -695,7 +682,7 @@ import CommonCrypto
695
682
 
696
683
  /// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
697
684
  private func getCurrentCapacitorServerPath() -> String? {
698
- guard let viewController = self.plugin?.bridge?.viewController as? CAPBridgeViewController else {
685
+ guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
699
686
  return nil
700
687
  }
701
688
  return viewController.getServerBasePath()
@@ -777,7 +764,7 @@ import CommonCrypto
777
764
  }
778
765
 
779
766
  private func notifyDownloadBundleProgressListeners(_ event: DownloadBundleProgressEvent) {
780
- plugin?.notifyDownloadBundleProgressListeners(event)
767
+ plugin.notifyDownloadBundleProgressListeners(event)
781
768
  }
782
769
 
783
770
  private func performAutoUpdate() {
@@ -854,7 +841,7 @@ import CommonCrypto
854
841
  }
855
842
 
856
843
  private func setCurrentCapacitorServerPath(path: String) {
857
- guard let viewController = self.plugin?.bridge?.viewController as? CAPBridgeViewController else {
844
+ guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
858
845
  return
859
846
  }
860
847
  viewController.setServerBasePath(path: path)
@@ -873,11 +860,11 @@ import CommonCrypto
873
860
 
874
861
  private func notifyNextBundleSetListeners(_ bundleId: String?) {
875
862
  let event = NextBundleSetEvent(bundleId: bundleId)
876
- plugin?.notifyNextBundleSetListeners(event)
863
+ plugin.notifyNextBundleSetListeners(event)
877
864
  }
878
865
 
879
866
  private func notifyReloadedListeners() {
880
- plugin?.notifyReloadedListeners()
867
+ plugin.notifyReloadedListeners()
881
868
  }
882
869
 
883
870
  private func addBlockedBundleId(_ bundleId: String) {
@@ -27,13 +27,16 @@ public class LiveUpdateHttpClient: NSObject {
27
27
 
28
28
  public func download(url: URL, destination: @escaping DownloadRequest.Destination, callback: ((Progress) -> Void)?) async throws -> AFDownloadResponse<Data> {
29
29
  var request = URLRequest(url: url)
30
+ // Ignore the URL cache so that a cached response is never replayed for a deterministic request URL
31
+ request.cachePolicy = .reloadIgnoringLocalCacheData
30
32
  request.httpMethod = HTTPMethod.get.rawValue
31
33
  request.timeoutInterval = Double(config.httpTimeout) / 1000.0
32
34
  if let deviceId = deviceId, !deviceId.isEmpty {
33
35
  request.setValue(deviceId, forHTTPHeaderField: "X-Capawesome-Device-Id")
34
36
  }
35
37
  return try await withCheckedThrowingContinuation { continuation in
36
- AF.download(request, to: destination).downloadProgress { progress in
38
+ // `validate()` treats non-2xx responses as errors so that an error body is never saved as a bundle file
39
+ AF.download(request, to: destination).validate().downloadProgress { progress in
37
40
  if let callback = callback {
38
41
  callback(progress)
39
42
  }
@@ -45,6 +48,8 @@ public class LiveUpdateHttpClient: NSObject {
45
48
 
46
49
  public func request<T: Decodable>(url: URL, type: T.Type) async throws -> AFDataResponse<T> {
47
50
  var request = URLRequest(url: url)
51
+ // Ignore the URL cache so that a cached response is never replayed for a deterministic request URL
52
+ request.cachePolicy = .reloadIgnoringLocalCacheData
48
53
  request.httpMethod = HTTPMethod.get.rawValue
49
54
  request.timeoutInterval = Double(config.httpTimeout) / 1000.0
50
55
  if let deviceId = deviceId, !deviceId.isEmpty {
@@ -47,7 +47,7 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
47
47
  private let eventReloaded = "reloaded"
48
48
 
49
49
  private var config: LiveUpdateConfig?
50
- var implementation: LiveUpdate?
50
+ private var implementation: LiveUpdate?
51
51
 
52
52
  override public func load() {
53
53
  self.config = liveUpdateConfig()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/capacitor-live-update",
3
- "version": "8.3.0-dev.f9cc1d15.1783929784",
3
+ "version": "8.4.0",
4
4
  "description": "Capacitor plugin to update your app remotely in real-time on Android and iOS.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -75,15 +75,15 @@
75
75
  },
76
76
  "devDependencies": {
77
77
  "@capacitor/android": "8.0.0",
78
- "@capacitor/cli": "8.0.0",
78
+ "@capacitor/cli": "8.4.2",
79
79
  "@capacitor/core": "8.0.0",
80
80
  "@capacitor/docgen": "0.3.1",
81
81
  "@capacitor/ios": "8.0.0",
82
82
  "@ionic/eslint-config": "0.4.0",
83
83
  "eslint": "8.57.0",
84
- "prettier-plugin-java": "2.6.7",
84
+ "prettier-plugin-java": "2.9.7",
85
85
  "rimraf": "6.1.2",
86
- "rollup": "4.53.3",
86
+ "rollup": "4.62.3",
87
87
  "swiftlint": "2.0.0",
88
88
  "typescript": "5.9.3"
89
89
  },
@@ -1,169 +0,0 @@
1
- package io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic
2
-
3
- import android.content.Context
4
- import android.content.SharedPreferences
5
- import com.getcapacitor.Logger
6
- import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdate
7
- import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdateConfig
8
- import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdatePlugin
9
- import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions
10
- import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions
11
- import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult
12
- import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType
13
- import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback
14
- import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback
15
- import io.ionic.liveupdateprovider.MetadataSyncResult
16
- import io.ionic.liveupdateprovider.ProviderError
17
- import io.ionic.liveupdateprovider.ProviderManager
18
- import io.ionic.liveupdateprovider.ProviderSyncResult
19
- import java.io.File
20
- import kotlin.coroutines.resume
21
- import kotlin.coroutines.resumeWithException
22
- import kotlin.coroutines.suspendCoroutine
23
- import org.json.JSONObject
24
-
25
- /**
26
- * Ionic Live Update Provider manager backed by the Capawesome live update plugin.
27
- *
28
- * Each manager instance is scoped by a `managerKey` so that multiple shells (Portals or
29
- * Federated Capacitor apps) can persist their own active bundle without colliding with each
30
- * other or with the standalone plugin's `currentBundleId` / `nextBundleId` state.
31
- *
32
- * Provider configuration keys (V1):
33
- * - `managerKey` (required) — scopes per-manager persisted state
34
- * - `appId` (optional) — Capawesome Cloud app UUID; falls back to plugin config
35
- * - `channel` (optional) — channel to sync; falls back to plugin config
36
- */
37
- class LiveUpdateIonicManager
38
- @Throws(ProviderError.InvalidConfiguration::class)
39
- constructor(context: Context, configuration: Map<String, *>, private val liveUpdate: LiveUpdate) : ProviderManager {
40
-
41
- /**
42
- * Creates a manager without a running Capacitor plugin, e.g. for Ionic Portals hosts
43
- * that construct the manager directly and attach it to a Portal via
44
- * `setLiveUpdateProviderManager(...)`. Uses a headless [LiveUpdate] instance with the
45
- * default configuration; pass `appId` and `channel` via the configuration map.
46
- */
47
- @Throws(ProviderError.InvalidConfiguration::class)
48
- constructor(context: Context, configuration: Map<String, *>) : this(context, configuration, LiveUpdate(LiveUpdateConfig(), context))
49
-
50
- companion object {
51
- const val SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdateIonicProvider"
52
- const val PREF_PREFIX_LAST_SYNCED_BUNDLE_ID = "lastSyncedBundleId."
53
- }
54
-
55
- override var latestAppDirectory: File? = null
56
- private set
57
-
58
- private val appId: String?
59
- private val channel: String?
60
- private val managerKey: String
61
- private val sharedPreferences: SharedPreferences
62
-
63
- init {
64
- val managerKeyValue = configuration["managerKey"]
65
- if (managerKeyValue !is String || managerKeyValue.isEmpty()) {
66
- throw ProviderError.InvalidConfiguration(LiveUpdatePlugin.ERROR_MANAGER_KEY_MISSING)
67
- }
68
- managerKey = managerKeyValue
69
- appId = configuration["appId"] as? String
70
- channel = configuration["channel"] as? String
71
- sharedPreferences = context.applicationContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
72
-
73
- // Restore the last synced bundle directory, if any.
74
- val persistedBundleId = sharedPreferences.getString(getLastSyncedBundleIdKey(), null)
75
- if (persistedBundleId != null) {
76
- latestAppDirectory = liveUpdate.getBundleDirectory(persistedBundleId)
77
- }
78
- }
79
-
80
- override suspend fun sync(): ProviderSyncResult? {
81
- try {
82
- val result = fetchLatestBundle()
83
-
84
- // No update available; nothing to report.
85
- val bundleId = result.bundleId ?: return null
86
-
87
- // Bundle already on disk — just point latestAppDirectory at it and persist.
88
- val existingDirectory = liveUpdate.getBundleDirectory(bundleId)
89
- if (existingDirectory != null) {
90
- applySyncedBundle(bundleId, existingDirectory)
91
- return MetadataSyncResult(buildMetadata(bundleId, result.customProperties))
92
- }
93
-
94
- // Otherwise, download and then apply.
95
- downloadBundle(bundleId, result)
96
- val downloadedDirectory =
97
- liveUpdate.getBundleDirectory(bundleId) ?: throw Exception(LiveUpdatePlugin.ERROR_BUNDLE_DIRECTORY_NOT_FOUND)
98
- applySyncedBundle(bundleId, downloadedDirectory)
99
- return MetadataSyncResult(buildMetadata(bundleId, result.customProperties))
100
- } catch (exception: Exception) {
101
- Logger.error(LiveUpdatePlugin.TAG, "Ionic provider sync failed: ${exception.message}", exception)
102
- throw exception
103
- }
104
- }
105
-
106
- private fun applySyncedBundle(bundleId: String, directory: File) {
107
- latestAppDirectory = directory
108
- sharedPreferences.edit().putString(getLastSyncedBundleIdKey(), bundleId).apply()
109
- }
110
-
111
- private fun buildMetadata(bundleId: String, customProperties: JSONObject?): Map<String, Any> {
112
- val metadata = mutableMapOf<String, Any>("bundleId" to bundleId)
113
- channel?.let { metadata["channel"] = it }
114
- customProperties?.let {
115
- val customPropertiesMap = jsonObjectToMap(it)
116
- if (customPropertiesMap.isNotEmpty()) {
117
- metadata["customProperties"] = customPropertiesMap
118
- }
119
- }
120
- return metadata
121
- }
122
-
123
- private suspend fun downloadBundle(bundleId: String, result: FetchLatestBundleResult) {
124
- val downloadUrl = result.downloadUrl
125
- if (downloadUrl.isNullOrEmpty()) {
126
- throw Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_URL_MISSING)
127
- }
128
- val artifactType = if (result.artifactType == ArtifactType.MANIFEST) "manifest" else "zip"
129
- val options = DownloadBundleOptions(artifactType, bundleId, result.checksum, result.signature, downloadUrl)
130
- return suspendCoroutine { continuation ->
131
- liveUpdate.downloadBundle(
132
- options,
133
- object : EmptyCallback {
134
- override fun success() = continuation.resume(Unit)
135
-
136
- override fun error(exception: Exception) = continuation.resumeWithException(exception)
137
- }
138
- )
139
- }
140
- }
141
-
142
- private suspend fun fetchLatestBundle(): FetchLatestBundleResult = suspendCoroutine { continuation ->
143
- liveUpdate.fetchLatestBundle(
144
- FetchLatestBundleOptions(appId, channel),
145
- object : NonEmptyCallback<FetchLatestBundleResult> {
146
- override fun success(result: FetchLatestBundleResult) = continuation.resume(result)
147
-
148
- override fun error(exception: Exception) = continuation.resumeWithException(exception)
149
- }
150
- )
151
- }
152
-
153
- private fun getLastSyncedBundleIdKey(): String {
154
- return PREF_PREFIX_LAST_SYNCED_BUNDLE_ID + managerKey
155
- }
156
-
157
- private fun jsonObjectToMap(jsonObject: JSONObject): Map<String, Any> {
158
- val map = mutableMapOf<String, Any>()
159
- val keys = jsonObject.keys()
160
- while (keys.hasNext()) {
161
- val key = keys.next()
162
- val value = jsonObject.opt(key)
163
- if (value != null && value != JSONObject.NULL) {
164
- map[key] = value
165
- }
166
- }
167
- return map
168
- }
169
- }
@@ -1,108 +0,0 @@
1
- #if CAPAWESOME_INCLUDE_IONIC_PROVIDER
2
- import Foundation
3
- import Capacitor
4
- import LiveUpdateProvider
5
-
6
- /// Ionic Live Update Provider manager backed by the Capawesome live update plugin.
7
- ///
8
- /// Each manager instance is scoped by a `managerKey` so that multiple shells (Portals or
9
- /// Federated Capacitor apps) can persist their own active bundle without colliding with
10
- /// each other or with the standalone plugin's `currentBundleId` / `nextBundleId` state.
11
- ///
12
- /// Provider configuration keys (V1):
13
- /// - `managerKey` (required) — scopes per-manager persisted state
14
- /// - `appId` (optional) — Capawesome Cloud app UUID; falls back to plugin config
15
- /// - `channel` (optional) — channel to sync; falls back to plugin config
16
- public final class LiveUpdateIonicManager: ProviderManager {
17
- private static let userDefaultsPrefix = "CapawesomeLiveUpdateIonicProvider.lastSyncedBundleId."
18
-
19
- public private(set) var latestAppDirectory: URL?
20
-
21
- private let appId: String?
22
- private let channel: String?
23
- private let liveUpdate: LiveUpdate
24
- private let managerKey: String
25
-
26
- /// Creates a manager without a running Capacitor plugin, e.g. for Ionic Portals hosts
27
- /// that construct the manager directly and attach it to a Portal via
28
- /// `liveUpdateSource: .provider(manager:)`. Uses a headless `LiveUpdate` instance with
29
- /// the default configuration; pass `appId` and `channel` via the configuration dictionary.
30
- public convenience init(configuration: [String: Any]) throws {
31
- try self.init(configuration: configuration, liveUpdate: LiveUpdate(config: LiveUpdateConfig()))
32
- }
33
-
34
- init(configuration: [String: Any], liveUpdate: LiveUpdate) throws {
35
- guard let managerKey = configuration["managerKey"] as? String, !managerKey.isEmpty else {
36
- throw ProviderError.invalidConfiguration(message: CustomError.managerKeyMissing.localizedDescription)
37
- }
38
- self.managerKey = managerKey
39
- self.appId = configuration["appId"] as? String
40
- self.channel = configuration["channel"] as? String
41
- self.liveUpdate = liveUpdate
42
-
43
- // Restore the last synced bundle directory, if any.
44
- if let persistedBundleId = UserDefaults.standard.string(forKey: lastSyncedBundleIdKey()) {
45
- self.latestAppDirectory = liveUpdate.getBundleDirectory(bundleId: persistedBundleId)
46
- }
47
- }
48
-
49
- public func sync() async throws -> (any ProviderSyncResult)? {
50
- do {
51
- let result = try await liveUpdate.fetchLatestBundle(FetchLatestBundleOptions(appId: appId, channel: channel))
52
-
53
- // No update available; nothing to report.
54
- guard let bundleId = result.getBundleId() else {
55
- return nil
56
- }
57
-
58
- // Bundle already on disk — just point latestAppDirectory at it and persist.
59
- if let existingDirectory = liveUpdate.getBundleDirectory(bundleId: bundleId) {
60
- applySyncedBundle(bundleId: bundleId, directory: existingDirectory)
61
- return MetadataSyncResult(metadata: buildMetadata(bundleId: bundleId, customProperties: result.getCustomProperties()))
62
- }
63
-
64
- // Otherwise, download and then apply.
65
- guard let downloadUrl = result.getDownloadUrl(), !downloadUrl.isEmpty else {
66
- throw CustomError.downloadUrlMissing
67
- }
68
- let downloadOptions = DownloadBundleOptions(
69
- artifactType: result.getArtifactType() ?? "zip",
70
- bundleId: bundleId,
71
- checksum: result.getChecksum(),
72
- signature: result.getSignature(),
73
- url: downloadUrl
74
- )
75
- try await liveUpdate.downloadBundle(downloadOptions)
76
-
77
- guard let downloadedDirectory = liveUpdate.getBundleDirectory(bundleId: bundleId) else {
78
- throw CustomError.bundleDirectoryNotFound
79
- }
80
- applySyncedBundle(bundleId: bundleId, directory: downloadedDirectory)
81
- return MetadataSyncResult(metadata: buildMetadata(bundleId: bundleId, customProperties: result.getCustomProperties()))
82
- } catch {
83
- CAPLog.print("[", LiveUpdatePlugin.tag, "] Ionic provider sync failed: ", error.localizedDescription)
84
- throw error
85
- }
86
- }
87
-
88
- private func applySyncedBundle(bundleId: String, directory: URL) {
89
- latestAppDirectory = directory
90
- UserDefaults.standard.set(bundleId, forKey: lastSyncedBundleIdKey())
91
- }
92
-
93
- private func buildMetadata(bundleId: String, customProperties: [String: Any]?) -> [String: Any] {
94
- var metadata: [String: Any] = ["bundleId": bundleId]
95
- if let channel = channel {
96
- metadata["channel"] = channel
97
- }
98
- if let customProperties = customProperties, !customProperties.isEmpty {
99
- metadata["customProperties"] = customProperties
100
- }
101
- return metadata
102
- }
103
-
104
- private func lastSyncedBundleIdKey() -> String {
105
- return Self.userDefaultsPrefix + managerKey
106
- }
107
- }
108
- #endif
@@ -1,15 +0,0 @@
1
- #if CAPAWESOME_INCLUDE_IONIC_PROVIDER
2
- import Foundation
3
- import LiveUpdateProvider
4
-
5
- extension LiveUpdatePlugin: LiveUpdateProvider {
6
- /// Creates a manager for the Ionic Live Update Provider SDK. Invoked natively by
7
- /// Federated Capacitor after resolving this plugin by its Capacitor plugin name.
8
- public func createManager(configuration: [String: Any]) throws -> any ProviderManager {
9
- guard let implementation = implementation else {
10
- throw ProviderError.invalidConfiguration(message: CustomError.pluginNotInitialized.localizedDescription)
11
- }
12
- return try LiveUpdateIonicManager(configuration: configuration, liveUpdate: implementation)
13
- }
14
- }
15
- #endif