@capawesome/capacitor-live-update 8.6.0 → 8.7.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 +15 -0
- package/Package.swift +21 -5
- package/README.md +35 -2
- package/android/build.gradle +19 -0
- package/android/consumer-rules.pro +5 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +123 -53
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +31 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +11 -5
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchLatestBundleOptions.java +23 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +30 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicManager.kt +229 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicPreferences.kt +85 -0
- package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +7 -6
- package/ios/Plugin/Classes/Options/FetchLatestBundleOptions.swift +19 -3
- package/ios/Plugin/Classes/Results/FetchLatestBundleResult.swift +24 -0
- package/ios/Plugin/Enums/CustomError.swift +13 -0
- package/ios/Plugin/LiveUpdate.swift +66 -39
- package/ios/Plugin/LiveUpdatePlugin.swift +2 -2
- package/ios/Plugin/Providers/Ionic/LiveUpdateIonicManager.swift +187 -0
- package/ios/Plugin/Providers/Ionic/LiveUpdateIonicPreferences.swift +81 -0
- package/ios/Plugin/Providers/Ionic/LiveUpdatePlugin+LiveUpdateProvider.swift +15 -0
- package/package.json +2 -1
|
@@ -6,17 +6,39 @@ import com.getcapacitor.PluginCall;
|
|
|
6
6
|
|
|
7
7
|
public class FetchLatestBundleOptions {
|
|
8
8
|
|
|
9
|
+
@Nullable
|
|
10
|
+
private final String appId;
|
|
11
|
+
|
|
12
|
+
@Nullable
|
|
13
|
+
private final String bundleId;
|
|
14
|
+
|
|
9
15
|
@Nullable
|
|
10
16
|
private final String channel;
|
|
11
17
|
|
|
12
18
|
public FetchLatestBundleOptions(@NonNull PluginCall call) {
|
|
13
|
-
this
|
|
19
|
+
this(null, null, call.getString("channel", null));
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
public FetchLatestBundleOptions(@Nullable String channel) {
|
|
23
|
+
this(null, null, channel);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public FetchLatestBundleOptions(@Nullable String appId, @Nullable String bundleId, @Nullable String channel) {
|
|
27
|
+
this.appId = appId;
|
|
28
|
+
this.bundleId = bundleId;
|
|
17
29
|
this.channel = channel;
|
|
18
30
|
}
|
|
19
31
|
|
|
32
|
+
@Nullable
|
|
33
|
+
public String getAppId() {
|
|
34
|
+
return appId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Nullable
|
|
38
|
+
public String getBundleId() {
|
|
39
|
+
return bundleId;
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
@Nullable
|
|
21
43
|
public String getChannel() {
|
|
22
44
|
return channel;
|
|
@@ -48,6 +48,36 @@ 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
|
+
|
|
51
81
|
@NonNull
|
|
52
82
|
public JSObject toJSObject() {
|
|
53
83
|
JSObject result = new JSObject();
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
package io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.getcapacitor.Logger
|
|
5
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdate
|
|
6
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdateConfig
|
|
7
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdatePlugin
|
|
8
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.DownloadBundleOptions
|
|
9
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.options.FetchLatestBundleOptions
|
|
10
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.classes.results.FetchLatestBundleResult
|
|
11
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.enums.ArtifactType
|
|
12
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback
|
|
13
|
+
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback
|
|
14
|
+
import io.ionic.liveupdateprovider.MetadataSyncResult
|
|
15
|
+
import io.ionic.liveupdateprovider.ProviderError
|
|
16
|
+
import io.ionic.liveupdateprovider.ProviderManager
|
|
17
|
+
import io.ionic.liveupdateprovider.ProviderSyncResult
|
|
18
|
+
import java.io.File
|
|
19
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
20
|
+
import kotlin.coroutines.resume
|
|
21
|
+
import kotlin.coroutines.resumeWithException
|
|
22
|
+
import kotlin.coroutines.suspendCoroutine
|
|
23
|
+
import kotlinx.coroutines.CoroutineScope
|
|
24
|
+
import kotlinx.coroutines.Deferred
|
|
25
|
+
import kotlinx.coroutines.SupervisorJob
|
|
26
|
+
import kotlinx.coroutines.async
|
|
27
|
+
import org.json.JSONObject
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Ionic Live Update Provider manager backed by the Capawesome live update plugin.
|
|
31
|
+
*
|
|
32
|
+
* Each manager persists its active bundle scoped by its resolved app ID and channel, so that
|
|
33
|
+
* multiple shells (Portals or Federated Capacitor apps) do not collide with each other or with
|
|
34
|
+
* the standalone plugin's `currentBundleId` / `nextBundleId` state.
|
|
35
|
+
*
|
|
36
|
+
* Provider configuration keys (V1):
|
|
37
|
+
* - `appId` (optional) — Capawesome Cloud app UUID; falls back to plugin config, required in either
|
|
38
|
+
* - `channel` (optional) — channel to sync; falls back to plugin config
|
|
39
|
+
* - `publicKey`, `serverDomain`, `httpTimeout` (optional) — same as the plugin config; only
|
|
40
|
+
* applied by the headless constructor, otherwise the running plugin's config is used
|
|
41
|
+
*/
|
|
42
|
+
class LiveUpdateIonicManager
|
|
43
|
+
@Throws(ProviderError.InvalidConfiguration::class)
|
|
44
|
+
constructor(context: Context, configuration: Map<String, *>, private val liveUpdate: LiveUpdate) : ProviderManager {
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a manager without a running Capacitor plugin, e.g. for Ionic Portals hosts
|
|
48
|
+
* that construct the manager directly and attach it to a Portal via
|
|
49
|
+
* `setLiveUpdateProviderManager(...)`. Uses a headless [LiveUpdate] instance configured
|
|
50
|
+
* via the configuration map.
|
|
51
|
+
*/
|
|
52
|
+
@Throws(ProviderError.InvalidConfiguration::class)
|
|
53
|
+
constructor(
|
|
54
|
+
context: Context,
|
|
55
|
+
configuration: Map<String, *>
|
|
56
|
+
) : this(context, configuration, LiveUpdate(createLiveUpdateConfig(configuration), context.applicationContext))
|
|
57
|
+
|
|
58
|
+
companion object {
|
|
59
|
+
/**
|
|
60
|
+
* Downloads in flight per bundle ID, shared by all managers, so that concurrent syncs
|
|
61
|
+
* download the same bundle only once.
|
|
62
|
+
*/
|
|
63
|
+
private val bundleDownloads = ConcurrentHashMap<String, Deferred<Unit>>()
|
|
64
|
+
private val bundleDownloadScope = CoroutineScope(SupervisorJob())
|
|
65
|
+
|
|
66
|
+
@Throws(ProviderError.InvalidConfiguration::class)
|
|
67
|
+
private fun createLiveUpdateConfig(configuration: Map<String, *>): LiveUpdateConfig {
|
|
68
|
+
val config = LiveUpdateConfig()
|
|
69
|
+
getOptionalValue<String>(configuration, "publicKey")?.let { config.publicKey = it }
|
|
70
|
+
getOptionalValue<String>(configuration, "serverDomain")?.let { config.serverDomain = it }
|
|
71
|
+
getOptionalValue<Number>(configuration, "httpTimeout")?.let { config.httpTimeout = it.toInt() }
|
|
72
|
+
return config
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Throws(ProviderError.InvalidConfiguration::class)
|
|
76
|
+
private inline fun <reified T> getOptionalValue(configuration: Map<String, *>, key: String): T? {
|
|
77
|
+
val value = configuration[key] ?: return null
|
|
78
|
+
return value as? T ?: throw ProviderError.InvalidConfiguration("$key has an invalid type.")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override var latestAppDirectory: File? = null
|
|
83
|
+
private set
|
|
84
|
+
|
|
85
|
+
private val appId: String
|
|
86
|
+
private val channel: String?
|
|
87
|
+
private val preferences: LiveUpdateIonicPreferences
|
|
88
|
+
private val versionCode: String = liveUpdate.versionCodeAsString
|
|
89
|
+
private var syncedBundleId: String? = null
|
|
90
|
+
|
|
91
|
+
init {
|
|
92
|
+
val resolvedAppId = configuration["appId"] as? String ?: liveUpdate.appId
|
|
93
|
+
if (resolvedAppId.isNullOrEmpty()) {
|
|
94
|
+
throw ProviderError.InvalidConfiguration(LiveUpdatePlugin.ERROR_APP_ID_MISSING)
|
|
95
|
+
}
|
|
96
|
+
appId = resolvedAppId
|
|
97
|
+
channel = configuration["channel"] as? String ?: liveUpdate.channel
|
|
98
|
+
val stateKey = if (channel.isNullOrEmpty()) appId else "$appId.$channel"
|
|
99
|
+
val sharedPreferences =
|
|
100
|
+
context.applicationContext.getSharedPreferences(LiveUpdateIonicPreferences.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
101
|
+
preferences = LiveUpdateIonicPreferences(sharedPreferences, stateKey)
|
|
102
|
+
preferences.resetLastSyncedBundleIdIfVersionChanged(versionCode)
|
|
103
|
+
restoreSyncedBundle()
|
|
104
|
+
deleteSupersededBundles()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
override suspend fun sync(): ProviderSyncResult? {
|
|
108
|
+
try {
|
|
109
|
+
val result = fetchLatestBundle()
|
|
110
|
+
|
|
111
|
+
// No update available; nothing to report.
|
|
112
|
+
val bundleId = result.bundleId
|
|
113
|
+
if (bundleId == null || bundleId == syncedBundleId) {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Use the bundle on disk, or download it (or wait for a running download of the same bundle).
|
|
118
|
+
val directory = liveUpdate.getBundleDirectory(bundleId) ?: run {
|
|
119
|
+
downloadBundleOnce(bundleId, result)
|
|
120
|
+
liveUpdate.getBundleDirectory(bundleId) ?: throw Exception(LiveUpdatePlugin.ERROR_BUNDLE_DIRECTORY_NOT_FOUND)
|
|
121
|
+
}
|
|
122
|
+
applySyncedBundle(bundleId, directory)
|
|
123
|
+
return MetadataSyncResult(buildMetadata(bundleId, result.customProperties))
|
|
124
|
+
} catch (exception: Exception) {
|
|
125
|
+
Logger.error(LiveUpdatePlugin.TAG, "Ionic provider sync failed: ${exception.message}", exception)
|
|
126
|
+
throw exception
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private fun applySyncedBundle(bundleId: String, directory: File) {
|
|
131
|
+
syncedBundleId = bundleId
|
|
132
|
+
latestAppDirectory = directory
|
|
133
|
+
preferences.setLastSyncedBundleId(bundleId, versionCode)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private fun buildMetadata(bundleId: String, customProperties: JSONObject?): Map<String, Any> {
|
|
137
|
+
val metadata = mutableMapOf<String, Any>("bundleId" to bundleId)
|
|
138
|
+
channel?.let { metadata["channel"] = it }
|
|
139
|
+
customProperties?.let {
|
|
140
|
+
val customPropertiesMap = jsonObjectToMap(it)
|
|
141
|
+
if (customPropertiesMap.isNotEmpty()) {
|
|
142
|
+
metadata["customProperties"] = customPropertiesMap
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return metadata
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Deletes the bundles this manager no longer points at, unless another manager or the
|
|
150
|
+
* plugin still uses them. Never runs right after a sync, because a visible portal may
|
|
151
|
+
* still serve the previous bundle.
|
|
152
|
+
*/
|
|
153
|
+
private fun deleteSupersededBundles() {
|
|
154
|
+
val supersededBundleIds = preferences.getSupersededBundleIds()
|
|
155
|
+
if (supersededBundleIds.isEmpty()) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
val referencedBundleIds = preferences.getLastSyncedBundleIdsOfAllManagers()
|
|
159
|
+
val deletedBundleIds = supersededBundleIds
|
|
160
|
+
.filter { it !in referencedBundleIds && liveUpdate.tryDeleteUnusedBundle(it) }
|
|
161
|
+
.toSet()
|
|
162
|
+
preferences.removeSupersededBundleIds(deletedBundleIds)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private suspend fun downloadBundle(bundleId: String, result: FetchLatestBundleResult) {
|
|
166
|
+
val downloadUrl = result.downloadUrl
|
|
167
|
+
if (downloadUrl.isNullOrEmpty()) {
|
|
168
|
+
throw Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_URL_MISSING)
|
|
169
|
+
}
|
|
170
|
+
val artifactType = result.artifactType ?: ArtifactType.ZIP
|
|
171
|
+
val options = DownloadBundleOptions(artifactType, bundleId, result.checksum, result.signature, downloadUrl)
|
|
172
|
+
suspendCoroutine { continuation ->
|
|
173
|
+
liveUpdate.downloadBundle(
|
|
174
|
+
options,
|
|
175
|
+
object : EmptyCallback {
|
|
176
|
+
override fun success() = continuation.resume(Unit)
|
|
177
|
+
|
|
178
|
+
override fun error(exception: Exception) = continuation.resumeWithException(exception)
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private suspend fun downloadBundleOnce(bundleId: String, result: FetchLatestBundleResult) {
|
|
185
|
+
val download = bundleDownloads.computeIfAbsent(bundleId) {
|
|
186
|
+
bundleDownloadScope.async { downloadBundle(bundleId, result) }
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
download.await()
|
|
190
|
+
} finally {
|
|
191
|
+
bundleDownloads.remove(bundleId, download)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private suspend fun fetchLatestBundle(): FetchLatestBundleResult = suspendCoroutine { continuation ->
|
|
196
|
+
liveUpdate.fetchLatestBundle(
|
|
197
|
+
FetchLatestBundleOptions(appId, syncedBundleId, channel),
|
|
198
|
+
object : NonEmptyCallback<FetchLatestBundleResult> {
|
|
199
|
+
override fun success(result: FetchLatestBundleResult) = continuation.resume(result)
|
|
200
|
+
|
|
201
|
+
override fun error(exception: Exception) = continuation.resumeWithException(exception)
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private fun jsonObjectToMap(jsonObject: JSONObject): Map<String, Any> {
|
|
207
|
+
val map = mutableMapOf<String, Any>()
|
|
208
|
+
val keys = jsonObject.keys()
|
|
209
|
+
while (keys.hasNext()) {
|
|
210
|
+
val key = keys.next()
|
|
211
|
+
val value = jsonObject.opt(key)
|
|
212
|
+
if (value != null && value != JSONObject.NULL) {
|
|
213
|
+
map[key] = value
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return map
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Restores the last synced bundle, if it is still on disk, so the host can load it offline
|
|
221
|
+
* and the next sync reports it as the bundle in use.
|
|
222
|
+
*/
|
|
223
|
+
private fun restoreSyncedBundle() {
|
|
224
|
+
val bundleId = preferences.getLastSyncedBundleId() ?: return
|
|
225
|
+
val directory = liveUpdate.getBundleDirectory(bundleId) ?: return
|
|
226
|
+
syncedBundleId = bundleId
|
|
227
|
+
latestAppDirectory = directory
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic
|
|
2
|
+
|
|
3
|
+
import android.content.SharedPreferences
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Persisted state of one provider manager, scoped by its state key (`<appId>` or `<appId>.<channel>`).
|
|
7
|
+
*
|
|
8
|
+
* All managers share the same [SharedPreferences] file, so a manager can see which bundles
|
|
9
|
+
* the other managers still point at.
|
|
10
|
+
*/
|
|
11
|
+
class LiveUpdateIonicPreferences(private val sharedPreferences: SharedPreferences, stateKey: String) {
|
|
12
|
+
|
|
13
|
+
companion object {
|
|
14
|
+
const val SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdateIonicProvider"
|
|
15
|
+
private const val LAST_SYNCED_BUNDLE_ID_PREFIX = "lastSyncedBundleId."
|
|
16
|
+
private const val LAST_VERSION_CODE_PREFIX = "lastVersionCode."
|
|
17
|
+
private const val SUPERSEDED_BUNDLE_IDS_PREFIX = "supersededBundleIds."
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private val lastSyncedBundleIdKey = LAST_SYNCED_BUNDLE_ID_PREFIX + stateKey
|
|
21
|
+
private val lastVersionCodeKey = LAST_VERSION_CODE_PREFIX + stateKey
|
|
22
|
+
private val supersededBundleIdsKey = SUPERSEDED_BUNDLE_IDS_PREFIX + stateKey
|
|
23
|
+
|
|
24
|
+
fun getLastSyncedBundleId(): String? = sharedPreferences.getString(lastSyncedBundleIdKey, null)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @return The last synced bundle IDs of all managers.
|
|
28
|
+
*/
|
|
29
|
+
fun getLastSyncedBundleIdsOfAllManagers(): Set<String> = sharedPreferences.all
|
|
30
|
+
.filterKeys { it.startsWith(LAST_SYNCED_BUNDLE_ID_PREFIX) }
|
|
31
|
+
.values
|
|
32
|
+
.filterIsInstance<String>()
|
|
33
|
+
.toSet()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @return The bundles this manager no longer points at and that may be deleted once unused.
|
|
37
|
+
*/
|
|
38
|
+
fun getSupersededBundleIds(): Set<String> = sharedPreferences.getStringSet(supersededBundleIdsKey, null)?.toSet() ?: emptySet()
|
|
39
|
+
|
|
40
|
+
fun removeSupersededBundleIds(bundleIds: Set<String>) {
|
|
41
|
+
if (bundleIds.isEmpty()) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
val remainingBundleIds = getSupersededBundleIds() - bundleIds
|
|
45
|
+
val editor = sharedPreferences.edit()
|
|
46
|
+
if (remainingBundleIds.isEmpty()) {
|
|
47
|
+
editor.remove(supersededBundleIdsKey)
|
|
48
|
+
} else {
|
|
49
|
+
editor.putStringSet(supersededBundleIdsKey, remainingBundleIds)
|
|
50
|
+
}
|
|
51
|
+
editor.apply()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Forgets the last synced bundle if it was synced on another app version code,
|
|
56
|
+
* because the bundle was built for that native version.
|
|
57
|
+
*/
|
|
58
|
+
fun resetLastSyncedBundleIdIfVersionChanged(versionCode: String) {
|
|
59
|
+
val lastSyncedBundleId = getLastSyncedBundleId() ?: return
|
|
60
|
+
val lastVersionCode = sharedPreferences.getString(lastVersionCodeKey, null) ?: return
|
|
61
|
+
if (lastVersionCode == versionCode) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
sharedPreferences.edit()
|
|
65
|
+
.putStringSet(supersededBundleIdsKey, getSupersededBundleIds() + lastSyncedBundleId)
|
|
66
|
+
.remove(lastSyncedBundleIdKey)
|
|
67
|
+
.remove(lastVersionCodeKey)
|
|
68
|
+
.apply()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Points this manager at the given bundle, synced on the given app version code,
|
|
73
|
+
* and records the previous bundle as superseded.
|
|
74
|
+
*/
|
|
75
|
+
fun setLastSyncedBundleId(bundleId: String, versionCode: String) {
|
|
76
|
+
val editor = sharedPreferences.edit()
|
|
77
|
+
val previousBundleId = getLastSyncedBundleId()
|
|
78
|
+
if (previousBundleId != null && previousBundleId != bundleId) {
|
|
79
|
+
editor.putStringSet(supersededBundleIdsKey, getSupersededBundleIds() + previousBundleId)
|
|
80
|
+
}
|
|
81
|
+
editor.putString(lastSyncedBundleIdKey, bundleId)
|
|
82
|
+
.putString(lastVersionCodeKey, versionCode)
|
|
83
|
+
.apply()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -8,12 +8,13 @@ import Capacitor
|
|
|
8
8
|
private var signature: String?
|
|
9
9
|
private var url: String
|
|
10
10
|
|
|
11
|
-
init(artifactType: String, bundleId: String, checksum: String?, signature: String?, url: String) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
convenience init(artifactType: String, bundleId: String, checksum: String?, signature: String?, url: String) {
|
|
12
|
+
let artifactType: ArtifactType = artifactType == "manifest" ? .manifest : .zip
|
|
13
|
+
self.init(artifactType: artifactType, bundleId: bundleId, checksum: checksum, signature: signature, url: url)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
init(artifactType: ArtifactType, bundleId: String, checksum: String?, signature: String?, url: String) {
|
|
17
|
+
self.artifactType = artifactType
|
|
17
18
|
self.bundleId = bundleId
|
|
18
19
|
self.checksum = checksum
|
|
19
20
|
self.signature = signature
|
|
@@ -2,16 +2,32 @@ import Foundation
|
|
|
2
2
|
import Capacitor
|
|
3
3
|
|
|
4
4
|
@objc public class FetchLatestBundleOptions: NSObject {
|
|
5
|
+
private var appId: String?
|
|
6
|
+
private var bundleId: String?
|
|
5
7
|
private var channel: String?
|
|
6
8
|
|
|
7
|
-
init(_ call: CAPPluginCall) {
|
|
8
|
-
self.channel
|
|
9
|
+
convenience init(_ call: CAPPluginCall) {
|
|
10
|
+
self.init(appId: nil, bundleId: nil, channel: call.getString("channel"))
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
init(channel: String?) {
|
|
13
|
+
convenience init(channel: String?) {
|
|
14
|
+
self.init(appId: nil, bundleId: nil, channel: channel)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
init(appId: String?, bundleId: String?, channel: String?) {
|
|
18
|
+
self.appId = appId
|
|
19
|
+
self.bundleId = bundleId
|
|
12
20
|
self.channel = channel
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
func getAppId() -> String? {
|
|
24
|
+
return appId
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func getBundleId() -> String? {
|
|
28
|
+
return bundleId
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
func getChannel() -> String? {
|
|
16
32
|
return channel
|
|
17
33
|
}
|
|
@@ -20,6 +20,30 @@ import Capacitor
|
|
|
20
20
|
self.signature = signature
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
func getArtifactType() -> ArtifactType? {
|
|
24
|
+
return artifactType
|
|
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
|
+
|
|
23
47
|
public func toJSObject() -> AnyObject {
|
|
24
48
|
var result = JSObject()
|
|
25
49
|
if artifactType == .manifest {
|
|
@@ -3,6 +3,7 @@ import Foundation
|
|
|
3
3
|
public enum CustomError: Error {
|
|
4
4
|
case appIdMissing
|
|
5
5
|
case bundleAlreadyExists
|
|
6
|
+
case bundleDirectoryNotFound
|
|
6
7
|
case bundleIdMissing
|
|
7
8
|
case bundleIndexHtmlMissing
|
|
8
9
|
case bundleNotFound
|
|
@@ -10,9 +11,12 @@ public enum CustomError: Error {
|
|
|
10
11
|
case channelMissing
|
|
11
12
|
case checksumCalculationFailed
|
|
12
13
|
case checksumMismatch
|
|
14
|
+
case configurationValueInvalid(key: String)
|
|
13
15
|
case customIdMissing
|
|
14
16
|
case downloadFailed
|
|
17
|
+
case downloadUrlMissing
|
|
15
18
|
case httpTimeout
|
|
19
|
+
case pluginNotInitialized
|
|
16
20
|
case signatureMissing
|
|
17
21
|
case signatureVerificationFailed
|
|
18
22
|
case syncInProgress
|
|
@@ -27,6 +31,8 @@ extension CustomError: LocalizedError {
|
|
|
27
31
|
return NSLocalizedString("appId must be configured.", comment: "appIdMissing")
|
|
28
32
|
case .bundleAlreadyExists:
|
|
29
33
|
return NSLocalizedString("bundle already exists.", comment: "bundleAlreadyExists")
|
|
34
|
+
case .bundleDirectoryNotFound:
|
|
35
|
+
return NSLocalizedString("Bundle directory could not be resolved.", comment: "bundleDirectoryNotFound")
|
|
30
36
|
case .bundleIdMissing:
|
|
31
37
|
return NSLocalizedString("bundleId must be provided.", comment: "bundleIdMissing")
|
|
32
38
|
case .bundleIndexHtmlMissing:
|
|
@@ -41,12 +47,19 @@ extension CustomError: LocalizedError {
|
|
|
41
47
|
return NSLocalizedString("Failed to calculate checksum.", comment: "checksumCalculationFailed")
|
|
42
48
|
case .checksumMismatch:
|
|
43
49
|
return NSLocalizedString("Checksum mismatch.", comment: "checksumMismatch")
|
|
50
|
+
case .configurationValueInvalid(let key):
|
|
51
|
+
let format = NSLocalizedString("%@ has an invalid type.", comment: "configurationValueInvalid")
|
|
52
|
+
return String(format: format, key)
|
|
44
53
|
case .customIdMissing:
|
|
45
54
|
return NSLocalizedString("customId must be provided.", comment: "customIdMissing")
|
|
46
55
|
case .downloadFailed:
|
|
47
56
|
return NSLocalizedString("Bundle could not be downloaded.", comment: "downloadFailed")
|
|
57
|
+
case .downloadUrlMissing:
|
|
58
|
+
return NSLocalizedString("Bundle does not have a valid download URL.", comment: "downloadUrlMissing")
|
|
48
59
|
case .httpTimeout:
|
|
49
60
|
return NSLocalizedString("Request timed out.", comment: "httpTimeout")
|
|
61
|
+
case .pluginNotInitialized:
|
|
62
|
+
return NSLocalizedString("Plugin is not initialized.", comment: "pluginNotInitialized")
|
|
50
63
|
case .signatureMissing:
|
|
51
64
|
return NSLocalizedString("Bundle does not contain a signature.", comment: "signatureMissing")
|
|
52
65
|
case .signatureVerificationFailed:
|