@capawesome/capacitor-live-update 5.0.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 +20 -0
- package/LICENSE +21 -0
- package/README.md +509 -0
- package/android/build.gradle +62 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +613 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +55 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +437 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +86 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +23 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DeleteBundleOptions.java +18 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetBundleOptions.java +18 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetChannelOptions.java +18 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetCustomIdOptions.java +18 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBundleResult.java +24 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBundlesResult.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetChannelResult.java +21 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetCustomIdResult.java +22 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetDeviceIdResult.java +22 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetVersionCodeResult.java +21 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetVersionNameResult.java +21 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/SyncResult.java +22 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/Callback.java +5 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/EmptyCallback.java +5 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/NonEmptyCallback.java +7 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/Result.java +7 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +785 -0
- package/dist/esm/definitions.d.ts +356 -0
- package/dist/esm/definitions.js +3 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +21 -0
- package/dist/esm/web.js +55 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +71 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +74 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/Classes/CustomError.swift +38 -0
- package/ios/Plugin/Classes/Options/DeleteBundleOptions.swift +14 -0
- package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +20 -0
- package/ios/Plugin/Classes/Options/SetBundleOptions.swift +14 -0
- package/ios/Plugin/Classes/Options/SetChannelOptions.swift +14 -0
- package/ios/Plugin/Classes/Options/SetCustomIdOptions.swift +14 -0
- package/ios/Plugin/Classes/Results/GetBundleResult.swift +16 -0
- package/ios/Plugin/Classes/Results/GetBundlesResult.swift +16 -0
- package/ios/Plugin/Classes/Results/GetChannelResult.swift +16 -0
- package/ios/Plugin/Classes/Results/GetCustomIdResult.swift +16 -0
- package/ios/Plugin/Classes/Results/GetDeviceIdResult.swift +16 -0
- package/ios/Plugin/Classes/Results/GetVersionCodeResult.swift +16 -0
- package/ios/Plugin/Classes/Results/GetVersionNameResult.swift +16 -0
- package/ios/Plugin/Classes/Results/SyncResult.swift +16 -0
- package/ios/Plugin/Info.plist +24 -0
- package/ios/Plugin/LiveUpdate.swift +512 -0
- package/ios/Plugin/LiveUpdateConfig.swift +7 -0
- package/ios/Plugin/LiveUpdatePlugin.h +10 -0
- package/ios/Plugin/LiveUpdatePlugin.m +23 -0
- package/ios/Plugin/LiveUpdatePlugin.swift +247 -0
- package/ios/Plugin/LiveUpdatePreferences.swift +60 -0
- package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +4 -0
- package/ios/Plugin/Protocols/Result.swift +6 -0
- package/package.json +92 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SSZipArchive
|
|
3
|
+
import Capacitor
|
|
4
|
+
import Alamofire
|
|
5
|
+
|
|
6
|
+
// swiftlint:disable type_body_length
|
|
7
|
+
@objc public class LiveUpdate: NSObject {
|
|
8
|
+
private let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
|
9
|
+
private let config: LiveUpdateConfig
|
|
10
|
+
private let defaultWebAssetDir = "public" // DO NOT CHANGE (See https://dub.sh/Buvz4yj)
|
|
11
|
+
private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE (See https://dub.sh/ceDl0zT)
|
|
12
|
+
private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
|
|
13
|
+
private let plugin: LiveUpdatePlugin
|
|
14
|
+
private let pluginDirectory = "NoCloud/ionic_built_snapshots" // DO NOT CHANGE (See https://dub.sh/BLluidt)
|
|
15
|
+
private let preferences: LiveUpdatePreferences
|
|
16
|
+
|
|
17
|
+
private var rollbackDispatchWorkItem: DispatchWorkItem?
|
|
18
|
+
|
|
19
|
+
init(config: LiveUpdateConfig, plugin: LiveUpdatePlugin) {
|
|
20
|
+
self.config = config
|
|
21
|
+
self.plugin = plugin
|
|
22
|
+
self.preferences = LiveUpdatePreferences()
|
|
23
|
+
super.init()
|
|
24
|
+
|
|
25
|
+
if config.enabled {
|
|
26
|
+
if wasUpdated() && config.resetOnUpdate {
|
|
27
|
+
reset()
|
|
28
|
+
} else {
|
|
29
|
+
startRollbackTimer()
|
|
30
|
+
}
|
|
31
|
+
saveCurrentBundleVersion()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@objc public func deleteBundle(_ options: DeleteBundleOptions, completion: @escaping (Error?) -> Void) {
|
|
36
|
+
let bundleId = options.getBundleId()
|
|
37
|
+
|
|
38
|
+
if !hasBundle(bundleId: bundleId) {
|
|
39
|
+
let error = CustomError.bundleNotFound
|
|
40
|
+
completion(error)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
do {
|
|
45
|
+
try deleteBundle(bundleId: bundleId)
|
|
46
|
+
completion(nil)
|
|
47
|
+
} catch {
|
|
48
|
+
completion(error)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@objc public func downloadBundle(_ options: DownloadBundleOptions, completion: @escaping (Error?) -> Void) {
|
|
53
|
+
let url = options.getUrl()
|
|
54
|
+
let bundleId = options.getBundleId()
|
|
55
|
+
|
|
56
|
+
// Check if the bundle already exists
|
|
57
|
+
if hasBundle(bundleId: bundleId) {
|
|
58
|
+
let error = CustomError.bundleAlreadyExists
|
|
59
|
+
completion(error)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Download the bundle
|
|
64
|
+
downloadBundle(bundleId: bundleId, url: url, completion: { error in
|
|
65
|
+
if let error = error {
|
|
66
|
+
completion(error)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
completion(nil)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@objc public func getBundle(completion: @escaping (Result?, Error?) -> Void) {
|
|
74
|
+
var bundleId = getCurrentBundleId()
|
|
75
|
+
if bundleId == defaultWebAssetDir {
|
|
76
|
+
bundleId = nil
|
|
77
|
+
}
|
|
78
|
+
let result = GetBundleResult(bundleId: bundleId)
|
|
79
|
+
completion(result, nil)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@objc public func getBundles(completion: @escaping (Result?, Error?) -> Void) {
|
|
83
|
+
let bundleIds = getBundleIds()
|
|
84
|
+
let result = GetBundlesResult(bundleIds: bundleIds)
|
|
85
|
+
completion(result, nil)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@objc public func getChannel(completion: @escaping (Result?, Error?) -> Void) {
|
|
89
|
+
let channel = preferences.getChannel()
|
|
90
|
+
let result = GetChannelResult(channel: channel)
|
|
91
|
+
completion(result, nil)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@objc public func getCustomId(completion: @escaping (Result?, Error?) -> Void) {
|
|
95
|
+
let customId = preferences.getCustomId()
|
|
96
|
+
|
|
97
|
+
let result = GetCustomIdResult(customId: customId)
|
|
98
|
+
completion(result, nil)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@objc public func getDeviceId(completion: @escaping (Result?, Error?) -> Void) {
|
|
102
|
+
let deviceId = getDeviceId()
|
|
103
|
+
let result = GetDeviceIdResult(deviceId: deviceId)
|
|
104
|
+
completion(result, nil)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@objc public func getVersionCode(completion: @escaping (Result?, Error?) -> Void) {
|
|
108
|
+
let versionCode = getVersionCode()
|
|
109
|
+
let result = GetVersionCodeResult(versionCode: versionCode)
|
|
110
|
+
completion(result, nil)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@objc public func getVersionName(completion: @escaping (Result?, Error?) -> Void) {
|
|
114
|
+
let versionName = getVersionName()
|
|
115
|
+
let result = GetVersionNameResult(versionName: versionName)
|
|
116
|
+
completion(result, nil)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@objc public func ready() {
|
|
120
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is ready.")
|
|
121
|
+
stopRollbackTimer()
|
|
122
|
+
if config.autoDeleteBundles {
|
|
123
|
+
deleteUnusedBundles()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@objc public func reload() {
|
|
128
|
+
var path: String?
|
|
129
|
+
if let currentPath = getCurrentCapacitorServerPath() {
|
|
130
|
+
path = currentPath
|
|
131
|
+
}
|
|
132
|
+
if let nextPath = getNextCapacitorServerPath() {
|
|
133
|
+
path = nextPath
|
|
134
|
+
}
|
|
135
|
+
if let path = path {
|
|
136
|
+
setCurrentCapacitorServerPath(path: path)
|
|
137
|
+
startRollbackTimer()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@objc public func reset() {
|
|
142
|
+
self.setNextCapacitorServerPathToDefaultWebAssetDir()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@objc public func setBundle(_ options: SetBundleOptions, completion: @escaping (Error?) -> Void) {
|
|
146
|
+
let bundleId = options.getBundleId()
|
|
147
|
+
|
|
148
|
+
// Check if the bundle already exists
|
|
149
|
+
if !hasBundle(bundleId: bundleId) {
|
|
150
|
+
let error = CustomError.bundleNotFound
|
|
151
|
+
completion(error)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setNextBundle(bundleId: bundleId)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@objc public func setChannel(_ options: SetChannelOptions, completion: @escaping (Error?) -> Void) {
|
|
159
|
+
let channel = options.getChannel()
|
|
160
|
+
|
|
161
|
+
preferences.setChannel(channel)
|
|
162
|
+
completion(nil)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@objc public func setCustomId(_ options: SetCustomIdOptions, completion: @escaping (Error?) -> Void) {
|
|
166
|
+
let customId = options.getCustomId()
|
|
167
|
+
|
|
168
|
+
preferences.setCustomId(customId)
|
|
169
|
+
completion(nil)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@objc public func sync(completion: @escaping (SyncResult?, Error?) -> Void) {
|
|
173
|
+
// Fetch the latest bundle
|
|
174
|
+
fetchLatestBundle(completion: { response, error in
|
|
175
|
+
if let _ = error {
|
|
176
|
+
// No update available
|
|
177
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "No update available.")
|
|
178
|
+
let result = SyncResult(nextBundleId: nil)
|
|
179
|
+
completion(result, nil)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if let response = response {
|
|
183
|
+
let latestBundleId = response.bundleId
|
|
184
|
+
// Check if the bundle already exists
|
|
185
|
+
if self.hasBundle(bundleId: latestBundleId) {
|
|
186
|
+
var nextBundleId: String?
|
|
187
|
+
let currentBundleId = self.getCurrentBundleId()
|
|
188
|
+
if latestBundleId != currentBundleId {
|
|
189
|
+
// Set the next bundle
|
|
190
|
+
self.setNextBundle(bundleId: latestBundleId)
|
|
191
|
+
nextBundleId = latestBundleId
|
|
192
|
+
}
|
|
193
|
+
let result = SyncResult(nextBundleId: nextBundleId)
|
|
194
|
+
completion(result, nil)
|
|
195
|
+
} else {
|
|
196
|
+
// Download and unzip the bundle
|
|
197
|
+
self.downloadBundle(bundleId: latestBundleId, url: response.url, completion: { error in
|
|
198
|
+
if let error = error {
|
|
199
|
+
completion(nil, error)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
// Set the next bundle
|
|
203
|
+
self.setNextBundle(bundleId: latestBundleId)
|
|
204
|
+
let result = SyncResult(nextBundleId: latestBundleId)
|
|
205
|
+
completion(result, nil)
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private func addBundle(bundleId: String, zipFile: URL, completion: @escaping (Error?) -> Void) {
|
|
213
|
+
// Unzip the bundle
|
|
214
|
+
self.unzipFile(zipFile: zipFile, completion: { url in
|
|
215
|
+
// Search folder with index.html file
|
|
216
|
+
guard let indexHtmlFile = self.searchIndexHtmlFile(url: url) else {
|
|
217
|
+
completion(CustomError.bundleIndexHtmlMissing)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
// Move the unzipped files to the bundles directory
|
|
221
|
+
let bundleDirectoryUrl = self.libraryDirectoryUrl.appendingPathComponent(self.pluginDirectory).appendingPathComponent(bundleId)
|
|
222
|
+
do {
|
|
223
|
+
try FileManager.default.moveItem(atPath: indexHtmlFile.deletingLastPathComponent().path, toPath: bundleDirectoryUrl.path)
|
|
224
|
+
completion(nil)
|
|
225
|
+
} catch {
|
|
226
|
+
completion(error)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private func buildCapacitorServerPathFor(bundleId: String) -> String {
|
|
233
|
+
let path: String
|
|
234
|
+
if bundleId == "public" {
|
|
235
|
+
path = Bundle.main.bundleURL.appendingPathComponent(bundleId).path
|
|
236
|
+
} else {
|
|
237
|
+
path = buildBundlePathFor(bundleId: bundleId)
|
|
238
|
+
}
|
|
239
|
+
return path
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private func buildBundlePathFor(bundleId: String) -> String {
|
|
243
|
+
let url = libraryDirectoryUrl.appendingPathComponent(pluginDirectory).appendingPathComponent(bundleId)
|
|
244
|
+
return url.path
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private func deleteBundle(bundleId: String) throws {
|
|
248
|
+
let bundleDirectoryUrl = libraryDirectoryUrl.appendingPathComponent(pluginDirectory).appendingPathComponent(bundleId)
|
|
249
|
+
try FileManager.default.removeItem(atPath: bundleDirectoryUrl.path)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private func deleteUnusedBundles() {
|
|
253
|
+
let bundleIds = getBundleIds()
|
|
254
|
+
let currentBundleId = getCurrentBundleId()
|
|
255
|
+
let nextBundleId = getNextBundleId()
|
|
256
|
+
|
|
257
|
+
for bundleId in bundleIds {
|
|
258
|
+
if bundleId != currentBundleId && bundleId != nextBundleId {
|
|
259
|
+
do {
|
|
260
|
+
try deleteBundle(bundleId: bundleId)
|
|
261
|
+
} catch {
|
|
262
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private func downloadBundle(bundleId: String, url: String, completion: @escaping (Error?) -> Void) {
|
|
269
|
+
// Download the bundle
|
|
270
|
+
downloadFile(url: url, completion: { url, error in
|
|
271
|
+
if let error = error {
|
|
272
|
+
completion(error)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
if let url = url {
|
|
276
|
+
// Add the bundle
|
|
277
|
+
self.addBundle(bundleId: bundleId, zipFile: url, completion: { error in
|
|
278
|
+
// Delete the temporary file
|
|
279
|
+
do {
|
|
280
|
+
try FileManager.default.removeItem(atPath: url.path)
|
|
281
|
+
} catch {
|
|
282
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete temporary file.")
|
|
283
|
+
}
|
|
284
|
+
if let error = error {
|
|
285
|
+
completion(error)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
completion(nil)
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private func downloadFile(url: String, completion: @escaping (URL?, Error?) -> Void) {
|
|
295
|
+
let destination: DownloadRequest.Destination = { _, _ in
|
|
296
|
+
let timestamp = String(Int(Date().timeIntervalSince1970))
|
|
297
|
+
let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
|
|
298
|
+
return (temporaryZipFileUrl, [.createIntermediateDirectories])
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
AF.download(url, to: destination).validate().response { response in
|
|
302
|
+
if let error = response.error {
|
|
303
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", error)
|
|
304
|
+
completion(nil, CustomError.downloadFailed)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
completion(response.fileURL, nil)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private func fetchLatestBundle(completion: @escaping (GetLatestBundleResponse?, Error?) -> Void) {
|
|
312
|
+
var parameters = [String: String]()
|
|
313
|
+
parameters["appVersionCode"] = getVersionCode()
|
|
314
|
+
parameters["appVersionName"] = getVersionName()
|
|
315
|
+
parameters["bundleId"] = getCurrentBundleId()
|
|
316
|
+
parameters["channelName"] = preferences.getChannel()
|
|
317
|
+
parameters["customId"] = preferences.getCustomId()
|
|
318
|
+
parameters["deviceId"] = getDeviceId()
|
|
319
|
+
parameters["osVersion"] = UIDevice.current.systemVersion
|
|
320
|
+
parameters["platform"] = "1"
|
|
321
|
+
parameters["pluginVersion"] = LiveUpdatePlugin.version
|
|
322
|
+
let url = URL(string: "https://api.cloud.capawesome.io/v1/apps/\(config.appId ?? "")/bundles/latest")!
|
|
323
|
+
AF.request(url, method: .get, parameters: parameters).validate().responseDecodable(of: GetLatestBundleResponse.self) { response in
|
|
324
|
+
if let error = response.error {
|
|
325
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", error)
|
|
326
|
+
completion(nil, error)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
completion(response.value, nil)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private func getVersionCode() -> String {
|
|
334
|
+
guard let appVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
|
335
|
+
return ""
|
|
336
|
+
}
|
|
337
|
+
return appVersionCode
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private func getVersionName() -> String {
|
|
341
|
+
guard let appVersionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
|
|
342
|
+
return ""
|
|
343
|
+
}
|
|
344
|
+
return appVersionName
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private func getBundleIds() -> [String] {
|
|
348
|
+
let url = libraryDirectoryUrl.appendingPathComponent(pluginDirectory)
|
|
349
|
+
do {
|
|
350
|
+
let pathExists = FileManager.default.fileExists(atPath: url.path)
|
|
351
|
+
var files: [String] = []
|
|
352
|
+
if pathExists {
|
|
353
|
+
files = try FileManager.default.contentsOfDirectory(atPath: url.path)
|
|
354
|
+
}
|
|
355
|
+
return files
|
|
356
|
+
} catch {
|
|
357
|
+
return []
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private func getCurrentBundleId() -> String? {
|
|
362
|
+
guard let path = getCurrentCapacitorServerPath() else {
|
|
363
|
+
return nil
|
|
364
|
+
}
|
|
365
|
+
return URL(fileURLWithPath: path).lastPathComponent
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private func getCurrentCapacitorServerPath() -> String? {
|
|
369
|
+
guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
|
|
370
|
+
return nil
|
|
371
|
+
}
|
|
372
|
+
return viewController.getServerBasePath()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private func getDeviceId() -> String {
|
|
376
|
+
if let deviceId = preferences.getDeviceIdForApp(appId: config.appId) {
|
|
377
|
+
return deviceId
|
|
378
|
+
}
|
|
379
|
+
let deviceId = UUID().uuidString
|
|
380
|
+
preferences.setDeviceIdForApp(appId: config.appId, deviceId)
|
|
381
|
+
return deviceId
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private func getNextBundleId() -> String? {
|
|
385
|
+
guard let path = getNextCapacitorServerPath() else {
|
|
386
|
+
return nil
|
|
387
|
+
}
|
|
388
|
+
return URL(fileURLWithPath: path).lastPathComponent
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private func getNextCapacitorServerPath() -> String? {
|
|
392
|
+
if let path = UserDefaults.standard.string(forKey: self.defaultServerPathKey) {
|
|
393
|
+
return path.isEmpty ? nil : path
|
|
394
|
+
}
|
|
395
|
+
return nil
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private func hasBundle(bundleId: String) -> Bool {
|
|
399
|
+
let url = libraryDirectoryUrl.appendingPathComponent(pluginDirectory).appendingPathComponent(bundleId)
|
|
400
|
+
return FileManager.default.fileExists(atPath: url.path)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private func rollback() {
|
|
404
|
+
if getCurrentBundleId() == defaultWebAssetDir {
|
|
405
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Rolling back to default bundle.")
|
|
409
|
+
setNextCapacitorServerPathToDefaultWebAssetDir()
|
|
410
|
+
setCurrentCapacitorServerPathToDefaultWebAssetDir()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private func saveCurrentBundleVersion() {
|
|
414
|
+
guard let currentBundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
preferences.setLastBundleVersion(currentBundleVersion)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private func searchIndexHtmlFile(url: URL) -> URL? {
|
|
421
|
+
let enumerator = FileManager.default.enumerator(atPath: url.path)
|
|
422
|
+
while let element = enumerator?.nextObject() as? String {
|
|
423
|
+
if element.hasSuffix("index.html") {
|
|
424
|
+
return url.appendingPathComponent(element)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return nil
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private func setCurrentCapacitorServerPath(path: String) {
|
|
431
|
+
guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
viewController.setServerBasePath(path: path)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private func setCurrentCapacitorServerPathToDefaultWebAssetDir() {
|
|
438
|
+
let path = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
|
|
439
|
+
setCurrentCapacitorServerPath(path: path)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private func setNextBundle(bundleId: String) {
|
|
443
|
+
let path = buildCapacitorServerPathFor(bundleId: bundleId)
|
|
444
|
+
setNextCapacitorServerPath(path: path)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private func setNextCapacitorServerPath(path: String) {
|
|
448
|
+
if path.hasSuffix("/public") {
|
|
449
|
+
// Reset the custom server base path
|
|
450
|
+
UserDefaults.standard.set("", forKey: self.defaultServerPathKey)
|
|
451
|
+
} else {
|
|
452
|
+
// Attention: Only the lastPathComponent is used (see https://dub.sh/BLluidt)
|
|
453
|
+
UserDefaults.standard.set(path, forKey: self.defaultServerPathKey)
|
|
454
|
+
}
|
|
455
|
+
UserDefaults.standard.synchronize()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private func setNextCapacitorServerPathToDefaultWebAssetDir() {
|
|
459
|
+
let path = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
|
|
460
|
+
setNextCapacitorServerPath(path: path)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private func startRollbackTimer() {
|
|
464
|
+
stopRollbackTimer()
|
|
465
|
+
rollbackDispatchWorkItem = DispatchWorkItem { [weak self] in
|
|
466
|
+
self?.rollback()
|
|
467
|
+
}
|
|
468
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + DispatchTimeInterval.milliseconds(config.readyTimeout), execute: rollbackDispatchWorkItem!)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private func stopRollbackTimer() {
|
|
472
|
+
rollbackDispatchWorkItem?.cancel()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private func unzipFile(zipFile: URL, completion: @escaping (URL) -> Void) {
|
|
476
|
+
let destinationDirectory = zipFile.deletingPathExtension()
|
|
477
|
+
DispatchQueue.background(background: {
|
|
478
|
+
SSZipArchive.unzipFile(atPath: zipFile.path, toDestination: destinationDirectory.path)
|
|
479
|
+
}, completion: {
|
|
480
|
+
completion(destinationDirectory)
|
|
481
|
+
})
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private func wasUpdated() -> Bool {
|
|
485
|
+
guard let lastBundleVersionString = preferences.getLastBundleVersion() else {
|
|
486
|
+
return false
|
|
487
|
+
}
|
|
488
|
+
guard let lastBundleVersion = Int(lastBundleVersionString) else {
|
|
489
|
+
return false
|
|
490
|
+
}
|
|
491
|
+
guard let currentBundleVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
|
492
|
+
return false
|
|
493
|
+
}
|
|
494
|
+
guard let currentBundleVersion = Int(currentBundleVersionString) else {
|
|
495
|
+
return false
|
|
496
|
+
}
|
|
497
|
+
return currentBundleVersion > lastBundleVersion
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
extension DispatchQueue {
|
|
502
|
+
static func background(background: (() -> Void)? = nil, completion: (() -> Void)? = nil) {
|
|
503
|
+
DispatchQueue.global(qos: .background).async {
|
|
504
|
+
background?()
|
|
505
|
+
if let completion = completion {
|
|
506
|
+
DispatchQueue.main.async {
|
|
507
|
+
completion()
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
//! Project version number for Plugin.
|
|
4
|
+
FOUNDATION_EXPORT double PluginVersionNumber;
|
|
5
|
+
|
|
6
|
+
//! Project version string for Plugin.
|
|
7
|
+
FOUNDATION_EXPORT const unsigned char PluginVersionString[];
|
|
8
|
+
|
|
9
|
+
// In this header, you should import all the public headers of your framework using statements like #import <Plugin/PublicHeader.h>
|
|
10
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <Capacitor/Capacitor.h>
|
|
3
|
+
|
|
4
|
+
// Define the plugin using the CAP_PLUGIN Macro, and
|
|
5
|
+
// each method the plugin supports using the CAP_PLUGIN_METHOD macro.
|
|
6
|
+
CAP_PLUGIN(LiveUpdatePlugin, "LiveUpdate",
|
|
7
|
+
CAP_PLUGIN_METHOD(deleteBundle, CAPPluginReturnPromise);
|
|
8
|
+
CAP_PLUGIN_METHOD(downloadBundle, CAPPluginReturnPromise);
|
|
9
|
+
CAP_PLUGIN_METHOD(getBundle, CAPPluginReturnPromise);
|
|
10
|
+
CAP_PLUGIN_METHOD(getBundles, CAPPluginReturnPromise);
|
|
11
|
+
CAP_PLUGIN_METHOD(getChannel, CAPPluginReturnPromise);
|
|
12
|
+
CAP_PLUGIN_METHOD(getDeviceId, CAPPluginReturnPromise);
|
|
13
|
+
CAP_PLUGIN_METHOD(getCustomId, CAPPluginReturnPromise);
|
|
14
|
+
CAP_PLUGIN_METHOD(getVersionCode, CAPPluginReturnPromise);
|
|
15
|
+
CAP_PLUGIN_METHOD(getVersionName, CAPPluginReturnPromise);
|
|
16
|
+
CAP_PLUGIN_METHOD(ready, CAPPluginReturnPromise);
|
|
17
|
+
CAP_PLUGIN_METHOD(reload, CAPPluginReturnPromise);
|
|
18
|
+
CAP_PLUGIN_METHOD(reset, CAPPluginReturnPromise);
|
|
19
|
+
CAP_PLUGIN_METHOD(setBundle, CAPPluginReturnPromise);
|
|
20
|
+
CAP_PLUGIN_METHOD(setChannel, CAPPluginReturnPromise);
|
|
21
|
+
CAP_PLUGIN_METHOD(setCustomId, CAPPluginReturnPromise);
|
|
22
|
+
CAP_PLUGIN_METHOD(sync, CAPPluginReturnPromise);
|
|
23
|
+
)
|