@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.
- package/CapawesomeCapacitorLiveUpdate.podspec +0 -15
- package/Package.swift +5 -21
- package/README.md +6 -11
- package/android/build.gradle +1 -17
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +26 -71
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +3 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +8 -38
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchLatestBundleOptions.java +0 -15
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +0 -30
- package/ios/Plugin/Classes/Options/FetchLatestBundleOptions.swift +0 -12
- package/ios/Plugin/Classes/Results/FetchLatestBundleResult.swift +0 -24
- package/ios/Plugin/Enums/CustomError.swift +0 -12
- package/ios/Plugin/LiveUpdate.swift +10 -23
- package/ios/Plugin/LiveUpdateHttpClient.swift +6 -1
- package/ios/Plugin/LiveUpdatePlugin.swift +1 -1
- package/package.json +4 -4
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicManager.kt +0 -169
- package/ios/Plugin/Providers/Ionic/LiveUpdateIonicManager.swift +0 -108
- package/ios/Plugin/Providers/Ionic/LiveUpdatePlugin+LiveUpdateProvider.swift +0 -15
|
@@ -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:
|
|
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/
|
|
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
|
-
- `$
|
|
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/
|
|
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/
|
|
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
|
|
package/android/build.gradle
CHANGED
|
@@ -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
|
-
|
|
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
|
-
@
|
|
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(
|
|
137
|
-
this.webViewSettingsEditor =
|
|
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(
|
|
554
|
+
return new File(plugin.getContext().getFilesDir(), bundlesDirectory);
|
|
583
555
|
}
|
|
584
556
|
|
|
585
557
|
private File buildBundleDirectoryFor(@NonNull String bundleId) {
|
|
586
|
-
return new File(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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()
|
|
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 =
|
|
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.
|
|
1505
|
+
String packageName = this.plugin.getContext().getPackageName();
|
|
1551
1506
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1552
|
-
return this.
|
|
1507
|
+
return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
|
|
1553
1508
|
} else {
|
|
1554
|
-
return this.
|
|
1509
|
+
return this.plugin.getContext().getPackageManager().getPackageInfo(packageName, 0);
|
|
1555
1510
|
}
|
|
1556
1511
|
}
|
|
1557
1512
|
}
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java
CHANGED
|
@@ -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) {
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java
CHANGED
|
@@ -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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/\(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
863
|
+
plugin.notifyNextBundleSetListeners(event)
|
|
877
864
|
}
|
|
878
865
|
|
|
879
866
|
private func notifyReloadedListeners() {
|
|
880
|
-
plugin
|
|
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
|
-
|
|
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
|
+
"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.
|
|
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.
|
|
84
|
+
"prettier-plugin-java": "2.9.7",
|
|
85
85
|
"rimraf": "6.1.2",
|
|
86
|
-
"rollup": "4.
|
|
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
|