@capgo/capacitor-updater 5.0.0-alpha.0 → 7.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.
Files changed (38) hide show
  1. package/CapgoCapacitorUpdater.podspec +1 -1
  2. package/LICENCE +373 -661
  3. package/README.md +339 -75
  4. package/android/build.gradle +13 -12
  5. package/android/src/main/AndroidManifest.xml +4 -2
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +205 -121
  7. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +32 -24
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +1041 -441
  9. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1217 -536
  10. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +153 -0
  11. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +62 -0
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +14 -0
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +126 -0
  14. package/dist/docs.json +727 -171
  15. package/dist/esm/definitions.d.ts +234 -45
  16. package/dist/esm/definitions.js +5 -0
  17. package/dist/esm/definitions.js.map +1 -1
  18. package/dist/esm/index.d.ts +2 -2
  19. package/dist/esm/index.js +9 -4
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/web.d.ts +12 -6
  22. package/dist/esm/web.js +64 -20
  23. package/dist/esm/web.js.map +1 -1
  24. package/dist/plugin.cjs.js +70 -23
  25. package/dist/plugin.cjs.js.map +1 -1
  26. package/dist/plugin.js +70 -23
  27. package/dist/plugin.js.map +1 -1
  28. package/ios/Plugin/BundleInfo.swift +38 -19
  29. package/ios/Plugin/BundleStatus.swift +11 -4
  30. package/ios/Plugin/CapacitorUpdater.swift +520 -192
  31. package/ios/Plugin/CapacitorUpdaterPlugin.m +8 -1
  32. package/ios/Plugin/CapacitorUpdaterPlugin.swift +447 -190
  33. package/ios/Plugin/CryptoCipher.swift +240 -0
  34. package/ios/Plugin/DelayCondition.swift +74 -0
  35. package/ios/Plugin/DelayUntilNext.swift +30 -0
  36. package/ios/Plugin/UserDefaultsExtension.swift +48 -0
  37. package/package.json +26 -20
  38. package/ios/Plugin/ObjectPreferences.swift +0 -97
@@ -1,3 +1,9 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
1
7
  import Foundation
2
8
  import Capacitor
3
9
  import Version
@@ -9,11 +15,15 @@ import Version
9
15
  @objc(CapacitorUpdaterPlugin)
10
16
  public class CapacitorUpdaterPlugin: CAPPlugin {
11
17
  private var implementation = CapacitorUpdater()
12
- static let autoUpdateUrlDefault = "https://xvwzpoazmxkqosrdewyv.functions.supabase.co/updates"
13
- static let statsUrlDefault = "https://xvwzpoazmxkqosrdewyv.functions.supabase.co/stats"
14
- static let DELAY_UPDATE = "delayUpdate"
15
- private var autoUpdateUrl = ""
18
+ private let PLUGIN_VERSION: String = "7.0.0"
19
+ static let updateUrlDefault = "https://api.capgo.app/updates"
20
+ static let statsUrlDefault = "https://api.capgo.app/stats"
21
+ static let channelUrlDefault = "https://api.capgo.app/channel_self"
22
+ let DELAY_CONDITION_PREFERENCES = ""
23
+ private var updateUrl = ""
16
24
  private var statsUrl = ""
25
+ private var defaultPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA4pW9olT0FBXXivRCzd3xcImlWZrqkwcF2xTkX/FwXmj9eh9H\nkBLrsQmfsC+PJisRXIOGq6a0z3bsGq6jBpp3/Jr9jiaW5VuPGaKeMaZZBRvi/N5f\nIMG3hZXSOcy0IYg+E1Q7RkYO1xq5GLHseqG+PXvJsNe4R8R/Bmd/ngq0xh/cvcrH\nHpXwO0Aj9tfprlb+rHaVV79EkVRWYPidOLnK1n0EFHFJ1d/MyDIp10TEGm2xHpf/\nBrlb1an8wXEuzoC0DgYaczgTjovwR+ewSGhSHJliQdM0Qa3o1iN87DldWtydImMs\nPjJ3DUwpsjAMRe5X8Et4+udFW2ciYnQo9H0CkwIDAQABAoIBAQCtjlMV/4qBxAU4\nu0ZcWA9yywwraX0aJ3v1xrfzQYV322Wk4Ea5dbSxA5UcqCE29DA1M824t1Wxv/6z\npWbcTP9xLuresnJMtmgTE7umfiubvTONy2sENT20hgDkIwcq1CfwOEm61zjQzPhQ\nkSB5AmEsyR/BZEsUNc+ygR6AWOUFB7tj4yMc32LOTWSbE/znnF2BkmlmnQykomG1\n2oVqM3lUFP7+m8ux1O7scO6IMts+Z/eFXjWfxpbebUSvSIR83GXPQZ34S/c0ehOg\nyHdmCSOel1r3VvInMe+30j54Jr+Ml/7Ee6axiwyE2e/bd85MsK9sVdp0OtelXaqA\nOZZqWvN5AoGBAP2Hn3lSq+a8GsDH726mHJw60xM0LPbVJTYbXsmQkg1tl3NKJTMM\nQqz41+5uys+phEgLHI9gVJ0r+HaGHXnJ4zewlFjsudstb/0nfctUvTqnhEhfNo9I\ny4kufVKPRF3sMEeo7CDVJs4GNBLycEyIBy6Mbv0VcO7VaZqggRwu4no9AoGBAOTK\n6NWYs1BWlkua2wmxexGOzehNGedInp0wGr2l4FDayWjkZLqvB+nNXUQ63NdHlSs4\nWB2Z1kQXZxVaI2tPYexGUKXEo2uFob63uflbuE029ovDXIIPFTPtGNdNXwhHT5a+\nPhmy3sMc+s2BSNM5qaNmfxQxhdd6gRU6oikE+c0PAoGAMn3cKNFqIt27hkFLUgIL\nGKIuf1iYy9/PNWNmEUaVj88PpopRtkTu0nwMpROzmH/uNFriKTvKHjMvnItBO4wV\nkHW+VadvrFL0Rrqituf9d7z8/1zXBNo+juePVe3qc7oiM2NVA4Tv4YAixtM5wkQl\nCgQ15nlqsGYYTg9BJ1e/CxECgYEAjEYPzO2reuUrjr0p8F59ev1YJ0YmTJRMk0ks\nC/yIdGo/tGzbiU3JB0LfHPcN8Xu07GPGOpfYM7U5gXDbaG6qNgfCaHAQVdr/mQPi\nJQ1kCQtay8QCkscWk9iZM1//lP7LwDtxraXqSCwbZSYP9VlUNZeg8EuQqNU2EUL6\nqzWexmcCgYEA0prUGNBacraTYEknB1CsbP36UPWsqFWOvevlz+uEC5JPxPuW5ZHh\nSQN7xl6+PHyjPBM7ttwPKyhgLOVTb3K7ex/PXnudojMUK5fh7vYfChVTSlx2p6r0\nDi58PdD+node08cJH+ie0Yphp7m+D4+R9XD0v0nEvnu4BtAW6DrJasw=\n-----END RSA PRIVATE KEY-----\n"
26
+ private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
17
27
  private var currentVersionNative: Version = "0.0.0"
18
28
  private var autoUpdate = false
19
29
  private var appReadyTimeout = 10000
@@ -21,34 +31,41 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
21
31
  private var resetWhenUpdate = true
22
32
  private var autoDeleteFailed = false
23
33
  private var autoDeletePrevious = false
24
-
34
+ private var backgroundWork: DispatchWorkItem?
35
+ private var taskRunning = false
36
+
25
37
  override public func load() {
38
+ print("\(self.implementation.TAG) init for device \(self.implementation.deviceID)")
26
39
  do {
27
- currentVersionNative = try Version(Bundle.main.buildVersionNumber ?? "0.0.0")
40
+ currentVersionNative = try Version(getConfig().getString("version", Bundle.main.versionName ?? "0.0.0")!)
28
41
  } catch {
29
42
  print("\(self.implementation.TAG) Cannot get version native \(currentVersionNative)")
30
43
  }
31
- autoDeleteFailed = getConfigValue("autoDeleteFailed") as? Bool ?? false
32
- autoDeletePrevious = getConfigValue("autoDeletePrevious") as? Bool ?? false
33
- autoUpdateUrl = getConfigValue("autoUpdateUrl") as? String ?? CapacitorUpdaterPlugin.autoUpdateUrlDefault
34
- autoUpdate = getConfigValue("autoUpdate") as? Bool ?? false
35
- appReadyTimeout = getConfigValue("appReadyTimeout") as? Int ?? 10000
36
- resetWhenUpdate = getConfigValue("resetWhenUpdate") as? Bool ?? true
37
-
38
- implementation.appId = Bundle.main.bundleIdentifier ?? ""
44
+ print("\(self.implementation.TAG) version native \(self.currentVersionNative.description)")
45
+ implementation.versionName = getConfig().getString("version", Bundle.main.versionName)!
46
+ autoDeleteFailed = getConfig().getBoolean("autoDeleteFailed", true)
47
+ autoDeletePrevious = getConfig().getBoolean("autoDeletePrevious", true)
48
+ updateUrl = getConfig().getString("updateUrl", CapacitorUpdaterPlugin.updateUrlDefault)!
49
+ autoUpdate = getConfig().getBoolean("autoUpdate", true)
50
+ appReadyTimeout = getConfig().getInt("appReadyTimeout", 10000)
51
+ resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
52
+
53
+ implementation.privateKey = getConfig().getString("privateKey", self.defaultPrivateKey)!
39
54
  implementation.notifyDownload = notifyDownload
55
+ implementation.PLUGIN_VERSION = self.PLUGIN_VERSION
40
56
  let config = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor().legacyConfig
41
- if (config?["appId"] != nil) {
57
+ if config?["appId"] != nil {
42
58
  implementation.appId = config?["appId"] as! String
43
59
  }
44
- implementation.statsUrl = getConfigValue("statsUrl") as? String ?? CapacitorUpdaterPlugin.statsUrlDefault
45
-
46
- if (resetWhenUpdate) {
60
+ implementation.statsUrl = getConfig().getString("statsUrl", CapacitorUpdaterPlugin.statsUrlDefault)!
61
+ implementation.channelUrl = getConfig().getString("channelUrl", CapacitorUpdaterPlugin.channelUrlDefault)!
62
+ if resetWhenUpdate {
47
63
  self.cleanupObsoleteVersions()
48
64
  }
49
65
  let nc = NotificationCenter.default
50
66
  nc.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
51
67
  nc.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
68
+ nc.addObserver(self, selector: #selector(appKilled), name: UIApplication.willTerminateNotification, object: nil)
52
69
  self.appMovedToForeground()
53
70
  }
54
71
 
@@ -59,14 +76,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
59
76
  } catch {
60
77
  print("\(self.implementation.TAG) Cannot get version native \(currentVersionNative)")
61
78
  }
62
- if (LatestVersionNative != "0.0.0" && currentVersionNative.major > LatestVersionNative.major) {
63
- _ = self._reset(toAutoUpdate: false)
64
- UserDefaults.standard.set("", forKey: "LatestVersionAutoUpdate")
65
- UserDefaults.standard.set("", forKey: "LatestVersionNameAutoUpdate")
79
+ if LatestVersionNative != "0.0.0" && self.currentVersionNative.description != LatestVersionNative.description {
80
+ _ = self._reset(toLastSuccessful: false)
66
81
  let res = implementation.list()
67
82
  res.forEach { version in
68
83
  print("\(self.implementation.TAG) Deleting obsolete bundle: \(version)")
69
- _ = implementation.delete(id: version.getId())
84
+ let res = implementation.delete(id: version.getId())
85
+ if !res {
86
+ print("\(self.implementation.TAG) Delete failed, id \(version.getId()) doesn't exist")
87
+ }
70
88
  }
71
89
  }
72
90
  UserDefaults.standard.set( self.currentVersionNative.description, forKey: "LatestVersionNative")
@@ -76,19 +94,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
76
94
  @objc func notifyDownload(id: String, percent: Int) {
77
95
  let bundle = self.implementation.getBundleInfo(id: id)
78
96
  self.notifyListeners("download", data: ["percent": percent, "bundle": bundle.toJSON()])
79
- if (percent == 100) {
97
+ if percent == 100 {
80
98
  self.notifyListeners("downloadComplete", data: ["bundle": bundle.toJSON()])
99
+ self.implementation.sendStats(action: "download_complete", versionName: bundle.getVersionName())
100
+ } else if percent.isMultiple(of: 10) {
101
+ self.implementation.sendStats(action: "download_\(percent)", versionName: bundle.getVersionName())
81
102
  }
82
103
  }
83
104
 
84
- @objc func getId(_ call: CAPPluginCall) {
85
- call.resolve(["id": implementation.deviceID])
105
+ @objc func getDeviceId(_ call: CAPPluginCall) {
106
+ call.resolve(["deviceId": implementation.deviceID])
86
107
  }
87
108
 
88
109
  @objc func getPluginVersion(_ call: CAPPluginCall) {
89
- call.resolve(["version": implementation.pluginVersion])
110
+ call.resolve(["version": self.PLUGIN_VERSION])
90
111
  }
91
-
112
+
92
113
  @objc func download(_ call: CAPPluginCall) {
93
114
  guard let urlString = call.getString("url") else {
94
115
  print("\(self.implementation.TAG) Download called without url")
@@ -100,13 +121,32 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
100
121
  call.reject("Download called without version")
101
122
  return
102
123
  }
124
+ let sessionKey = call.getString("sessionKey", "")
125
+ let checksum = call.getString("checksum", "")
103
126
  let url = URL(string: urlString)
104
- print("\(self.implementation.TAG) Downloading \(url!)")
105
- do {
106
- let res = try implementation.download(url: url!, version: version)
107
- call.resolve(res.toJSON())
108
- } catch {
109
- call.reject("download failed", error.localizedDescription)
127
+ print("\(self.implementation.TAG) Downloading \(String(describing: url))")
128
+ DispatchQueue.global(qos: .background).async {
129
+ do {
130
+ let next = try self.implementation.download(url: url!, version: version, sessionKey: sessionKey)
131
+ if checksum != "" && next.getChecksum() != checksum {
132
+ print("\(self.implementation.TAG) Error checksum", next.getChecksum(), checksum)
133
+ self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
134
+ let id = next.getId()
135
+ let resDel = self.implementation.delete(id: id)
136
+ if !resDel {
137
+ print("\(self.implementation.TAG) Delete failed, id \(id) doesn't exist")
138
+ }
139
+ throw ObjectSavableError.checksum
140
+ }
141
+ self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
142
+ call.resolve(next.toJSON())
143
+ } catch {
144
+ print("\(self.implementation.TAG) Failed to download from: \(String(describing: url)) \(error.localizedDescription)")
145
+ self.notifyListeners("downloadFailed", data: ["version": version])
146
+ let current: BundleInfo = self.implementation.getCurrentBundle()
147
+ self.implementation.sendStats(action: "download_fail", versionName: current.getVersionName())
148
+ call.reject("Failed to download from: \(url!)", error.localizedDescription)
149
+ }
110
150
  }
111
151
  }
112
152
 
@@ -118,17 +158,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
118
158
  if let vc = bridge.viewController as? CAPBridgeViewController {
119
159
  vc.setServerBasePath(path: destHot.path)
120
160
  self.checkAppReady()
161
+ self.notifyListeners("appReloaded", data: [:])
121
162
  return true
122
163
  }
123
164
  return false
124
165
  }
125
-
166
+
126
167
  @objc func reload(_ call: CAPPluginCall) {
127
- if (self._reload()) {
168
+ if self._reload() {
128
169
  call.resolve()
129
170
  } else {
130
- call.reject("Reload failed")
131
171
  print("\(self.implementation.TAG) Reload failed")
172
+ call.reject("Reload failed")
132
173
  }
133
174
  }
134
175
 
@@ -138,15 +179,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
138
179
  call.reject("Next called without id")
139
180
  return
140
181
  }
141
-
142
182
  print("\(self.implementation.TAG) Setting next active id \(id)")
143
- if (!self.implementation.setNextVersion(next: id)) {
183
+ if !self.implementation.setNextBundle(next: id) {
184
+ print("\(self.implementation.TAG) Set next version failed. id \(id) does not exist.")
144
185
  call.reject("Set next version failed. id \(id) does not exist.")
145
186
  } else {
146
187
  call.resolve(self.implementation.getBundleInfo(id: id).toJSON())
147
188
  }
148
189
  }
149
-
190
+
150
191
  @objc func set(_ call: CAPPluginCall) {
151
192
  guard let id = call.getString("id") else {
152
193
  print("\(self.implementation.TAG) Set called without id")
@@ -155,7 +196,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
155
196
  }
156
197
  let res = implementation.set(id: id)
157
198
  print("\(self.implementation.TAG) Set active bundle: \(id)")
158
- if (!res) {
199
+ if !res {
159
200
  print("\(self.implementation.TAG) Bundle successfully set to: \(id) ")
160
201
  call.reject("Update failed, id \(id) doesn't exist")
161
202
  } else {
@@ -170,9 +211,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
170
211
  return
171
212
  }
172
213
  let res = implementation.delete(id: id)
173
- if (res) {
214
+ if res {
174
215
  call.resolve()
175
216
  } else {
217
+ print("\(self.implementation.TAG) Delete failed, id \(id) doesn't exist")
176
218
  call.reject("Delete failed, id \(id) doesn't exist")
177
219
  }
178
220
  }
@@ -184,68 +226,210 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
184
226
  resArr.append(v.toJSON())
185
227
  }
186
228
  call.resolve([
187
- "versions": resArr
229
+ "bundles": resArr
188
230
  ])
189
231
  }
190
232
 
191
- @objc func _reset(toAutoUpdate: Bool) -> Bool {
192
- guard let bridge = self.bridge else { return false }
193
- if let vc = bridge.viewController as? CAPBridgeViewController {
194
- self.implementation.reset()
195
-
196
- let LatestVersionAutoUpdate = UserDefaults.standard.string(forKey: "LatestVersionAutoUpdate") ?? ""
197
- let LatestVersionNameAutoUpdate = UserDefaults.standard.string(forKey: "LatestVersionNameAutoUpdate") ?? ""
198
- if(toAutoUpdate && LatestVersionAutoUpdate != "" && LatestVersionNameAutoUpdate != "") {
199
- let res = implementation.set(id: LatestVersionNameAutoUpdate)
200
- return res && self._reload()
233
+ @objc func getLatest(_ call: CAPPluginCall) {
234
+ DispatchQueue.global(qos: .background).async {
235
+ let res = self.implementation.getLatest(url: URL(string: self.updateUrl)!)
236
+ if res.error != nil {
237
+ call.reject( res.error!)
238
+ } else {
239
+ call.resolve(res.toDict())
201
240
  }
202
- implementation.reset()
203
- vc.setServerBasePath(path: "")
204
- DispatchQueue.main.async {
205
- vc.loadView()
206
- vc.viewDidLoad()
207
- print("\(self.implementation.TAG) Reset to builtin version")
241
+ }
242
+ }
243
+
244
+ @objc func setChannel(_ call: CAPPluginCall) {
245
+ guard let channel = call.getString("channel") else {
246
+ print("\(self.implementation.TAG) setChannel called without channel")
247
+ call.reject("setChannel called without channel")
248
+ return
249
+ }
250
+ DispatchQueue.global(qos: .background).async {
251
+ let res = self.implementation.setChannel(channel: channel)
252
+ if res.error != "" {
253
+ call.reject(res.error)
254
+ } else {
255
+ call.resolve(res.toDict())
208
256
  }
209
- return true
210
257
  }
258
+ }
259
+
260
+ @objc func getChannel(_ call: CAPPluginCall) {
261
+ DispatchQueue.global(qos: .background).async {
262
+ let res = self.implementation.getChannel()
263
+ if res.error != "" {
264
+ call.reject(res.error)
265
+ } else {
266
+ call.resolve(res.toDict())
267
+ }
268
+ }
269
+ }
270
+ @objc func setCustomId(_ call: CAPPluginCall) {
271
+ guard let customId = call.getString("customId") else {
272
+ print("\(self.implementation.TAG) setCustomId called without customId")
273
+ call.reject("setCustomId called without customId")
274
+ return
275
+ }
276
+ self.implementation.customId = customId
277
+ }
278
+
279
+ @objc func _reset(toLastSuccessful: Bool) -> Bool {
280
+ guard let bridge = self.bridge else { return false }
281
+
282
+ if (bridge.viewController as? CAPBridgeViewController) != nil {
283
+ let fallback: BundleInfo = self.implementation.getFallbackBundle()
284
+
285
+ // If developer wants to reset to the last successful bundle, and that bundle is not
286
+ // the built-in bundle, set it as the bundle to use and reload.
287
+ if toLastSuccessful && !fallback.isBuiltin() {
288
+ print("\(self.implementation.TAG) Resetting to: \(fallback.toString())")
289
+ return self.implementation.set(bundle: fallback) && self._reload()
290
+ }
291
+
292
+ print("\(self.implementation.TAG) Resetting to builtin version")
293
+
294
+ // Otherwise, reset back to the built-in bundle and reload.
295
+ self.implementation.reset()
296
+ return self._reload()
297
+ }
298
+
211
299
  return false
212
300
  }
213
301
 
214
302
  @objc func reset(_ call: CAPPluginCall) {
215
- let toAutoUpdate = call.getBool("toAutoUpdate") ?? false
216
- if (self._reset(toAutoUpdate: toAutoUpdate)) {
217
- return call.resolve()
303
+ let toLastSuccessful = call.getBool("toLastSuccessful") ?? false
304
+ if self._reset(toLastSuccessful: toLastSuccessful) {
305
+ call.resolve()
306
+ } else {
307
+ print("\(self.implementation.TAG) Reset failed")
308
+ call.reject("\(self.implementation.TAG) Reset failed")
218
309
  }
219
- call.reject("\(self.implementation.TAG) Reset failed")
220
310
  }
221
-
311
+
222
312
  @objc func current(_ call: CAPPluginCall) {
223
313
  let bundle: BundleInfo = self.implementation.getCurrentBundle()
224
314
  call.resolve([
225
315
  "bundle": bundle.toJSON(),
226
- "native": self.currentVersionNative
316
+ "native": self.currentVersionNative.description
227
317
  ])
228
318
  }
229
319
 
230
320
  @objc func notifyAppReady(_ call: CAPPluginCall) {
231
- print("\(self.implementation.TAG) Current bundle loaded successfully. ['notifyAppReady()' was called]")
232
321
  let version = self.implementation.getCurrentBundle()
233
- self.implementation.commit(bundle: version)
322
+ self.implementation.setSuccess(bundle: version, autoDeletePrevious: self.autoDeletePrevious)
323
+ print("\(self.implementation.TAG) Current bundle loaded successfully. ['notifyAppReady()' was called] \(version.toString())")
234
324
  call.resolve()
235
325
  }
236
-
237
- @objc func setDelay(_ call: CAPPluginCall) {
238
- guard let delay = call.getBool("delay") else {
239
- print("\(self.implementation.TAG) setDelay called without delay")
240
- call.reject("setDelay called without delay")
326
+
327
+ @objc func setMultiDelay(_ call: CAPPluginCall) {
328
+ guard let delayConditionList = call.getValue("delayConditions") else {
329
+ print("\(self.implementation.TAG) setMultiDelay called without delayCondition")
330
+ call.reject("setMultiDelay called without delayCondition")
241
331
  return
242
332
  }
243
- UserDefaults.standard.set(delay, forKey: "delayUpdate")
333
+ let delayConditions: String = toJson(object: delayConditionList)
334
+ if _setMultiDelay(delayConditions: delayConditions) {
335
+ call.resolve()
336
+ } else {
337
+ call.reject("Failed to delay update")
338
+ }
339
+ }
340
+
341
+ private func _setMultiDelay(delayConditions: String?) -> Bool {
342
+ if delayConditions != nil && "" != delayConditions {
343
+ UserDefaults.standard.set(delayConditions, forKey: DELAY_CONDITION_PREFERENCES)
344
+ UserDefaults.standard.synchronize()
345
+ print("\(self.implementation.TAG) Delay update saved.")
346
+ return true
347
+ } else {
348
+ print("\(self.implementation.TAG) Failed to delay update, [Error calling '_setMultiDelay()']")
349
+ return false
350
+ }
351
+ }
352
+
353
+ private func _cancelDelay(source: String) {
354
+ print("\(self.implementation.TAG) delay Canceled from \(source)")
355
+ UserDefaults.standard.removeObject(forKey: DELAY_CONDITION_PREFERENCES)
356
+ UserDefaults.standard.synchronize()
357
+ }
358
+
359
+ @objc func cancelDelay(_ call: CAPPluginCall) {
360
+ self._cancelDelay(source: "JS")
244
361
  call.resolve()
245
362
  }
246
-
363
+
364
+ private func _checkCancelDelay(killed: Bool) {
365
+ let delayUpdatePreferences = UserDefaults.standard.string(forKey: DELAY_CONDITION_PREFERENCES) ?? "[]"
366
+ let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
367
+ let kind: String = obj.value(forKey: "kind") as! String
368
+ let value: String? = obj.value(forKey: "value") as? String
369
+ return DelayCondition(kind: kind, value: value)
370
+ }
371
+ for condition in delayConditionList {
372
+ let kind: String? = condition.getKind()
373
+ let value: String? = condition.getValue()
374
+ if kind != nil {
375
+ switch kind {
376
+ case "background":
377
+ if !killed {
378
+ self._cancelDelay(source: "background check")
379
+ }
380
+ break
381
+ case "kill":
382
+ if killed {
383
+ self._cancelDelay(source: "kill check")
384
+ // instant install for kill action
385
+ self.installNext()
386
+ }
387
+ break
388
+ case "date":
389
+ if value != nil && value != "" {
390
+ let dateFormatter = ISO8601DateFormatter()
391
+ dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
392
+ guard let ExpireDate = dateFormatter.date(from: value!) else {
393
+ self._cancelDelay(source: "date parsing issue")
394
+ return
395
+ }
396
+ if ExpireDate < Date() {
397
+ self._cancelDelay(source: "date expired")
398
+ }
399
+ } else {
400
+ self._cancelDelay(source: "delayVal absent")
401
+ }
402
+ break
403
+ case "nativeVersion":
404
+ if value != nil && value != "" {
405
+ do {
406
+ let versionLimit = try Version(value!)
407
+ if self.currentVersionNative >= versionLimit {
408
+ self._cancelDelay(source: "nativeVersion above limit")
409
+ }
410
+ } catch {
411
+ self._cancelDelay(source: "nativeVersion parsing issue")
412
+ }
413
+ } else {
414
+ self._cancelDelay(source: "delayVal absent")
415
+ }
416
+ break
417
+ case .none:
418
+ print("\(self.implementation.TAG) _checkCancelDelay switch case none error")
419
+ case .some:
420
+ print("\(self.implementation.TAG) _checkCancelDelay switch case some error")
421
+ }
422
+ }
423
+ }
424
+ // self.checkAppReady() why this here?
425
+ }
426
+
247
427
  private func _isAutoUpdateEnabled() -> Bool {
248
- return self.autoUpdate && self.autoUpdateUrl != ""
428
+ let instanceDescriptor = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor()
429
+ if instanceDescriptor?.serverURL != nil {
430
+ print("⚠️ \(self.implementation.TAG) AutoUpdate is automatic disabled when serverUrl is set.")
431
+ }
432
+ return self.autoUpdate && self.updateUrl != "" && instanceDescriptor?.serverURL == nil
249
433
  }
250
434
 
251
435
  @objc func isAutoUpdateEnabled(_ call: CAPPluginCall) {
@@ -263,156 +447,229 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
263
447
  DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.appReadyTimeout), execute: self.appReadyCheck!)
264
448
  }
265
449
 
266
- func DeferredNotifyAppReadyCheck() {
450
+ func checkRevert() {
267
451
  // Automatically roll back to fallback version if notifyAppReady has not been called yet
268
452
  let current: BundleInfo = self.implementation.getCurrentBundle()
269
- if(current.isBuiltin()) {
453
+ if current.isBuiltin() {
270
454
  print("\(self.implementation.TAG) Built-in bundle is active. Nothing to do.")
271
455
  return
272
456
  }
273
457
 
274
- if(BundleStatus.SUCCESS.localizedString != current.getStatus()) {
458
+ print("\(self.implementation.TAG) Current bundle is: \(current.toString())")
459
+
460
+ if BundleStatus.SUCCESS.localizedString != current.getStatus() {
275
461
  print("\(self.implementation.TAG) notifyAppReady was not called, roll back current bundle: \(current.toString())")
276
- self.implementation.rollback(bundle: current)
277
- let res = self._reset(toAutoUpdate: true)
278
- if (!res) {
279
- return
462
+ print("\(self.implementation.TAG) Did you forget to call 'notifyAppReady()' in your Capacitor App code?")
463
+ self.notifyListeners("updateFailed", data: [
464
+ "bundle": current.toJSON()
465
+ ])
466
+ self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
467
+ self.implementation.setError(bundle: current)
468
+ _ = self._reset(toLastSuccessful: true)
469
+ if self.autoDeleteFailed && !current.isBuiltin() {
470
+ print("\(self.implementation.TAG) Deleting failing bundle: \(current.toString())")
471
+ let res = self.implementation.delete(id: current.getId(), removeInfo: false)
472
+ if !res {
473
+ print("\(self.implementation.TAG) Delete version deleted: \(current.toString())")
474
+ } else {
475
+ print("\(self.implementation.TAG) Failed to delete failed bundle: \(current.toString())")
476
+ }
280
477
  }
281
478
  } else {
282
479
  print("\(self.implementation.TAG) notifyAppReady was called. This is fine: \(current.toString())")
283
480
  }
481
+ }
482
+
483
+ func DeferredNotifyAppReadyCheck() {
484
+ self.checkRevert()
284
485
  self.appReadyCheck = nil
285
486
  }
286
487
 
287
- @objc func appMovedToForeground() {
288
- if (self._isAutoUpdateEnabled()) {
289
- DispatchQueue.global(qos: .background).async {
290
- print("\(self.implementation.TAG) Check for update via \(self.autoUpdateUrl)")
291
- let url = URL(string: self.autoUpdateUrl)!
292
- let res = self.implementation.getLatest(url: url)
293
- if (res == nil) {
294
- print("\(self.implementation.TAG) No result found in \(self.autoUpdateUrl)")
295
- return
488
+ func endBackGroundTask() {
489
+ UIApplication.shared.endBackgroundTask(self.backgroundTaskID)
490
+ self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
491
+ }
492
+
493
+ func backgroundDownload() {
494
+ DispatchQueue.global(qos: .background).async {
495
+ self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Download Tasks") {
496
+ // End the task if time expires.
497
+ self.endBackGroundTask()
498
+ }
499
+ print("\(self.implementation.TAG) Check for update via \(self.updateUrl)")
500
+ let url = URL(string: self.updateUrl)!
501
+ let res = self.implementation.getLatest(url: url)
502
+ let current = self.implementation.getCurrentBundle()
503
+
504
+ if (res.message) != nil {
505
+ print("\(self.implementation.TAG) message \(res.message ?? "")")
506
+ if res.major == true {
507
+ self.notifyListeners("majorAvailable", data: ["version": res.version])
296
508
  }
297
- guard let downloadUrl = URL(string: res?.url ?? "") else {
298
- print("\(self.implementation.TAG) Error \(res?.message ?? "Unknow error")")
299
- if (res?.major == true) {
300
- self.notifyListeners("majorAvailable", data: ["version": res?.version ?? "0.0.0"])
509
+ self.notifyListeners("noNeedUpdate", data: ["bundle": current.toJSON()])
510
+ self.endBackGroundTask()
511
+ return
512
+ }
513
+ let sessionKey = res.sessionKey ?? ""
514
+ guard let downloadUrl = URL(string: res.url) else {
515
+ print("\(self.implementation.TAG) Error no url or wrong format")
516
+ self.notifyListeners("noNeedUpdate", data: ["bundle": current.toJSON()])
517
+ self.endBackGroundTask()
518
+ return
519
+ }
520
+ let latestVersionName = res.version
521
+ if latestVersionName != "" && current.getVersionName() != latestVersionName {
522
+ let latest = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
523
+ if latest != nil {
524
+ if latest!.isErrorStatus() {
525
+ print("\(self.implementation.TAG) Latest version already exists, and is in error state. Aborting update.")
526
+ self.notifyListeners("noNeedUpdate", data: ["bundle": current.toJSON()])
527
+ self.endBackGroundTask()
528
+ return
301
529
  }
302
- return
303
- }
304
- let current = self.implementation.getCurrentBundle()
305
- let latestVersionName = res?.version
306
- if (latestVersionName != nil && latestVersionName != "" && current.getVersionName() != latestVersionName) {
307
- let latest = self.implementation.getBundleInfoByVersionName(version: latestVersionName!)
308
- if (latest != nil) {
309
- if(latest!.isErrorStatus()) {
310
- print("\(self.implementation.TAG) Latest version already exists, and is in error state. Aborting update.")
311
- return
312
- }
313
- if(latest!.isDownloaded()){
314
- print("\(self.implementation.TAG) Latest version already exists and download is NOT required. Update will occur next time app moves to background.")
315
- let _ = self.implementation.setNextVersion(next: latest!.getId())
316
- return
530
+ if latest!.isDownloaded() {
531
+ print("\(self.implementation.TAG) Latest version already exists and download is NOT required. Update will occur next time app moves to background.")
532
+ self.notifyListeners("updateAvailable", data: ["bundle": current.toJSON()])
533
+ _ = self.implementation.setNextBundle(next: latest!.getId())
534
+ self.endBackGroundTask()
535
+ return
536
+ }
537
+ if latest!.isDeleted() {
538
+ print("\(self.implementation.TAG) Latest bundle already exists and will be deleted, download will overwrite it.")
539
+ let res = self.implementation.delete(id: latest!.getId(), removeInfo: true)
540
+ if !res {
541
+ print("\(self.implementation.TAG) Delete version deleted: \(latest!.toString())")
542
+ } else {
543
+ print("\(self.implementation.TAG) Failed to delete failed bundle: \(latest!.toString())")
317
544
  }
318
545
  }
546
+ }
319
547
 
320
- do {
321
- print("\(self.implementation.TAG) New bundle: \(latestVersionName!) found. Current is: \(current.getVersionName()). Update will occur next time app moves to background.")
322
- let next = try self.implementation.download(url: downloadUrl, version: latestVersionName!)
323
-
324
- let _ = self.implementation.setNextVersion(next: next.getId())
325
- } catch {
326
- print("\(self.implementation.TAG) Error downloading file", error.localizedDescription)
548
+ do {
549
+ print("\(self.implementation.TAG) New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). Update will occur next time app moves to background.")
550
+ let next = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey)
551
+ if res.checksum != "" && next.getChecksum() != res.checksum {
552
+ print("\(self.implementation.TAG) Error checksum", next.getChecksum(), res.checksum)
553
+ self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
554
+ let id = next.getId()
555
+ let resDel = self.implementation.delete(id: id)
556
+ if !resDel {
557
+ print("\(self.implementation.TAG) Delete failed, id \(id) doesn't exist")
558
+ }
559
+ self.endBackGroundTask()
560
+ throw ObjectSavableError.checksum
327
561
  }
562
+ self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
563
+ _ = self.implementation.setNextBundle(next: next.getId())
564
+ } catch {
565
+ print("\(self.implementation.TAG) Error downloading file", error.localizedDescription)
566
+ let current: BundleInfo = self.implementation.getCurrentBundle()
567
+ self.implementation.sendStats(action: "download_fail", versionName: current.getVersionName())
568
+ self.notifyListeners("downloadFailed", data: ["version": latestVersionName])
569
+ self.notifyListeners("noNeedUpdate", data: ["bundle": current.toJSON()])
328
570
  }
571
+ } else {
572
+ print("\(self.implementation.TAG) No need to update, \(current.getId()) is the latest bundle.")
573
+ self.notifyListeners("noNeedUpdate", data: ["bundle": current.toJSON()])
329
574
  }
575
+ self.endBackGroundTask()
330
576
  }
577
+ }
331
578
 
332
- self.checkAppReady()
579
+ @objc func appKilled() {
580
+ print("\(self.implementation.TAG) onActivityDestroyed: all activity destroyed")
581
+ self._checkCancelDelay(killed: true)
333
582
  }
334
583
 
335
- @objc func appMovedToBackground() {
336
- print("\(self.implementation.TAG) Check for waiting update")
337
- let delayUpdate = UserDefaults.standard.bool(forKey: "delayUpdate")
338
- UserDefaults.standard.set(false, forKey: "delayUpdate")
339
- if (delayUpdate) {
584
+ private func installNext() {
585
+ let delayUpdatePreferences = UserDefaults.standard.string(forKey: DELAY_CONDITION_PREFERENCES) ?? "[]"
586
+ let delayConditionList: [DelayCondition]? = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
587
+ let kind: String = obj.value(forKey: "kind") as! String
588
+ let value: String? = obj.value(forKey: "value") as? String
589
+ return DelayCondition(kind: kind, value: value)
590
+ }
591
+ if delayConditionList != nil && delayConditionList?.capacity != 0 {
340
592
  print("\(self.implementation.TAG) Update delayed to next backgrounding")
341
593
  return
342
594
  }
343
-
344
- let fallback: BundleInfo = self.implementation.getFallbackVersion()
345
595
  let current: BundleInfo = self.implementation.getCurrentBundle()
346
- let next: BundleInfo? = self.implementation.getNextVersion()
347
-
348
- let success: Bool = current.getStatus() == BundleStatus.SUCCESS.localizedString
596
+ let next: BundleInfo? = self.implementation.getNextBundle()
349
597
 
350
- print("\(self.implementation.TAG) Fallback bundle is: \(fallback.toString())")
351
- print("\(self.implementation.TAG) Current bundle is: \(current.toString())")
352
-
353
- if (next != nil && !next!.isErrorStatus() && (next!.getVersionName() != current.getVersionName())) {
598
+ if next != nil && !next!.isErrorStatus() && next!.getVersionName() != current.getVersionName() {
354
599
  print("\(self.implementation.TAG) Next bundle is: \(next!.toString())")
355
- if (self.implementation.set(bundle: next!) && self._reload()) {
356
- print("\(self.implementation.TAG) Updated to bundle: \(next!)")
357
- let _ = self.implementation.setNextVersion(next: Optional<String>.none)
600
+ if self.implementation.set(bundle: next!) && self._reload() {
601
+ print("\(self.implementation.TAG) Updated to bundle: \(next!.toString())")
602
+ _ = self.implementation.setNextBundle(next: Optional<String>.none)
358
603
  } else {
359
- print("\(self.implementation.TAG) Updated to bundle: \(next!) Failed!")
604
+ print("\(self.implementation.TAG) Update to bundle: \(next!.toString()) Failed!")
360
605
  }
361
- } else if (!success) {
362
- // There is a no next version, and the current version has failed
363
-
364
- if(!current.isBuiltin()) {
365
- // Don't try to roll back the builtin version. Nothing we can do.
366
-
367
- self.implementation.rollback(bundle: current)
368
-
369
- print("\(self.implementation.TAG) Update failed: 'notifyAppReady()' was never called.")
370
- print("\(self.implementation.TAG) Version: \(current.toString()), is in error state.")
371
- print("\(self.implementation.TAG) Will fallback to: \(fallback.toString()) on application restart.")
372
- print("\(self.implementation.TAG) Did you forget to call 'notifyAppReady()' in your Capacitor App code?")
373
-
374
- self.notifyListeners("updateFailed", data: [
375
- "bundle": current.toJSON()
376
- ])
377
- self.implementation.sendStats(action: "revert", bundle: current)
378
- if (!fallback.isBuiltin() && !(fallback == current)) {
379
- let res = self.implementation.set(bundle: fallback)
380
- if (res && self._reload()) {
381
- print("\(self.implementation.TAG) Revert to bundle: \(fallback.toString())")
382
- } else {
383
- print("\(self.implementation.TAG) Revert to bundle: \(fallback.toString()) Failed!")
384
- }
385
- } else {
386
- if (self._reset(toAutoUpdate: false)) {
387
- print("\(self.implementation.TAG) Reverted to 'builtin' bundle.")
388
- }
389
- }
606
+ }
607
+ }
390
608
 
391
- if (self.autoDeleteFailed) {
392
- print("\(self.implementation.TAG) Deleting failing bundle: \(current.toString())")
393
- let res = self.implementation.delete(id: current.getId())
394
- if (!res) {
395
- print("\(self.implementation.TAG) Delete version deleted: \(current.toString())")
396
- } else {
397
- print("\(self.implementation.TAG) Failed to delete failed bundle: \(current.toString())")
398
- }
399
- }
400
- } else {
401
- // Nothing we can/should do by default if the 'builtin' bundle fails to call 'notifyAppReady()'.
402
- }
403
- } else if (!fallback.isBuiltin()) {
404
- // There is a no next version, and the current version has succeeded
405
- self.implementation.commit(bundle: current)
406
-
407
- if(self.autoDeletePrevious) {
408
- print("\(self.implementation.TAG) Version successfully loaded: \(current.toString())")
409
- let res = self.implementation.delete(id: fallback.getId())
410
- if (res) {
411
- print("\(self.implementation.TAG) Deleted previous bundle: \(fallback.toString())")
412
- } else {
413
- print("\(self.implementation.TAG) Failed to delete previous bundle: \(fallback.toString())")
414
- }
609
+ @objc private func toJson(object: Any) -> String {
610
+ guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
611
+ return ""
612
+ }
613
+ return String(data: data, encoding: String.Encoding.utf8) ?? ""
614
+ }
615
+
616
+ @objc private func fromJsonArr(json: String) -> [NSObject] {
617
+ let jsonData = json.data(using: .utf8)!
618
+ let object = try? JSONSerialization.jsonObject(
619
+ with: jsonData,
620
+ options: .mutableContainers
621
+ ) as? [NSObject]
622
+ return object ?? []
623
+ }
624
+
625
+ @objc func appMovedToForeground() {
626
+ let current: BundleInfo = self.implementation.getCurrentBundle()
627
+ self.implementation.sendStats(action: "app_moved_to_foreground", versionName: current.getVersionName())
628
+ if backgroundWork != nil && taskRunning {
629
+ backgroundWork!.cancel()
630
+ print("\(self.implementation.TAG) Background Timer Task canceled, Activity resumed before timer completes")
631
+ }
632
+ if self._isAutoUpdateEnabled() {
633
+ self.backgroundDownload()
634
+ } else {
635
+ print("\(self.implementation.TAG) Auto update is disabled")
636
+ }
637
+ self.checkAppReady()
638
+ }
639
+
640
+ @objc func appMovedToBackground() {
641
+ let current: BundleInfo = self.implementation.getCurrentBundle()
642
+ self.implementation.sendStats(action: "app_moved_to_background", versionName: current.getVersionName())
643
+ print("\(self.implementation.TAG) Check for pending update")
644
+ let delayUpdatePreferences = UserDefaults.standard.string(forKey: DELAY_CONDITION_PREFERENCES) ?? "[]"
645
+
646
+ let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
647
+ let kind: String = obj.value(forKey: "kind") as! String
648
+ let value: String? = obj.value(forKey: "value") as? String
649
+ return DelayCondition(kind: kind, value: value)
650
+ }
651
+ var backgroundValue: String?
652
+ for delayCondition in delayConditionList {
653
+ if delayCondition.getKind() == "background" {
654
+ let value: String? = delayCondition.getValue()
655
+ backgroundValue = (value != nil && value != "") ? value! : "0"
415
656
  }
416
657
  }
658
+ if backgroundValue != nil {
659
+ self.taskRunning = true
660
+ let interval: Double = (Double(backgroundValue!) ?? 0.0) / 1000
661
+ self.backgroundWork?.cancel()
662
+ self.backgroundWork = DispatchWorkItem(block: {
663
+ // IOS never executes this task in background
664
+ self.taskRunning = false
665
+ self._checkCancelDelay(killed: false)
666
+ self.installNext()
667
+ })
668
+ DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + interval, execute: self.backgroundWork!)
669
+ } else {
670
+ self._checkCancelDelay(killed: false)
671
+ self.installNext()
672
+ }
673
+
417
674
  }
418
675
  }