@capawesome/capacitor-live-update 8.0.0 → 8.1.0-dev.97c86c2c.1768835749

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.
@@ -11,10 +11,10 @@ Pod::Spec.new do |s|
11
11
  s.author = package['author']
12
12
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
13
  s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
14
- s.ios.deployment_target = '16.0'
14
+ s.ios.deployment_target = '15.0'
15
15
  s.dependency 'Capacitor'
16
16
  s.dependency 'Alamofire', '~> 5.10.2'
17
- s.dependency 'SSZipArchive', '~> 2.6.0'
17
+ s.dependency 'ZIPFoundation', '~> 0.9.0'
18
18
  s.swift_version = '5.1'
19
19
  s.static_framework = true
20
20
  end
package/Package.swift CHANGED
@@ -3,7 +3,7 @@ import PackageDescription
3
3
 
4
4
  let package = Package(
5
5
  name: "CapawesomeCapacitorLiveUpdate",
6
- platforms: [.iOS(.v16)],
6
+ platforms: [.iOS(.v15)],
7
7
  products: [
8
8
  .library(
9
9
  name: "CapawesomeCapacitorLiveUpdate",
@@ -12,7 +12,7 @@ let package = Package(
12
12
  dependencies: [
13
13
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
14
14
  .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMinor(from: "5.10.2")),
15
- .package(url: "https://github.com/ZipArchive/ZipArchive.git", .upToNextMinor(from: "2.6.0"))
15
+ .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.0"))
16
16
  ],
17
17
  targets: [
18
18
  .target(
@@ -21,7 +21,7 @@ let package = Package(
21
21
  .product(name: "Capacitor", package: "capacitor-swift-pm"),
22
22
  .product(name: "Cordova", package: "capacitor-swift-pm"),
23
23
  .product(name: "Alamofire", package: "Alamofire"),
24
- .product(name: "ZipArchive", package: "ZipArchive")
24
+ .product(name: "ZIPFoundation", package: "ZIPFoundation")
25
25
  ],
26
26
  path: "ios/Plugin"),
27
27
  .testTarget(
package/README.md CHANGED
@@ -13,7 +13,7 @@ Capacitor plugin that allows you to update your app remotely in real-time withou
13
13
  We are proud to offer one of the most complete and feature-rich Capacitor plugins for Over-the-Air (OTA) updates. Here are some of the key features:
14
14
 
15
15
  - 🔋 Supports **Android and iOS**
16
- - ⚡️ **Capacitor 7** support
16
+ - ⚡️ **Capacitor 6/7/8** support
17
17
  - 📦 **Bundle Management**: Download, set, and delete bundles.
18
18
  - ☁️ **Cloud Support**: Use the [Capawesome Cloud](https://cloud.capawesome.io/) to manage your app updates.
19
19
  - 📺 **Channel Support**: Set a channel for the app to manage different versions.
@@ -38,12 +38,8 @@ Missing a feature? Just [open an issue](https://github.com/capawesome-team/capac
38
38
 
39
39
  ## Guides
40
40
 
41
- - [Announcing the Capacitor Live Update Plugin](https://capawesome.io/blog/announcing-the-capacitor-live-update-plugin/)
42
- - [Exploring the Capacitor Live Update API](https://capawesome.io/blog/exploring-the-capacitor-live-update-api/)
43
- - [How Live Updates for Capacitor work](https://capawesome.io/blog/how-live-updates-for-capacitor-work/)
44
- - [How to gradually roll out Capacitor Live Updates](https://capawesome.io/blog/how-to-gradually-roll-out-capacitor-live-updates/#update-a-bundle)
45
- - [How to restrict Capacitor Live Updates to Native Versions](https://capawesome.io/blog/how-to-restrict-capacitor-live-updates-to-native-versions/)
46
- - [Live Updates for Nuxt Capacitor Apps with Capawesome Cloud](https://capawesome.io/blog/live-updates-for-nuxt-capacitor/)
41
+ - [Getting Started with Capawesome Cloud Live Updates](https://capawesome.io/cloud/live-updates/setup/)
42
+ - [Migrating from Ionic Appflow to Capawesome Cloud](https://capawesome.io/blog/migrating-from-ionic-appflow-to-capawesome-cloud/)
47
43
  - [Migrating from App Center to Capawesome Cloud](https://capawesome.io/blog/migrating-from-app-center-to-capawesome-cloud/)
48
44
 
49
45
  ## Installation
@@ -93,21 +89,6 @@ Add the `NSPrivacyAccessedAPICategoryUserDefaults` dictionary key to your [Priva
93
89
 
94
90
  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.
95
91
 
96
- #### Minimum Deployment Target
97
-
98
- If you are using **Swift Package Manager**, make sure that your iOS deployment target is set to at least `16.0` in your Xcode project settings (usually in `ios/App/App.xcodeproj`):
99
-
100
- ```diff
101
- -IPHONEOS_DEPLOYMENT_TARGET = 15.0
102
- +IPHONEOS_DEPLOYMENT_TARGET = 16.0
103
- ```
104
-
105
- If you are using **CocoaPods**, make sure that your iOS deployment target is set to at least `16.0` in your `Podfile`:
106
-
107
- ```ruby
108
- platform :ios, '16.0'
109
- ```
110
-
111
92
  ## Configuration
112
93
 
113
94
  <docgen-config>
@@ -1,6 +1,7 @@
1
1
  package io.capawesome.capacitorjs.plugins.liveupdate;
2
2
 
3
3
  import android.app.Activity;
4
+ import android.content.Context;
4
5
  import android.content.SharedPreferences;
5
6
  import android.content.pm.PackageInfo;
6
7
  import android.content.pm.PackageManager;
@@ -13,6 +14,7 @@ import androidx.annotation.Nullable;
13
14
  import com.getcapacitor.Bridge;
14
15
  import com.getcapacitor.Logger;
15
16
  import com.getcapacitor.plugin.WebView;
17
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.CustomExceptions;
16
18
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.Manifest;
17
19
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.ManifestItem;
18
20
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.api.GetLatestBundleResponse;
@@ -44,6 +46,7 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.SyncResult;
44
46
  import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType;
45
47
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.DownloadProgressCallback;
46
48
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
49
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.LiveUpdateEventEmitter;
47
50
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
48
51
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
49
52
  import java.io.File;
@@ -77,18 +80,32 @@ import org.json.JSONObject;
77
80
 
78
81
  public class LiveUpdate {
79
82
 
83
+ public static final String TAG = "LiveUpdate";
84
+ public static final String VERSION = "8.1.1";
85
+
86
+ @Nullable
87
+ private static LiveUpdate instance = null;
88
+
89
+ private static final Object LOCK = new Object();
90
+
80
91
  private final long autoUpdateIntervalMs = 15 * 60 * 1000; // 15 minutes
81
92
 
93
+ @NonNull
94
+ private final Bridge bridge;
95
+
82
96
  @NonNull
83
97
  private final LiveUpdateConfig config;
84
98
 
99
+ @NonNull
100
+ private final Context context;
101
+
85
102
  private final String defaultWebAssetDir = Bridge.DEFAULT_WEB_ASSET_DIR;
86
103
 
87
- @NonNull
88
- private final LiveUpdateHttpClient httpClient;
104
+ @Nullable
105
+ private final LiveUpdateEventEmitter eventEmitter;
89
106
 
90
107
  @NonNull
91
- private final LiveUpdatePlugin plugin;
108
+ private final LiveUpdateHttpClient httpClient;
92
109
 
93
110
  @NonNull
94
111
  private final LiveUpdatePreferences preferences;
@@ -105,12 +122,19 @@ public class LiveUpdate {
105
122
  private boolean rollbackPerformed = false;
106
123
  private boolean syncInProgress = false;
107
124
 
108
- public LiveUpdate(@NonNull LiveUpdateConfig config, @NonNull LiveUpdatePlugin plugin) throws PackageManager.NameNotFoundException {
125
+ private LiveUpdate(
126
+ @NonNull Context context,
127
+ @NonNull Bridge bridge,
128
+ @NonNull LiveUpdateConfig config,
129
+ @Nullable LiveUpdateEventEmitter eventEmitter
130
+ ) throws PackageManager.NameNotFoundException {
131
+ this.context = context;
132
+ this.bridge = bridge;
109
133
  this.config = config;
134
+ this.eventEmitter = eventEmitter;
110
135
  this.httpClient = new LiveUpdateHttpClient(config);
111
- this.plugin = plugin;
112
- this.preferences = new LiveUpdatePreferences(plugin.getContext());
113
- this.webViewSettingsEditor = plugin.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
136
+ this.preferences = new LiveUpdatePreferences(context);
137
+ this.webViewSettingsEditor = context.getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE).edit();
114
138
 
115
139
  // Check version and reset config if version changed
116
140
  checkAndResetConfigIfVersionChanged();
@@ -120,6 +144,31 @@ public class LiveUpdate {
120
144
  startRollbackTimer();
121
145
  }
122
146
 
147
+ @NonNull
148
+ public static LiveUpdate getInstance(
149
+ @NonNull Context context,
150
+ @NonNull Bridge bridge,
151
+ @NonNull LiveUpdateConfig config,
152
+ @Nullable LiveUpdateEventEmitter eventEmitter
153
+ ) throws PackageManager.NameNotFoundException {
154
+ if (instance == null) {
155
+ synchronized (LOCK) {
156
+ if (instance == null) {
157
+ instance = new LiveUpdate(context, bridge, config, eventEmitter);
158
+ }
159
+ }
160
+ }
161
+ return instance;
162
+ }
163
+
164
+ @NonNull
165
+ public static LiveUpdate getInstance() {
166
+ if (instance == null) {
167
+ throw new IllegalStateException("LiveUpdate not initialized. Call getInstance with dependencies first.");
168
+ }
169
+ return instance;
170
+ }
171
+
123
172
  public void clearBlockedBundles() {
124
173
  preferences.setBlockedBundleIds(null);
125
174
  }
@@ -128,7 +177,7 @@ public class LiveUpdate {
128
177
  String bundleId = options.getBundleId();
129
178
 
130
179
  if (!hasBundleById(bundleId)) {
131
- Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
180
+ Exception exception = CustomExceptions.BUNDLE_NOT_FOUND;
132
181
  callback.error(exception);
133
182
  return;
134
183
  }
@@ -284,9 +333,9 @@ public class LiveUpdate {
284
333
  }
285
334
 
286
335
  public void ready(@NonNull NonEmptyCallback callback) {
287
- Logger.debug(LiveUpdatePlugin.TAG, "App is ready.");
336
+ Logger.debug(LiveUpdate.TAG, "App is ready.");
288
337
  if (config.getReadyTimeout() <= 0) {
289
- Logger.warn(LiveUpdatePlugin.TAG, "Ready timeout is set to 0. Automatic rollback is disabled.");
338
+ Logger.warn(LiveUpdate.TAG, "Ready timeout is set to 0. Automatic rollback is disabled.");
290
339
  }
291
340
  // Stop the rollback timer
292
341
  stopRollbackTimer();
@@ -352,7 +401,7 @@ public class LiveUpdate {
352
401
  if (hasBundleById(bundleId)) {
353
402
  setNextBundleById(bundleId);
354
403
  } else {
355
- Exception exception = new Exception(LiveUpdatePlugin.ERROR_BUNDLE_NOT_FOUND);
404
+ Exception exception = CustomExceptions.BUNDLE_NOT_FOUND;
356
405
  callback.error(exception);
357
406
  return;
358
407
  }
@@ -362,7 +411,7 @@ public class LiveUpdate {
362
411
 
363
412
  public void sync(@NonNull SyncOptions options, @NonNull NonEmptyCallback<Result> callback) {
364
413
  if (syncInProgress) {
365
- Exception exception = new Exception(LiveUpdatePlugin.ERROR_SYNC_IN_PROGRESS);
414
+ Exception exception = CustomExceptions.SYNC_IN_PROGRESS;
366
415
  callback.error(exception);
367
416
  return;
368
417
  }
@@ -378,7 +427,7 @@ public class LiveUpdate {
378
427
  public void success(@Nullable GetLatestBundleResponse response) {
379
428
  try {
380
429
  if (response == null) {
381
- Logger.debug(LiveUpdatePlugin.TAG, "No update available.");
430
+ Logger.debug(LiveUpdate.TAG, "No update available.");
382
431
  syncInProgress = false;
383
432
  SyncResult syncResult = new SyncResult(null);
384
433
  callback.success(syncResult);
@@ -391,7 +440,7 @@ public class LiveUpdate {
391
440
  String url = response.getUrl();
392
441
  // Check if the bundle is blocked
393
442
  if (isBlockedBundleId(latestBundleId)) {
394
- Logger.warn(LiveUpdatePlugin.TAG, "Bundle is blocked and will not be downloaded.");
443
+ Logger.warn(LiveUpdate.TAG, "Bundle is blocked and will not be downloaded.");
395
444
  syncInProgress = false;
396
445
  SyncResult syncResult = new SyncResult(null);
397
446
  callback.success(syncResult);
@@ -459,7 +508,7 @@ public class LiveUpdate {
459
508
  // Search folder with index.html file
460
509
  File indexHtmlFile = searchIndexHtmlFile(sourceDirectory);
461
510
  if (indexHtmlFile == null) {
462
- throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_INDEX_HTML_MISSING);
511
+ throw CustomExceptions.BUNDLE_INDEX_HTML_MISSING;
463
512
  }
464
513
 
465
514
  // Create the bundles directory if it does not exist
@@ -482,21 +531,21 @@ public class LiveUpdate {
482
531
  }
483
532
 
484
533
  private File buildBundlesDirectory() {
485
- return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
534
+ return new File(context.getFilesDir(), bundlesDirectory);
486
535
  }
487
536
 
488
537
  private File buildBundleDirectoryFor(@NonNull String bundleId) {
489
- return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
538
+ return new File(context.getFilesDir(), bundlesDirectory + "/" + bundleId);
490
539
  }
491
540
 
492
541
  private File buildTemporaryDirectory() {
493
542
  String fileName = UUID.randomUUID().toString();
494
- return new File(plugin.getContext().getCacheDir(), fileName);
543
+ return new File(context.getCacheDir(), fileName);
495
544
  }
496
545
 
497
546
  private File buildTemporaryZipFile() {
498
547
  String fileName = UUID.randomUUID().toString() + ".zip";
499
- return new File(plugin.getContext().getCacheDir(), fileName);
548
+ return new File(context.getCacheDir(), fileName);
500
549
  }
501
550
 
502
551
  private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
@@ -504,7 +553,7 @@ public class LiveUpdate {
504
553
  String currentBundleId = getCurrentBundleId();
505
554
  if (currentBundleId == null) {
506
555
  // Create the source input stream
507
- AssetManager assets = plugin.getContext().getAssets();
556
+ AssetManager assets = context.getAssets();
508
557
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + href);
509
558
  // Create the destination file
510
559
  File destination = new File(destinationDirectory, href);
@@ -532,7 +581,7 @@ public class LiveUpdate {
532
581
  for (ManifestItem fileToCopy : filesToCopy) {
533
582
  boolean success = tryCopyCurrentBundleFile(fileToCopy, destinationDirectory);
534
583
  if (!success) {
535
- Logger.warn(LiveUpdatePlugin.TAG, "Failed to copy file: " + fileToCopy.getHref());
584
+ Logger.warn(LiveUpdate.TAG, "Failed to copy file: " + fileToCopy.getHref());
536
585
  // If the file could not be copied, add it to the list of missing items
537
586
  missingItems.add(fileToCopy);
538
587
  }
@@ -578,8 +627,8 @@ public class LiveUpdate {
578
627
  KeyFactory keyFactory = KeyFactory.getInstance("RSA");
579
628
  return keyFactory.generatePublic(X509publicKey);
580
629
  } catch (Exception exception) {
581
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
582
- throw new Exception(LiveUpdatePlugin.ERROR_PUBLIC_KEY_INVALID);
630
+ Logger.error(LiveUpdate.TAG, exception.getMessage(), exception);
631
+ throw CustomExceptions.PUBLIC_KEY_INVALID;
583
632
  }
584
633
  }
585
634
 
@@ -645,8 +694,8 @@ public class LiveUpdate {
645
694
  completionCallback.success();
646
695
  } else {
647
696
  String errorMessage = response.body().string();
648
- Exception exception = new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED);
649
- Logger.error(LiveUpdatePlugin.TAG, errorMessage, exception);
697
+ Exception exception = CustomExceptions.DOWNLOAD_FAILED;
698
+ Logger.error(LiveUpdate.TAG, errorMessage, exception);
650
699
  completionCallback.error(exception);
651
700
  }
652
701
  } catch (Exception e) {
@@ -749,7 +798,7 @@ public class LiveUpdate {
749
798
  public void error(@NonNull Exception e) {
750
799
  // Capture first error and cancel all remaining downloads
751
800
  if (firstError.compareAndSet(null, e)) {
752
- Logger.error(LiveUpdatePlugin.TAG, "Failed to download file: " + item.getHref(), e);
801
+ Logger.error(LiveUpdate.TAG, "Failed to download file: " + item.getHref(), e);
753
802
  // Cancel all in-flight downloads (fail-fast)
754
803
  synchronized (activeCalls) {
755
804
  for (Call activeCall : activeCalls) {
@@ -924,10 +973,10 @@ public class LiveUpdate {
924
973
  .addQueryParameter("deviceId", getDeviceId())
925
974
  .addQueryParameter("osVersion", String.valueOf(Build.VERSION.SDK_INT))
926
975
  .addQueryParameter("platform", "0")
927
- .addQueryParameter("pluginVersion", LiveUpdatePlugin.VERSION)
976
+ .addQueryParameter("pluginVersion", LiveUpdate.VERSION)
928
977
  .build()
929
978
  .toString();
930
- Logger.debug(LiveUpdatePlugin.TAG, "Fetching latest bundle: " + url);
979
+ Logger.debug(LiveUpdate.TAG, "Fetching latest bundle: " + url);
931
980
 
932
981
  httpClient.enqueue(
933
982
  url,
@@ -936,7 +985,7 @@ public class LiveUpdate {
936
985
  public void success(@NonNull Response response) {
937
986
  try {
938
987
  String responseBodyString = response.body().string();
939
- Logger.debug(LiveUpdatePlugin.TAG, "Latest bundle response: " + responseBodyString);
988
+ Logger.debug(LiveUpdate.TAG, "Latest bundle response: " + responseBodyString);
940
989
  if (response.isSuccessful()) {
941
990
  JSONObject responseJson = new JSONObject(responseBodyString);
942
991
  GetLatestBundleResponse result = new GetLatestBundleResponse(responseJson);
@@ -986,8 +1035,8 @@ public class LiveUpdate {
986
1035
  source.close();
987
1036
  return digest.digest();
988
1037
  } catch (IOException exception) {
989
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
990
- throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_CALCULATION_FAILED);
1038
+ Logger.error(LiveUpdate.TAG, exception.getMessage(), exception);
1039
+ throw CustomExceptions.CHECKSUM_CALCULATION_FAILED;
991
1040
  }
992
1041
  }
993
1042
 
@@ -1037,7 +1086,7 @@ public class LiveUpdate {
1037
1086
  * @return The absolute path to the current bundle directory (`public` for the built-in bundle).
1038
1087
  */
1039
1088
  private String getCurrentCapacitorServerPath() {
1040
- return plugin.getBridge().getServerBasePath();
1089
+ return bridge.getServerBasePath();
1041
1090
  }
1042
1091
 
1043
1092
  @NonNull
@@ -1067,8 +1116,7 @@ public class LiveUpdate {
1067
1116
  */
1068
1117
  @NonNull
1069
1118
  private String getNextCapacitorServerPath() {
1070
- String path = plugin
1071
- .getContext()
1119
+ String path = context
1072
1120
  .getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE)
1073
1121
  .getString(WebView.CAP_SERVER_PATH, defaultWebAssetDir);
1074
1122
  // Empty path means default path
@@ -1113,7 +1161,7 @@ public class LiveUpdate {
1113
1161
  private Manifest loadCurrentManifest() throws Exception {
1114
1162
  String currentBundleId = getCurrentBundleId();
1115
1163
  if (currentBundleId == null) {
1116
- AssetManager assets = plugin.getContext().getAssets();
1164
+ AssetManager assets = context.getAssets();
1117
1165
  boolean manifestFileExists = Arrays.asList(assets.list(defaultWebAssetDir)).contains(manifestFileName);
1118
1166
  if (manifestFileExists) {
1119
1167
  InputStream inputStream = assets.open(defaultWebAssetDir + "/" + manifestFileName);
@@ -1145,14 +1193,17 @@ public class LiveUpdate {
1145
1193
  }
1146
1194
 
1147
1195
  private void notifyDownloadBundleProgressListeners(@NonNull final DownloadBundleProgressEvent event) {
1148
- plugin.notifyDownloadBundleProgressListeners(event);
1196
+ if (eventEmitter == null) {
1197
+ return;
1198
+ }
1199
+ eventEmitter.onDownloadBundleProgress(event);
1149
1200
  }
1150
1201
 
1151
1202
  private void performAutoUpdate() {
1152
1203
  // Check if enough time has passed since the last check
1153
1204
  long now = System.currentTimeMillis();
1154
1205
  if (lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs) {
1155
- Logger.debug(LiveUpdatePlugin.TAG, "Auto-update skipped. Last check was less than 15 minutes ago.");
1206
+ Logger.debug(LiveUpdate.TAG, "Auto-update skipped. Last check was less than 15 minutes ago.");
1156
1207
  return;
1157
1208
  }
1158
1209
 
@@ -1160,17 +1211,17 @@ public class LiveUpdate {
1160
1211
  lastAutoUpdateCheckTimestamp = now;
1161
1212
 
1162
1213
  // Run sync
1163
- Logger.debug(LiveUpdatePlugin.TAG, "Auto-update started.");
1214
+ Logger.debug(LiveUpdate.TAG, "Auto-update started.");
1164
1215
  SyncOptions options = new SyncOptions((String) null);
1165
1216
  NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
1166
1217
  @Override
1167
1218
  public void success(@NonNull Result result) {
1168
- Logger.debug(LiveUpdatePlugin.TAG, "Auto-update completed successfully.");
1219
+ Logger.debug(LiveUpdate.TAG, "Auto-update completed successfully.");
1169
1220
  }
1170
1221
 
1171
1222
  @Override
1172
1223
  public void error(@NonNull Exception exception) {
1173
- Logger.error(LiveUpdatePlugin.TAG, "Auto-update failed: " + exception.getMessage(), exception);
1224
+ Logger.error(LiveUpdate.TAG, "Auto-update failed: " + exception.getMessage(), exception);
1174
1225
  }
1175
1226
  };
1176
1227
  sync(options, callback);
@@ -1184,9 +1235,9 @@ public class LiveUpdate {
1184
1235
  setPreviousBundleId(currentBundleId);
1185
1236
  // Log the rollback result
1186
1237
  if (currentBundleId == null) {
1187
- Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Default bundle is already in use.");
1238
+ Logger.debug(LiveUpdate.TAG, "App is not ready. Default bundle is already in use.");
1188
1239
  } else {
1189
- Logger.debug(LiveUpdatePlugin.TAG, "App is not ready. Rolling back to default bundle.");
1240
+ Logger.debug(LiveUpdate.TAG, "App is not ready. Rolling back to default bundle.");
1190
1241
  // Rollback to the default bundle
1191
1242
  setNextBundleById(null);
1192
1243
  setCurrentBundleById(null);
@@ -1232,11 +1283,11 @@ public class LiveUpdate {
1232
1283
 
1233
1284
  private void setCurrentCapacitorServerPath(@NonNull String path) {
1234
1285
  if (path.equals(defaultWebAssetDir)) {
1235
- this.plugin.getBridge().setServerAssetPath(path);
1286
+ this.bridge.setServerAssetPath(path);
1236
1287
  } else {
1237
- this.plugin.getBridge().setServerBasePath(path);
1288
+ this.bridge.setServerBasePath(path);
1238
1289
  }
1239
- this.plugin.getBridge().reload();
1290
+ this.bridge.reload();
1240
1291
  // Notify listeners
1241
1292
  notifyReloadedListeners();
1242
1293
  }
@@ -1257,12 +1308,18 @@ public class LiveUpdate {
1257
1308
  }
1258
1309
 
1259
1310
  private void notifyNextBundleSetListeners(@Nullable String bundleId) {
1311
+ if (eventEmitter == null) {
1312
+ return;
1313
+ }
1260
1314
  NextBundleSetEvent event = new NextBundleSetEvent(bundleId);
1261
- plugin.notifyNextBundleSetListeners(event);
1315
+ eventEmitter.onNextBundleSet(event);
1262
1316
  }
1263
1317
 
1264
1318
  private void notifyReloadedListeners() {
1265
- plugin.notifyReloadedListeners();
1319
+ if (eventEmitter == null) {
1320
+ return;
1321
+ }
1322
+ eventEmitter.onReloaded();
1266
1323
  }
1267
1324
 
1268
1325
  private void addBlockedBundleId(@NonNull String bundleId) {
@@ -1292,7 +1349,7 @@ public class LiveUpdate {
1292
1349
  String newBlockedIds = String.join(",", blockedList);
1293
1350
  preferences.setBlockedBundleIds(newBlockedIds);
1294
1351
 
1295
- Logger.debug(LiveUpdatePlugin.TAG, "Bundle blocked: " + bundleId);
1352
+ Logger.debug(LiveUpdate.TAG, "Bundle blocked: " + bundleId);
1296
1353
  }
1297
1354
 
1298
1355
  private boolean isBlockedBundleId(@NonNull String bundleId) {
@@ -1316,7 +1373,7 @@ public class LiveUpdate {
1316
1373
 
1317
1374
  if (lastVersionCode == -1 || lastVersionCode != currentVersionCode) {
1318
1375
  Logger.debug(
1319
- LiveUpdatePlugin.TAG,
1376
+ LiveUpdate.TAG,
1320
1377
  "App version changed (last: " + lastVersionCode + ", current: " + currentVersionCode + "), resetting config."
1321
1378
  );
1322
1379
  resetConfig();
@@ -1366,12 +1423,12 @@ public class LiveUpdate {
1366
1423
  if (publicKey != null) {
1367
1424
  // Verify the signature
1368
1425
  if (signature == null) {
1369
- throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_MISSING);
1426
+ throw CustomExceptions.SIGNATURE_MISSING;
1370
1427
  }
1371
1428
  // Verify the signature
1372
1429
  boolean verified = verifySignatureForFile(file, signature, publicKey);
1373
1430
  if (!verified) {
1374
- throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
1431
+ throw CustomExceptions.SIGNATURE_VERIFICATION_FAILED;
1375
1432
  }
1376
1433
  }
1377
1434
  // Verify the checksum
@@ -1379,7 +1436,7 @@ public class LiveUpdate {
1379
1436
  // Calculate the checksum
1380
1437
  boolean verified = verifyChecksumForFile(file, checksum);
1381
1438
  if (!verified) {
1382
- throw new Exception(LiveUpdatePlugin.ERROR_CHECKSUM_MISMATCH);
1439
+ throw CustomExceptions.CHECKSUM_MISMATCH;
1383
1440
  }
1384
1441
  }
1385
1442
  }
@@ -1402,17 +1459,17 @@ public class LiveUpdate {
1402
1459
  source.close();
1403
1460
  return sig.verify(signatureBytes);
1404
1461
  } catch (Exception exception) {
1405
- Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
1406
- throw new Exception(LiveUpdatePlugin.ERROR_SIGNATURE_VERIFICATION_FAILED);
1462
+ Logger.error(LiveUpdate.TAG, exception.getMessage(), exception);
1463
+ throw CustomExceptions.SIGNATURE_VERIFICATION_FAILED;
1407
1464
  }
1408
1465
  }
1409
1466
 
1410
1467
  private PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
1411
- String packageName = this.plugin.getContext().getPackageName();
1468
+ String packageName = this.context.getPackageName();
1412
1469
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1413
- return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
1470
+ return this.context.getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
1414
1471
  } else {
1415
- return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, 0);
1472
+ return this.context.getPackageManager().getPackageInfo(packageName, 0);
1416
1473
  }
1417
1474
  }
1418
1475
  }
@@ -28,15 +28,15 @@ import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetDownloade
28
28
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.GetNextBundleResult;
29
29
  import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.IsSyncingResult;
30
30
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
31
+ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.LiveUpdateEventEmitter;
31
32
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
32
33
  import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
33
34
 
34
35
  @CapacitorPlugin(name = "LiveUpdate")
35
36
  public class LiveUpdatePlugin extends Plugin {
36
37
 
37
- public static final String TAG = "LiveUpdate";
38
- public static final String VERSION = "7.4.0";
39
- public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
38
+ public static final String TAG = LiveUpdate.TAG;
39
+ public static final String VERSION = LiveUpdate.VERSION;
40
40
  public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
41
41
  public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
42
42
  public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
@@ -68,7 +68,8 @@ public class LiveUpdatePlugin extends Plugin {
68
68
  public void load() {
69
69
  try {
70
70
  config = getLiveUpdateConfig();
71
- implementation = new LiveUpdate(config, this);
71
+ LiveUpdateEventEmitter eventEmitter = new PluginLiveUpdateEventEmitter();
72
+ implementation = LiveUpdate.getInstance(getContext(), getBridge(), config, eventEmitter);
72
73
  } catch (Exception exception) {
73
74
  Logger.error(TAG, exception.getMessage(), exception);
74
75
  }
@@ -684,4 +685,22 @@ public class LiveUpdatePlugin extends Plugin {
684
685
  Logger.error(TAG, message, exception);
685
686
  call.reject(message);
686
687
  }
688
+
689
+ private class PluginLiveUpdateEventEmitter implements LiveUpdateEventEmitter {
690
+
691
+ @Override
692
+ public void onDownloadBundleProgress(@NonNull DownloadBundleProgressEvent event) {
693
+ notifyDownloadBundleProgressListeners(event);
694
+ }
695
+
696
+ @Override
697
+ public void onNextBundleSet(@NonNull NextBundleSetEvent event) {
698
+ notifyNextBundleSetListeners(event);
699
+ }
700
+
701
+ @Override
702
+ public void onReloaded() {
703
+ notifyReloadedListeners();
704
+ }
705
+ }
687
706
  }
@@ -9,6 +9,8 @@ import java.util.UUID;
9
9
 
10
10
  public class LiveUpdatePreferences {
11
11
 
12
+ private static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
13
+
12
14
  @NonNull
13
15
  private final Context context;
14
16
 
@@ -25,53 +27,45 @@ public class LiveUpdatePreferences {
25
27
 
26
28
  public LiveUpdatePreferences(@NonNull Context context) {
27
29
  this.context = context;
28
- this.settingsEditor = context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).edit();
30
+ this.settingsEditor = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).edit();
29
31
  }
30
32
 
31
33
  @Nullable
32
34
  public String getAppId() {
33
- return context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(appIdKey, null);
35
+ return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(appIdKey, null);
34
36
  }
35
37
 
36
38
  @Nullable
37
39
  public String getChannel() {
38
- return context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(channelKey, null);
40
+ return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(channelKey, null);
39
41
  }
40
42
 
41
43
  @Nullable
42
44
  public String getCustomId() {
43
- return context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(customIdKey, null);
45
+ return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(customIdKey, null);
44
46
  }
45
47
 
46
48
  @Nullable
47
49
  public String getDeviceIdForApp(@Nullable String appId) {
48
50
  if (appId == null) {
49
- return context
50
- .getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE)
51
- .getString(deviceIdKey, null);
51
+ return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(deviceIdKey, null);
52
52
  } else {
53
- return context
54
- .getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE)
55
- .getString(deviceIdKey + "_" + appId, null);
53
+ return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(deviceIdKey + "_" + appId, null);
56
54
  }
57
55
  }
58
56
 
59
57
  public int getLastVersionCode() {
60
- return context.getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getInt(lastVersionCodeKey, -1);
58
+ return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getInt(lastVersionCodeKey, -1);
61
59
  }
62
60
 
63
61
  @Nullable
64
62
  public String getPreviousBundleId() {
65
- return context
66
- .getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE)
67
- .getString(previousBundleIdKey, null);
63
+ return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(previousBundleIdKey, null);
68
64
  }
69
65
 
70
66
  @Nullable
71
67
  public String getBlockedBundleIds() {
72
- return context
73
- .getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE)
74
- .getString(blockedBundleIdsKey, null);
68
+ return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(blockedBundleIdsKey, null);
75
69
  }
76
70
 
77
71
  public void setAppId(@Nullable String appId) {
@@ -0,0 +1,20 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.classes;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import androidx.annotation.Nullable;
5
+
6
+ public class CustomException extends Exception {
7
+
8
+ @Nullable
9
+ private final String code;
10
+
11
+ public CustomException(@Nullable String code, @NonNull String message) {
12
+ super(message);
13
+ this.code = code;
14
+ }
15
+
16
+ @Nullable
17
+ public String getCode() {
18
+ return code;
19
+ }
20
+ }
@@ -0,0 +1,33 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.classes;
2
+
3
+ public class CustomExceptions {
4
+
5
+ public static final CustomException APP_ID_MISSING = new CustomException("APP_ID_MISSING", "appId must be configured.");
6
+ public static final CustomException BUNDLE_EXISTS = new CustomException("BUNDLE_EXISTS", "bundle already exists.");
7
+ public static final CustomException BUNDLE_ID_MISSING = new CustomException(null, "bundleId must be provided.");
8
+ public static final CustomException BUNDLE_INDEX_HTML_MISSING = new CustomException(
9
+ "BUNDLE_INDEX_HTML_MISSING",
10
+ "The bundle does not contain an index.html file."
11
+ );
12
+ public static final CustomException BUNDLE_NOT_FOUND = new CustomException("BUNDLE_NOT_FOUND", "bundle not found.");
13
+ public static final CustomException CHECKSUM_CALCULATION_FAILED = new CustomException(
14
+ "CHECKSUM_CALCULATION_FAILED",
15
+ "Failed to calculate checksum."
16
+ );
17
+ public static final CustomException CHECKSUM_MISMATCH = new CustomException("CHECKSUM_MISMATCH", "Checksum mismatch.");
18
+ public static final CustomException CUSTOM_ID_MISSING = new CustomException(null, "customId must be provided.");
19
+ public static final CustomException DOWNLOAD_FAILED = new CustomException("DOWNLOAD_FAILED", "Bundle could not be downloaded.");
20
+ public static final CustomException HTTP_TIMEOUT = new CustomException("HTTP_TIMEOUT", "Request timed out.");
21
+ public static final CustomException URL_MISSING = new CustomException(null, "url must be provided.");
22
+ public static final CustomException SIGNATURE_VERIFICATION_FAILED = new CustomException(
23
+ "SIGNATURE_VERIFICATION_FAILED",
24
+ "Signature verification failed."
25
+ );
26
+ public static final CustomException PUBLIC_KEY_INVALID = new CustomException("PUBLIC_KEY_INVALID", "Invalid public key.");
27
+ public static final CustomException SIGNATURE_MISSING = new CustomException(
28
+ "SIGNATURE_MISSING",
29
+ "Bundle does not contain a signature."
30
+ );
31
+ public static final CustomException SYNC_IN_PROGRESS = new CustomException("SYNC_IN_PROGRESS", "Sync is already in progress.");
32
+ public static final CustomException UNKNOWN_ERROR = new CustomException("UNKNOWN_ERROR", "An unknown error has occurred.");
33
+ }
@@ -0,0 +1,13 @@
1
+ package io.capawesome.capacitorjs.plugins.liveupdate.interfaces;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.DownloadBundleProgressEvent;
5
+ import io.capawesome.capacitorjs.plugins.liveupdate.classes.events.NextBundleSetEvent;
6
+
7
+ public interface LiveUpdateEventEmitter {
8
+ void onDownloadBundleProgress(@NonNull DownloadBundleProgressEvent event);
9
+
10
+ void onNextBundleSet(@NonNull NextBundleSetEvent event);
11
+
12
+ void onReloaded();
13
+ }
@@ -1,26 +1,29 @@
1
1
  import Foundation
2
2
  import CryptoKit
3
- #if canImport(ZipArchive)
4
- import ZipArchive
5
- #else
6
- import SSZipArchive
7
- #endif
3
+ import ZIPFoundation
8
4
  import Capacitor
9
5
  import Alamofire
10
6
  import CommonCrypto
11
7
 
12
8
  // swiftlint:disable type_body_length
13
9
  @objc public class LiveUpdate: NSObject {
10
+ public static let tag = "LiveUpdate"
11
+ public static let version = "8.1.1"
12
+
13
+ private static var _sharedInstance: LiveUpdate?
14
+ private static let queue = DispatchQueue(label: "io.capawesome.liveupdate.singleton")
15
+
14
16
  private let autoUpdateIntervalMs: Int64 = 15 * 60 * 1000 // 15 minutes
17
+ private let bridge: LiveUpdateBridge
15
18
  private let bundlesDirectory = "NoCloud/ionic_built_snapshots" // DO NOT CHANGE! (See https://dub.sh/BLluidt)
16
19
  private let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
17
20
  private let config: LiveUpdateConfig
18
21
  private let defaultWebAssetDir = "public" // DO NOT CHANGE! (See https://dub.sh/Buvz4yj)
19
22
  private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE! (See https://dub.sh/ceDl0zT)
23
+ private let eventEmitter: LiveUpdateEventEmitter?
20
24
  private let httpClient: LiveUpdateHttpClient
21
25
  private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
22
26
  private let manifestFileName = "capawesome-live-update-manifest.json" // DO NOT CHANGE!
23
- private let plugin: LiveUpdatePlugin
24
27
  private let preferences: LiveUpdatePreferences
25
28
 
26
29
  private var rollbackDispatchWorkItem: DispatchWorkItem?
@@ -28,10 +31,11 @@ import CommonCrypto
28
31
  private var lastAutoUpdateCheckTimestamp: Int64 = 0
29
32
  private var syncInProgress = false
30
33
 
31
- init(config: LiveUpdateConfig, plugin: LiveUpdatePlugin) {
34
+ private init(bridge: LiveUpdateBridge, config: LiveUpdateConfig, eventEmitter: LiveUpdateEventEmitter?) {
35
+ self.bridge = bridge
32
36
  self.config = config
37
+ self.eventEmitter = eventEmitter
33
38
  self.httpClient = LiveUpdateHttpClient(config: config)
34
- self.plugin = plugin
35
39
  self.preferences = LiveUpdatePreferences()
36
40
  super.init()
37
41
 
@@ -43,6 +47,28 @@ import CommonCrypto
43
47
  startRollbackTimer()
44
48
  }
45
49
 
50
+ public static func getInstance(
51
+ bridge: LiveUpdateBridge,
52
+ config: LiveUpdateConfig,
53
+ eventEmitter: LiveUpdateEventEmitter?
54
+ ) -> LiveUpdate {
55
+ return queue.sync {
56
+ if _sharedInstance == nil {
57
+ _sharedInstance = LiveUpdate(bridge: bridge, config: config, eventEmitter: eventEmitter)
58
+ }
59
+ return _sharedInstance!
60
+ }
61
+ }
62
+
63
+ public static func getInstance() -> LiveUpdate {
64
+ return queue.sync {
65
+ guard let instance = _sharedInstance else {
66
+ fatalError("LiveUpdate not initialized. Call getInstance(bridge:config:eventEmitter:) first.")
67
+ }
68
+ return instance
69
+ }
70
+ }
71
+
46
72
  @objc public func clearBlockedBundles() {
47
73
  preferences.setBlockedBundleIds(nil)
48
74
  }
@@ -178,9 +204,9 @@ import CommonCrypto
178
204
  }
179
205
 
180
206
  @objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
181
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is ready.")
207
+ CAPLog.print("[", LiveUpdate.tag, "] ", "App is ready.")
182
208
  if config.readyTimeout <= 0 {
183
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Ready timeout is set to 0. Automatic rollback is disabled.")
209
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Ready timeout is set to 0. Automatic rollback is disabled.")
184
210
  }
185
211
  // Stop the rollback timer
186
212
  stopRollbackTimer()
@@ -268,7 +294,7 @@ import CommonCrypto
268
294
  // Fetch the latest bundle
269
295
  let fetchLatestBundleOptions = FetchLatestBundleOptions(channel: channel)
270
296
  guard let response = try await fetchLatestBundle(fetchLatestBundleOptions) else {
271
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "No update available.")
297
+ CAPLog.print("[", LiveUpdate.tag, "] ", "No update available.")
272
298
  return SyncResult(nextBundleId: nil)
273
299
  }
274
300
  let artifactType = response.artifactType
@@ -278,7 +304,7 @@ import CommonCrypto
278
304
  let downloadUrl = response.url
279
305
  // Check if the bundle is blocked
280
306
  if isBlockedBundleId(latestBundleId) {
281
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Bundle is blocked and will not be downloaded.")
307
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Bundle is blocked and will not be downloaded.")
282
308
  return SyncResult(nextBundleId: nil)
283
309
  }
284
310
  // Check if bundle already exists
@@ -323,7 +349,7 @@ import CommonCrypto
323
349
 
324
350
  private func addBundleOfTypeZip(bundleId: String, zipFile: URL) async throws {
325
351
  // Unzip the bundle
326
- let unzippedDirectory = self.unzipFile(zipFile: zipFile)
352
+ let unzippedDirectory = try self.unzipFile(zipFile: zipFile)
327
353
  // Add the bundle
328
354
  try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
329
355
  }
@@ -376,7 +402,7 @@ import CommonCrypto
376
402
  for fileToCopy in filesToCopy {
377
403
  let success = tryCopyCurrentBundleFile(fileToCopy: fileToCopy, toDirectory: toDirectory)
378
404
  if !success {
379
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to copy file: \(fileToCopy.href)")
405
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to copy file: \(fileToCopy.href)")
380
406
  // If the file could not be copied, add it to the list of missing items
381
407
  missingItems.append(fileToCopy)
382
408
  }
@@ -391,7 +417,7 @@ import CommonCrypto
391
417
  do {
392
418
  try FileManager.default.createDirectory(at: bundlesDirectoryUrl, withIntermediateDirectories: true, attributes: nil)
393
419
  } catch {
394
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to create bundles directory.")
420
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to create bundles directory.")
395
421
  }
396
422
  }
397
423
  }
@@ -423,7 +449,7 @@ import CommonCrypto
423
449
  do {
424
450
  try deleteBundleById(bundleId)
425
451
  } catch {
426
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
452
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
427
453
  }
428
454
  }
429
455
  }
@@ -436,7 +462,7 @@ import CommonCrypto
436
462
  let urlComponents = URLComponents(string: url)!
437
463
  let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
438
464
  if let error = result.error {
439
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to download file: \(error)")
465
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to download file: \(error)")
440
466
  if let urlError = error.underlyingError as? URLError {
441
467
  if urlError.code == .timedOut {
442
468
  throw urlError
@@ -558,14 +584,14 @@ import CommonCrypto
558
584
  parameters["deviceId"] = getDeviceId()
559
585
  parameters["osVersion"] = await UIDevice.current.systemVersion
560
586
  parameters["platform"] = "1"
561
- parameters["pluginVersion"] = LiveUpdatePlugin.version
587
+ parameters["pluginVersion"] = LiveUpdate.version
562
588
  var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(getAppId() ?? "")/bundles/latest")!
563
589
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
564
590
  let url = try urlComponents.asURL()
565
- CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
591
+ CAPLog.print("[", LiveUpdate.tag, "] Fetching latest bundle: ", url)
566
592
  let response = try await self.httpClient.request(url: url, type: GetLatestBundleResponse.self)
567
593
  if let data = response.data {
568
- CAPLog.print("[", LiveUpdatePlugin.tag, "] Latest bundle response: ", String(decoding: data, as: UTF8.self))
594
+ CAPLog.print("[", LiveUpdate.tag, "] Latest bundle response: ", String(decoding: data, as: UTF8.self))
569
595
  }
570
596
  if let error = response.error {
571
597
  if let urlError = error.underlyingError as? URLError {
@@ -640,12 +666,9 @@ import CommonCrypto
640
666
  return bundleId
641
667
  }
642
668
 
643
- /// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
669
+ /// - Returns: The path to the current bundle directory or `nil` if the bridge does not provide a path.
644
670
  private func getCurrentCapacitorServerPath() -> String? {
645
- guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
646
- return nil
647
- }
648
- return viewController.getServerBasePath()
671
+ return bridge.getServerBasePath()
649
672
  }
650
673
 
651
674
  private func getDeviceId() -> String {
@@ -724,14 +747,16 @@ import CommonCrypto
724
747
  }
725
748
 
726
749
  private func notifyDownloadBundleProgressListeners(_ event: DownloadBundleProgressEvent) {
727
- plugin.notifyDownloadBundleProgressListeners(event)
750
+ if let eventEmitter = eventEmitter {
751
+ eventEmitter.onDownloadBundleProgress(event)
752
+ }
728
753
  }
729
754
 
730
755
  private func performAutoUpdate() {
731
756
  // Check if enough time has passed since the last check
732
757
  let now = Int64(Date().timeIntervalSince1970 * 1000)
733
758
  if lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs {
734
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update skipped. Last check was less than 15 minutes ago.")
759
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Auto-update skipped. Last check was less than 15 minutes ago.")
735
760
  return
736
761
  }
737
762
 
@@ -741,12 +766,12 @@ import CommonCrypto
741
766
  // Run sync in background task
742
767
  Task {
743
768
  do {
744
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update started.")
769
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Auto-update started.")
745
770
  let options = SyncOptions(channel: nil)
746
771
  _ = try await sync(options)
747
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update completed successfully.")
772
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Auto-update completed successfully.")
748
773
  } catch {
749
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update failed: ", error.localizedDescription)
774
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Auto-update failed: ", error.localizedDescription)
750
775
  }
751
776
  }
752
777
  }
@@ -759,11 +784,11 @@ import CommonCrypto
759
784
  setPreviousBundleId(bundleId: currentBundleId)
760
785
  // Perform the rollback
761
786
  if let _ = currentBundleId {
762
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Rolling back to default bundle.")
787
+ CAPLog.print("[", LiveUpdate.tag, "] ", "App is not ready. Rolling back to default bundle.")
763
788
  setNextBundleById(nil)
764
789
  setCurrentBundleById(nil)
765
790
  } else {
766
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
791
+ CAPLog.print("[", LiveUpdate.tag, "] ", "App is not ready. Default bundle is already in use.")
767
792
  }
768
793
  }
769
794
 
@@ -789,7 +814,7 @@ import CommonCrypto
789
814
  }
790
815
  }
791
816
  } catch {
792
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to search index.html file: \(error.localizedDescription)")
817
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to search index.html file: \(error.localizedDescription)")
793
818
  }
794
819
  return nil
795
820
  }
@@ -801,10 +826,7 @@ import CommonCrypto
801
826
  }
802
827
 
803
828
  private func setCurrentCapacitorServerPath(path: String) {
804
- guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
805
- return
806
- }
807
- viewController.setServerBasePath(path: path)
829
+ bridge.setServerBasePath(path: path)
808
830
  // Notify listeners
809
831
  notifyReloadedListeners()
810
832
  }
@@ -819,12 +841,16 @@ import CommonCrypto
819
841
  }
820
842
 
821
843
  private func notifyNextBundleSetListeners(_ bundleId: String?) {
822
- let event = NextBundleSetEvent(bundleId: bundleId)
823
- plugin.notifyNextBundleSetListeners(event)
844
+ if let eventEmitter = eventEmitter {
845
+ let event = NextBundleSetEvent(bundleId: bundleId)
846
+ eventEmitter.onNextBundleSet(event)
847
+ }
824
848
  }
825
849
 
826
850
  private func notifyReloadedListeners() {
827
- plugin.notifyReloadedListeners()
851
+ if let eventEmitter = eventEmitter {
852
+ eventEmitter.onReloaded()
853
+ }
828
854
  }
829
855
 
830
856
  private func addBlockedBundleId(_ bundleId: String) {
@@ -852,7 +878,7 @@ import CommonCrypto
852
878
  let newBlockedIds = blockedList.joined(separator: ",")
853
879
  preferences.setBlockedBundleIds(newBlockedIds)
854
880
 
855
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Bundle blocked: ", bundleId)
881
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Bundle blocked: ", bundleId)
856
882
  }
857
883
 
858
884
  private func isBlockedBundleId(_ bundleId: String) -> Bool {
@@ -882,7 +908,7 @@ import CommonCrypto
882
908
 
883
909
  if lastVersionCode == nil || lastVersionName == nil || lastVersionCode != currentVersionCode || lastVersionName != currentVersionName {
884
910
  CAPLog.print(
885
- "[", LiveUpdatePlugin.tag, "] ",
911
+ "[", LiveUpdate.tag, "] ",
886
912
  "App version changed (last: \(lastVersionName ?? "nil")/\(lastVersionCode ?? "nil"), current: \(currentVersionName)/\(currentVersionCode)), resetting config."
887
913
  )
888
914
  resetConfig()
@@ -923,9 +949,10 @@ import CommonCrypto
923
949
  }
924
950
  }
925
951
 
926
- private func unzipFile(zipFile: URL) -> URL {
952
+ private func unzipFile(zipFile: URL) throws -> URL {
927
953
  let destinationDirectory = zipFile.deletingPathExtension()
928
- SSZipArchive.unzipFile(atPath: zipFile.path, toDestination: destinationDirectory.path)
954
+ try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true, attributes: nil)
955
+ try FileManager.default.unzipItem(at: zipFile, to: destinationDirectory)
929
956
  return destinationDirectory
930
957
  }
931
958
 
@@ -967,7 +994,7 @@ import CommonCrypto
967
994
  .replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
968
995
  .replacingOccurrences(of: "\n", with: "")
969
996
  guard let publicKeyData = Data(base64Encoded: publicKeyAsBase64) else {
970
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to decode public key.")
997
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to decode public key.")
971
998
  return false
972
999
  }
973
1000
  let publicKeyAttributes: [CFString: Any] = [
@@ -979,12 +1006,12 @@ import CommonCrypto
979
1006
  var secKeyCreateWithDataError: Unmanaged<CFError>?
980
1007
  guard let publicKey = SecKeyCreateWithData(publicKeyData as CFData, publicKeyAttributes as CFDictionary, &secKeyCreateWithDataError) else {
981
1008
  if let error = secKeyCreateWithDataError?.takeRetainedValue() {
982
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to create public key with error: \(error)")
1009
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to create public key with error: \(error)")
983
1010
  }
984
1011
  return false
985
1012
  }
986
1013
  guard let signatureData = Data(base64Encoded: signature) else {
987
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to decode signature.")
1014
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to decode signature.")
988
1015
  return false
989
1016
  }
990
1017
 
@@ -1014,7 +1041,7 @@ import CommonCrypto
1014
1041
  let signatureAlgorithm = SecKeyAlgorithm.rsaSignatureDigestPKCS1v15SHA256
1015
1042
  let verificationResult = SecKeyVerifySignature(publicKey, signatureAlgorithm, digest as CFData, signatureData as CFData, &secKeyVerifySignatureError)
1016
1043
  if let error = secKeyVerifySignatureError?.takeRetainedValue() {
1017
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to verify signature with error: \(error)")
1044
+ CAPLog.print("[", LiveUpdate.tag, "] ", "Failed to verify signature with error: \(error)")
1018
1045
  }
1019
1046
  return verificationResult
1020
1047
  }
@@ -8,4 +8,6 @@ public struct LiveUpdateConfig {
8
8
  var publicKey: String?
9
9
  var readyTimeout = 0
10
10
  var serverDomain = "api.cloud.capawesome.io"
11
+
12
+ public init() {}
11
13
  }
@@ -7,9 +7,8 @@ import Capacitor
7
7
  */
8
8
  @objc(LiveUpdatePlugin)
9
9
  public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
10
- public static let tag = "LiveUpdate"
11
- public static let version = "7.4.0"
12
- public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
10
+ public static let tag = LiveUpdate.tag
11
+ public static let version = LiveUpdate.version
13
12
 
14
13
  public let identifier = "LiveUpdatePlugin"
15
14
  public let jsName = "LiveUpdate"
@@ -49,8 +48,14 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
49
48
  private var implementation: LiveUpdate?
50
49
 
51
50
  override public func load() {
52
- self.config = liveUpdateConfig()
53
- self.implementation = LiveUpdate(config: config!, plugin: self)
51
+ guard let bridge = bridge else {
52
+ return
53
+ }
54
+ let config = liveUpdateConfig()
55
+ self.config = config
56
+ let bridgeWrapper = PluginLiveUpdateBridge(bridge: bridge)
57
+ let eventEmitter = PluginLiveUpdateEventEmitter(plugin: self)
58
+ self.implementation = LiveUpdate.getInstance(bridge: bridgeWrapper, config: config, eventEmitter: eventEmitter)
54
59
 
55
60
  // Notify implementation about load
56
61
  implementation?.handleLoad()
@@ -422,4 +427,46 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
422
427
  private func resolveCall(_ call: CAPPluginCall, _ result: JSObject) {
423
428
  call.resolve(result)
424
429
  }
430
+
431
+ private class PluginLiveUpdateBridge: NSObject, LiveUpdateBridge {
432
+ private weak var bridge: CAPBridgeProtocol?
433
+
434
+ init(bridge: CAPBridgeProtocol) {
435
+ self.bridge = bridge
436
+ }
437
+
438
+ func getServerBasePath() -> String? {
439
+ guard let viewController = bridge?.viewController as? CAPBridgeViewController else {
440
+ return nil
441
+ }
442
+ return viewController.getServerBasePath()
443
+ }
444
+
445
+ func setServerBasePath(path: String) {
446
+ guard let viewController = bridge?.viewController as? CAPBridgeViewController else {
447
+ return
448
+ }
449
+ viewController.setServerBasePath(path: path)
450
+ }
451
+ }
452
+
453
+ private class PluginLiveUpdateEventEmitter: NSObject, LiveUpdateEventEmitter {
454
+ weak var plugin: LiveUpdatePlugin?
455
+
456
+ init(plugin: LiveUpdatePlugin) {
457
+ self.plugin = plugin
458
+ }
459
+
460
+ func onDownloadBundleProgress(_ event: DownloadBundleProgressEvent) {
461
+ plugin?.notifyDownloadBundleProgressListeners(event)
462
+ }
463
+
464
+ func onNextBundleSet(_ event: NextBundleSetEvent) {
465
+ plugin?.notifyNextBundleSetListeners(event)
466
+ }
467
+
468
+ func onReloaded() {
469
+ plugin?.notifyReloadedListeners()
470
+ }
471
+ }
425
472
  }
@@ -1,6 +1,8 @@
1
1
  import Foundation
2
2
 
3
3
  public class LiveUpdatePreferences: NSObject {
4
+ private static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
5
+
4
6
  private let appIdKey = "appId" // DO NOT CHANGE
5
7
  private let blockedBundleIdsKey = "blockedBundleIds" // DO NOT CHANGE
6
8
  private let channelKey = "channel" // DO NOT CHANGE
@@ -97,6 +99,6 @@ public class LiveUpdatePreferences: NSObject {
97
99
  }
98
100
 
99
101
  private func applyPrefix(to key: String) -> String {
100
- return LiveUpdatePlugin.userDefaultsPrefix + "." + key
102
+ return LiveUpdatePreferences.userDefaultsPrefix + "." + key
101
103
  }
102
104
  }
@@ -0,0 +1,6 @@
1
+ import Foundation
2
+
3
+ @objc public protocol LiveUpdateBridge {
4
+ func getServerBasePath() -> String?
5
+ func setServerBasePath(path: String)
6
+ }
@@ -0,0 +1,8 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public protocol LiveUpdateEventEmitter {
5
+ func onDownloadBundleProgress(_ event: DownloadBundleProgressEvent)
6
+ func onNextBundleSet(_ event: NextBundleSetEvent)
7
+ func onReloaded()
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/capacitor-live-update",
3
- "version": "8.0.0",
3
+ "version": "8.1.0-dev.97c86c2c.1768835749",
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",