@capgo/capacitor-updater 7.43.3 → 7.50.1
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/Package.swift +5 -2
- package/README.md +643 -115
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3121 -353
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +924 -290
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +49 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +75 -32
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +2 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +401 -27
- package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
- package/dist/docs.json +1590 -196
- package/dist/esm/definitions.d.ts +755 -66
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +11 -2
- package/dist/esm/web.js +59 -5
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +59 -5
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +59 -5
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AppHealthTracker.swift +82 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -16
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +2732 -417
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1191 -363
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +37 -16
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +80 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +438 -39
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +24 -9
|
@@ -37,6 +37,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
37
37
|
CAPPluginMethod(name: "setStatsUrl", returnType: CAPPluginReturnPromise),
|
|
38
38
|
CAPPluginMethod(name: "setChannelUrl", returnType: CAPPluginReturnPromise),
|
|
39
39
|
CAPPluginMethod(name: "set", returnType: CAPPluginReturnPromise),
|
|
40
|
+
CAPPluginMethod(name: "startPreviewSession", returnType: CAPPluginReturnPromise),
|
|
41
|
+
CAPPluginMethod(name: "listPreviews", returnType: CAPPluginReturnPromise),
|
|
42
|
+
CAPPluginMethod(name: "setPreview", returnType: CAPPluginReturnPromise),
|
|
43
|
+
CAPPluginMethod(name: "resetPreview", returnType: CAPPluginReturnPromise),
|
|
44
|
+
CAPPluginMethod(name: "deletePreview", returnType: CAPPluginReturnPromise),
|
|
45
|
+
CAPPluginMethod(name: "checkPreviewUpdate", returnType: CAPPluginReturnPromise),
|
|
46
|
+
CAPPluginMethod(name: "updatePreview", returnType: CAPPluginReturnPromise),
|
|
40
47
|
CAPPluginMethod(name: "list", returnType: CAPPluginReturnPromise),
|
|
41
48
|
CAPPluginMethod(name: "delete", returnType: CAPPluginReturnPromise),
|
|
42
49
|
CAPPluginMethod(name: "setBundleError", returnType: CAPPluginReturnPromise),
|
|
@@ -44,12 +51,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
44
51
|
CAPPluginMethod(name: "current", returnType: CAPPluginReturnPromise),
|
|
45
52
|
CAPPluginMethod(name: "reload", returnType: CAPPluginReturnPromise),
|
|
46
53
|
CAPPluginMethod(name: "notifyAppReady", returnType: CAPPluginReturnPromise),
|
|
47
|
-
CAPPluginMethod(name: "setDelay", returnType: CAPPluginReturnPromise),
|
|
48
54
|
CAPPluginMethod(name: "setMultiDelay", returnType: CAPPluginReturnPromise),
|
|
49
55
|
CAPPluginMethod(name: "cancelDelay", returnType: CAPPluginReturnPromise),
|
|
50
56
|
CAPPluginMethod(name: "getLatest", returnType: CAPPluginReturnPromise),
|
|
57
|
+
CAPPluginMethod(name: "getMissingBundleFiles", returnType: CAPPluginReturnPromise),
|
|
58
|
+
CAPPluginMethod(name: "getBundleDownloadSize", returnType: CAPPluginReturnPromise),
|
|
59
|
+
CAPPluginMethod(name: "triggerUpdateCheck", returnType: CAPPluginReturnPromise),
|
|
51
60
|
CAPPluginMethod(name: "setChannel", returnType: CAPPluginReturnPromise),
|
|
52
61
|
CAPPluginMethod(name: "unsetChannel", returnType: CAPPluginReturnPromise),
|
|
62
|
+
CAPPluginMethod(name: "reportWebViewError", returnType: CAPPluginReturnPromise),
|
|
53
63
|
CAPPluginMethod(name: "getChannel", returnType: CAPPluginReturnPromise),
|
|
54
64
|
CAPPluginMethod(name: "listChannels", returnType: CAPPluginReturnPromise),
|
|
55
65
|
CAPPluginMethod(name: "setCustomId", returnType: CAPPluginReturnPromise),
|
|
@@ -65,6 +75,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
65
75
|
CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise),
|
|
66
76
|
CAPPluginMethod(name: "setShakeChannelSelector", returnType: CAPPluginReturnPromise),
|
|
67
77
|
CAPPluginMethod(name: "isShakeChannelSelectorEnabled", returnType: CAPPluginReturnPromise),
|
|
78
|
+
CAPPluginMethod(name: "getAppId", returnType: CAPPluginReturnPromise),
|
|
79
|
+
CAPPluginMethod(name: "setAppId", returnType: CAPPluginReturnPromise),
|
|
68
80
|
// App Store update methods
|
|
69
81
|
CAPPluginMethod(name: "getAppUpdateInfo", returnType: CAPPluginReturnPromise),
|
|
70
82
|
CAPPluginMethod(name: "openAppStore", returnType: CAPPluginReturnPromise),
|
|
@@ -73,10 +85,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
73
85
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
74
86
|
]
|
|
75
87
|
public var implementation = CapgoUpdater()
|
|
76
|
-
private let pluginVersion: String = "7.
|
|
88
|
+
private let pluginVersion: String = "7.50.1"
|
|
77
89
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
78
90
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
79
91
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
92
|
+
static let autoUpdateModeOff = "off"
|
|
93
|
+
static let autoUpdateModeBackground = "atBackground"
|
|
94
|
+
static let autoUpdateModeInstall = "atInstall"
|
|
95
|
+
static let autoUpdateModeLaunch = "onLaunch"
|
|
96
|
+
static let autoUpdateModeAlways = "always"
|
|
97
|
+
static let autoUpdateModeOnlyDownload = "onlyDownload"
|
|
98
|
+
static let shakeMenuGestureShake = "shake"
|
|
99
|
+
static let shakeMenuGestureThreeFingerPinch = "threeFingerPinch"
|
|
100
|
+
private static let previewLoaderTimeoutMs = 60000
|
|
80
101
|
private let keepUrlPathFlagKey = "__capgo_keep_url_path_after_reload"
|
|
81
102
|
private let customIdDefaultsKey = "CapacitorUpdater.customId"
|
|
82
103
|
private let updateUrlDefaultsKey = "CapacitorUpdater.updateUrl"
|
|
@@ -84,12 +105,36 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
84
105
|
private let channelUrlDefaultsKey = "CapacitorUpdater.channelUrl"
|
|
85
106
|
private let defaultChannelDefaultsKey = "CapacitorUpdater.defaultChannel"
|
|
86
107
|
private let lastFailedBundleDefaultsKey = "CapacitorUpdater.lastFailedBundle"
|
|
108
|
+
private let lastVersionOsDefaultsKey = "CapacitorUpdater.lastVersionOs"
|
|
109
|
+
private let lastVersionBuildDefaultsKey = "CapacitorUpdater.lastVersionBuild"
|
|
110
|
+
private let lastVersionCodeDefaultsKey = "CapacitorUpdater.lastVersionCode"
|
|
111
|
+
private let osVersionChangedAction = "os_version_changed"
|
|
112
|
+
private let nativeAppVersionChangedAction = "native_app_version_changed"
|
|
113
|
+
private let previewSessionDefaultsKey = "CapacitorUpdater.previewSession"
|
|
114
|
+
private let previewPreviousShakeMenuDefaultsKey = "CapacitorUpdater.previewPreviousShakeMenu"
|
|
115
|
+
private let previewPreviousShakeChannelSelectorDefaultsKey = "CapacitorUpdater.previewPreviousShakeChannelSelector"
|
|
116
|
+
private let previewPreviousNextBundleDefaultsKey = "CapacitorUpdater.previewPreviousNextBundle"
|
|
117
|
+
private let previewPreviousAppIdDefaultsKey = "CapacitorUpdater.previewPreviousAppId"
|
|
118
|
+
private let previewPreviousDefaultChannelDefaultsKey = "CapacitorUpdater.previewPreviousDefaultChannel"
|
|
119
|
+
private let previewPreviousDefaultChannelWasSetDefaultsKey = "CapacitorUpdater.previewPreviousDefaultChannelWasSet"
|
|
120
|
+
private let previewAppIdDefaultsKey = "CapacitorUpdater.previewAppId"
|
|
121
|
+
private let previewPayloadUrlDefaultsKey = "CapacitorUpdater.previewPayloadUrl"
|
|
122
|
+
private let previewNameDefaultsKey = "CapacitorUpdater.previewName"
|
|
123
|
+
private let previewSourceDefaultsKey = "CapacitorUpdater.previewSource"
|
|
124
|
+
private let previewSessionsDefaultsKey = "CapacitorUpdater.previewSessions"
|
|
125
|
+
private let previewSessionAlertPendingDefaultsKey = "CapacitorUpdater.previewSessionAlertPending"
|
|
126
|
+
private let previewDeepLinkScheme = "capgo"
|
|
127
|
+
private let previewDeepLinkRootComponent = "preview"
|
|
128
|
+
private let previewDeepLinkChannelComponent = "channel"
|
|
129
|
+
private let previewDeepLinkBundleComponent = "bundle"
|
|
130
|
+
private let previewPathSeparator = Character(UnicodeScalar(UInt8(47)))
|
|
87
131
|
// Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
|
|
88
132
|
private var updateUrl = ""
|
|
89
133
|
private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
|
90
134
|
private var currentVersionNative: Version = "0.0.0"
|
|
91
135
|
private var currentBuildVersion: String = "0"
|
|
92
136
|
private var autoUpdate = false
|
|
137
|
+
private var autoUpdateMode = CapacitorUpdaterPlugin.autoUpdateModeOff
|
|
93
138
|
private var appReadyTimeout = 10000
|
|
94
139
|
private var appReadyCheck: DispatchWorkItem?
|
|
95
140
|
private var resetWhenUpdate = true
|
|
@@ -103,7 +148,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
103
148
|
private var autoSplashscreenTimeoutWorkItem: DispatchWorkItem?
|
|
104
149
|
private var splashscreenLoaderView: UIActivityIndicatorView?
|
|
105
150
|
private var splashscreenLoaderContainer: UIView?
|
|
151
|
+
private var previewTransitionLoaderView: UIActivityIndicatorView?
|
|
152
|
+
private var previewTransitionLoaderContainer: UIView?
|
|
153
|
+
private var previewTransitionLoaderTimeoutWorkItem: DispatchWorkItem?
|
|
154
|
+
private var previewTransitionLoaderRequested = false
|
|
155
|
+
private let splashscreenPluginName = "SplashScreen"
|
|
156
|
+
private let splashscreenRetryDelayMilliseconds = 100
|
|
157
|
+
private let splashscreenMaxRetries = 20
|
|
106
158
|
private var autoSplashscreenTimedOut = false
|
|
159
|
+
private var splashscreenInvocationToken = 0
|
|
107
160
|
private var autoDeleteFailed = false
|
|
108
161
|
private var autoDeletePrevious = false
|
|
109
162
|
var allowSetDefaultChannel = true
|
|
@@ -112,9 +165,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
112
165
|
private var taskRunning = false
|
|
113
166
|
private var periodCheckDelay = 0
|
|
114
167
|
private let downloadLock = NSLock()
|
|
168
|
+
private let onLaunchDirectUpdateStateLock = NSLock()
|
|
115
169
|
private var downloadInProgress = false
|
|
116
170
|
private var downloadStartTime: Date?
|
|
117
|
-
private let downloadTimeout: TimeInterval =
|
|
171
|
+
private let downloadTimeout: TimeInterval = 600 // 10 minute timeout
|
|
118
172
|
|
|
119
173
|
// Lock to ensure cleanup completes before downloads start
|
|
120
174
|
private let cleanupLock = NSLock()
|
|
@@ -123,9 +177,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
123
177
|
private var persistCustomId = false
|
|
124
178
|
private var persistModifyUrl = false
|
|
125
179
|
private var allowManualBundleError = false
|
|
180
|
+
private var allowPreview = false
|
|
126
181
|
private var keepUrlPathFlagLastValue: Bool?
|
|
182
|
+
private var appHealthTracker: AppHealthTracker?
|
|
183
|
+
private var webViewStatsReporter: WebViewStatsReporter?
|
|
127
184
|
public var shakeMenuEnabled = false
|
|
128
185
|
public var shakeChannelSelectorEnabled = false
|
|
186
|
+
public var shakeMenuGesture = CapacitorUpdaterPlugin.shakeMenuGestureShake
|
|
187
|
+
var shakeMenuPinchGestureRecognizer: ThreeFingerPinchGestureRecognizer?
|
|
188
|
+
var shakeMenuPinchGestureTriggered = false
|
|
189
|
+
public var previewSessionEnabled = false
|
|
190
|
+
private var previewSessionAlertPending = false
|
|
191
|
+
private var isLeavingPreviewForIncomingLink = false
|
|
192
|
+
private var previewTransitionClearWorkItem: DispatchWorkItem?
|
|
129
193
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
130
194
|
|
|
131
195
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
@@ -139,6 +203,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
139
203
|
} else {
|
|
140
204
|
logger.error("Failed to get webView for logging")
|
|
141
205
|
}
|
|
206
|
+
let webViewStatsReporter = WebViewStatsReporter(implementation: implementation)
|
|
207
|
+
self.webViewStatsReporter = webViewStatsReporter
|
|
208
|
+
webViewStatsReporter.install(on: self.bridge?.webView)
|
|
142
209
|
#if targetEnvironment(simulator)
|
|
143
210
|
logger.info("::::: SIMULATOR :::::")
|
|
144
211
|
logger.info("Application directory: \(NSHomeDirectory())")
|
|
@@ -158,6 +225,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
158
225
|
}
|
|
159
226
|
persistModifyUrl = getConfig().getBoolean("persistModifyUrl", false)
|
|
160
227
|
allowManualBundleError = getConfig().getBoolean("allowManualBundleError", false)
|
|
228
|
+
allowPreview = getConfig().getBoolean("allowPreview", false)
|
|
161
229
|
logger.info("init for device \(self.implementation.deviceID)")
|
|
162
230
|
guard let versionName = getConfig().getString("version", Bundle.main.versionName) else {
|
|
163
231
|
logger.error("Cannot get version name")
|
|
@@ -177,33 +245,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
177
245
|
keepUrlPathAfterReload = getConfig().getBoolean("keepUrlPathAfterReload", false)
|
|
178
246
|
syncKeepUrlPathFlag(enabled: keepUrlPathAfterReload)
|
|
179
247
|
|
|
180
|
-
// Handle directUpdate configuration - support string values and backward compatibility
|
|
181
|
-
if let directUpdateString = getConfig().getString("directUpdate") {
|
|
182
|
-
// Handle backward compatibility for boolean true
|
|
183
|
-
if directUpdateString == "true" {
|
|
184
|
-
directUpdateMode = "always"
|
|
185
|
-
directUpdate = true
|
|
186
|
-
} else {
|
|
187
|
-
directUpdateMode = directUpdateString
|
|
188
|
-
directUpdate = directUpdateString == "always" || directUpdateString == "atInstall" || directUpdateString == "onLaunch"
|
|
189
|
-
// Validate directUpdate value
|
|
190
|
-
if directUpdateString != "false" && directUpdateString != "always" && directUpdateString != "atInstall" && directUpdateString != "onLaunch" {
|
|
191
|
-
logger.error("Invalid directUpdate value: \"\(directUpdateString)\". Supported values are: \"false\", \"true\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\".")
|
|
192
|
-
directUpdateMode = "false"
|
|
193
|
-
directUpdate = false
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} else {
|
|
197
|
-
let directUpdateBool = getConfig().getBoolean("directUpdate", false)
|
|
198
|
-
if directUpdateBool {
|
|
199
|
-
directUpdateMode = "always" // backward compatibility: true = always
|
|
200
|
-
directUpdate = true
|
|
201
|
-
} else {
|
|
202
|
-
directUpdateMode = "false"
|
|
203
|
-
directUpdate = false
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
248
|
autoSplashscreen = getConfig().getBoolean("autoSplashscreen", false)
|
|
208
249
|
autoSplashscreenLoader = getConfig().getBoolean("autoSplashscreenLoader", false)
|
|
209
250
|
let splashscreenTimeoutValue = getConfig().getInt("autoSplashscreenTimeout", 10000)
|
|
@@ -213,21 +254,40 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
213
254
|
updateUrl = storedUpdateUrl
|
|
214
255
|
logger.info("Loaded persisted updateUrl")
|
|
215
256
|
}
|
|
216
|
-
|
|
257
|
+
configureAutoUpdateModeFromConfig()
|
|
217
258
|
appReadyTimeout = max(1000, getConfig().getInt("appReadyTimeout", 10000)) // Minimum 1 second
|
|
218
259
|
implementation.timeout = Double(getConfig().getInt("responseTimeout", 20))
|
|
219
260
|
resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
|
|
220
261
|
shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
221
262
|
shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
263
|
+
shakeMenuGesture = Self.normalizedShakeMenuGesture(getConfig().getString("shakeMenuGesture", Self.shakeMenuGestureShake))
|
|
264
|
+
let storedPreviewSessionEnabled = UserDefaults.standard.bool(forKey: previewSessionDefaultsKey)
|
|
265
|
+
let shouldClearPreviewSessionBecauseDisabled = !allowPreview && storedPreviewSessionEnabled
|
|
266
|
+
previewSessionEnabled = allowPreview && storedPreviewSessionEnabled
|
|
267
|
+
implementation.previewSession = previewSessionEnabled
|
|
268
|
+
if previewSessionEnabled {
|
|
269
|
+
previewSessionAlertPending = UserDefaults.standard.object(forKey: previewSessionAlertPendingDefaultsKey) as? Bool ?? true
|
|
270
|
+
shakeMenuEnabled = true
|
|
271
|
+
shakeChannelSelectorEnabled = UserDefaults.standard.object(forKey: previewPreviousShakeChannelSelectorDefaultsKey) as? Bool
|
|
272
|
+
?? shakeChannelSelectorEnabled
|
|
227
273
|
}
|
|
274
|
+
syncShakeMenuGestureRecognizer()
|
|
275
|
+
periodCheckDelay = Self.normalizedPeriodCheckDelaySeconds(getConfig().getInt("periodCheckDelay", 0))
|
|
228
276
|
|
|
229
277
|
implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
|
|
230
278
|
implementation.notifyDownloadRaw = notifyDownload
|
|
279
|
+
implementation.notifyListeners = { [weak self] eventName, data in
|
|
280
|
+
let emit = {
|
|
281
|
+
self?.notifyListeners(eventName, data: data)
|
|
282
|
+
}
|
|
283
|
+
if Thread.isMainThread {
|
|
284
|
+
emit()
|
|
285
|
+
} else {
|
|
286
|
+
DispatchQueue.main.async {
|
|
287
|
+
emit()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
231
291
|
implementation.pluginVersion = self.pluginVersion
|
|
232
292
|
|
|
233
293
|
// Set logger for shared classes
|
|
@@ -249,6 +309,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
249
309
|
// crash the app on purpose it should not happen
|
|
250
310
|
fatalError("appId is missing in capacitor.config.json or plugin config, and cannot be retrieved from the native app, please add it globally or in the plugin config")
|
|
251
311
|
}
|
|
312
|
+
if shouldClearPreviewSessionBecauseDisabled {
|
|
313
|
+
clearPreviewSessionBecauseDisabled()
|
|
314
|
+
}
|
|
315
|
+
if previewSessionEnabled,
|
|
316
|
+
let previewAppId = UserDefaults.standard.string(forKey: previewAppIdDefaultsKey),
|
|
317
|
+
!previewAppId.isEmpty {
|
|
318
|
+
implementation.appId = previewAppId
|
|
319
|
+
logger.info("Using preview appId \(previewAppId)")
|
|
320
|
+
}
|
|
252
321
|
logger.info("appId \(implementation.appId)")
|
|
253
322
|
implementation.statsUrl = getConfig().getString("statsUrl", CapacitorUpdaterPlugin.statsUrlDefault)!
|
|
254
323
|
implementation.channelUrl = getConfig().getString("channelUrl", CapacitorUpdaterPlugin.channelUrlDefault)!
|
|
@@ -271,13 +340,24 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
271
340
|
implementation.defaultChannel = getConfig().getString("defaultChannel", "")!
|
|
272
341
|
}
|
|
273
342
|
self.implementation.autoReset()
|
|
343
|
+
let appHealthTracker = AppHealthTracker(implementation: self.implementation)
|
|
344
|
+
self.appHealthTracker = appHealthTracker
|
|
345
|
+
appHealthTracker.reportPreviousUncleanForegroundExit()
|
|
346
|
+
appHealthTracker.startSession()
|
|
274
347
|
|
|
275
|
-
// Check if app was recently installed/updated BEFORE
|
|
348
|
+
// Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
|
|
276
349
|
self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
|
|
350
|
+
let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
|
|
351
|
+
if nativeBuildVersionChanged {
|
|
352
|
+
self.clearPreviewSessionForNativeBuildChange()
|
|
353
|
+
}
|
|
354
|
+
self.leavePreviewSessionForLaunchURLIfNeeded()
|
|
277
355
|
|
|
278
356
|
if resetWhenUpdate {
|
|
279
|
-
self.
|
|
357
|
+
let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
|
|
358
|
+
self.cleanupObsoleteVersions(didResetCurrentBundle: didResetCurrentBundle)
|
|
280
359
|
}
|
|
360
|
+
self.reportNativeVersionStatsIfChanged()
|
|
281
361
|
|
|
282
362
|
// Load the server
|
|
283
363
|
// This is very much swift specific, android does not do that
|
|
@@ -290,9 +370,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
290
370
|
logger.error("unable to force reload, the plugin might fallback to the builtin version")
|
|
291
371
|
}
|
|
292
372
|
|
|
293
|
-
|
|
294
|
-
nc.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
295
|
-
nc.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
373
|
+
self.registerNotificationObservers()
|
|
296
374
|
|
|
297
375
|
// Check for 'kill' delay condition on app launch
|
|
298
376
|
// This handles cases where the app was killed (willTerminateNotification is not reliable for system kills)
|
|
@@ -300,6 +378,47 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
300
378
|
|
|
301
379
|
self.appMovedToForeground()
|
|
302
380
|
self.checkForUpdateAfterDelay()
|
|
381
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private func registerNotificationObservers() {
|
|
385
|
+
let notificationCenter = NotificationCenter.default
|
|
386
|
+
notificationCenter.addObserver(
|
|
387
|
+
self,
|
|
388
|
+
selector: #selector(appMovedToBackground),
|
|
389
|
+
name: UIApplication.didEnterBackgroundNotification,
|
|
390
|
+
object: nil
|
|
391
|
+
)
|
|
392
|
+
notificationCenter.addObserver(
|
|
393
|
+
self,
|
|
394
|
+
selector: #selector(appMovedToForeground),
|
|
395
|
+
name: UIApplication.willEnterForegroundNotification,
|
|
396
|
+
object: nil
|
|
397
|
+
)
|
|
398
|
+
notificationCenter.addObserver(
|
|
399
|
+
self,
|
|
400
|
+
selector: #selector(appWillTerminate),
|
|
401
|
+
name: UIApplication.willTerminateNotification,
|
|
402
|
+
object: nil
|
|
403
|
+
)
|
|
404
|
+
notificationCenter.addObserver(
|
|
405
|
+
self,
|
|
406
|
+
selector: #selector(appDidReceiveMemoryWarning),
|
|
407
|
+
name: UIApplication.didReceiveMemoryWarningNotification,
|
|
408
|
+
object: nil
|
|
409
|
+
)
|
|
410
|
+
notificationCenter.addObserver(
|
|
411
|
+
self,
|
|
412
|
+
selector: #selector(handleOpenURLForPreviewSession(notification:)),
|
|
413
|
+
name: Notification.Name.capacitorOpenURL,
|
|
414
|
+
object: nil
|
|
415
|
+
)
|
|
416
|
+
notificationCenter.addObserver(
|
|
417
|
+
self,
|
|
418
|
+
selector: #selector(handleOpenURLForPreviewSession(notification:)),
|
|
419
|
+
name: Notification.Name.capacitorOpenUniversalLink,
|
|
420
|
+
object: nil
|
|
421
|
+
)
|
|
303
422
|
}
|
|
304
423
|
|
|
305
424
|
private func syncKeepUrlPathFlag(enabled: Bool) {
|
|
@@ -349,6 +468,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
349
468
|
}
|
|
350
469
|
}
|
|
351
470
|
|
|
471
|
+
@objc private func appWillTerminate() {
|
|
472
|
+
appHealthTracker?.markForeground(false)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@objc private func appDidReceiveMemoryWarning() {
|
|
476
|
+
appHealthTracker?.reportMemoryWarning()
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
@objc func reportWebViewError(_ call: CAPPluginCall) {
|
|
480
|
+
guard let webViewStatsReporter = webViewStatsReporter else {
|
|
481
|
+
call.resolve()
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
webViewStatsReporter.reportError(call)
|
|
485
|
+
}
|
|
486
|
+
|
|
352
487
|
private func initialLoad() -> Bool {
|
|
353
488
|
guard let bridge = self.bridge else { return false }
|
|
354
489
|
if keepUrlPathAfterReload {
|
|
@@ -392,7 +527,109 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
392
527
|
semaphoreReady.signal()
|
|
393
528
|
}
|
|
394
529
|
|
|
395
|
-
|
|
530
|
+
func storedNativeBuildVersion() -> String {
|
|
531
|
+
UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
func hasNativeBuildVersionChanged() -> Bool {
|
|
535
|
+
let previous = self.storedNativeBuildVersion()
|
|
536
|
+
return previous != "0" && self.currentBuildVersion != previous
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
func reportNativeVersionStatsIfChanged() {
|
|
540
|
+
self.reportNativeVersionStatsIfChanged(
|
|
541
|
+
currentVersionBuild: self.implementation.versionBuild,
|
|
542
|
+
currentVersionCode: self.currentBuildVersion,
|
|
543
|
+
currentVersionOs: UIDevice.current.systemVersion
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
func reportNativeVersionStatsIfChanged(currentVersionBuild: String?, currentVersionCode: String?, currentVersionOs: String?) {
|
|
548
|
+
let normalizedVersionBuild = self.normalizedStatsValue(currentVersionBuild)
|
|
549
|
+
let normalizedVersionCode = self.normalizedStatsValue(currentVersionCode)
|
|
550
|
+
let normalizedVersionOs = self.normalizedStatsValue(currentVersionOs)
|
|
551
|
+
let userDefaults = UserDefaults.standard
|
|
552
|
+
let previousVersionOs = userDefaults.string(forKey: self.lastVersionOsDefaultsKey) ?? ""
|
|
553
|
+
let previousVersionBuild = userDefaults.string(forKey: self.lastVersionBuildDefaultsKey) ?? ""
|
|
554
|
+
let previousVersionCode = userDefaults.string(forKey: self.lastVersionCodeDefaultsKey) ?? ""
|
|
555
|
+
let osVersionChanged = !normalizedVersionOs.isEmpty && !previousVersionOs.isEmpty && previousVersionOs != normalizedVersionOs
|
|
556
|
+
|
|
557
|
+
if osVersionChanged {
|
|
558
|
+
self.implementation.sendStats(
|
|
559
|
+
action: self.osVersionChangedAction,
|
|
560
|
+
versionName: self.implementation.getCurrentBundle().getVersionName(),
|
|
561
|
+
oldVersionName: "",
|
|
562
|
+
metadata: [
|
|
563
|
+
"previous_version_os": previousVersionOs,
|
|
564
|
+
"current_version_os": normalizedVersionOs
|
|
565
|
+
],
|
|
566
|
+
onSent: { [weak self] in
|
|
567
|
+
self?.persistLastVersionOs(normalizedVersionOs)
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
let hasPreviousNativeVersion = !previousVersionBuild.isEmpty || !previousVersionCode.isEmpty
|
|
573
|
+
let nativeVersionChanged = hasPreviousNativeVersion &&
|
|
574
|
+
(previousVersionBuild != normalizedVersionBuild || previousVersionCode != normalizedVersionCode)
|
|
575
|
+
if nativeVersionChanged {
|
|
576
|
+
self.implementation.sendStats(
|
|
577
|
+
action: self.nativeAppVersionChangedAction,
|
|
578
|
+
versionName: self.implementation.getCurrentBundle().getVersionName(),
|
|
579
|
+
oldVersionName: "",
|
|
580
|
+
metadata: [
|
|
581
|
+
"previous_version_build": previousVersionBuild,
|
|
582
|
+
"current_version_build": normalizedVersionBuild,
|
|
583
|
+
"previous_version_code": previousVersionCode,
|
|
584
|
+
"current_version_code": normalizedVersionCode
|
|
585
|
+
],
|
|
586
|
+
onSent: { [weak self] in
|
|
587
|
+
self?.persistLastNativeAppVersion(build: normalizedVersionBuild, code: normalizedVersionCode)
|
|
588
|
+
}
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if !osVersionChanged || !nativeVersionChanged {
|
|
593
|
+
if !osVersionChanged {
|
|
594
|
+
userDefaults.set(normalizedVersionOs, forKey: self.lastVersionOsDefaultsKey)
|
|
595
|
+
}
|
|
596
|
+
if !nativeVersionChanged {
|
|
597
|
+
userDefaults.set(normalizedVersionBuild, forKey: self.lastVersionBuildDefaultsKey)
|
|
598
|
+
userDefaults.set(normalizedVersionCode, forKey: self.lastVersionCodeDefaultsKey)
|
|
599
|
+
}
|
|
600
|
+
userDefaults.synchronize()
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private func persistLastVersionOs(_ versionOs: String) {
|
|
605
|
+
UserDefaults.standard.set(versionOs, forKey: self.lastVersionOsDefaultsKey)
|
|
606
|
+
UserDefaults.standard.synchronize()
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private func persistLastNativeAppVersion(build: String, code: String) {
|
|
610
|
+
UserDefaults.standard.set(build, forKey: self.lastVersionBuildDefaultsKey)
|
|
611
|
+
UserDefaults.standard.set(code, forKey: self.lastVersionCodeDefaultsKey)
|
|
612
|
+
UserDefaults.standard.synchronize()
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private func normalizedStatsValue(_ value: String?) -> String {
|
|
616
|
+
value ?? ""
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
@discardableResult
|
|
620
|
+
func resetCurrentBundleForNativeBuildChangeIfNeeded() -> Bool {
|
|
621
|
+
let previous = self.storedNativeBuildVersion()
|
|
622
|
+
guard previous != "0" && self.currentBuildVersion != previous else {
|
|
623
|
+
return false
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Reset startup state synchronously so initialLoad() boots from the builtin bundle.
|
|
627
|
+
self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting startup bundle to builtin.")
|
|
628
|
+
self.implementation.reset(isInternal: true)
|
|
629
|
+
return true
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private func cleanupObsoleteVersions(didResetCurrentBundle: Bool = false) {
|
|
396
633
|
cleanupThread = Thread {
|
|
397
634
|
self.cleanupLock.lock()
|
|
398
635
|
defer {
|
|
@@ -427,9 +664,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
427
664
|
// 1. Write "LatestVersionNative" - this fixes the part 1 of this bug
|
|
428
665
|
// 2. Compare both keys. If any is not equal to "currentBuildVersion", then revert to builtin version. This fixes the part 2 of this bug
|
|
429
666
|
|
|
430
|
-
let previous =
|
|
667
|
+
let previous = self.storedNativeBuildVersion()
|
|
431
668
|
if previous != "0" && self.currentBuildVersion != previous {
|
|
432
|
-
|
|
669
|
+
if !didResetCurrentBundle {
|
|
670
|
+
self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting current bundle to builtin.")
|
|
671
|
+
self.implementation.reset(isInternal: true)
|
|
672
|
+
}
|
|
433
673
|
let res = self.implementation.list()
|
|
434
674
|
for version in res {
|
|
435
675
|
// Check if thread was cancelled
|
|
@@ -489,11 +729,88 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
489
729
|
logger.info("Cleanup finished, proceeding with download")
|
|
490
730
|
}
|
|
491
731
|
|
|
732
|
+
private func resolveCall(_ call: CAPPluginCall, data: PluginCallResultData? = nil) {
|
|
733
|
+
let resolve = {
|
|
734
|
+
let savedCall = self.bridge?.savedCall(withID: call.callbackId)
|
|
735
|
+
let targetCall = savedCall ?? call
|
|
736
|
+
|
|
737
|
+
if let data {
|
|
738
|
+
targetCall.resolve(data)
|
|
739
|
+
} else {
|
|
740
|
+
targetCall.resolve()
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if savedCall != nil {
|
|
744
|
+
self.bridge?.releaseCall(withID: call.callbackId)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if Thread.isMainThread {
|
|
749
|
+
resolve()
|
|
750
|
+
} else {
|
|
751
|
+
DispatchQueue.main.async {
|
|
752
|
+
resolve()
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private func rejectCall(_ call: CAPPluginCall, message: String, code: String? = nil, error: Error? = nil, data: PluginCallResultData? = nil) {
|
|
758
|
+
let reject = {
|
|
759
|
+
let savedCall = self.bridge?.savedCall(withID: call.callbackId)
|
|
760
|
+
let targetCall = savedCall ?? call
|
|
761
|
+
|
|
762
|
+
targetCall.reject(message, code, error, data)
|
|
763
|
+
|
|
764
|
+
if savedCall != nil {
|
|
765
|
+
self.bridge?.releaseCall(withID: call.callbackId)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if Thread.isMainThread {
|
|
770
|
+
reject()
|
|
771
|
+
} else {
|
|
772
|
+
DispatchQueue.main.async {
|
|
773
|
+
reject()
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private func saveCallForAsyncHandling(_ call: CAPPluginCall) {
|
|
779
|
+
bridge?.saveCall(call)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private func notifyListenersOnMain(_ eventName: String, data: JSObject) {
|
|
783
|
+
let notify = {
|
|
784
|
+
self.notifyListeners(eventName, data: data)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if Thread.isMainThread {
|
|
788
|
+
notify()
|
|
789
|
+
} else {
|
|
790
|
+
DispatchQueue.main.async {
|
|
791
|
+
notify()
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private func bundlePayload(_ bundleInfo: BundleInfo) -> JSObject {
|
|
797
|
+
var payload: JSObject = [:]
|
|
798
|
+
for (key, value) in bundleInfo.toJSON() {
|
|
799
|
+
payload[key] = value
|
|
800
|
+
}
|
|
801
|
+
return payload
|
|
802
|
+
}
|
|
803
|
+
|
|
492
804
|
@objc func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false, bundle: BundleInfo? = nil) {
|
|
493
805
|
let bundleInfo = bundle ?? self.implementation.getBundleInfo(id: id)
|
|
494
|
-
|
|
806
|
+
var downloadPayload: JSObject = [:]
|
|
807
|
+
downloadPayload["percent"] = percent
|
|
808
|
+
downloadPayload["bundle"] = bundlePayload(bundleInfo)
|
|
809
|
+
self.notifyListenersOnMain("download", data: downloadPayload)
|
|
495
810
|
if percent == 100 {
|
|
496
|
-
|
|
811
|
+
var downloadCompletePayload: JSObject = [:]
|
|
812
|
+
downloadCompletePayload["bundle"] = bundlePayload(bundleInfo)
|
|
813
|
+
self.notifyListenersOnMain("downloadComplete", data: downloadCompletePayload)
|
|
497
814
|
self.implementation.sendStats(action: "download_complete", versionName: bundleInfo.getVersionName())
|
|
498
815
|
} else if percent.isMultiple(of: 10) || ignoreMultipleOfTen {
|
|
499
816
|
self.implementation.sendStats(action: "download_\(percent)", versionName: bundleInfo.getVersionName())
|
|
@@ -542,206 +859,1420 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
542
859
|
call.resolve()
|
|
543
860
|
}
|
|
544
861
|
|
|
545
|
-
@objc func setChannelUrl(_ call: CAPPluginCall) {
|
|
546
|
-
if !getConfig().getBoolean("allowModifyUrl", false) {
|
|
547
|
-
logger.error("setChannelUrl called without allowModifyUrl")
|
|
548
|
-
call.reject("setChannelUrl called without allowModifyUrl set allowModifyUrl in your config to true to allow it")
|
|
549
|
-
return
|
|
862
|
+
@objc func setChannelUrl(_ call: CAPPluginCall) {
|
|
863
|
+
if !getConfig().getBoolean("allowModifyUrl", false) {
|
|
864
|
+
logger.error("setChannelUrl called without allowModifyUrl")
|
|
865
|
+
call.reject("setChannelUrl called without allowModifyUrl set allowModifyUrl in your config to true to allow it")
|
|
866
|
+
return
|
|
867
|
+
}
|
|
868
|
+
guard let url = call.getString("url") else {
|
|
869
|
+
logger.error("setChannelUrl called without url")
|
|
870
|
+
call.reject("setChannelUrl called without url")
|
|
871
|
+
return
|
|
872
|
+
}
|
|
873
|
+
self.implementation.channelUrl = url
|
|
874
|
+
if persistModifyUrl {
|
|
875
|
+
UserDefaults.standard.set(url, forKey: channelUrlDefaultsKey)
|
|
876
|
+
UserDefaults.standard.synchronize()
|
|
877
|
+
}
|
|
878
|
+
call.resolve()
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
@objc func getBuiltinVersion(_ call: CAPPluginCall) {
|
|
882
|
+
call.resolve(["version": implementation.versionBuild])
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
@objc func getDeviceId(_ call: CAPPluginCall) {
|
|
886
|
+
call.resolve(["deviceId": implementation.deviceID])
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
890
|
+
call.resolve(["version": self.pluginVersion])
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private func manifestEntries(from manifestArray: [Any]?) -> [ManifestEntry]? {
|
|
894
|
+
guard let manifestArray = manifestArray else {
|
|
895
|
+
return nil
|
|
896
|
+
}
|
|
897
|
+
var manifestEntries: [ManifestEntry] = []
|
|
898
|
+
for item in manifestArray {
|
|
899
|
+
if let manifestDict = item as? [String: Any] {
|
|
900
|
+
manifestEntries.append(ManifestEntry(
|
|
901
|
+
file_name: manifestDict["file_name"] as? String,
|
|
902
|
+
file_hash: manifestDict["file_hash"] as? String,
|
|
903
|
+
download_url: manifestDict["download_url"] as? String
|
|
904
|
+
))
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return manifestEntries
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private struct PreviewPayload: Decodable {
|
|
911
|
+
let version: String?
|
|
912
|
+
let url: String?
|
|
913
|
+
let checksum: String?
|
|
914
|
+
let sessionKey: String?
|
|
915
|
+
let manifest: [ManifestEntry]?
|
|
916
|
+
let message: String?
|
|
917
|
+
let error: String?
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private func normalizedPreviewMetadataValue(_ rawValue: String?) -> String? {
|
|
921
|
+
guard let rawValue else {
|
|
922
|
+
return nil
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
let value = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
926
|
+
guard !value.isEmpty else {
|
|
927
|
+
return nil
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
let lowercased = value.lowercased()
|
|
931
|
+
guard lowercased != "undefined", lowercased != "null" else {
|
|
932
|
+
return nil
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return value
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private func previewSessions() -> [String: [String: Any]] {
|
|
939
|
+
guard let rawSessions = UserDefaults.standard.dictionary(forKey: self.previewSessionsDefaultsKey) else {
|
|
940
|
+
return [:]
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
var sessions: [String: [String: Any]] = [:]
|
|
944
|
+
for (id, rawValue) in rawSessions {
|
|
945
|
+
if let session = rawValue as? [String: Any] {
|
|
946
|
+
sessions[id] = session
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return sessions
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
private func savePreviewSessions(_ sessions: [String: [String: Any]]) {
|
|
953
|
+
UserDefaults.standard.set(sessions, forKey: self.previewSessionsDefaultsKey)
|
|
954
|
+
UserDefaults.standard.synchronize()
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private func metadataString(_ metadata: [String: Any], _ key: String) -> String? {
|
|
958
|
+
self.normalizedPreviewMetadataValue(metadata[key] as? String)
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private func currentPreviewMetadataValue(forKey key: String) -> String? {
|
|
962
|
+
self.normalizedPreviewMetadataValue(UserDefaults.standard.string(forKey: key))
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private func previewInfo(
|
|
966
|
+
id: String,
|
|
967
|
+
metadata: [String: Any],
|
|
968
|
+
availableBundleIds: Set<String>,
|
|
969
|
+
currentBundleId: String
|
|
970
|
+
) -> [String: Any]? {
|
|
971
|
+
let bundle = self.implementation.getBundleInfo(id: id)
|
|
972
|
+
if !bundle.isBuiltin() && !availableBundleIds.contains(id) {
|
|
973
|
+
return nil
|
|
974
|
+
}
|
|
975
|
+
if bundle.isDeleted() || bundle.isErrorStatus() {
|
|
976
|
+
return nil
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
var info: [String: Any] = [
|
|
980
|
+
"id": id,
|
|
981
|
+
"bundle": bundle.toJSON(),
|
|
982
|
+
"createdAt": self.metadataString(metadata, "createdAt") ?? Date().iso8601withFractionalSeconds,
|
|
983
|
+
"updatedAt": self.metadataString(metadata, "updatedAt") ?? Date().iso8601withFractionalSeconds,
|
|
984
|
+
"lastUsedAt": self.metadataString(metadata, "lastUsedAt") ?? Date().iso8601withFractionalSeconds,
|
|
985
|
+
"isActive": self.previewSessionEnabled && currentBundleId == id
|
|
986
|
+
]
|
|
987
|
+
|
|
988
|
+
for key in ["name", "source", "appId", "payloadUrl"] {
|
|
989
|
+
if let value = self.metadataString(metadata, key) {
|
|
990
|
+
info[key] = value
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return info
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private func listPreviewInfos(cleanup: Bool = true) -> [[String: Any]] {
|
|
998
|
+
let availableBundleIds = Set(self.implementation.list().map { $0.getId() })
|
|
999
|
+
let currentBundleId = self.implementation.getCurrentBundleId()
|
|
1000
|
+
var sessions = self.previewSessions()
|
|
1001
|
+
var previews: [[String: Any]] = []
|
|
1002
|
+
var staleIds: [String] = []
|
|
1003
|
+
|
|
1004
|
+
for (id, metadata) in sessions {
|
|
1005
|
+
if let info = self.previewInfo(
|
|
1006
|
+
id: id,
|
|
1007
|
+
metadata: metadata,
|
|
1008
|
+
availableBundleIds: availableBundleIds,
|
|
1009
|
+
currentBundleId: currentBundleId
|
|
1010
|
+
) {
|
|
1011
|
+
previews.append(info)
|
|
1012
|
+
} else {
|
|
1013
|
+
staleIds.append(id)
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if cleanup && !staleIds.isEmpty {
|
|
1018
|
+
for id in staleIds {
|
|
1019
|
+
sessions.removeValue(forKey: id)
|
|
1020
|
+
}
|
|
1021
|
+
self.savePreviewSessions(sessions)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return previews.sorted { first, second in
|
|
1025
|
+
let firstUsed = first["lastUsedAt"] as? String ?? ""
|
|
1026
|
+
let secondUsed = second["lastUsedAt"] as? String ?? ""
|
|
1027
|
+
return firstUsed > secondUsed
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private func storedPreviewInfo(id: String) -> [String: Any]? {
|
|
1032
|
+
let sessions = self.previewSessions()
|
|
1033
|
+
guard let metadata = sessions[id] else {
|
|
1034
|
+
return nil
|
|
1035
|
+
}
|
|
1036
|
+
let availableBundleIds = Set(self.implementation.list().map { $0.getId() })
|
|
1037
|
+
return self.previewInfo(
|
|
1038
|
+
id: id,
|
|
1039
|
+
metadata: metadata,
|
|
1040
|
+
availableBundleIds: availableBundleIds,
|
|
1041
|
+
currentBundleId: self.implementation.getCurrentBundleId()
|
|
1042
|
+
)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
@discardableResult
|
|
1046
|
+
private func recordPreviewBundle(_ bundle: BundleInfo, replacing oldId: String? = nil) -> [String: Any] {
|
|
1047
|
+
let now = Date().iso8601withFractionalSeconds
|
|
1048
|
+
var sessions = self.previewSessions()
|
|
1049
|
+
let id = bundle.getId()
|
|
1050
|
+
let replacingPreview = oldId.map { $0 != id } ?? false
|
|
1051
|
+
var metadata = sessions[id] ?? (replacingPreview ? sessions[oldId ?? ""] ?? [:] : [:])
|
|
1052
|
+
|
|
1053
|
+
if metadata["createdAt"] == nil {
|
|
1054
|
+
metadata["createdAt"] = now
|
|
1055
|
+
}
|
|
1056
|
+
metadata["updatedAt"] = now
|
|
1057
|
+
if metadata["lastUsedAt"] == nil || self.implementation.getCurrentBundleId() == id {
|
|
1058
|
+
metadata["lastUsedAt"] = now
|
|
1059
|
+
}
|
|
1060
|
+
metadata["version"] = bundle.getVersionName()
|
|
1061
|
+
|
|
1062
|
+
if !replacingPreview {
|
|
1063
|
+
if let appId = self.currentPreviewMetadataValue(forKey: self.previewAppIdDefaultsKey) {
|
|
1064
|
+
metadata["appId"] = appId
|
|
1065
|
+
} else {
|
|
1066
|
+
metadata.removeValue(forKey: "appId")
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if let payloadUrl = self.currentPreviewMetadataValue(forKey: self.previewPayloadUrlDefaultsKey) {
|
|
1070
|
+
metadata["payloadUrl"] = payloadUrl
|
|
1071
|
+
} else {
|
|
1072
|
+
metadata.removeValue(forKey: "payloadUrl")
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if !replacingPreview {
|
|
1077
|
+
if let name = self.currentPreviewMetadataValue(forKey: self.previewNameDefaultsKey) {
|
|
1078
|
+
metadata["name"] = name
|
|
1079
|
+
} else {
|
|
1080
|
+
metadata.removeValue(forKey: "name")
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if self.metadataString(metadata, "name") == nil {
|
|
1084
|
+
metadata["name"] = bundle.getVersionName()
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if !replacingPreview {
|
|
1088
|
+
if let source = self.currentPreviewMetadataValue(forKey: self.previewSourceDefaultsKey) {
|
|
1089
|
+
metadata["source"] = source
|
|
1090
|
+
} else {
|
|
1091
|
+
metadata.removeValue(forKey: "source")
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if let oldId, oldId != id {
|
|
1096
|
+
sessions.removeValue(forKey: oldId)
|
|
1097
|
+
}
|
|
1098
|
+
sessions[id] = metadata
|
|
1099
|
+
self.savePreviewSessions(sessions)
|
|
1100
|
+
|
|
1101
|
+
return self.storedPreviewInfo(id: id) ?? [
|
|
1102
|
+
"id": id,
|
|
1103
|
+
"bundle": bundle.toJSON(),
|
|
1104
|
+
"createdAt": now,
|
|
1105
|
+
"updatedAt": now,
|
|
1106
|
+
"lastUsedAt": now,
|
|
1107
|
+
"isActive": self.previewSessionEnabled && self.implementation.getCurrentBundleId() == id
|
|
1108
|
+
]
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
private func updateCurrentPreviewSessionMetadata(from preview: [String: Any]) {
|
|
1112
|
+
if let appId = self.metadataString(preview, "appId") {
|
|
1113
|
+
self.implementation.appId = appId
|
|
1114
|
+
UserDefaults.standard.set(appId, forKey: self.previewAppIdDefaultsKey)
|
|
1115
|
+
} else {
|
|
1116
|
+
self.restorePreviewPreviousAppId()
|
|
1117
|
+
UserDefaults.standard.removeObject(forKey: self.previewAppIdDefaultsKey)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if let payloadUrl = self.metadataString(preview, "payloadUrl") {
|
|
1121
|
+
UserDefaults.standard.set(payloadUrl, forKey: self.previewPayloadUrlDefaultsKey)
|
|
1122
|
+
} else {
|
|
1123
|
+
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if let name = self.metadataString(preview, "name") {
|
|
1127
|
+
UserDefaults.standard.set(name, forKey: self.previewNameDefaultsKey)
|
|
1128
|
+
} else {
|
|
1129
|
+
UserDefaults.standard.removeObject(forKey: self.previewNameDefaultsKey)
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if let source = self.metadataString(preview, "source") {
|
|
1133
|
+
UserDefaults.standard.set(source, forKey: self.previewSourceDefaultsKey)
|
|
1134
|
+
} else {
|
|
1135
|
+
UserDefaults.standard.removeObject(forKey: self.previewSourceDefaultsKey)
|
|
1136
|
+
}
|
|
1137
|
+
UserDefaults.standard.synchronize()
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private func makePreviewError(_ message: String) -> NSError {
|
|
1141
|
+
NSError(domain: "CapacitorUpdaterPreview", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
private func downloadBundle(urlString: String, version: String, sessionKey: String, checksum rawChecksum: String, manifestEntries: [ManifestEntry]?) throws -> BundleInfo {
|
|
1145
|
+
guard let url = URL(string: urlString) else {
|
|
1146
|
+
throw makePreviewError("Invalid download URL")
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
var checksum = rawChecksum
|
|
1150
|
+
let next: BundleInfo
|
|
1151
|
+
if let manifestEntries = manifestEntries {
|
|
1152
|
+
next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
|
|
1153
|
+
} else {
|
|
1154
|
+
next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if self.implementation.publicKey != "" && checksum == "" {
|
|
1158
|
+
self.logger.error("Public key present but no checksum provided")
|
|
1159
|
+
self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
|
|
1160
|
+
let id = next.getId()
|
|
1161
|
+
let resDel = self.implementation.delete(id: id)
|
|
1162
|
+
if !resDel {
|
|
1163
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1164
|
+
}
|
|
1165
|
+
throw ObjectSavableError.checksum
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
1169
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
1170
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
1171
|
+
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
1172
|
+
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
1173
|
+
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
1174
|
+
let id = next.getId()
|
|
1175
|
+
let resDel = self.implementation.delete(id: id)
|
|
1176
|
+
if !resDel {
|
|
1177
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1178
|
+
}
|
|
1179
|
+
throw ObjectSavableError.checksum
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
1183
|
+
return next
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
@objc func download(_ call: CAPPluginCall) {
|
|
1187
|
+
guard let urlString = call.getString("url") else {
|
|
1188
|
+
logger.error("Download called without url")
|
|
1189
|
+
call.reject("Download called without url")
|
|
1190
|
+
return
|
|
1191
|
+
}
|
|
1192
|
+
guard let version = call.getString("version") else {
|
|
1193
|
+
logger.error("Download called without version")
|
|
1194
|
+
call.reject("Download called without version")
|
|
1195
|
+
return
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
let sessionKey = call.getString("sessionKey", "")
|
|
1199
|
+
let checksum = call.getString("checksum", "")
|
|
1200
|
+
let manifestArray = call.getArray("manifest")
|
|
1201
|
+
logger.info("Downloading \(urlString)")
|
|
1202
|
+
self.saveCallForAsyncHandling(call)
|
|
1203
|
+
self.runBackgroundDownloadWork {
|
|
1204
|
+
do {
|
|
1205
|
+
let next = try self.downloadBundle(
|
|
1206
|
+
urlString: urlString,
|
|
1207
|
+
version: version,
|
|
1208
|
+
sessionKey: sessionKey,
|
|
1209
|
+
checksum: checksum,
|
|
1210
|
+
manifestEntries: self.manifestEntries(from: manifestArray)
|
|
1211
|
+
)
|
|
1212
|
+
var updateAvailablePayload: JSObject = [:]
|
|
1213
|
+
updateAvailablePayload["bundle"] = self.bundlePayload(next)
|
|
1214
|
+
self.notifyListenersOnMain("updateAvailable", data: updateAvailablePayload)
|
|
1215
|
+
self.resolveCall(call, data: next.toJSON())
|
|
1216
|
+
} catch {
|
|
1217
|
+
self.logger.error("Failed to download from: \(urlString) \(error.localizedDescription)")
|
|
1218
|
+
var downloadFailedPayload: JSObject = [:]
|
|
1219
|
+
downloadFailedPayload["version"] = version
|
|
1220
|
+
self.notifyListenersOnMain("downloadFailed", data: downloadFailedPayload)
|
|
1221
|
+
self.implementation.sendStats(action: "download_fail")
|
|
1222
|
+
self.rejectCall(call, message: "Failed to download from: \(urlString) - \(error.localizedDescription)")
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private func currentReloadDestination() -> URL {
|
|
1228
|
+
let id = self.implementation.getCurrentBundleId()
|
|
1229
|
+
if BundleInfo.ID_BUILTIN == id {
|
|
1230
|
+
return Bundle.main.resourceURL!.appendingPathComponent("public")
|
|
1231
|
+
} else {
|
|
1232
|
+
return self.implementation.getBundleDirectory(id: id)
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
private func applyCurrentBundleToBridge(_ bridge: CAPBridgeProtocol) -> Bool {
|
|
1237
|
+
let id = self.implementation.getCurrentBundleId()
|
|
1238
|
+
let dest = self.currentReloadDestination()
|
|
1239
|
+
logger.info("Reloading \(id)")
|
|
1240
|
+
|
|
1241
|
+
guard let vc = bridge.viewController as? CAPBridgeViewController else {
|
|
1242
|
+
self.logger.error("Cannot get viewController")
|
|
1243
|
+
return false
|
|
1244
|
+
}
|
|
1245
|
+
guard let capBridge = vc.bridge else {
|
|
1246
|
+
self.logger.error("Cannot get capBridge")
|
|
1247
|
+
return false
|
|
1248
|
+
}
|
|
1249
|
+
if self.keepUrlPathAfterReload {
|
|
1250
|
+
if let currentURL = vc.webView?.url {
|
|
1251
|
+
capBridge.setServerBasePath(dest.path)
|
|
1252
|
+
var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
|
|
1253
|
+
urlComponents.path = currentURL.path
|
|
1254
|
+
urlComponents.query = currentURL.query
|
|
1255
|
+
urlComponents.fragment = currentURL.fragment
|
|
1256
|
+
if let finalUrl = urlComponents.url {
|
|
1257
|
+
_ = vc.webView?.load(URLRequest(url: finalUrl))
|
|
1258
|
+
} else {
|
|
1259
|
+
self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
|
|
1260
|
+
vc.setServerBasePath(path: dest.path)
|
|
1261
|
+
}
|
|
1262
|
+
} else {
|
|
1263
|
+
self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
|
|
1264
|
+
vc.setServerBasePath(path: dest.path)
|
|
1265
|
+
}
|
|
1266
|
+
} else {
|
|
1267
|
+
vc.setServerBasePath(path: dest.path)
|
|
1268
|
+
}
|
|
1269
|
+
return true
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
func restoreLiveBundleStateAfterFailedReload() {
|
|
1273
|
+
guard let bridge = self.bridge else {
|
|
1274
|
+
return
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
let restoreLiveState = {
|
|
1278
|
+
_ = self.applyCurrentBundleToBridge(bridge)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if Thread.isMainThread {
|
|
1282
|
+
restoreLiveState()
|
|
1283
|
+
} else {
|
|
1284
|
+
DispatchQueue.main.sync {
|
|
1285
|
+
restoreLiveState()
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
public func _reload() -> Bool {
|
|
1291
|
+
guard let bridge = self.bridge else { return false }
|
|
1292
|
+
self.semaphoreUp()
|
|
1293
|
+
|
|
1294
|
+
let performReload: () -> Bool = {
|
|
1295
|
+
guard self.applyCurrentBundleToBridge(bridge) else {
|
|
1296
|
+
return false
|
|
1297
|
+
}
|
|
1298
|
+
self.checkAppReady()
|
|
1299
|
+
self.notifyListeners("appReloaded", data: [:])
|
|
1300
|
+
return true
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if Thread.isMainThread {
|
|
1304
|
+
return performReload()
|
|
1305
|
+
} else {
|
|
1306
|
+
var result = false
|
|
1307
|
+
DispatchQueue.main.sync {
|
|
1308
|
+
result = performReload()
|
|
1309
|
+
}
|
|
1310
|
+
return result
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
func reloadWithoutWaitingForAppReady() -> Bool {
|
|
1315
|
+
guard let bridge = self.bridge else { return false }
|
|
1316
|
+
|
|
1317
|
+
let performReload: () -> Bool = {
|
|
1318
|
+
guard self.applyCurrentBundleToBridge(bridge) else {
|
|
1319
|
+
return false
|
|
1320
|
+
}
|
|
1321
|
+
self.checkAppReady()
|
|
1322
|
+
self.notifyListeners("appReloaded", data: [:])
|
|
1323
|
+
return true
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if Thread.isMainThread {
|
|
1327
|
+
return performReload()
|
|
1328
|
+
} else {
|
|
1329
|
+
var result = false
|
|
1330
|
+
DispatchQueue.main.sync {
|
|
1331
|
+
result = performReload()
|
|
1332
|
+
}
|
|
1333
|
+
return result
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
@objc func reload(_ call: CAPPluginCall) {
|
|
1338
|
+
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1339
|
+
let next: BundleInfo? = self.implementation.getNextBundle()
|
|
1340
|
+
|
|
1341
|
+
if !self.isPreviewSessionStateActive(),
|
|
1342
|
+
let next = next,
|
|
1343
|
+
!next.isErrorStatus(),
|
|
1344
|
+
next.getId() != current.getId() {
|
|
1345
|
+
let previousState = self.implementation.captureResetState()
|
|
1346
|
+
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
1347
|
+
logger.info("Applying pending bundle before reload: \(next.toString())")
|
|
1348
|
+
let didApplyPendingBundle: Bool
|
|
1349
|
+
if next.isBuiltin() {
|
|
1350
|
+
self.implementation.prepareResetStateForTransition()
|
|
1351
|
+
didApplyPendingBundle = true
|
|
1352
|
+
} else {
|
|
1353
|
+
didApplyPendingBundle = self.implementation.stagePendingReload(bundle: next)
|
|
1354
|
+
}
|
|
1355
|
+
if didApplyPendingBundle && self._reload() {
|
|
1356
|
+
if next.isBuiltin() {
|
|
1357
|
+
self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: false)
|
|
1358
|
+
} else {
|
|
1359
|
+
self.implementation.finalizePendingReload(bundle: next, previousBundleName: previousBundleName)
|
|
1360
|
+
}
|
|
1361
|
+
self.notifyBundleSet(next)
|
|
1362
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
1363
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
1364
|
+
call.resolve()
|
|
1365
|
+
return
|
|
1366
|
+
}
|
|
1367
|
+
self.implementation.restoreResetState(previousState)
|
|
1368
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
1369
|
+
logger.error("Reload failed after applying pending bundle: \(next.toString())")
|
|
1370
|
+
call.reject("Reload failed after applying pending bundle")
|
|
1371
|
+
return
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if self._reload() {
|
|
1375
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
1376
|
+
call.resolve()
|
|
1377
|
+
} else {
|
|
1378
|
+
logger.error("Reload failed")
|
|
1379
|
+
call.reject("Reload failed")
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private func applyDownloadedBundleForDirectUpdate(_ next: BundleInfo) -> Bool {
|
|
1384
|
+
let previousState = self.implementation.captureResetState()
|
|
1385
|
+
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
1386
|
+
|
|
1387
|
+
guard self.implementation.stagePendingReload(bundle: next) else {
|
|
1388
|
+
self.implementation.restoreResetState(previousState)
|
|
1389
|
+
logger.error("Direct update failed to stage downloaded bundle: \(next.toString())")
|
|
1390
|
+
return false
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if self._reload() {
|
|
1394
|
+
self.implementation.finalizePendingReload(bundle: next, previousBundleName: previousBundleName)
|
|
1395
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
1396
|
+
return true
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
self.implementation.restoreResetState(previousState)
|
|
1400
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
1401
|
+
logger.error("Direct update reload failed after staging bundle: \(next.toString())")
|
|
1402
|
+
return false
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
@objc func next(_ call: CAPPluginCall) {
|
|
1406
|
+
guard let id = call.getString("id") else {
|
|
1407
|
+
logger.error("Next called without id")
|
|
1408
|
+
call.reject("Next called without id")
|
|
1409
|
+
return
|
|
1410
|
+
}
|
|
1411
|
+
logger.info("Setting next active id \(id)")
|
|
1412
|
+
if !self.implementation.setNextBundle(next: id) {
|
|
1413
|
+
logger.error("Set next version failed. id \(id) does not exist.")
|
|
1414
|
+
call.reject("Set next version failed. id \(id) does not exist.")
|
|
1415
|
+
} else {
|
|
1416
|
+
call.resolve(self.implementation.getBundleInfo(id: id).toJSON())
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
@objc func set(_ call: CAPPluginCall) {
|
|
1421
|
+
guard let id = call.getString("id") else {
|
|
1422
|
+
logger.error("Set called without id")
|
|
1423
|
+
call.reject("Set called without id")
|
|
1424
|
+
return
|
|
1425
|
+
}
|
|
1426
|
+
let res = implementation.set(id: id)
|
|
1427
|
+
logger.info("Set active bundle: \(id)")
|
|
1428
|
+
if !res {
|
|
1429
|
+
logger.info("Bundle successfully set to: \(id) ")
|
|
1430
|
+
call.reject("Update failed, id \(id) doesn't exist")
|
|
1431
|
+
} else if self.previewSessionEnabled {
|
|
1432
|
+
let bundle = self.implementation.getBundleInfo(id: id)
|
|
1433
|
+
_ = self.recordPreviewBundle(bundle)
|
|
1434
|
+
if !self.reloadWithoutWaitingForAppReady() {
|
|
1435
|
+
call.reject("Reload failed after setting preview bundle \(id)")
|
|
1436
|
+
return
|
|
1437
|
+
}
|
|
1438
|
+
self.notifyBundleSet(bundle)
|
|
1439
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
1440
|
+
call.resolve()
|
|
1441
|
+
} else if !self._reload() {
|
|
1442
|
+
call.reject("Reload failed after setting bundle \(id)")
|
|
1443
|
+
} else {
|
|
1444
|
+
self.notifyBundleSet(self.implementation.getBundleInfo(id: id))
|
|
1445
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
1446
|
+
call.resolve()
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
private func isPreviewSessionStateActive() -> Bool {
|
|
1451
|
+
self.previewSessionEnabled || self.isLeavingPreviewForIncomingLink || self.implementation.previewSession
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private func shouldBlockAutoUpdateForPreviewSession() -> Bool {
|
|
1455
|
+
guard self.isPreviewSessionStateActive() else {
|
|
1456
|
+
return false
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
logger.info("Preview session is active. Skipping normal auto-update work.")
|
|
1460
|
+
return true
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
private func clearIncomingPreviewTransition() {
|
|
1464
|
+
self.previewTransitionClearWorkItem?.cancel()
|
|
1465
|
+
self.previewTransitionClearWorkItem = nil
|
|
1466
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
1467
|
+
if !self.previewSessionEnabled {
|
|
1468
|
+
self.implementation.previewSession = false
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private func scheduleIncomingPreviewTransitionFallbackClear() {
|
|
1473
|
+
self.previewTransitionClearWorkItem?.cancel()
|
|
1474
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
1475
|
+
self?.clearIncomingPreviewTransition()
|
|
1476
|
+
}
|
|
1477
|
+
self.previewTransitionClearWorkItem = workItem
|
|
1478
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.appReadyTimeout), execute: workItem)
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
private func preparePreviewFallbackIfNeeded() -> Bool {
|
|
1482
|
+
if self.previewSessionEnabled {
|
|
1483
|
+
return true
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
let current = self.implementation.getCurrentBundle()
|
|
1487
|
+
guard self.implementation.setPreviewFallbackBundle(fallback: current.getId()) else {
|
|
1488
|
+
logger.error("Could not save current bundle as preview fallback")
|
|
1489
|
+
return false
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if let previousNext = self.implementation.getNextBundle(),
|
|
1493
|
+
!previousNext.isDeleted(),
|
|
1494
|
+
!previousNext.isErrorStatus() {
|
|
1495
|
+
UserDefaults.standard.set(previousNext.getId(), forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
1496
|
+
} else {
|
|
1497
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
UserDefaults.standard.set(self.implementation.appId, forKey: self.previewPreviousAppIdDefaultsKey)
|
|
1501
|
+
if let previousDefaultChannel = UserDefaults.standard.object(forKey: self.defaultChannelDefaultsKey) as? String {
|
|
1502
|
+
UserDefaults.standard.set(previousDefaultChannel, forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1503
|
+
UserDefaults.standard.set(true, forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1504
|
+
} else {
|
|
1505
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1506
|
+
UserDefaults.standard.set(false, forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1507
|
+
}
|
|
1508
|
+
UserDefaults.standard.set(self.shakeMenuEnabled, forKey: self.previewPreviousShakeMenuDefaultsKey)
|
|
1509
|
+
UserDefaults.standard.set(self.shakeChannelSelectorEnabled, forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
|
|
1510
|
+
logger.info("Preview session started with fallback bundle: \(current.toString())")
|
|
1511
|
+
return true
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
private func activatePreviewSessionState() {
|
|
1515
|
+
self.clearIncomingPreviewTransition()
|
|
1516
|
+
self.hidePreviewTransitionLoader(reason: "preview-session-started")
|
|
1517
|
+
self.previewSessionEnabled = true
|
|
1518
|
+
self.previewSessionAlertPending = true
|
|
1519
|
+
self.implementation.previewSession = true
|
|
1520
|
+
self.shakeMenuEnabled = true
|
|
1521
|
+
self.syncShakeMenuGestureRecognizer()
|
|
1522
|
+
UserDefaults.standard.set(true, forKey: self.previewSessionDefaultsKey)
|
|
1523
|
+
UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
1524
|
+
UserDefaults.standard.synchronize()
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
@objc func startPreviewSession(_ call: CAPPluginCall) {
|
|
1528
|
+
guard self.allowPreview else {
|
|
1529
|
+
self.hidePreviewTransitionLoader(reason: "preview-session-not-allowed")
|
|
1530
|
+
logger.error("startPreviewSession called without allowPreview")
|
|
1531
|
+
call.reject("startPreviewSession not allowed. Set allowPreview to true in your config to enable it.")
|
|
1532
|
+
return
|
|
1533
|
+
}
|
|
1534
|
+
let previewAppId = self.normalizedPreviewAppId(call.getString("appId"))
|
|
1535
|
+
let rawPayloadUrl = call.getString("payloadUrl")
|
|
1536
|
+
let previewPayloadUrl = self.normalizedPreviewPayloadUrl(rawPayloadUrl)
|
|
1537
|
+
if let rawPayloadUrl = rawPayloadUrl, !rawPayloadUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, previewPayloadUrl == nil {
|
|
1538
|
+
self.hidePreviewTransitionLoader(reason: "preview-session-invalid-payload")
|
|
1539
|
+
logger.error("startPreviewSession called with invalid payloadUrl")
|
|
1540
|
+
call.reject("Invalid preview payloadUrl")
|
|
1541
|
+
return
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
guard self.preparePreviewFallbackIfNeeded() else {
|
|
1545
|
+
self.hidePreviewTransitionLoader(reason: "preview-session-fallback-failed")
|
|
1546
|
+
call.reject("Could not save current bundle as preview fallback")
|
|
1547
|
+
return
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if let previewAppId = previewAppId, !previewAppId.isEmpty {
|
|
1551
|
+
self.implementation.appId = previewAppId
|
|
1552
|
+
UserDefaults.standard.set(previewAppId, forKey: self.previewAppIdDefaultsKey)
|
|
1553
|
+
logger.info("Preview session using appId: \(previewAppId)")
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
if let previewPayloadUrl = previewPayloadUrl {
|
|
1557
|
+
UserDefaults.standard.set(previewPayloadUrl.absoluteString, forKey: self.previewPayloadUrlDefaultsKey)
|
|
1558
|
+
logger.info("Preview session using payload URL")
|
|
1559
|
+
} else {
|
|
1560
|
+
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if let previewName = self.normalizedPreviewMetadataValue(call.getString("name")) {
|
|
1564
|
+
UserDefaults.standard.set(previewName, forKey: self.previewNameDefaultsKey)
|
|
1565
|
+
} else {
|
|
1566
|
+
UserDefaults.standard.removeObject(forKey: self.previewNameDefaultsKey)
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if let previewSource = self.normalizedPreviewMetadataValue(call.getString("source")) {
|
|
1570
|
+
UserDefaults.standard.set(previewSource, forKey: self.previewSourceDefaultsKey)
|
|
1571
|
+
} else {
|
|
1572
|
+
UserDefaults.standard.removeObject(forKey: self.previewSourceDefaultsKey)
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
self.activatePreviewSessionState()
|
|
1576
|
+
call.resolve()
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
@objc func listPreviews(_ call: CAPPluginCall) {
|
|
1580
|
+
guard self.allowPreview else {
|
|
1581
|
+
call.reject("listPreviews not allowed. Set allowPreview to true in your config to enable it.")
|
|
1582
|
+
return
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
let previews = self.listPreviewInfos()
|
|
1586
|
+
var result: [String: Any] = [
|
|
1587
|
+
"previews": previews,
|
|
1588
|
+
"currentBundle": self.implementation.getCurrentBundle().toJSON()
|
|
1589
|
+
]
|
|
1590
|
+
if let currentPreview = previews.first(where: { ($0["isActive"] as? Bool) == true }) {
|
|
1591
|
+
result["current"] = currentPreview
|
|
1592
|
+
}
|
|
1593
|
+
if let liveBundle = self.implementation.getPreviewFallbackBundle() {
|
|
1594
|
+
result["liveBundle"] = liveBundle.toJSON()
|
|
1595
|
+
}
|
|
1596
|
+
call.resolve(result)
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
@objc func setPreview(_ call: CAPPluginCall) {
|
|
1600
|
+
guard self.allowPreview else {
|
|
1601
|
+
call.reject("setPreview not allowed. Set allowPreview to true in your config to enable it.")
|
|
1602
|
+
return
|
|
1603
|
+
}
|
|
1604
|
+
guard let id = call.getString("id"), !id.isEmpty else {
|
|
1605
|
+
call.reject("setPreview called without id")
|
|
1606
|
+
return
|
|
1607
|
+
}
|
|
1608
|
+
guard let preview = self.storedPreviewInfo(id: id) else {
|
|
1609
|
+
call.reject("Preview \(id) is not available locally")
|
|
1610
|
+
return
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
self.showPreviewTransitionLoader(reason: "set-preview")
|
|
1614
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1615
|
+
guard self.preparePreviewFallbackIfNeeded() else {
|
|
1616
|
+
self.hidePreviewTransitionLoader(reason: "set-preview-fallback-failed")
|
|
1617
|
+
call.reject("Could not save current bundle as preview fallback")
|
|
1618
|
+
return
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
guard self.implementation.set(id: id) else {
|
|
1622
|
+
self.hidePreviewTransitionLoader(reason: "set-preview-failed")
|
|
1623
|
+
call.reject("Preview \(id) cannot be applied")
|
|
1624
|
+
return
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
let bundle = self.implementation.getBundleInfo(id: id)
|
|
1628
|
+
self.updateCurrentPreviewSessionMetadata(from: preview)
|
|
1629
|
+
self.activatePreviewSessionState()
|
|
1630
|
+
_ = self.recordPreviewBundle(bundle)
|
|
1631
|
+
guard self.reloadWithoutWaitingForAppReady() else {
|
|
1632
|
+
self.hidePreviewTransitionLoader(reason: "set-preview-reload-failed")
|
|
1633
|
+
call.reject("Reload failed after setting preview \(id)")
|
|
1634
|
+
return
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
self.notifyBundleSet(bundle)
|
|
1638
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
1639
|
+
call.resolve()
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
func previewMenuPreviews() -> [[String: Any]] {
|
|
1644
|
+
self.listPreviewInfos()
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
func setPreviewFromShakeMenu(id: String) -> Bool {
|
|
1648
|
+
guard self.allowPreview, let preview = self.storedPreviewInfo(id: id) else {
|
|
1649
|
+
return false
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
self.showPreviewTransitionLoader(reason: "set-preview-menu")
|
|
1653
|
+
guard self.preparePreviewFallbackIfNeeded() else {
|
|
1654
|
+
self.hidePreviewTransitionLoader(reason: "set-preview-menu-fallback-failed")
|
|
1655
|
+
return false
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
guard self.implementation.set(id: id) else {
|
|
1659
|
+
self.hidePreviewTransitionLoader(reason: "set-preview-menu-failed")
|
|
1660
|
+
return false
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
let bundle = self.implementation.getBundleInfo(id: id)
|
|
1664
|
+
self.updateCurrentPreviewSessionMetadata(from: preview)
|
|
1665
|
+
self.activatePreviewSessionState()
|
|
1666
|
+
_ = self.recordPreviewBundle(bundle)
|
|
1667
|
+
guard self.reloadWithoutWaitingForAppReady() else {
|
|
1668
|
+
self.hidePreviewTransitionLoader(reason: "set-preview-menu-reload-failed")
|
|
1669
|
+
return false
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
self.notifyBundleSet(bundle)
|
|
1673
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
1674
|
+
return true
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
@objc func resetPreview(_ call: CAPPluginCall) {
|
|
1678
|
+
guard self.previewSessionEnabled else {
|
|
1679
|
+
call.resolve()
|
|
1680
|
+
return
|
|
1681
|
+
}
|
|
1682
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1683
|
+
if self.leavePreviewSessionFromShakeMenu() {
|
|
1684
|
+
call.resolve()
|
|
1685
|
+
} else {
|
|
1686
|
+
call.reject("Could not leave preview session")
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
@objc func deletePreview(_ call: CAPPluginCall) {
|
|
1692
|
+
guard self.allowPreview else {
|
|
1693
|
+
call.reject("deletePreview not allowed. Set allowPreview to true in your config to enable it.")
|
|
1694
|
+
return
|
|
1695
|
+
}
|
|
1696
|
+
guard let id = call.getString("id"), !id.isEmpty else {
|
|
1697
|
+
call.reject("deletePreview called without id")
|
|
1698
|
+
return
|
|
1699
|
+
}
|
|
1700
|
+
if self.previewSessionEnabled && self.implementation.getCurrentBundleId() == id {
|
|
1701
|
+
call.reject("Cannot delete the active preview")
|
|
1702
|
+
return
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
var sessions = self.previewSessions()
|
|
1706
|
+
let removed = sessions.removeValue(forKey: id) != nil
|
|
1707
|
+
self.savePreviewSessions(sessions)
|
|
1708
|
+
|
|
1709
|
+
var deleted = false
|
|
1710
|
+
let fallbackId = self.implementation.getPreviewFallbackBundle()?.getId()
|
|
1711
|
+
let nextId = self.implementation.getNextBundle()?.getId()
|
|
1712
|
+
if removed, id != fallbackId, id != nextId, id != BundleInfo.ID_BUILTIN {
|
|
1713
|
+
deleted = self.implementation.delete(id: id, removeInfo: false)
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
call.resolve(["removed": removed, "deleted": deleted])
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
@objc func checkPreviewUpdate(_ call: CAPPluginCall) {
|
|
1720
|
+
self.handlePreviewUpdate(call, shouldDownload: false)
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
@objc func updatePreview(_ call: CAPPluginCall) {
|
|
1724
|
+
self.handlePreviewUpdate(call, shouldDownload: true)
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
private func handlePreviewUpdate(_ call: CAPPluginCall, shouldDownload: Bool) {
|
|
1728
|
+
guard self.allowPreview else {
|
|
1729
|
+
call.reject("Preview updates not allowed. Set allowPreview to true in your config to enable it.")
|
|
1730
|
+
return
|
|
1731
|
+
}
|
|
1732
|
+
guard let id = call.getString("id"), !id.isEmpty else {
|
|
1733
|
+
call.reject("Preview update called without id")
|
|
1734
|
+
return
|
|
1735
|
+
}
|
|
1736
|
+
guard let preview = self.storedPreviewInfo(id: id),
|
|
1737
|
+
let payloadUrlString = preview["payloadUrl"] as? String,
|
|
1738
|
+
let payloadUrl = self.normalizedPreviewPayloadUrl(payloadUrlString) else {
|
|
1739
|
+
call.reject("Preview \(id) has no payloadUrl to update from")
|
|
1740
|
+
return
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1744
|
+
do {
|
|
1745
|
+
let payload = try self.fetchPreviewPayload(payloadUrl)
|
|
1746
|
+
guard let version = payload.version, !version.isEmpty else {
|
|
1747
|
+
throw self.makePreviewError("Preview payload is missing a version")
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
let currentPreviewBundle = self.implementation.getBundleInfo(id: id)
|
|
1751
|
+
let upToDate = currentPreviewBundle.getVersionName() == version
|
|
1752
|
+
if upToDate || !shouldDownload {
|
|
1753
|
+
call.resolve([
|
|
1754
|
+
"preview": preview,
|
|
1755
|
+
"latestVersion": version,
|
|
1756
|
+
"upToDate": upToDate,
|
|
1757
|
+
"updated": false,
|
|
1758
|
+
"bundle": currentPreviewBundle.toJSON()
|
|
1759
|
+
])
|
|
1760
|
+
return
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
guard payload.url != nil || payload.manifest?.isEmpty == false else {
|
|
1764
|
+
throw self.makePreviewError("Preview payload is missing download information")
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
let next = try self.downloadBundle(
|
|
1768
|
+
// Fallback URL is only provided when payload.url is missing; when manifestEntries is present,
|
|
1769
|
+
// downloadBundle routes through downloadManifest and ignores urlString.
|
|
1770
|
+
urlString: payload.url ?? "https://404.capgo.app/no.zip",
|
|
1771
|
+
version: version,
|
|
1772
|
+
sessionKey: payload.sessionKey ?? "",
|
|
1773
|
+
checksum: payload.checksum ?? "",
|
|
1774
|
+
manifestEntries: payload.manifest
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
let wasActive = self.previewSessionEnabled && self.implementation.getCurrentBundleId() == id
|
|
1778
|
+
if wasActive {
|
|
1779
|
+
guard self.implementation.set(id: next.getId()) else {
|
|
1780
|
+
throw self.makePreviewError("Downloaded preview bundle cannot be applied")
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
let savedPreview = self.recordPreviewBundle(next, replacing: id)
|
|
1785
|
+
if wasActive {
|
|
1786
|
+
guard self.reloadWithoutWaitingForAppReady() else {
|
|
1787
|
+
throw self.makePreviewError("Reload failed after updating preview")
|
|
1788
|
+
}
|
|
1789
|
+
self.notifyBundleSet(next)
|
|
1790
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
call.resolve([
|
|
1794
|
+
"preview": savedPreview,
|
|
1795
|
+
"latestVersion": version,
|
|
1796
|
+
"upToDate": false,
|
|
1797
|
+
"updated": true,
|
|
1798
|
+
"bundle": next.toJSON()
|
|
1799
|
+
])
|
|
1800
|
+
} catch {
|
|
1801
|
+
self.logger.error("Could not update preview: \(error.localizedDescription)")
|
|
1802
|
+
call.reject("Could not update preview: \(error.localizedDescription)")
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
func leavePreviewSessionFromShakeMenu() -> Bool {
|
|
1808
|
+
self.showPreviewTransitionLoader(reason: "leave-preview-session")
|
|
1809
|
+
let didReset = self.resetToPreviewFallbackBundle()
|
|
1810
|
+
guard didReset else {
|
|
1811
|
+
self.hidePreviewTransitionLoader(reason: "leave-preview-session-failed")
|
|
1812
|
+
return false
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
self.endPreviewSession(keepPreviewGuard: true)
|
|
1816
|
+
return true
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
private func leavePreviewSessionForLaunchURLIfNeeded() {
|
|
1820
|
+
guard self.previewSessionEnabled,
|
|
1821
|
+
!self.isLeavingPreviewForIncomingLink,
|
|
1822
|
+
let launchUrl = ApplicationDelegateProxy.shared.lastURL,
|
|
1823
|
+
self.isPreviewDeepLink(launchUrl) else {
|
|
1824
|
+
return
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
self.isLeavingPreviewForIncomingLink = true
|
|
1828
|
+
self.showPreviewTransitionLoader(reason: "preview-launch-deeplink")
|
|
1829
|
+
logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load")
|
|
1830
|
+
if !self.leavePreviewSessionWithoutReload() {
|
|
1831
|
+
logger.error("Could not leave preview session before initial preview deeplink routing")
|
|
1832
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
1833
|
+
self.hidePreviewTransitionLoader(reason: "preview-launch-deeplink-failed")
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
private func leavePreviewSessionWithoutReload(keepPreviewGuard: Bool = false) -> Bool {
|
|
1838
|
+
guard let previewFallbackBundle = self.resolvePreviewFallbackBundle(reason: "preview deeplink launch") else {
|
|
1839
|
+
return false
|
|
1840
|
+
}
|
|
1841
|
+
guard self.implementation.stagePreviewFallbackReload(bundle: previewFallbackBundle) else {
|
|
1842
|
+
logger.error("Could not stage preview fallback bundle")
|
|
1843
|
+
return false
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
self.endPreviewSession(keepPreviewGuard: keepPreviewGuard)
|
|
1847
|
+
return true
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
private func leavePreviewSessionForIncomingPreviewLink() -> Bool {
|
|
1851
|
+
self.showPreviewTransitionLoader(reason: "incoming-preview-deeplink")
|
|
1852
|
+
guard let previewFallbackBundle = self.resolvePreviewFallbackBundle(reason: "incoming preview deeplink") else {
|
|
1853
|
+
self.clearIncomingPreviewTransition()
|
|
1854
|
+
self.hidePreviewTransitionLoader(reason: "incoming-preview-deeplink-failed")
|
|
1855
|
+
return false
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
let previousState = self.implementation.captureResetState()
|
|
1859
|
+
guard self.implementation.stagePreviewFallbackReload(bundle: previewFallbackBundle) else {
|
|
1860
|
+
logger.error("Could not stage preview fallback bundle")
|
|
1861
|
+
self.clearIncomingPreviewTransition()
|
|
1862
|
+
self.hidePreviewTransitionLoader(reason: "incoming-preview-deeplink-failed")
|
|
1863
|
+
return false
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
let didReload = self.reloadWithoutWaitingForAppReady()
|
|
1867
|
+
if didReload {
|
|
1868
|
+
self.endPreviewSession(keepPreviewGuard: true)
|
|
1869
|
+
self.scheduleIncomingPreviewTransitionFallbackClear()
|
|
1870
|
+
} else {
|
|
1871
|
+
self.implementation.restoreResetState(previousState)
|
|
1872
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
1873
|
+
self.clearIncomingPreviewTransition()
|
|
1874
|
+
self.hidePreviewTransitionLoader(reason: "incoming-preview-deeplink-reload-failed")
|
|
1875
|
+
}
|
|
1876
|
+
return didReload
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
func reloadPreviewSessionFromShakeMenu() -> Bool {
|
|
1880
|
+
self.showPreviewTransitionLoader(reason: "reload-preview-session")
|
|
1881
|
+
let didReload: Bool
|
|
1882
|
+
if let payloadUrl = self.storedPreviewPayloadUrl() {
|
|
1883
|
+
didReload = self.refreshPreviewSessionFromPayloadUrl(payloadUrl)
|
|
1884
|
+
} else {
|
|
1885
|
+
didReload = self.reloadWithoutWaitingForAppReady()
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if !didReload {
|
|
1889
|
+
self.hidePreviewTransitionLoader(reason: "reload-preview-session-failed")
|
|
1890
|
+
}
|
|
1891
|
+
return didReload
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
func hasActivePreviewSession() -> Bool {
|
|
1895
|
+
self.previewSessionEnabled
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
func resetToPreviewFallbackBundle() -> Bool {
|
|
1899
|
+
guard self.canPerformResetTransition() else { return false }
|
|
1900
|
+
guard let fallback = self.resolvePreviewFallbackBundle(reason: "leave preview") else {
|
|
1901
|
+
return false
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
let previousState = self.implementation.captureResetState()
|
|
1905
|
+
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
1906
|
+
logger.info("Resetting to preview fallback bundle: \(fallback.toString())")
|
|
1907
|
+
if self.implementation.stagePreviewFallbackReload(bundle: fallback) && self.reloadWithoutWaitingForAppReady() {
|
|
1908
|
+
self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: false)
|
|
1909
|
+
self.notifyBundleSet(fallback)
|
|
1910
|
+
return true
|
|
1911
|
+
}
|
|
1912
|
+
self.implementation.restoreResetState(previousState)
|
|
1913
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
1914
|
+
return false
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
private func resolvePreviewFallbackBundle(reason: String) -> BundleInfo? {
|
|
1918
|
+
let fallback = self.implementation.getPreviewFallbackBundle()
|
|
1919
|
+
if let fallback, !fallback.isErrorStatus(), self.implementation.canSet(bundle: fallback) {
|
|
1920
|
+
return fallback
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
if let fallback {
|
|
1924
|
+
if fallback.isErrorStatus() {
|
|
1925
|
+
logger.warn("Preview fallback bundle is in error state for \(reason). Falling back to builtin bundle.")
|
|
1926
|
+
} else {
|
|
1927
|
+
logger.warn("Preview fallback bundle is not installable for \(reason). Falling back to builtin bundle.")
|
|
1928
|
+
}
|
|
1929
|
+
} else {
|
|
1930
|
+
logger.warn("No preview fallback bundle available for \(reason). Falling back to builtin bundle.")
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
let builtin = self.implementation.getBundleInfo(id: BundleInfo.ID_BUILTIN)
|
|
1934
|
+
if !builtin.isErrorStatus(), self.implementation.canSet(bundle: builtin) {
|
|
1935
|
+
return builtin
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
logger.error("Builtin bundle is not available to leave preview for \(reason)")
|
|
1939
|
+
return nil
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
private func endPreviewSession(keepPreviewGuard: Bool = false) {
|
|
1943
|
+
let previousShakeMenuEnabled = UserDefaults.standard.object(forKey: self.previewPreviousShakeMenuDefaultsKey) as? Bool
|
|
1944
|
+
?? self.getBooleanConfig("shakeMenu", defaultValue: false)
|
|
1945
|
+
let previousShakeChannelSelectorEnabled = UserDefaults.standard.object(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey) as? Bool
|
|
1946
|
+
?? self.getBooleanConfig("allowShakeChannelSelector", defaultValue: false)
|
|
1947
|
+
self.restorePreviewPreviousNextBundle()
|
|
1948
|
+
self.restorePreviewPreviousAppId()
|
|
1949
|
+
self.restorePreviewPreviousDefaultChannel()
|
|
1950
|
+
|
|
1951
|
+
self.previewSessionEnabled = false
|
|
1952
|
+
self.previewSessionAlertPending = false
|
|
1953
|
+
if keepPreviewGuard {
|
|
1954
|
+
self.implementation.previewSession = true
|
|
1955
|
+
} else {
|
|
1956
|
+
self.clearIncomingPreviewTransition()
|
|
1957
|
+
}
|
|
1958
|
+
self.shakeMenuEnabled = previousShakeMenuEnabled
|
|
1959
|
+
self.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled
|
|
1960
|
+
self.syncShakeMenuGestureRecognizer()
|
|
1961
|
+
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
1962
|
+
self.clearPreviewSessionPreferences()
|
|
1963
|
+
logger.info("Preview session ended")
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
private func clearPreviewSessionBecauseDisabled() {
|
|
1967
|
+
logger.info("Preview session disabled by config; restoring preview fallback")
|
|
1968
|
+
if let bundleToRestore = self.resolvePreviewFallbackBundle(reason: "preview disabled") {
|
|
1969
|
+
_ = self.implementation.stagePreviewFallbackReload(bundle: bundleToRestore)
|
|
1970
|
+
} else {
|
|
1971
|
+
logger.warn("Could not restore preview fallback while disabling preview")
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
self.restorePreviewPreviousNextBundle()
|
|
1975
|
+
self.restorePreviewPreviousAppId()
|
|
1976
|
+
self.restorePreviewPreviousDefaultChannel()
|
|
1977
|
+
self.previewSessionEnabled = false
|
|
1978
|
+
self.previewSessionAlertPending = false
|
|
1979
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
1980
|
+
self.implementation.previewSession = false
|
|
1981
|
+
self.hidePreviewTransitionLoader(reason: "preview-session-disabled")
|
|
1982
|
+
self.shakeMenuEnabled = self.getBooleanConfig("shakeMenu", defaultValue: false)
|
|
1983
|
+
self.shakeChannelSelectorEnabled = self.getBooleanConfig("allowShakeChannelSelector", defaultValue: false)
|
|
1984
|
+
self.shakeMenuGesture = Self.normalizedShakeMenuGesture(self.getStringConfig("shakeMenuGesture", defaultValue: Self.shakeMenuGestureShake))
|
|
1985
|
+
self.syncShakeMenuGestureRecognizer()
|
|
1986
|
+
self.clearPreviewSessionPreferences()
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
private func getBooleanConfig(_ key: String, defaultValue: Bool) -> Bool {
|
|
1990
|
+
guard self.bridge != nil else {
|
|
1991
|
+
return defaultValue
|
|
1992
|
+
}
|
|
1993
|
+
return getConfig().getBoolean(key, defaultValue)
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
private func getStringConfig(_ key: String, defaultValue: String) -> String {
|
|
1997
|
+
guard self.bridge != nil else {
|
|
1998
|
+
return defaultValue
|
|
1999
|
+
}
|
|
2000
|
+
return getConfig().getString(key, defaultValue) ?? defaultValue
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
private func clearPreviewSessionPreferences() {
|
|
2004
|
+
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
2005
|
+
UserDefaults.standard.removeObject(forKey: self.previewSessionDefaultsKey)
|
|
2006
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeMenuDefaultsKey)
|
|
2007
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
|
|
2008
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
2009
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousAppIdDefaultsKey)
|
|
2010
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
2011
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
2012
|
+
UserDefaults.standard.removeObject(forKey: self.previewAppIdDefaultsKey)
|
|
2013
|
+
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
2014
|
+
UserDefaults.standard.removeObject(forKey: self.previewNameDefaultsKey)
|
|
2015
|
+
UserDefaults.standard.removeObject(forKey: self.previewSourceDefaultsKey)
|
|
2016
|
+
UserDefaults.standard.removeObject(forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
2017
|
+
UserDefaults.standard.synchronize()
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
private func restorePreviewPreviousAppId() {
|
|
2021
|
+
guard let previousAppId = UserDefaults.standard.string(forKey: self.previewPreviousAppIdDefaultsKey),
|
|
2022
|
+
!previousAppId.isEmpty else {
|
|
2023
|
+
return
|
|
2024
|
+
}
|
|
2025
|
+
self.implementation.appId = previousAppId
|
|
2026
|
+
logger.info("Restored appId after preview: \(previousAppId)")
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
private func restorePreviewPreviousDefaultChannel() {
|
|
2030
|
+
let configDefaultChannel = self.getStringConfig("defaultChannel", defaultValue: "")
|
|
2031
|
+
let hadPreviousDefaultChannel = UserDefaults.standard.object(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey) as? Bool ?? false
|
|
2032
|
+
|
|
2033
|
+
guard hadPreviousDefaultChannel,
|
|
2034
|
+
let previousDefaultChannel = UserDefaults.standard.string(forKey: self.previewPreviousDefaultChannelDefaultsKey) else {
|
|
2035
|
+
UserDefaults.standard.removeObject(forKey: self.defaultChannelDefaultsKey)
|
|
2036
|
+
self.implementation.defaultChannel = configDefaultChannel
|
|
2037
|
+
logger.info("Restored defaultChannel after preview to config value")
|
|
2038
|
+
return
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
UserDefaults.standard.set(previousDefaultChannel, forKey: self.defaultChannelDefaultsKey)
|
|
2042
|
+
self.implementation.defaultChannel = previousDefaultChannel
|
|
2043
|
+
logger.info("Restored defaultChannel after preview")
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
private func normalizedPreviewAppId(_ rawAppId: String?) -> String? {
|
|
2047
|
+
guard let rawAppId else {
|
|
2048
|
+
return nil
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
let appId = rawAppId.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2052
|
+
guard !appId.isEmpty else {
|
|
2053
|
+
return nil
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
let lowercasedAppId = appId.lowercased()
|
|
2057
|
+
if lowercasedAppId == "undefined" || lowercasedAppId == "null" {
|
|
2058
|
+
return nil
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
return appId
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
private func normalizedPreviewPayloadUrl(_ rawPayloadUrl: String?) -> URL? {
|
|
2065
|
+
guard let rawPayloadUrl else {
|
|
2066
|
+
return nil
|
|
550
2067
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
2068
|
+
|
|
2069
|
+
let payloadUrl = rawPayloadUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2070
|
+
guard !payloadUrl.isEmpty,
|
|
2071
|
+
let url = URL(string: payloadUrl),
|
|
2072
|
+
url.scheme == "https" || url.scheme == "http" else {
|
|
2073
|
+
return nil
|
|
555
2074
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
2075
|
+
|
|
2076
|
+
return url
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
private func storedPreviewPayloadUrl() -> URL? {
|
|
2080
|
+
normalizedPreviewPayloadUrl(UserDefaults.standard.string(forKey: self.previewPayloadUrlDefaultsKey))
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
private func previewPath(from url: URL) -> String {
|
|
2084
|
+
if url.scheme == self.previewDeepLinkScheme {
|
|
2085
|
+
var components: [String] = []
|
|
2086
|
+
if let host = url.host, !host.isEmpty {
|
|
2087
|
+
components.append(host)
|
|
2088
|
+
}
|
|
2089
|
+
components.append(contentsOf: url.path.split(separator: self.previewPathSeparator).map(String.init))
|
|
2090
|
+
return self.normalizedPreviewPath(components)
|
|
560
2091
|
}
|
|
561
|
-
|
|
2092
|
+
|
|
2093
|
+
return url.path
|
|
562
2094
|
}
|
|
563
2095
|
|
|
564
|
-
|
|
565
|
-
|
|
2096
|
+
private func normalizedPreviewPath(_ components: [String]) -> String {
|
|
2097
|
+
let separator = String(self.previewPathSeparator)
|
|
2098
|
+
return separator + components.filter { !$0.isEmpty }.joined(separator: separator)
|
|
566
2099
|
}
|
|
567
2100
|
|
|
568
|
-
|
|
569
|
-
|
|
2101
|
+
private func previewDeepLinkPath(_ leafComponent: String) -> String {
|
|
2102
|
+
self.normalizedPreviewPath([self.previewDeepLinkRootComponent, leafComponent])
|
|
570
2103
|
}
|
|
571
2104
|
|
|
572
|
-
|
|
573
|
-
|
|
2105
|
+
private func isPreviewDeepLink(_ url: URL) -> Bool {
|
|
2106
|
+
let path = self.previewPath(from: url)
|
|
2107
|
+
return path == self.previewDeepLinkPath(self.previewDeepLinkChannelComponent) ||
|
|
2108
|
+
path == self.previewDeepLinkPath(self.previewDeepLinkBundleComponent)
|
|
574
2109
|
}
|
|
575
2110
|
|
|
576
|
-
@objc func
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
logger.error("Download called without version")
|
|
584
|
-
call.reject("Download called without version")
|
|
2111
|
+
@objc private func handleOpenURLForPreviewSession(notification: NSNotification) {
|
|
2112
|
+
let rawUrl = (notification.object as? [String: Any])?["url"]
|
|
2113
|
+
let url = rawUrl as? URL ?? (rawUrl as? NSURL).map { $0 as URL }
|
|
2114
|
+
guard self.previewSessionEnabled,
|
|
2115
|
+
!self.isLeavingPreviewForIncomingLink,
|
|
2116
|
+
let url,
|
|
2117
|
+
self.isPreviewDeepLink(url) else {
|
|
585
2118
|
return
|
|
586
2119
|
}
|
|
587
2120
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
// Convert JSArray to [ManifestEntry]
|
|
598
|
-
var manifestEntries: [ManifestEntry] = []
|
|
599
|
-
for item in manifestArray {
|
|
600
|
-
if let manifestDict = item as? [String: Any] {
|
|
601
|
-
let entry = ManifestEntry(
|
|
602
|
-
file_name: manifestDict["file_name"] as? String,
|
|
603
|
-
file_hash: manifestDict["file_hash"] as? String,
|
|
604
|
-
download_url: manifestDict["download_url"] as? String
|
|
605
|
-
)
|
|
606
|
-
manifestEntries.append(entry)
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
|
|
610
|
-
} else {
|
|
611
|
-
next = try self.implementation.download(url: url!, version: version, sessionKey: sessionKey)
|
|
612
|
-
}
|
|
613
|
-
// If public key is present but no checksum provided, refuse installation
|
|
614
|
-
if self.implementation.publicKey != "" && checksum == "" {
|
|
615
|
-
self.logger.error("Public key present but no checksum provided")
|
|
616
|
-
self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
|
|
617
|
-
let id = next.getId()
|
|
618
|
-
let resDel = self.implementation.delete(id: id)
|
|
619
|
-
if !resDel {
|
|
620
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
621
|
-
}
|
|
622
|
-
throw ObjectSavableError.checksum
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
626
|
-
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
627
|
-
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
628
|
-
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
629
|
-
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
630
|
-
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
631
|
-
let id = next.getId()
|
|
632
|
-
let resDel = self.implementation.delete(id: id)
|
|
633
|
-
if !resDel {
|
|
634
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
635
|
-
}
|
|
636
|
-
throw ObjectSavableError.checksum
|
|
637
|
-
} else {
|
|
638
|
-
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
639
|
-
}
|
|
640
|
-
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
641
|
-
call.resolve(next.toJSON())
|
|
642
|
-
} catch {
|
|
643
|
-
self.logger.error("Failed to download from: \(String(describing: url)) \(error.localizedDescription)")
|
|
644
|
-
self.notifyListeners("downloadFailed", data: ["version": version])
|
|
645
|
-
self.implementation.sendStats(action: "download_fail")
|
|
646
|
-
call.reject("Failed to download from: \(url!) - \(error.localizedDescription)")
|
|
2121
|
+
self.isLeavingPreviewForIncomingLink = true
|
|
2122
|
+
self.showPreviewTransitionLoader(reason: "incoming-preview-deeplink")
|
|
2123
|
+
logger.info("Preview deeplink received while preview session is active; restoring fallback before routing")
|
|
2124
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
2125
|
+
let didLeave = self.leavePreviewSessionForIncomingPreviewLink()
|
|
2126
|
+
if !didLeave {
|
|
2127
|
+
self.logger.error("Could not leave preview session before routing incoming preview deeplink")
|
|
2128
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
2129
|
+
self.hidePreviewTransitionLoader(reason: "incoming-preview-deeplink-failed")
|
|
647
2130
|
}
|
|
648
2131
|
}
|
|
649
2132
|
}
|
|
650
2133
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
let
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
2134
|
+
private func fetchPreviewPayload(_ payloadUrl: URL) throws -> PreviewPayload {
|
|
2135
|
+
var request = URLRequest(url: payloadUrl)
|
|
2136
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
2137
|
+
|
|
2138
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
2139
|
+
var responseData: Data?
|
|
2140
|
+
var response: URLResponse?
|
|
2141
|
+
var responseError: Error?
|
|
2142
|
+
|
|
2143
|
+
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
|
|
2144
|
+
responseData = data
|
|
2145
|
+
response = urlResponse
|
|
2146
|
+
responseError = error
|
|
2147
|
+
semaphore.signal()
|
|
2148
|
+
}.resume()
|
|
2149
|
+
|
|
2150
|
+
if semaphore.wait(timeout: .now() + 60) == .timedOut {
|
|
2151
|
+
throw makePreviewError("Preview payload request timed out")
|
|
660
2152
|
}
|
|
661
|
-
logger.info("Reloading \(id)")
|
|
662
2153
|
|
|
663
|
-
let
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
2154
|
+
if let responseError = responseError {
|
|
2155
|
+
throw responseError
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
let data = responseData ?? Data()
|
|
2159
|
+
if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
|
|
2160
|
+
if let payload = try? JSONDecoder().decode(PreviewPayload.self, from: data) {
|
|
2161
|
+
throw makePreviewError(payload.message ?? payload.error ?? "Preview payload request failed with HTTP \(httpResponse.statusCode)")
|
|
667
2162
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
2163
|
+
let message = String(data: data, encoding: .utf8) ?? "Preview payload request failed with HTTP \(httpResponse.statusCode)"
|
|
2164
|
+
throw makePreviewError(message)
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
return try JSONDecoder().decode(PreviewPayload.self, from: data)
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
private func refreshPreviewSessionFromPayloadUrl(_ payloadUrl: URL) -> Bool {
|
|
2171
|
+
do {
|
|
2172
|
+
let payload = try self.fetchPreviewPayload(payloadUrl)
|
|
2173
|
+
guard let version = payload.version, !version.isEmpty else {
|
|
2174
|
+
throw makePreviewError("Preview payload is missing a version")
|
|
671
2175
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
capBridge.setServerBasePath(dest.path)
|
|
675
|
-
var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
|
|
676
|
-
urlComponents.path = currentURL.path
|
|
677
|
-
urlComponents.query = currentURL.query
|
|
678
|
-
urlComponents.fragment = currentURL.fragment
|
|
679
|
-
if let finalUrl = urlComponents.url {
|
|
680
|
-
_ = vc.webView?.load(URLRequest(url: finalUrl))
|
|
681
|
-
} else {
|
|
682
|
-
self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
|
|
683
|
-
vc.setServerBasePath(path: dest.path)
|
|
684
|
-
}
|
|
685
|
-
} else {
|
|
686
|
-
self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
|
|
687
|
-
vc.setServerBasePath(path: dest.path)
|
|
688
|
-
}
|
|
689
|
-
} else {
|
|
690
|
-
vc.setServerBasePath(path: dest.path)
|
|
2176
|
+
guard payload.url != nil || payload.manifest?.isEmpty == false else {
|
|
2177
|
+
throw makePreviewError("Preview payload is missing download information")
|
|
691
2178
|
}
|
|
692
|
-
self.checkAppReady()
|
|
693
|
-
self.notifyListeners("appReloaded", data: [:])
|
|
694
|
-
return true
|
|
695
|
-
}
|
|
696
2179
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
DispatchQueue.main.sync {
|
|
702
|
-
result = performReload()
|
|
2180
|
+
let current = self.implementation.getCurrentBundle()
|
|
2181
|
+
if current.getVersionName() == version {
|
|
2182
|
+
self.logger.info("Preview payload unchanged, reloading current bundle")
|
|
2183
|
+
return self.reloadWithoutWaitingForAppReady()
|
|
703
2184
|
}
|
|
704
|
-
|
|
2185
|
+
|
|
2186
|
+
let next = try self.downloadBundle(
|
|
2187
|
+
// Fallback URL is only provided when payload.url is missing; when manifestEntries is present,
|
|
2188
|
+
// downloadBundle routes through downloadManifest and ignores urlString.
|
|
2189
|
+
urlString: payload.url ?? "https://404.capgo.app/no.zip",
|
|
2190
|
+
version: version,
|
|
2191
|
+
sessionKey: payload.sessionKey ?? "",
|
|
2192
|
+
checksum: payload.checksum ?? "",
|
|
2193
|
+
manifestEntries: payload.manifest
|
|
2194
|
+
)
|
|
2195
|
+
|
|
2196
|
+
guard self.implementation.set(id: next.getId()) else {
|
|
2197
|
+
throw makePreviewError("Downloaded preview bundle cannot be applied")
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
_ = self.recordPreviewBundle(next, replacing: current.getId())
|
|
2201
|
+
self.notifyBundleSet(next)
|
|
2202
|
+
return self.reloadWithoutWaitingForAppReady()
|
|
2203
|
+
} catch {
|
|
2204
|
+
self.logger.error("Could not refresh preview session: \(error.localizedDescription)")
|
|
2205
|
+
return false
|
|
705
2206
|
}
|
|
706
2207
|
}
|
|
707
2208
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
} else {
|
|
712
|
-
logger.error("Reload failed")
|
|
713
|
-
call.reject("Reload failed")
|
|
2209
|
+
private func clearPreviewSessionForNativeBuildChange() {
|
|
2210
|
+
guard self.previewSessionEnabled || self.implementation.getPreviewFallbackBundle() != nil || !self.previewSessions().isEmpty else {
|
|
2211
|
+
return
|
|
714
2212
|
}
|
|
2213
|
+
logger.info("Native build changed; clearing preview session state")
|
|
2214
|
+
self.previewSessionEnabled = false
|
|
2215
|
+
self.previewSessionAlertPending = false
|
|
2216
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
2217
|
+
self.implementation.previewSession = false
|
|
2218
|
+
self.shakeMenuEnabled = self.getBooleanConfig("shakeMenu", defaultValue: false)
|
|
2219
|
+
self.shakeChannelSelectorEnabled = self.getBooleanConfig("allowShakeChannelSelector", defaultValue: false)
|
|
2220
|
+
self.shakeMenuGesture = Self.normalizedShakeMenuGesture(self.getStringConfig("shakeMenuGesture", defaultValue: Self.shakeMenuGestureShake))
|
|
2221
|
+
self.syncShakeMenuGestureRecognizer()
|
|
2222
|
+
self.restorePreviewPreviousAppId()
|
|
2223
|
+
self.restorePreviewPreviousDefaultChannel()
|
|
2224
|
+
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
2225
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
2226
|
+
self.clearPreviewSessionPreferences()
|
|
2227
|
+
UserDefaults.standard.removeObject(forKey: self.previewSessionsDefaultsKey)
|
|
2228
|
+
UserDefaults.standard.synchronize()
|
|
715
2229
|
}
|
|
716
2230
|
|
|
717
|
-
|
|
718
|
-
guard let
|
|
719
|
-
|
|
720
|
-
|
|
2231
|
+
private func restorePreviewPreviousNextBundle() {
|
|
2232
|
+
guard let previousNextBundleId = UserDefaults.standard.string(forKey: self.previewPreviousNextBundleDefaultsKey),
|
|
2233
|
+
!previousNextBundleId.isEmpty else {
|
|
2234
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
721
2235
|
return
|
|
722
2236
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
call.reject("Set next version failed. id \(id) does not exist.")
|
|
727
|
-
} else {
|
|
728
|
-
call.resolve(self.implementation.getBundleInfo(id: id).toJSON())
|
|
2237
|
+
if !self.implementation.setNextBundle(next: previousNextBundleId) {
|
|
2238
|
+
logger.warn("Could not restore pre-preview next bundle: \(previousNextBundleId)")
|
|
2239
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
729
2240
|
}
|
|
730
2241
|
}
|
|
731
2242
|
|
|
732
|
-
|
|
733
|
-
guard
|
|
734
|
-
logger.error("Set called without id")
|
|
735
|
-
call.reject("Set called without id")
|
|
2243
|
+
private func showPreviewSessionNoticeIfNeeded() {
|
|
2244
|
+
guard self.previewSessionEnabled && self.previewSessionAlertPending else {
|
|
736
2245
|
return
|
|
737
2246
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
2247
|
+
self.previewSessionAlertPending = false
|
|
2248
|
+
UserDefaults.standard.set(false, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
2249
|
+
UserDefaults.standard.synchronize()
|
|
2250
|
+
|
|
2251
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(600)) {
|
|
2252
|
+
guard self.previewSessionEnabled else {
|
|
2253
|
+
return
|
|
2254
|
+
}
|
|
2255
|
+
if let topVC = UIApplication.topViewController(),
|
|
2256
|
+
topVC.isKind(of: UIAlertController.self) {
|
|
2257
|
+
self.previewSessionAlertPending = true
|
|
2258
|
+
UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
2259
|
+
UserDefaults.standard.synchronize()
|
|
2260
|
+
return
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
let alert = UIAlertController(
|
|
2264
|
+
title: "Preview started",
|
|
2265
|
+
message: "Shake your device anytime to reload or leave the test app.",
|
|
2266
|
+
preferredStyle: .alert
|
|
2267
|
+
)
|
|
2268
|
+
alert.addAction(UIAlertAction(title: "Got it", style: .default))
|
|
2269
|
+
if let topVC = UIApplication.topViewController() {
|
|
2270
|
+
topVC.present(alert, animated: true)
|
|
2271
|
+
} else {
|
|
2272
|
+
self.previewSessionAlertPending = true
|
|
2273
|
+
UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
2274
|
+
UserDefaults.standard.synchronize()
|
|
2275
|
+
}
|
|
745
2276
|
}
|
|
746
2277
|
}
|
|
747
2278
|
|
|
@@ -804,25 +2335,132 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
804
2335
|
|
|
805
2336
|
@objc func getLatest(_ call: CAPPluginCall) {
|
|
806
2337
|
let channel = call.getString("channel")
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
2338
|
+
let includeBundleSize = call.getBool("includeBundleSize", false)
|
|
2339
|
+
let appId = self.normalizedPreviewAppId(call.getString("appId"))
|
|
2340
|
+
if appId != nil && !self.allowPreview {
|
|
2341
|
+
logger.error("getLatest preview override called without allowPreview")
|
|
2342
|
+
call.reject("getLatest preview override not allowed. Set allowPreview to true in your config to enable it.")
|
|
2343
|
+
return
|
|
2344
|
+
}
|
|
2345
|
+
self.saveCallForAsyncHandling(call)
|
|
2346
|
+
runGetLatestWork {
|
|
2347
|
+
let res = self.implementation.getLatest(
|
|
2348
|
+
url: URL(string: self.updateUrl)!,
|
|
2349
|
+
channel: channel,
|
|
2350
|
+
appIdOverride: appId
|
|
2351
|
+
)
|
|
2352
|
+
if includeBundleSize {
|
|
2353
|
+
self.attachBundleSize(to: res)
|
|
2354
|
+
}
|
|
2355
|
+
if let error = res.error, !error.isEmpty {
|
|
2356
|
+
let responseKind = self.updateResponseKind(kind: res.kind)
|
|
2357
|
+
res.kind = responseKind
|
|
2358
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
|
|
2359
|
+
if responseKind == "failed" {
|
|
2360
|
+
self.rejectCall(call, message: error)
|
|
2361
|
+
} else {
|
|
2362
|
+
if res.version.isEmpty {
|
|
2363
|
+
res.version = self.implementation.getCurrentBundle().getVersionName()
|
|
2364
|
+
}
|
|
2365
|
+
self.resolveCall(call, data: res.toDict())
|
|
2366
|
+
}
|
|
2367
|
+
} else if let kind = res.kind, !kind.isEmpty {
|
|
2368
|
+
let responseKind = self.updateResponseKind(kind: kind)
|
|
2369
|
+
res.kind = responseKind
|
|
2370
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
|
|
2371
|
+
if responseKind != "failed" {
|
|
2372
|
+
if res.version.isEmpty {
|
|
2373
|
+
res.version = self.implementation.getCurrentBundle().getVersionName()
|
|
2374
|
+
}
|
|
2375
|
+
self.resolveCall(call, data: res.toDict())
|
|
2376
|
+
} else {
|
|
2377
|
+
self.rejectCall(call, message: res.message ?? "server did not provide a message")
|
|
2378
|
+
}
|
|
2379
|
+
} else if let message = res.message, !message.isEmpty {
|
|
2380
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
|
|
2381
|
+
self.rejectCall(call, message: message)
|
|
813
2382
|
} else {
|
|
814
|
-
|
|
2383
|
+
self.resolveCall(call, data: res.toDict())
|
|
815
2384
|
}
|
|
816
2385
|
}
|
|
817
2386
|
}
|
|
818
2387
|
|
|
2388
|
+
private func attachBundleSize(to res: AppVersion) {
|
|
2389
|
+
guard let manifest = res.manifest, !manifest.isEmpty, let updateUrl = URL(string: self.updateUrl) else {
|
|
2390
|
+
return
|
|
2391
|
+
}
|
|
2392
|
+
let missing = self.implementation.getMissingBundleFiles(manifest: manifest, sessionKey: res.sessionKey ?? "")
|
|
2393
|
+
res.missing = [
|
|
2394
|
+
"missing": missing.map { $0.toDict() },
|
|
2395
|
+
"total": manifest.count,
|
|
2396
|
+
"missingCount": missing.count,
|
|
2397
|
+
"reusableCount": manifest.count - missing.count
|
|
2398
|
+
]
|
|
2399
|
+
res.downloadSize = self.implementation.getBundleDownloadSize(updateUrl: updateUrl, version: res.version, manifest: missing)
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
@objc func getMissingBundleFiles(_ call: CAPPluginCall) {
|
|
2403
|
+
guard let manifest = manifestEntries(from: call.getArray("manifest")) else {
|
|
2404
|
+
call.reject("getMissingBundleFiles called without manifest")
|
|
2405
|
+
return
|
|
2406
|
+
}
|
|
2407
|
+
let sessionKey = call.getString("sessionKey", "")
|
|
2408
|
+
self.saveCallForAsyncHandling(call)
|
|
2409
|
+
DispatchQueue.global(qos: .utility).async {
|
|
2410
|
+
let res = self.implementation.missingBundleFilesResult(manifest: manifest, sessionKey: sessionKey)
|
|
2411
|
+
self.resolveCall(call, data: res)
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
@objc func getBundleDownloadSize(_ call: CAPPluginCall) {
|
|
2416
|
+
guard let manifest = manifestEntries(from: call.getArray("manifest")) else {
|
|
2417
|
+
call.reject("getBundleDownloadSize called without manifest")
|
|
2418
|
+
return
|
|
2419
|
+
}
|
|
2420
|
+
guard let updateUrl = URL(string: self.updateUrl) else {
|
|
2421
|
+
call.reject("getBundleDownloadSize called without valid updateUrl")
|
|
2422
|
+
return
|
|
2423
|
+
}
|
|
2424
|
+
let version = call.getString("version")
|
|
2425
|
+
self.saveCallForAsyncHandling(call)
|
|
2426
|
+
DispatchQueue.global(qos: .utility).async {
|
|
2427
|
+
let res = self.implementation.getBundleDownloadSize(updateUrl: updateUrl, version: version, manifest: manifest)
|
|
2428
|
+
self.resolveCall(call, data: res)
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
public func triggerBackgroundUpdateCheck() -> String {
|
|
2433
|
+
guard !self.updateUrl.isEmpty, URL(string: self.updateUrl) != nil else {
|
|
2434
|
+
logger.error("Error no url or wrong format")
|
|
2435
|
+
return "unavailable"
|
|
2436
|
+
}
|
|
2437
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
2438
|
+
return "preview_session"
|
|
2439
|
+
}
|
|
2440
|
+
if self.isDownloadStuckOrTimedOut() {
|
|
2441
|
+
logger.info("Download already in progress, skipping duplicate download request")
|
|
2442
|
+
return "already_running"
|
|
2443
|
+
}
|
|
2444
|
+
self.backgroundDownload()
|
|
2445
|
+
return "queued"
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
@objc func triggerUpdateCheck(_ call: CAPPluginCall) {
|
|
2449
|
+
let status = self.triggerBackgroundUpdateCheck()
|
|
2450
|
+
call.resolve([
|
|
2451
|
+
"status": status,
|
|
2452
|
+
"queued": status == "queued"
|
|
2453
|
+
])
|
|
2454
|
+
}
|
|
2455
|
+
|
|
819
2456
|
@objc func unsetChannel(_ call: CAPPluginCall) {
|
|
820
2457
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate", false)
|
|
821
|
-
|
|
2458
|
+
self.saveCallForAsyncHandling(call)
|
|
2459
|
+
DispatchQueue.global(qos: .utility).async {
|
|
822
2460
|
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
823
2461
|
let res = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
|
|
824
2462
|
if res.error != "" {
|
|
825
|
-
|
|
2463
|
+
self.rejectCall(call, message: res.error, code: "UNSETCHANNEL_FAILED", data: [
|
|
826
2464
|
"message": res.error,
|
|
827
2465
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
828
2466
|
])
|
|
@@ -836,7 +2474,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
836
2474
|
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
837
2475
|
}
|
|
838
2476
|
}
|
|
839
|
-
|
|
2477
|
+
self.resolveCall(call, data: res.toDict())
|
|
840
2478
|
}
|
|
841
2479
|
}
|
|
842
2480
|
}
|
|
@@ -851,17 +2489,24 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
851
2489
|
return
|
|
852
2490
|
}
|
|
853
2491
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate") ?? false
|
|
854
|
-
|
|
855
|
-
|
|
2492
|
+
self.saveCallForAsyncHandling(call)
|
|
2493
|
+
DispatchQueue.global(qos: .utility).async {
|
|
2494
|
+
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
2495
|
+
let res = self.implementation.setChannel(
|
|
2496
|
+
channel: channel,
|
|
2497
|
+
defaultChannelKey: self.defaultChannelDefaultsKey,
|
|
2498
|
+
allowSetDefaultChannel: self.allowSetDefaultChannel,
|
|
2499
|
+
configDefaultChannel: configDefaultChannel
|
|
2500
|
+
)
|
|
856
2501
|
if res.error != "" {
|
|
857
2502
|
// Fire channelPrivate event if channel doesn't allow self-assignment
|
|
858
2503
|
if res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed") {
|
|
859
|
-
self.
|
|
2504
|
+
self.notifyListenersOnMain("channelPrivate", data: [
|
|
860
2505
|
"channel": channel,
|
|
861
2506
|
"message": res.error
|
|
862
2507
|
])
|
|
863
2508
|
}
|
|
864
|
-
|
|
2509
|
+
self.rejectCall(call, message: res.error, code: "SETCHANNEL_FAILED", data: [
|
|
865
2510
|
"message": res.error,
|
|
866
2511
|
"error": res.error.contains("Channel URL") ? "missing_config" : (res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed")) ? "channel_private" : "request_failed"
|
|
867
2512
|
])
|
|
@@ -875,35 +2520,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
875
2520
|
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
876
2521
|
}
|
|
877
2522
|
}
|
|
878
|
-
|
|
2523
|
+
self.resolveCall(call, data: res.toDict())
|
|
879
2524
|
}
|
|
880
2525
|
}
|
|
881
2526
|
}
|
|
882
2527
|
|
|
883
2528
|
@objc func getChannel(_ call: CAPPluginCall) {
|
|
884
|
-
|
|
885
|
-
|
|
2529
|
+
self.saveCallForAsyncHandling(call)
|
|
2530
|
+
DispatchQueue.global(qos: .utility).async {
|
|
2531
|
+
let res = self.implementation.getChannel(defaultChannelKey: self.defaultChannelDefaultsKey)
|
|
886
2532
|
if res.error != "" {
|
|
887
|
-
|
|
2533
|
+
self.rejectCall(call, message: res.error, code: "GETCHANNEL_FAILED", data: [
|
|
888
2534
|
"message": res.error,
|
|
889
2535
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
890
2536
|
])
|
|
891
2537
|
} else {
|
|
892
|
-
|
|
2538
|
+
self.resolveCall(call, data: res.toDict())
|
|
893
2539
|
}
|
|
894
2540
|
}
|
|
895
2541
|
}
|
|
896
2542
|
|
|
897
2543
|
@objc func listChannels(_ call: CAPPluginCall) {
|
|
898
|
-
|
|
2544
|
+
self.saveCallForAsyncHandling(call)
|
|
2545
|
+
DispatchQueue.global(qos: .utility).async {
|
|
899
2546
|
let res = self.implementation.listChannels()
|
|
900
2547
|
if res.error != "" {
|
|
901
|
-
|
|
2548
|
+
self.rejectCall(call, message: res.error, code: "LISTCHANNELS_FAILED", data: [
|
|
902
2549
|
"message": res.error,
|
|
903
2550
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
904
2551
|
])
|
|
905
2552
|
} else {
|
|
906
|
-
|
|
2553
|
+
var payload: JSObject = [:]
|
|
2554
|
+
payload["channels"] = res.channels
|
|
2555
|
+
self.resolveCall(call, data: payload)
|
|
907
2556
|
}
|
|
908
2557
|
}
|
|
909
2558
|
}
|
|
@@ -926,32 +2575,90 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
926
2575
|
call.resolve()
|
|
927
2576
|
}
|
|
928
2577
|
|
|
929
|
-
@objc func _reset(toLastSuccessful: Bool) -> Bool {
|
|
930
|
-
|
|
2578
|
+
@objc func _reset(toLastSuccessful: Bool, usePendingBundle: Bool) -> Bool {
|
|
2579
|
+
self.performReset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle, isInternal: false)
|
|
2580
|
+
}
|
|
931
2581
|
|
|
932
|
-
|
|
933
|
-
|
|
2582
|
+
func performReset(toLastSuccessful: Bool, usePendingBundle: Bool, isInternal: Bool) -> Bool {
|
|
2583
|
+
guard self.canPerformResetTransition() else { return false }
|
|
934
2584
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
return self.implementation.set(bundle: fallback) && self._reload()
|
|
940
|
-
}
|
|
2585
|
+
let fallback: BundleInfo = self.implementation.getFallbackBundle()
|
|
2586
|
+
let pending: BundleInfo? = self.implementation.getNextBundle()
|
|
2587
|
+
let previousState = self.implementation.captureResetState()
|
|
2588
|
+
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
941
2589
|
|
|
942
|
-
|
|
2590
|
+
if usePendingBundle {
|
|
2591
|
+
guard let pending = pending, !pending.isErrorStatus() else {
|
|
2592
|
+
logger.error("No pending bundle available to reset to")
|
|
2593
|
+
return false
|
|
2594
|
+
}
|
|
2595
|
+
guard self.implementation.canSet(bundle: pending) else {
|
|
2596
|
+
logger.error("Pending bundle is not installable")
|
|
2597
|
+
return false
|
|
2598
|
+
}
|
|
2599
|
+
self.implementation.prepareResetStateForTransition()
|
|
2600
|
+
logger.info("Resetting to pending bundle: \(pending.toString())")
|
|
2601
|
+
let didApplyPendingBundle: Bool
|
|
2602
|
+
if pending.isBuiltin() {
|
|
2603
|
+
didApplyPendingBundle = true
|
|
2604
|
+
} else {
|
|
2605
|
+
didApplyPendingBundle = self.implementation.set(bundle: pending)
|
|
2606
|
+
}
|
|
2607
|
+
if didApplyPendingBundle && self._reload() {
|
|
2608
|
+
self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
|
|
2609
|
+
self.notifyBundleSet(pending)
|
|
2610
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
2611
|
+
return true
|
|
2612
|
+
}
|
|
2613
|
+
self.implementation.restoreResetState(previousState)
|
|
2614
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
2615
|
+
return false
|
|
2616
|
+
}
|
|
943
2617
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
2618
|
+
// If developer wants to reset to the last successful bundle, and that bundle is not
|
|
2619
|
+
// the built-in bundle, set it as the bundle to use and reload.
|
|
2620
|
+
if toLastSuccessful && !fallback.isBuiltin() {
|
|
2621
|
+
if self.implementation.canSet(bundle: fallback) {
|
|
2622
|
+
self.implementation.prepareResetStateForTransition()
|
|
2623
|
+
logger.info("Resetting to: \(fallback.toString())")
|
|
2624
|
+
if self.implementation.set(bundle: fallback) && self._reload() {
|
|
2625
|
+
self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
|
|
2626
|
+
self.notifyBundleSet(fallback)
|
|
2627
|
+
return true
|
|
2628
|
+
}
|
|
2629
|
+
if !isInternal {
|
|
2630
|
+
self.implementation.restoreResetState(previousState)
|
|
2631
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
2632
|
+
return false
|
|
2633
|
+
}
|
|
2634
|
+
logger.warn("Fallback reload failed during internal reset, resetting to builtin instead")
|
|
2635
|
+
} else {
|
|
2636
|
+
logger.warn("Fallback bundle is not installable, resetting to builtin instead")
|
|
2637
|
+
}
|
|
947
2638
|
}
|
|
948
2639
|
|
|
2640
|
+
self.implementation.prepareResetStateForTransition()
|
|
2641
|
+
logger.info("Resetting to builtin version")
|
|
2642
|
+
if self._reload() {
|
|
2643
|
+
self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
|
|
2644
|
+
return true
|
|
2645
|
+
}
|
|
2646
|
+
if !isInternal {
|
|
2647
|
+
self.implementation.restoreResetState(previousState)
|
|
2648
|
+
self.restoreLiveBundleStateAfterFailedReload()
|
|
2649
|
+
}
|
|
949
2650
|
return false
|
|
950
2651
|
}
|
|
951
2652
|
|
|
2653
|
+
func canPerformResetTransition() -> Bool {
|
|
2654
|
+
guard let bridge = self.bridge else { return false }
|
|
2655
|
+
return (bridge.viewController as? CAPBridgeViewController) != nil
|
|
2656
|
+
}
|
|
2657
|
+
|
|
952
2658
|
@objc func reset(_ call: CAPPluginCall) {
|
|
953
2659
|
let toLastSuccessful = call.getBool("toLastSuccessful") ?? false
|
|
954
|
-
|
|
2660
|
+
let usePendingBundle = call.getBool("usePendingBundle") ?? false
|
|
2661
|
+
if self._reset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle) {
|
|
955
2662
|
call.resolve()
|
|
956
2663
|
} else {
|
|
957
2664
|
logger.error("Reset failed")
|
|
@@ -972,6 +2679,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
972
2679
|
let bundle = self.implementation.getCurrentBundle()
|
|
973
2680
|
self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
|
|
974
2681
|
logger.info("Current bundle loaded successfully. [notifyAppReady was called] \(bundle.toString())")
|
|
2682
|
+
self.clearIncomingPreviewTransition()
|
|
2683
|
+
self.hidePreviewTransitionLoader(reason: "notify-app-ready")
|
|
2684
|
+
|
|
975
2685
|
call.resolve(["bundle": bundle.toJSON()])
|
|
976
2686
|
}
|
|
977
2687
|
|
|
@@ -1021,6 +2731,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1021
2731
|
// Note: _checkCancelDelay method has been moved to DelayUpdateUtils class
|
|
1022
2732
|
|
|
1023
2733
|
private func _isAutoUpdateEnabled() -> Bool {
|
|
2734
|
+
if self.isPreviewSessionStateActive() {
|
|
2735
|
+
return false
|
|
2736
|
+
}
|
|
1024
2737
|
let instanceDescriptor = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor()
|
|
1025
2738
|
if instanceDescriptor?.serverURL != nil {
|
|
1026
2739
|
logger.warn("AutoUpdate is automatic disabled when serverUrl is set.")
|
|
@@ -1058,10 +2771,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1058
2771
|
logger.info("Built-in bundle is active. We skip the check for notifyAppReady.")
|
|
1059
2772
|
return
|
|
1060
2773
|
}
|
|
2774
|
+
if self.isPreviewSessionStateActive() {
|
|
2775
|
+
logger.info("Preview session is active. We skip the check for notifyAppReady.")
|
|
2776
|
+
return
|
|
2777
|
+
}
|
|
1061
2778
|
|
|
1062
2779
|
logger.info("Current bundle is: \(current.toString())")
|
|
1063
2780
|
|
|
1064
|
-
if BundleStatus.SUCCESS.
|
|
2781
|
+
if BundleStatus.SUCCESS.storedValue != current.getStatus() {
|
|
1065
2782
|
logger.error("notifyAppReady was not called, roll back current bundle: \(current.toString())")
|
|
1066
2783
|
logger.error("Did you forget to call 'notifyAppReady()' in your Capacitor App code?")
|
|
1067
2784
|
self.notifyListeners("updateFailed", data: [
|
|
@@ -1070,7 +2787,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1070
2787
|
self.persistLastFailedBundle(current)
|
|
1071
2788
|
self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
|
|
1072
2789
|
self.implementation.setError(bundle: current)
|
|
1073
|
-
_ = self.
|
|
2790
|
+
_ = self.performReset(toLastSuccessful: true, usePendingBundle: false, isInternal: true)
|
|
1074
2791
|
if self.autoDeleteFailed && !current.isBuiltin() {
|
|
1075
2792
|
logger.info("Deleting failing bundle: \(current.toString())")
|
|
1076
2793
|
let res = self.implementation.delete(id: current.getId(), removeInfo: false)
|
|
@@ -1095,6 +2812,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1095
2812
|
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
|
|
1096
2813
|
}
|
|
1097
2814
|
|
|
2815
|
+
private func notifyBundleSet(_ bundle: BundleInfo) {
|
|
2816
|
+
self.notifyListeners("set", data: ["bundle": bundle.toJSON()], retainUntilConsumed: true)
|
|
2817
|
+
}
|
|
2818
|
+
|
|
1098
2819
|
func sendReadyToJs(current: BundleInfo, msg: String) {
|
|
1099
2820
|
logger.info("sendReadyToJs")
|
|
1100
2821
|
DispatchQueue.global().async {
|
|
@@ -1106,6 +2827,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1106
2827
|
if self.autoSplashscreen {
|
|
1107
2828
|
self.hideSplashscreen()
|
|
1108
2829
|
}
|
|
2830
|
+
self.hidePreviewTransitionLoader(reason: "app-ready")
|
|
1109
2831
|
}
|
|
1110
2832
|
}
|
|
1111
2833
|
|
|
@@ -1122,32 +2844,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1122
2844
|
private func performHideSplashscreen() {
|
|
1123
2845
|
self.cancelSplashscreenTimeout()
|
|
1124
2846
|
self.removeSplashscreenLoader()
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
self.logger.info("Splashscreen hidden automatically")
|
|
1134
|
-
}, error: { (_) in
|
|
1135
|
-
self.logger.error("Failed to auto-hide splashscreen")
|
|
1136
|
-
})
|
|
1137
|
-
|
|
1138
|
-
// Try to call the SplashScreen hide method directly through the bridge
|
|
1139
|
-
if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
|
|
1140
|
-
// Use runtime method invocation to call hide method
|
|
1141
|
-
let selector = NSSelectorFromString("hide:")
|
|
1142
|
-
if splashScreenPlugin.responds(to: selector) {
|
|
1143
|
-
_ = splashScreenPlugin.perform(selector, with: call)
|
|
1144
|
-
self.logger.info("Called SplashScreen hide method")
|
|
1145
|
-
} else {
|
|
1146
|
-
self.logger.warn("autoSplashscreen: SplashScreen plugin does not respond to hide: method. Make sure @capacitor/splash-screen plugin is properly installed.")
|
|
1147
|
-
}
|
|
1148
|
-
} else {
|
|
1149
|
-
self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
|
|
1150
|
-
}
|
|
2847
|
+
self.splashscreenInvocationToken += 1
|
|
2848
|
+
self.invokeSplashscreenMethod(
|
|
2849
|
+
methodName: "hide",
|
|
2850
|
+
callbackId: "autoHideSplashscreen",
|
|
2851
|
+
options: self.splashscreenOptions(methodName: "hide"),
|
|
2852
|
+
retriesRemaining: self.splashscreenMaxRetries,
|
|
2853
|
+
requestToken: self.splashscreenInvocationToken
|
|
2854
|
+
)
|
|
1151
2855
|
}
|
|
1152
2856
|
|
|
1153
2857
|
private func showSplashscreen() {
|
|
@@ -1158,40 +2862,182 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1158
2862
|
self.performShowSplashscreen()
|
|
1159
2863
|
}
|
|
1160
2864
|
}
|
|
1161
|
-
}
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
private func performShowSplashscreen() {
|
|
2868
|
+
self.cancelSplashscreenTimeout()
|
|
2869
|
+
self.autoSplashscreenTimedOut = false
|
|
2870
|
+
self.splashscreenInvocationToken += 1
|
|
2871
|
+
self.invokeSplashscreenMethod(
|
|
2872
|
+
methodName: "show",
|
|
2873
|
+
callbackId: "autoShowSplashscreen",
|
|
2874
|
+
options: self.splashscreenOptions(methodName: "show"),
|
|
2875
|
+
retriesRemaining: self.splashscreenMaxRetries,
|
|
2876
|
+
requestToken: self.splashscreenInvocationToken
|
|
2877
|
+
)
|
|
2878
|
+
|
|
2879
|
+
self.addSplashscreenLoaderIfNeeded()
|
|
2880
|
+
self.scheduleSplashscreenTimeout()
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
private func splashscreenOptions(methodName: String) -> [String: Any] {
|
|
2884
|
+
methodName == "show" ? ["autoHide": false] : [:]
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
private func splashscreenCompletedMessage(methodName: String) -> String {
|
|
2888
|
+
methodName == "show" ? "Splashscreen shown automatically" : "Splashscreen hidden automatically"
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
func splashscreenOptionsForTesting(methodName: String) -> [String: Any] {
|
|
2892
|
+
self.splashscreenOptions(methodName: methodName)
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
func isCurrentSplashscreenInvocationTokenForTesting(_ requestToken: Int) -> Bool {
|
|
2896
|
+
requestToken == self.splashscreenInvocationToken
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
func advanceSplashscreenInvocationTokenForTesting() {
|
|
2900
|
+
self.splashscreenInvocationToken += 1
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
private func makeSplashscreenCall(callbackId: String, options: [String: Any], methodName: String) -> CAPPluginCall {
|
|
2904
|
+
CAPPluginCall(callbackId: callbackId, options: options, success: { [weak self] (_, _) in
|
|
2905
|
+
guard let self = self else { return }
|
|
2906
|
+
self.logger.info(self.splashscreenCompletedMessage(methodName: methodName))
|
|
2907
|
+
}, error: { [weak self] (_) in
|
|
2908
|
+
guard let self = self else { return }
|
|
2909
|
+
self.logger.error("Failed to auto-\(methodName) splashscreen")
|
|
2910
|
+
})
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
private func invokeSplashscreenMethod(
|
|
2914
|
+
methodName: String,
|
|
2915
|
+
callbackId: String,
|
|
2916
|
+
options: [String: Any],
|
|
2917
|
+
retriesRemaining: Int,
|
|
2918
|
+
requestToken: Int
|
|
2919
|
+
) {
|
|
2920
|
+
guard requestToken == self.splashscreenInvocationToken else {
|
|
2921
|
+
return
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
guard let bridge = self.bridge else {
|
|
2925
|
+
self.retrySplashscreenMethod(
|
|
2926
|
+
methodName: methodName,
|
|
2927
|
+
callbackId: callbackId,
|
|
2928
|
+
options: options,
|
|
2929
|
+
retriesRemaining: retriesRemaining,
|
|
2930
|
+
requestToken: requestToken,
|
|
2931
|
+
message: "Bridge not available for \(methodName == "show" ? "showing" : "hiding") splashscreen with autoSplashscreen"
|
|
2932
|
+
)
|
|
2933
|
+
return
|
|
2934
|
+
}
|
|
1162
2935
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
2936
|
+
guard let splashScreenPlugin = bridge.plugin(withName: self.splashscreenPluginName) else {
|
|
2937
|
+
self.retrySplashscreenMethod(
|
|
2938
|
+
methodName: methodName,
|
|
2939
|
+
callbackId: callbackId,
|
|
2940
|
+
options: options,
|
|
2941
|
+
retriesRemaining: retriesRemaining,
|
|
2942
|
+
requestToken: requestToken,
|
|
2943
|
+
message: "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
|
|
2944
|
+
)
|
|
2945
|
+
return
|
|
2946
|
+
}
|
|
1166
2947
|
|
|
1167
|
-
|
|
1168
|
-
|
|
2948
|
+
let selector = NSSelectorFromString("\(methodName):")
|
|
2949
|
+
guard splashScreenPlugin.responds(to: selector) else {
|
|
2950
|
+
self.retrySplashscreenMethod(
|
|
2951
|
+
methodName: methodName,
|
|
2952
|
+
callbackId: callbackId,
|
|
2953
|
+
options: options,
|
|
2954
|
+
retriesRemaining: retriesRemaining,
|
|
2955
|
+
requestToken: requestToken,
|
|
2956
|
+
message: "autoSplashscreen: SplashScreen plugin does not respond to \(methodName): method. Make sure @capacitor/splash-screen plugin is properly installed."
|
|
2957
|
+
)
|
|
1169
2958
|
return
|
|
1170
2959
|
}
|
|
1171
2960
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
self.logger.error("Failed to auto-show splashscreen")
|
|
1177
|
-
})
|
|
2961
|
+
let call = self.makeSplashscreenCall(callbackId: callbackId, options: options, methodName: methodName)
|
|
2962
|
+
_ = splashScreenPlugin.perform(selector, with: call)
|
|
2963
|
+
self.logger.info("Called SplashScreen \(methodName) method")
|
|
2964
|
+
}
|
|
1178
2965
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
2966
|
+
private func retrySplashscreenMethod(
|
|
2967
|
+
methodName: String,
|
|
2968
|
+
callbackId: String,
|
|
2969
|
+
options: [String: Any],
|
|
2970
|
+
retriesRemaining: Int,
|
|
2971
|
+
requestToken: Int,
|
|
2972
|
+
message: String
|
|
2973
|
+
) {
|
|
2974
|
+
guard retriesRemaining > 0 else {
|
|
2975
|
+
if methodName == "show" {
|
|
2976
|
+
self.logger.warn(message)
|
|
1186
2977
|
} else {
|
|
1187
|
-
self.logger.
|
|
2978
|
+
self.logger.error(message)
|
|
2979
|
+
}
|
|
2980
|
+
return
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
self.logger.info("\(message). Retrying.")
|
|
2984
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.splashscreenRetryDelayMilliseconds)) { [weak self] in
|
|
2985
|
+
guard let self = self, requestToken == self.splashscreenInvocationToken else {
|
|
2986
|
+
return
|
|
1188
2987
|
}
|
|
2988
|
+
self.invokeSplashscreenMethod(
|
|
2989
|
+
methodName: methodName,
|
|
2990
|
+
callbackId: callbackId,
|
|
2991
|
+
options: options,
|
|
2992
|
+
retriesRemaining: retriesRemaining - 1,
|
|
2993
|
+
requestToken: requestToken
|
|
2994
|
+
)
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
private func createLoaderOverlay(
|
|
2999
|
+
backgroundColor: UIColor,
|
|
3000
|
+
isUserInteractionEnabled: Bool,
|
|
3001
|
+
indicatorColor: UIColor?
|
|
3002
|
+
) -> (container: UIView, indicator: UIActivityIndicatorView) {
|
|
3003
|
+
let container = UIView()
|
|
3004
|
+
container.translatesAutoresizingMaskIntoConstraints = false
|
|
3005
|
+
container.backgroundColor = backgroundColor
|
|
3006
|
+
container.isUserInteractionEnabled = isUserInteractionEnabled
|
|
3007
|
+
|
|
3008
|
+
let indicatorStyle: UIActivityIndicatorView.Style
|
|
3009
|
+
if #available(iOS 13.0, *) {
|
|
3010
|
+
indicatorStyle = .large
|
|
1189
3011
|
} else {
|
|
1190
|
-
|
|
3012
|
+
indicatorStyle = .whiteLarge
|
|
1191
3013
|
}
|
|
1192
3014
|
|
|
1193
|
-
|
|
1194
|
-
|
|
3015
|
+
let indicator = UIActivityIndicatorView(style: indicatorStyle)
|
|
3016
|
+
indicator.translatesAutoresizingMaskIntoConstraints = false
|
|
3017
|
+
indicator.hidesWhenStopped = false
|
|
3018
|
+
if let indicatorColor = indicatorColor {
|
|
3019
|
+
indicator.color = indicatorColor
|
|
3020
|
+
}
|
|
3021
|
+
indicator.startAnimating()
|
|
3022
|
+
|
|
3023
|
+
return (container, indicator)
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
private func attachLoaderOverlay(
|
|
3027
|
+
_ overlay: (container: UIView, indicator: UIActivityIndicatorView),
|
|
3028
|
+
to rootView: UIView
|
|
3029
|
+
) {
|
|
3030
|
+
overlay.container.addSubview(overlay.indicator)
|
|
3031
|
+
rootView.addSubview(overlay.container)
|
|
3032
|
+
|
|
3033
|
+
NSLayoutConstraint.activate([
|
|
3034
|
+
overlay.container.leadingAnchor.constraint(equalTo: rootView.leadingAnchor),
|
|
3035
|
+
overlay.container.trailingAnchor.constraint(equalTo: rootView.trailingAnchor),
|
|
3036
|
+
overlay.container.topAnchor.constraint(equalTo: rootView.topAnchor),
|
|
3037
|
+
overlay.container.bottomAnchor.constraint(equalTo: rootView.bottomAnchor),
|
|
3038
|
+
overlay.indicator.centerXAnchor.constraint(equalTo: overlay.container.centerXAnchor),
|
|
3039
|
+
overlay.indicator.centerYAnchor.constraint(equalTo: overlay.container.centerYAnchor)
|
|
3040
|
+
])
|
|
1195
3041
|
}
|
|
1196
3042
|
|
|
1197
3043
|
private func addSplashscreenLoaderIfNeeded() {
|
|
@@ -1208,40 +3054,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1208
3054
|
return
|
|
1209
3055
|
}
|
|
1210
3056
|
|
|
1211
|
-
let
|
|
1212
|
-
container.translatesAutoresizingMaskIntoConstraints = false
|
|
1213
|
-
container.backgroundColor = UIColor.clear
|
|
1214
|
-
container.isUserInteractionEnabled = false
|
|
1215
|
-
|
|
1216
|
-
let indicatorStyle: UIActivityIndicatorView.Style
|
|
3057
|
+
let indicatorColor: UIColor?
|
|
1217
3058
|
if #available(iOS 13.0, *) {
|
|
1218
|
-
|
|
3059
|
+
indicatorColor = UIColor.label
|
|
1219
3060
|
} else {
|
|
1220
|
-
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
let indicator = UIActivityIndicatorView(style: indicatorStyle)
|
|
1224
|
-
indicator.translatesAutoresizingMaskIntoConstraints = false
|
|
1225
|
-
indicator.hidesWhenStopped = false
|
|
1226
|
-
if #available(iOS 13.0, *) {
|
|
1227
|
-
indicator.color = UIColor.label
|
|
3061
|
+
indicatorColor = nil
|
|
1228
3062
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
container.topAnchor.constraint(equalTo: rootView.topAnchor),
|
|
1238
|
-
container.bottomAnchor.constraint(equalTo: rootView.bottomAnchor),
|
|
1239
|
-
indicator.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
|
1240
|
-
indicator.centerYAnchor.constraint(equalTo: container.centerYAnchor)
|
|
1241
|
-
])
|
|
1242
|
-
|
|
1243
|
-
self.splashscreenLoaderContainer = container
|
|
1244
|
-
self.splashscreenLoaderView = indicator
|
|
3063
|
+
let overlay = self.createLoaderOverlay(
|
|
3064
|
+
backgroundColor: UIColor.clear,
|
|
3065
|
+
isUserInteractionEnabled: false,
|
|
3066
|
+
indicatorColor: indicatorColor
|
|
3067
|
+
)
|
|
3068
|
+
self.attachLoaderOverlay(overlay, to: rootView)
|
|
3069
|
+
self.splashscreenLoaderContainer = overlay.container
|
|
3070
|
+
self.splashscreenLoaderView = overlay.indicator
|
|
1245
3071
|
}
|
|
1246
3072
|
|
|
1247
3073
|
if Thread.isMainThread {
|
|
@@ -1270,6 +3096,97 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1270
3096
|
}
|
|
1271
3097
|
}
|
|
1272
3098
|
|
|
3099
|
+
private func showPreviewTransitionLoader(reason: String) {
|
|
3100
|
+
self.previewTransitionLoaderRequested = true
|
|
3101
|
+
let showLoader = {
|
|
3102
|
+
guard self.previewTransitionLoaderRequested else {
|
|
3103
|
+
return
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
if let container = self.previewTransitionLoaderContainer {
|
|
3107
|
+
self.previewTransitionLoaderTimeoutWorkItem?.cancel()
|
|
3108
|
+
self.schedulePreviewTransitionLoaderTimeout()
|
|
3109
|
+
container.superview?.bringSubviewToFront(container)
|
|
3110
|
+
return
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
guard let rootView = self.bridge?.viewController?.view else {
|
|
3114
|
+
self.logger.warn("Preview transition loader unavailable: root view missing for \(reason)")
|
|
3115
|
+
self.previewTransitionLoaderRequested = false
|
|
3116
|
+
return
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
self.previewTransitionLoaderTimeoutWorkItem?.cancel()
|
|
3120
|
+
self.schedulePreviewTransitionLoaderTimeout()
|
|
3121
|
+
|
|
3122
|
+
let indicatorColor: UIColor?
|
|
3123
|
+
if #available(iOS 13.0, *) {
|
|
3124
|
+
indicatorColor = UIColor.white
|
|
3125
|
+
} else {
|
|
3126
|
+
indicatorColor = nil
|
|
3127
|
+
}
|
|
3128
|
+
let overlay = self.createLoaderOverlay(
|
|
3129
|
+
backgroundColor: UIColor.black.withAlphaComponent(0.18),
|
|
3130
|
+
isUserInteractionEnabled: true,
|
|
3131
|
+
indicatorColor: indicatorColor
|
|
3132
|
+
)
|
|
3133
|
+
self.attachLoaderOverlay(overlay, to: rootView)
|
|
3134
|
+
self.previewTransitionLoaderContainer = overlay.container
|
|
3135
|
+
self.previewTransitionLoaderView = overlay.indicator
|
|
3136
|
+
self.logger.info("Preview transition loader shown: \(reason)")
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
if Thread.isMainThread {
|
|
3140
|
+
showLoader()
|
|
3141
|
+
} else {
|
|
3142
|
+
DispatchQueue.main.async {
|
|
3143
|
+
showLoader()
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
private func hidePreviewTransitionLoader(reason: String) {
|
|
3149
|
+
if !self.previewTransitionLoaderRequested &&
|
|
3150
|
+
self.previewTransitionLoaderContainer == nil &&
|
|
3151
|
+
self.previewTransitionLoaderTimeoutWorkItem == nil {
|
|
3152
|
+
return
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
let hideLoader = {
|
|
3156
|
+
self.previewTransitionLoaderRequested = false
|
|
3157
|
+
self.previewTransitionLoaderTimeoutWorkItem?.cancel()
|
|
3158
|
+
self.previewTransitionLoaderTimeoutWorkItem = nil
|
|
3159
|
+
guard self.previewTransitionLoaderContainer != nil else {
|
|
3160
|
+
return
|
|
3161
|
+
}
|
|
3162
|
+
self.previewTransitionLoaderView?.stopAnimating()
|
|
3163
|
+
self.previewTransitionLoaderContainer?.removeFromSuperview()
|
|
3164
|
+
self.previewTransitionLoaderView = nil
|
|
3165
|
+
self.previewTransitionLoaderContainer = nil
|
|
3166
|
+
self.logger.info("Preview transition loader hidden: \(reason)")
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
if Thread.isMainThread {
|
|
3170
|
+
hideLoader()
|
|
3171
|
+
} else {
|
|
3172
|
+
DispatchQueue.main.async {
|
|
3173
|
+
hideLoader()
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
private func schedulePreviewTransitionLoaderTimeout() {
|
|
3179
|
+
self.previewTransitionLoaderTimeoutWorkItem?.cancel()
|
|
3180
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
3181
|
+
self?.hidePreviewTransitionLoader(reason: "preview-transition-timeout")
|
|
3182
|
+
}
|
|
3183
|
+
self.previewTransitionLoaderTimeoutWorkItem = workItem
|
|
3184
|
+
DispatchQueue.main.asyncAfter(
|
|
3185
|
+
deadline: .now() + .milliseconds(Self.previewLoaderTimeoutMs),
|
|
3186
|
+
execute: workItem
|
|
3187
|
+
)
|
|
3188
|
+
}
|
|
3189
|
+
|
|
1273
3190
|
private func scheduleSplashscreenTimeout() {
|
|
1274
3191
|
guard self.autoSplashscreenTimeout > 0 else {
|
|
1275
3192
|
return
|
|
@@ -1329,6 +3246,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1329
3246
|
}
|
|
1330
3247
|
|
|
1331
3248
|
private func shouldUseDirectUpdate() -> Bool {
|
|
3249
|
+
if !self.autoUpdate || self.autoUpdateMode == Self.autoUpdateModeOnlyDownload {
|
|
3250
|
+
return false
|
|
3251
|
+
}
|
|
1332
3252
|
if self.autoSplashscreenTimedOut {
|
|
1333
3253
|
return false
|
|
1334
3254
|
}
|
|
@@ -1345,7 +3265,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1345
3265
|
}
|
|
1346
3266
|
return false
|
|
1347
3267
|
case "onLaunch":
|
|
1348
|
-
if !self.
|
|
3268
|
+
if !self.getOnLaunchDirectUpdateUsed() {
|
|
1349
3269
|
return true
|
|
1350
3270
|
}
|
|
1351
3271
|
return false
|
|
@@ -1355,6 +3275,183 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1355
3275
|
}
|
|
1356
3276
|
}
|
|
1357
3277
|
|
|
3278
|
+
private func configureAutoUpdateModeFromConfig() {
|
|
3279
|
+
if let configuredMode = getConfig().getString("autoUpdate"),
|
|
3280
|
+
configuredMode != "",
|
|
3281
|
+
configuredMode != "true",
|
|
3282
|
+
configuredMode != "false" {
|
|
3283
|
+
autoUpdateMode = Self.normalizedAutoUpdateMode(configuredMode)
|
|
3284
|
+
if autoUpdateMode != configuredMode {
|
|
3285
|
+
logger.error(
|
|
3286
|
+
"Invalid autoUpdate value: \"\(configuredMode)\". Supported values are: true, false, " +
|
|
3287
|
+
"\"off\", \"atBackground\", \"atInstall\", \"onLaunch\", \"always\", \"onlyDownload\". Defaulting to \"atBackground\"."
|
|
3288
|
+
)
|
|
3289
|
+
}
|
|
3290
|
+
} else {
|
|
3291
|
+
let configuredMode = getConfig().getString("autoUpdate")
|
|
3292
|
+
let enabled = configuredMode != nil ? configuredMode == "true" : getConfig().getBoolean("autoUpdate", true)
|
|
3293
|
+
autoUpdateMode = enabled
|
|
3294
|
+
? Self.autoUpdateModeForLegacyDirectUpdateMode(resolveLegacyDirectUpdateModeFromConfig())
|
|
3295
|
+
: Self.autoUpdateModeOff
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
autoUpdate = Self.isAutoUpdateModeEnabled(autoUpdateMode)
|
|
3299
|
+
directUpdateMode = Self.directUpdateModeForAutoUpdateMode(autoUpdateMode)
|
|
3300
|
+
directUpdate = Self.isDirectUpdateMode(directUpdateMode)
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
private func resolveLegacyDirectUpdateModeFromConfig() -> String {
|
|
3304
|
+
if let directUpdateString = getConfig().getString("directUpdate") {
|
|
3305
|
+
if directUpdateString == "true" {
|
|
3306
|
+
return Self.autoUpdateModeAlways
|
|
3307
|
+
}
|
|
3308
|
+
if directUpdateString == "false" || Self.isDirectUpdateMode(directUpdateString) {
|
|
3309
|
+
return directUpdateString
|
|
3310
|
+
}
|
|
3311
|
+
logger.error(
|
|
3312
|
+
"Invalid directUpdate value: \"\(directUpdateString)\". Supported values are: false, true, " +
|
|
3313
|
+
"\"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\"."
|
|
3314
|
+
)
|
|
3315
|
+
return "false"
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
return getConfig().getBoolean("directUpdate", false) ? Self.autoUpdateModeAlways : "false"
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
static func normalizedAutoUpdateMode(_ value: String?) -> String {
|
|
3322
|
+
guard let value else {
|
|
3323
|
+
return autoUpdateModeBackground
|
|
3324
|
+
}
|
|
3325
|
+
switch value {
|
|
3326
|
+
case "false", autoUpdateModeOff:
|
|
3327
|
+
return autoUpdateModeOff
|
|
3328
|
+
case "true", autoUpdateModeBackground:
|
|
3329
|
+
return autoUpdateModeBackground
|
|
3330
|
+
case autoUpdateModeInstall, autoUpdateModeLaunch, autoUpdateModeAlways, autoUpdateModeOnlyDownload:
|
|
3331
|
+
return value
|
|
3332
|
+
default:
|
|
3333
|
+
return autoUpdateModeBackground
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
static func normalizedShakeMenuGesture(_ value: String?) -> String {
|
|
3338
|
+
guard let value, !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
3339
|
+
return shakeMenuGestureShake
|
|
3340
|
+
}
|
|
3341
|
+
let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
3342
|
+
if normalized == shakeMenuGestureThreeFingerPinch {
|
|
3343
|
+
return shakeMenuGestureThreeFingerPinch
|
|
3344
|
+
}
|
|
3345
|
+
return shakeMenuGestureShake
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
static func isSupportedShakeMenuGesture(_ value: String?) -> Bool {
|
|
3349
|
+
guard let value else {
|
|
3350
|
+
return true
|
|
3351
|
+
}
|
|
3352
|
+
let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
3353
|
+
if normalized.isEmpty {
|
|
3354
|
+
return false
|
|
3355
|
+
}
|
|
3356
|
+
return normalized == shakeMenuGestureShake || normalized == shakeMenuGestureThreeFingerPinch
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
static func autoUpdateModeForLegacyDirectUpdateMode(_ directUpdateMode: String) -> String {
|
|
3360
|
+
switch directUpdateMode {
|
|
3361
|
+
case autoUpdateModeInstall, autoUpdateModeLaunch, autoUpdateModeAlways:
|
|
3362
|
+
return directUpdateMode
|
|
3363
|
+
default:
|
|
3364
|
+
return autoUpdateModeBackground
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
static func directUpdateModeForAutoUpdateMode(_ autoUpdateMode: String) -> String {
|
|
3369
|
+
switch autoUpdateMode {
|
|
3370
|
+
case autoUpdateModeInstall, autoUpdateModeLaunch, autoUpdateModeAlways:
|
|
3371
|
+
return autoUpdateMode
|
|
3372
|
+
default:
|
|
3373
|
+
return "false"
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
static func isAutoUpdateModeEnabled(_ autoUpdateMode: String) -> Bool {
|
|
3378
|
+
autoUpdateMode != autoUpdateModeOff
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
static func shouldAutoUpdateModeSetNextBundle(_ autoUpdateMode: String) -> Bool {
|
|
3382
|
+
isAutoUpdateModeEnabled(autoUpdateMode) && autoUpdateMode != autoUpdateModeOnlyDownload
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
static func isDirectUpdateMode(_ directUpdateMode: String) -> Bool {
|
|
3386
|
+
directUpdateMode == autoUpdateModeInstall || directUpdateMode == autoUpdateModeLaunch || directUpdateMode == autoUpdateModeAlways
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
private func shouldAutoSetNextBundle() -> Bool {
|
|
3390
|
+
Self.shouldAutoUpdateModeSetNextBundle(autoUpdateMode)
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
static func shouldConsumeOnLaunchDirectUpdate(directUpdateMode: String, plannedDirectUpdate: Bool) -> Bool {
|
|
3394
|
+
plannedDirectUpdate && directUpdateMode == "onLaunch"
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
static func normalizedPeriodCheckDelaySeconds(_ value: Int) -> Int {
|
|
3398
|
+
guard value > 0 else {
|
|
3399
|
+
return 0
|
|
3400
|
+
}
|
|
3401
|
+
return max(600, value)
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
private func getOnLaunchDirectUpdateUsed() -> Bool {
|
|
3405
|
+
self.onLaunchDirectUpdateStateLock.lock()
|
|
3406
|
+
defer { self.onLaunchDirectUpdateStateLock.unlock() }
|
|
3407
|
+
return self.onLaunchDirectUpdateUsed
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
private func setOnLaunchDirectUpdateUsed(_ used: Bool) {
|
|
3411
|
+
self.onLaunchDirectUpdateStateLock.lock()
|
|
3412
|
+
self.onLaunchDirectUpdateUsed = used
|
|
3413
|
+
self.onLaunchDirectUpdateStateLock.unlock()
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
private func consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: Bool) {
|
|
3417
|
+
guard Self.shouldConsumeOnLaunchDirectUpdate(directUpdateMode: self.directUpdateMode, plannedDirectUpdate: plannedDirectUpdate) else {
|
|
3418
|
+
return
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
self.setOnLaunchDirectUpdateUsed(true)
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
func configureDirectUpdateModeForTesting(_ directUpdateMode: String, onLaunchDirectUpdateUsed: Bool = false) {
|
|
3425
|
+
self.directUpdateMode = directUpdateMode
|
|
3426
|
+
self.autoUpdateMode = Self.autoUpdateModeForLegacyDirectUpdateMode(directUpdateMode)
|
|
3427
|
+
self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
|
|
3428
|
+
self.directUpdate = Self.isDirectUpdateMode(self.directUpdateMode)
|
|
3429
|
+
self.setOnLaunchDirectUpdateUsed(onLaunchDirectUpdateUsed)
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
func setUpdateUrlForTesting(_ updateUrl: String) {
|
|
3433
|
+
self.updateUrl = updateUrl
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
func setAutoUpdateModeForTesting(_ autoUpdateMode: String) {
|
|
3437
|
+
self.autoUpdateMode = Self.normalizedAutoUpdateMode(autoUpdateMode)
|
|
3438
|
+
self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
|
|
3439
|
+
self.directUpdateMode = Self.directUpdateModeForAutoUpdateMode(self.autoUpdateMode)
|
|
3440
|
+
self.directUpdate = Self.isDirectUpdateMode(self.directUpdateMode)
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
func setCurrentBuildVersionForTesting(_ currentBuildVersion: String) {
|
|
3444
|
+
self.currentBuildVersion = currentBuildVersion
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
func shouldUseDirectUpdateForTesting() -> Bool {
|
|
3448
|
+
self.shouldUseDirectUpdate()
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
var hasConsumedOnLaunchDirectUpdateForTesting: Bool {
|
|
3452
|
+
self.getOnLaunchDirectUpdateUsed()
|
|
3453
|
+
}
|
|
3454
|
+
|
|
1358
3455
|
private func notifyBreakingEvents(version: String) {
|
|
1359
3456
|
guard !version.isEmpty else {
|
|
1360
3457
|
return
|
|
@@ -1364,14 +3461,82 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1364
3461
|
self.notifyListeners("majorAvailable", data: payload)
|
|
1365
3462
|
}
|
|
1366
3463
|
|
|
3464
|
+
private func shouldNotifyBreakingEvents(response: AppVersion) -> Bool {
|
|
3465
|
+
if response.breaking == true {
|
|
3466
|
+
return true
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
return response.error == "disable_auto_update_to_major" || response.message == "store_update_required"
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
private func notifyBreakingEventsIfNeeded(response: AppVersion, version: String) {
|
|
3473
|
+
if self.shouldNotifyBreakingEvents(response: response) {
|
|
3474
|
+
let eventVersion = version.isEmpty ? self.implementation.getCurrentBundle().getVersionName() : version
|
|
3475
|
+
self.notifyBreakingEvents(version: eventVersion)
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
static func normalizedUpdateResponseKind(kind: String?) -> String {
|
|
3480
|
+
if let kind, ["up_to_date", "blocked", "failed"].contains(kind) {
|
|
3481
|
+
return kind
|
|
3482
|
+
}
|
|
3483
|
+
return "failed"
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
private func updateResponseKind(kind: String?) -> String {
|
|
3487
|
+
Self.normalizedUpdateResponseKind(kind: kind)
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
private func endBackgroundDownloadAfterLatestError(
|
|
3491
|
+
backendError: String,
|
|
3492
|
+
res: AppVersion,
|
|
3493
|
+
current: BundleInfo,
|
|
3494
|
+
plannedDirectUpdate: Bool
|
|
3495
|
+
) {
|
|
3496
|
+
let statusCode = res.statusCode
|
|
3497
|
+
let responseKind = self.updateResponseKind(kind: res.kind)
|
|
3498
|
+
let responseMessage = res.message?.isEmpty == false ? res.message : nil
|
|
3499
|
+
let message = responseMessage ?? (backendError.isEmpty ? "server did not provide a message" : backendError)
|
|
3500
|
+
let latestVersionName = res.version.isEmpty ? current.getVersionName() : res.version
|
|
3501
|
+
self.notifyListeners("updateCheckResult", data: [
|
|
3502
|
+
"kind": responseKind,
|
|
3503
|
+
"error": backendError,
|
|
3504
|
+
"message": message,
|
|
3505
|
+
"statusCode": statusCode,
|
|
3506
|
+
"version": latestVersionName,
|
|
3507
|
+
"bundle": current.toJSON()
|
|
3508
|
+
])
|
|
3509
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
|
|
3510
|
+
|
|
3511
|
+
if responseKind == "up_to_date" {
|
|
3512
|
+
self.logger.info("No new version available")
|
|
3513
|
+
} else if responseKind == "blocked" {
|
|
3514
|
+
self.logger.info("Update check blocked with error: \(backendError)")
|
|
3515
|
+
} else {
|
|
3516
|
+
self.logger.error("getLatest failed with error: \(backendError)")
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
let isFailure = responseKind == "failed"
|
|
3520
|
+
self.endBackGroundTaskWithNotif(
|
|
3521
|
+
msg: message,
|
|
3522
|
+
latestVersionName: latestVersionName,
|
|
3523
|
+
current: current,
|
|
3524
|
+
error: isFailure,
|
|
3525
|
+
plannedDirectUpdate: plannedDirectUpdate,
|
|
3526
|
+
sendStats: isFailure
|
|
3527
|
+
)
|
|
3528
|
+
}
|
|
3529
|
+
|
|
1367
3530
|
func endBackGroundTaskWithNotif(
|
|
1368
3531
|
msg: String,
|
|
1369
3532
|
latestVersionName: String,
|
|
1370
3533
|
current: BundleInfo,
|
|
1371
3534
|
error: Bool = true,
|
|
3535
|
+
plannedDirectUpdate: Bool = false,
|
|
1372
3536
|
failureAction: String = "download_fail",
|
|
1373
3537
|
failureEvent: String = "downloadFailed",
|
|
1374
|
-
sendStats: Bool = true
|
|
3538
|
+
sendStats: Bool = true,
|
|
3539
|
+
notifyNoNeedUpdate: Bool = true
|
|
1375
3540
|
) {
|
|
1376
3541
|
// Clear download in progress flag - this is called at the end of every download attempt
|
|
1377
3542
|
// whether it succeeds, fails, or is skipped (e.g., already up to date)
|
|
@@ -1380,13 +3545,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1380
3545
|
downloadInProgress = false
|
|
1381
3546
|
downloadStartTime = nil
|
|
1382
3547
|
|
|
3548
|
+
self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
|
|
3549
|
+
|
|
1383
3550
|
if error {
|
|
1384
3551
|
if sendStats {
|
|
1385
3552
|
self.implementation.sendStats(action: failureAction, versionName: current.getVersionName())
|
|
1386
3553
|
}
|
|
1387
3554
|
self.notifyListeners(failureEvent, data: ["version": latestVersionName])
|
|
1388
3555
|
}
|
|
1389
|
-
|
|
3556
|
+
if notifyNoNeedUpdate {
|
|
3557
|
+
self.notifyListeners("noNeedUpdate", data: ["bundle": current.toJSON()])
|
|
3558
|
+
}
|
|
1390
3559
|
self.sendReadyToJs(current: current, msg: msg)
|
|
1391
3560
|
logger.info("endBackGroundTaskWithNotif \(msg) current: \(current.getVersionName()) latestVersionName: \(latestVersionName)")
|
|
1392
3561
|
self.endBackGroundTask()
|
|
@@ -1414,7 +3583,41 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1414
3583
|
return true
|
|
1415
3584
|
}
|
|
1416
3585
|
|
|
3586
|
+
private func clearDownloadInProgressState() {
|
|
3587
|
+
downloadLock.lock()
|
|
3588
|
+
defer { downloadLock.unlock() }
|
|
3589
|
+
downloadInProgress = false
|
|
3590
|
+
downloadStartTime = nil
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
|
|
3594
|
+
// Live update checks/downloads are user-visible work. Using `.background`
|
|
3595
|
+
// lets the scheduler starve them for minutes while the app is active.
|
|
3596
|
+
DispatchQueue.global(qos: .utility).async(execute: work)
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
private func beginDownloadBackgroundTask() {
|
|
3600
|
+
let registerTask = {
|
|
3601
|
+
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Download Tasks") {
|
|
3602
|
+
self.endBackGroundTask()
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
if Thread.isMainThread {
|
|
3607
|
+
registerTask()
|
|
3608
|
+
} else {
|
|
3609
|
+
DispatchQueue.main.sync(execute: registerTask)
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
func runGetLatestWork(_ work: @escaping () -> Void) {
|
|
3614
|
+
DispatchQueue.global(qos: .background).async(execute: work)
|
|
3615
|
+
}
|
|
3616
|
+
|
|
1417
3617
|
func backgroundDownload() {
|
|
3618
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3619
|
+
return
|
|
3620
|
+
}
|
|
1418
3621
|
// Set download in progress flag (thread-safe)
|
|
1419
3622
|
downloadLock.lock()
|
|
1420
3623
|
downloadInProgress = true
|
|
@@ -1422,7 +3625,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1422
3625
|
downloadLock.unlock()
|
|
1423
3626
|
|
|
1424
3627
|
let plannedDirectUpdate = self.shouldUseDirectUpdate()
|
|
1425
|
-
let messageUpdate
|
|
3628
|
+
let messageUpdate: String
|
|
3629
|
+
if plannedDirectUpdate {
|
|
3630
|
+
messageUpdate = "Update will occur now."
|
|
3631
|
+
} else if self.shouldAutoSetNextBundle() {
|
|
3632
|
+
messageUpdate = "Update will occur next time app moves to background."
|
|
3633
|
+
} else {
|
|
3634
|
+
messageUpdate = "Update will be downloaded and made available."
|
|
3635
|
+
}
|
|
1426
3636
|
guard let url = URL(string: self.updateUrl) else {
|
|
1427
3637
|
logger.error("Error no url or wrong format")
|
|
1428
3638
|
// Clear the flag if we return early
|
|
@@ -1433,28 +3643,32 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1433
3643
|
return
|
|
1434
3644
|
}
|
|
1435
3645
|
|
|
1436
|
-
|
|
3646
|
+
self.runBackgroundDownloadWork {
|
|
1437
3647
|
// Wait for cleanup to complete before starting download
|
|
1438
3648
|
self.waitForCleanupIfNeeded()
|
|
1439
|
-
self.
|
|
1440
|
-
|
|
1441
|
-
|
|
3649
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3650
|
+
self.clearDownloadInProgressState()
|
|
3651
|
+
return
|
|
1442
3652
|
}
|
|
3653
|
+
self.beginDownloadBackgroundTask()
|
|
1443
3654
|
self.logger.info("Check for update via \(self.updateUrl)")
|
|
1444
3655
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
1445
3656
|
let current = self.implementation.getCurrentBundle()
|
|
3657
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3658
|
+
self.clearDownloadInProgressState()
|
|
3659
|
+
self.endBackGroundTask()
|
|
3660
|
+
return
|
|
3661
|
+
}
|
|
1446
3662
|
|
|
1447
3663
|
// Handle network errors and other failures first
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
latestVersionName: res.version,
|
|
3664
|
+
let backendError = res.error ?? ""
|
|
3665
|
+
let backendKind = res.kind ?? ""
|
|
3666
|
+
if !backendError.isEmpty || !backendKind.isEmpty {
|
|
3667
|
+
self.endBackgroundDownloadAfterLatestError(
|
|
3668
|
+
backendError: backendError,
|
|
3669
|
+
res: res,
|
|
1455
3670
|
current: current,
|
|
1456
|
-
|
|
1457
|
-
sendStats: !responseIsOk
|
|
3671
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1458
3672
|
)
|
|
1459
3673
|
return
|
|
1460
3674
|
}
|
|
@@ -1463,29 +3677,58 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1463
3677
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
1464
3678
|
if directUpdateAllowed {
|
|
1465
3679
|
self.logger.info("Direct update to builtin version")
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
3680
|
+
_ = self._reset(toLastSuccessful: false, usePendingBundle: false)
|
|
3681
|
+
self.endBackGroundTaskWithNotif(
|
|
3682
|
+
msg: "Updated to builtin version",
|
|
3683
|
+
latestVersionName: res.version,
|
|
3684
|
+
current: self.implementation.getCurrentBundle(),
|
|
3685
|
+
error: false,
|
|
3686
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3687
|
+
)
|
|
3688
|
+
} else if self.shouldAutoSetNextBundle() {
|
|
1473
3689
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
1474
3690
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will apply later.")
|
|
1475
3691
|
}
|
|
1476
3692
|
self.logger.info("Setting next bundle to builtin")
|
|
1477
3693
|
_ = self.implementation.setNextBundle(next: BundleInfo.ID_BUILTIN)
|
|
1478
|
-
self.endBackGroundTaskWithNotif(
|
|
3694
|
+
self.endBackGroundTaskWithNotif(
|
|
3695
|
+
msg: "Next update will be to builtin version",
|
|
3696
|
+
latestVersionName: res.version,
|
|
3697
|
+
current: current,
|
|
3698
|
+
error: false,
|
|
3699
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3700
|
+
)
|
|
3701
|
+
} else {
|
|
3702
|
+
self.logger.info("autoUpdate is set to onlyDownload, builtin version will not be set as next bundle")
|
|
3703
|
+
let builtinUpdateAvailable = !current.isBuiltin()
|
|
3704
|
+
if builtinUpdateAvailable {
|
|
3705
|
+
let builtinBundle = self.implementation.getBundleInfo(id: BundleInfo.ID_BUILTIN)
|
|
3706
|
+
self.notifyListeners("updateAvailable", data: ["bundle": builtinBundle.toJSON()], retainUntilConsumed: true)
|
|
3707
|
+
}
|
|
3708
|
+
self.endBackGroundTaskWithNotif(
|
|
3709
|
+
msg: "Latest version is builtin, autoUpdate onlyDownload",
|
|
3710
|
+
latestVersionName: res.version,
|
|
3711
|
+
current: current,
|
|
3712
|
+
error: false,
|
|
3713
|
+
plannedDirectUpdate: plannedDirectUpdate,
|
|
3714
|
+
notifyNoNeedUpdate: !builtinUpdateAvailable
|
|
3715
|
+
)
|
|
1479
3716
|
}
|
|
1480
3717
|
return
|
|
1481
3718
|
}
|
|
1482
3719
|
let sessionKey = res.sessionKey ?? ""
|
|
3720
|
+
let latestVersionName = res.version
|
|
1483
3721
|
guard let downloadUrl = URL(string: res.url) else {
|
|
3722
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: latestVersionName)
|
|
1484
3723
|
self.logger.error("Error no url or wrong format")
|
|
1485
|
-
self.endBackGroundTaskWithNotif(
|
|
3724
|
+
self.endBackGroundTaskWithNotif(
|
|
3725
|
+
msg: "Error no url or wrong format",
|
|
3726
|
+
latestVersionName: latestVersionName,
|
|
3727
|
+
current: current,
|
|
3728
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3729
|
+
)
|
|
1486
3730
|
return
|
|
1487
3731
|
}
|
|
1488
|
-
let latestVersionName = res.version
|
|
1489
3732
|
if latestVersionName != "" && current.getVersionName() != latestVersionName {
|
|
1490
3733
|
do {
|
|
1491
3734
|
self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
|
|
@@ -1500,6 +3743,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1500
3743
|
self.logger.error("Failed to delete failed bundle: \(nextImpl!.toString())")
|
|
1501
3744
|
}
|
|
1502
3745
|
}
|
|
3746
|
+
self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
|
|
1503
3747
|
if res.manifest != nil {
|
|
1504
3748
|
nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
|
|
1505
3749
|
} else {
|
|
@@ -1508,12 +3752,27 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1508
3752
|
}
|
|
1509
3753
|
guard let next = nextImpl else {
|
|
1510
3754
|
self.logger.error("Error downloading file")
|
|
1511
|
-
self.endBackGroundTaskWithNotif(
|
|
3755
|
+
self.endBackGroundTaskWithNotif(
|
|
3756
|
+
msg: "Error downloading file",
|
|
3757
|
+
latestVersionName: latestVersionName,
|
|
3758
|
+
current: current,
|
|
3759
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3760
|
+
)
|
|
1512
3761
|
return
|
|
1513
3762
|
}
|
|
1514
3763
|
if next.isErrorStatus() {
|
|
1515
3764
|
self.logger.error("Latest bundle already exists and is in error state. Aborting update.")
|
|
1516
|
-
self.endBackGroundTaskWithNotif(
|
|
3765
|
+
self.endBackGroundTaskWithNotif(
|
|
3766
|
+
msg: "Latest version is in error state. Aborting update.",
|
|
3767
|
+
latestVersionName: latestVersionName,
|
|
3768
|
+
current: current,
|
|
3769
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3770
|
+
)
|
|
3771
|
+
return
|
|
3772
|
+
}
|
|
3773
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3774
|
+
self.clearDownloadInProgressState()
|
|
3775
|
+
self.endBackGroundTask()
|
|
1517
3776
|
return
|
|
1518
3777
|
}
|
|
1519
3778
|
res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
|
|
@@ -1527,7 +3786,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1527
3786
|
if !resDel {
|
|
1528
3787
|
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1529
3788
|
}
|
|
1530
|
-
self.endBackGroundTaskWithNotif(
|
|
3789
|
+
self.endBackGroundTaskWithNotif(
|
|
3790
|
+
msg: "Error checksum",
|
|
3791
|
+
latestVersionName: latestVersionName,
|
|
3792
|
+
current: current,
|
|
3793
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3794
|
+
)
|
|
3795
|
+
return
|
|
3796
|
+
}
|
|
3797
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3798
|
+
self.clearDownloadInProgressState()
|
|
3799
|
+
self.endBackGroundTask()
|
|
1531
3800
|
return
|
|
1532
3801
|
}
|
|
1533
3802
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
@@ -1540,40 +3809,90 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1540
3809
|
}
|
|
1541
3810
|
if !delayConditionList.isEmpty {
|
|
1542
3811
|
self.logger.info("Update delayed until delay conditions met")
|
|
1543
|
-
self.endBackGroundTaskWithNotif(
|
|
3812
|
+
self.endBackGroundTaskWithNotif(
|
|
3813
|
+
msg: "Update delayed until delay conditions met",
|
|
3814
|
+
latestVersionName: latestVersionName,
|
|
3815
|
+
current: next,
|
|
3816
|
+
error: false,
|
|
3817
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3818
|
+
)
|
|
1544
3819
|
return
|
|
1545
3820
|
}
|
|
1546
|
-
if self.
|
|
1547
|
-
self.
|
|
1548
|
-
self.
|
|
3821
|
+
if self.applyDownloadedBundleForDirectUpdate(next) {
|
|
3822
|
+
self.notifyBundleSet(next)
|
|
3823
|
+
self.endBackGroundTaskWithNotif(
|
|
3824
|
+
msg: "update installed",
|
|
3825
|
+
latestVersionName: latestVersionName,
|
|
3826
|
+
current: next,
|
|
3827
|
+
error: false,
|
|
3828
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3829
|
+
)
|
|
3830
|
+
} else {
|
|
3831
|
+
_ = self.implementation.setNextBundle(next: next.getId())
|
|
3832
|
+
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
3833
|
+
self.endBackGroundTaskWithNotif(
|
|
3834
|
+
msg: "Direct update reload failed, update will install next background",
|
|
3835
|
+
latestVersionName: latestVersionName,
|
|
3836
|
+
current: self.implementation.getCurrentBundle(),
|
|
3837
|
+
error: false,
|
|
3838
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3839
|
+
)
|
|
1549
3840
|
}
|
|
1550
|
-
|
|
1551
|
-
_ = self._reload()
|
|
1552
|
-
self.endBackGroundTaskWithNotif(msg: "update installed", latestVersionName: latestVersionName, current: next, error: false)
|
|
1553
|
-
} else {
|
|
3841
|
+
} else if self.shouldAutoSetNextBundle() {
|
|
1554
3842
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
1555
3843
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
1556
3844
|
}
|
|
1557
3845
|
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
1558
3846
|
_ = self.implementation.setNextBundle(next: next.getId())
|
|
1559
|
-
self.endBackGroundTaskWithNotif(
|
|
3847
|
+
self.endBackGroundTaskWithNotif(
|
|
3848
|
+
msg: "update downloaded, will install next background",
|
|
3849
|
+
latestVersionName: latestVersionName,
|
|
3850
|
+
current: current,
|
|
3851
|
+
error: false,
|
|
3852
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3853
|
+
)
|
|
3854
|
+
} else {
|
|
3855
|
+
self.logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle")
|
|
3856
|
+
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()], retainUntilConsumed: true)
|
|
3857
|
+
self.endBackGroundTaskWithNotif(
|
|
3858
|
+
msg: "update downloaded, autoUpdate onlyDownload",
|
|
3859
|
+
latestVersionName: latestVersionName,
|
|
3860
|
+
current: current,
|
|
3861
|
+
error: false,
|
|
3862
|
+
plannedDirectUpdate: plannedDirectUpdate,
|
|
3863
|
+
notifyNoNeedUpdate: false
|
|
3864
|
+
)
|
|
1560
3865
|
}
|
|
1561
3866
|
return
|
|
1562
3867
|
} catch {
|
|
1563
3868
|
self.logger.error("Error downloading file \(error.localizedDescription)")
|
|
1564
3869
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1565
|
-
self.endBackGroundTaskWithNotif(
|
|
3870
|
+
self.endBackGroundTaskWithNotif(
|
|
3871
|
+
msg: "Error downloading file",
|
|
3872
|
+
latestVersionName: latestVersionName,
|
|
3873
|
+
current: current,
|
|
3874
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3875
|
+
)
|
|
1566
3876
|
return
|
|
1567
3877
|
}
|
|
1568
3878
|
} else {
|
|
1569
3879
|
self.logger.info("No need to update, \(current.getId()) is the latest bundle.")
|
|
1570
|
-
self.endBackGroundTaskWithNotif(
|
|
3880
|
+
self.endBackGroundTaskWithNotif(
|
|
3881
|
+
msg: "No need to update, \(current.getId()) is the latest bundle.",
|
|
3882
|
+
latestVersionName: latestVersionName,
|
|
3883
|
+
current: current,
|
|
3884
|
+
error: false,
|
|
3885
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3886
|
+
)
|
|
1571
3887
|
return
|
|
1572
3888
|
}
|
|
1573
3889
|
}
|
|
1574
3890
|
}
|
|
1575
3891
|
|
|
1576
3892
|
private func installNext() {
|
|
3893
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3894
|
+
return
|
|
3895
|
+
}
|
|
1577
3896
|
let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
|
|
1578
3897
|
let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
|
|
1579
3898
|
let kind: String = obj.value(forKey: "kind") as! String
|
|
@@ -1591,6 +3910,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1591
3910
|
logger.info("Next bundle is: \(next!.toString())")
|
|
1592
3911
|
if self.implementation.set(bundle: next!) && self._reload() {
|
|
1593
3912
|
logger.info("Updated to bundle: \(next!.toString())")
|
|
3913
|
+
self.notifyBundleSet(next!)
|
|
1594
3914
|
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
1595
3915
|
} else {
|
|
1596
3916
|
logger.error("Update to bundle: \(next!.toString()) Failed!")
|
|
@@ -1617,6 +3937,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1617
3937
|
}
|
|
1618
3938
|
|
|
1619
3939
|
@objc func appMovedToForeground() {
|
|
3940
|
+
appHealthTracker?.markForeground(true)
|
|
1620
3941
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1621
3942
|
self.implementation.sendStats(action: "app_moved_to_foreground", versionName: current.getVersionName())
|
|
1622
3943
|
self.delayUpdateUtils.checkCancelDelay(source: .foreground)
|
|
@@ -1662,9 +3983,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1662
3983
|
timer.invalidate()
|
|
1663
3984
|
return
|
|
1664
3985
|
}
|
|
1665
|
-
DispatchQueue.global(qos: .
|
|
3986
|
+
DispatchQueue.global(qos: .utility).async {
|
|
3987
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3988
|
+
return
|
|
3989
|
+
}
|
|
1666
3990
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
1667
3991
|
let current = self.implementation.getCurrentBundle()
|
|
3992
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3993
|
+
return
|
|
3994
|
+
}
|
|
1668
3995
|
|
|
1669
3996
|
if res.version != current.getVersionName() {
|
|
1670
3997
|
self.logger.info("New version found: \(res.version)")
|
|
@@ -1683,6 +4010,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1683
4010
|
@objc func appMovedToBackground() {
|
|
1684
4011
|
// Reset timeout flag at start of each background cycle
|
|
1685
4012
|
self.autoSplashscreenTimedOut = false
|
|
4013
|
+
appHealthTracker?.markForeground(false)
|
|
1686
4014
|
|
|
1687
4015
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1688
4016
|
self.implementation.sendStats(action: "app_moved_to_background", versionName: current.getVersionName())
|
|
@@ -1749,13 +4077,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1749
4077
|
}
|
|
1750
4078
|
|
|
1751
4079
|
self.shakeMenuEnabled = enabled
|
|
1752
|
-
|
|
4080
|
+
self.syncShakeMenuGestureRecognizer()
|
|
4081
|
+
logger.info("Shake menu \(enabled ? "enabled" : "disabled") with \(self.shakeMenuGesture) gesture")
|
|
1753
4082
|
call.resolve()
|
|
1754
4083
|
}
|
|
1755
4084
|
|
|
1756
4085
|
@objc func isShakeMenuEnabled(_ call: CAPPluginCall) {
|
|
1757
4086
|
call.resolve([
|
|
1758
|
-
"enabled": self.shakeMenuEnabled
|
|
4087
|
+
"enabled": self.shakeMenuEnabled,
|
|
4088
|
+
"gesture": self.shakeMenuGesture
|
|
1759
4089
|
])
|
|
1760
4090
|
}
|
|
1761
4091
|
|
|
@@ -1767,6 +4097,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1767
4097
|
}
|
|
1768
4098
|
|
|
1769
4099
|
self.shakeChannelSelectorEnabled = enabled
|
|
4100
|
+
self.syncShakeMenuGestureRecognizer()
|
|
1770
4101
|
logger.info("Shake channel selector \(enabled ? "enabled" : "disabled")")
|
|
1771
4102
|
call.resolve()
|
|
1772
4103
|
}
|
|
@@ -1814,29 +4145,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1814
4145
|
|
|
1815
4146
|
logger.info("Getting App Store update info for \(bundleId) in country \(country)")
|
|
1816
4147
|
|
|
4148
|
+
self.saveCallForAsyncHandling(call)
|
|
1817
4149
|
DispatchQueue.global(qos: .background).async {
|
|
1818
4150
|
let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=\(country)"
|
|
1819
4151
|
guard let url = URL(string: urlString) else {
|
|
1820
|
-
|
|
4152
|
+
self.rejectCall(call, message: "Invalid URL for App Store lookup")
|
|
1821
4153
|
return
|
|
1822
4154
|
}
|
|
1823
4155
|
|
|
1824
4156
|
let task = URLSession.shared.dataTask(with: url) { data, _, error in
|
|
1825
4157
|
if let error = error {
|
|
1826
4158
|
self.logger.error("App Store lookup failed: \(error.localizedDescription)")
|
|
1827
|
-
|
|
4159
|
+
self.rejectCall(call, message: "App Store lookup failed: \(error.localizedDescription)")
|
|
1828
4160
|
return
|
|
1829
4161
|
}
|
|
1830
4162
|
|
|
1831
4163
|
guard let data = data else {
|
|
1832
|
-
|
|
4164
|
+
self.rejectCall(call, message: "No data received from App Store")
|
|
1833
4165
|
return
|
|
1834
4166
|
}
|
|
1835
4167
|
|
|
1836
4168
|
do {
|
|
1837
4169
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
1838
4170
|
let resultCount = json["resultCount"] as? Int else {
|
|
1839
|
-
|
|
4171
|
+
self.rejectCall(call, message: "Invalid response from App Store")
|
|
1840
4172
|
return
|
|
1841
4173
|
}
|
|
1842
4174
|
|
|
@@ -1893,10 +4225,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1893
4225
|
self.logger.info("App not found in App Store for bundleId: \(bundleId)")
|
|
1894
4226
|
}
|
|
1895
4227
|
|
|
1896
|
-
|
|
4228
|
+
self.resolveCall(call, data: result)
|
|
1897
4229
|
} catch {
|
|
1898
4230
|
self.logger.error("Failed to parse App Store response: \(error.localizedDescription)")
|
|
1899
|
-
|
|
4231
|
+
self.rejectCall(call, message: "Failed to parse App Store response: \(error.localizedDescription)")
|
|
1900
4232
|
}
|
|
1901
4233
|
}
|
|
1902
4234
|
task.resume()
|
|
@@ -1905,37 +4237,48 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1905
4237
|
|
|
1906
4238
|
@objc func openAppStore(_ call: CAPPluginCall) {
|
|
1907
4239
|
let appId = call.getString("appId")
|
|
4240
|
+
let bundleId = implementation.appId
|
|
4241
|
+
self.saveCallForAsyncHandling(call)
|
|
1908
4242
|
|
|
1909
|
-
|
|
1910
|
-
// Open App Store with provided app ID
|
|
1911
|
-
let urlString = "https://apps.apple.com/app/id\(appId)"
|
|
4243
|
+
func openAppStorePage(urlString: String, invalidMessage: String = "Invalid App Store URL", failureMessage: String = "Failed to open App Store") {
|
|
1912
4244
|
guard let url = URL(string: urlString) else {
|
|
1913
|
-
|
|
4245
|
+
self.rejectCall(call, message: invalidMessage)
|
|
1914
4246
|
return
|
|
1915
4247
|
}
|
|
1916
4248
|
DispatchQueue.main.async {
|
|
1917
4249
|
UIApplication.shared.open(url) { success in
|
|
1918
4250
|
if success {
|
|
1919
|
-
|
|
4251
|
+
self.resolveCall(call)
|
|
1920
4252
|
} else {
|
|
1921
|
-
|
|
4253
|
+
self.rejectCall(call, message: failureMessage)
|
|
1922
4254
|
}
|
|
1923
4255
|
}
|
|
1924
4256
|
}
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
func openFallbackAppStorePage() {
|
|
4260
|
+
guard let encodedBundleId = bundleId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
|
4261
|
+
self.rejectCall(call, message: "Failed to build App Store fallback URL")
|
|
4262
|
+
return
|
|
4263
|
+
}
|
|
4264
|
+
openAppStorePage(urlString: "https://apps.apple.com/app/\(encodedBundleId)")
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
if let appId = appId {
|
|
4268
|
+
openAppStorePage(urlString: "https://apps.apple.com/app/id\(appId)")
|
|
1925
4269
|
} else {
|
|
1926
|
-
// Look up app ID using bundle identifier
|
|
1927
|
-
let bundleId = implementation.appId
|
|
1928
4270
|
let lookupUrl = "https://itunes.apple.com/lookup?bundleId=\(bundleId)"
|
|
1929
4271
|
|
|
1930
4272
|
DispatchQueue.global(qos: .background).async {
|
|
1931
4273
|
guard let url = URL(string: lookupUrl) else {
|
|
1932
|
-
|
|
4274
|
+
openFallbackAppStorePage()
|
|
1933
4275
|
return
|
|
1934
4276
|
}
|
|
1935
4277
|
|
|
1936
4278
|
let task = URLSession.shared.dataTask(with: url) { data, _, error in
|
|
1937
4279
|
if let error = error {
|
|
1938
|
-
|
|
4280
|
+
self.logger.error("App Store lookup failed: \(error.localizedDescription)")
|
|
4281
|
+
openFallbackAppStorePage()
|
|
1939
4282
|
return
|
|
1940
4283
|
}
|
|
1941
4284
|
|
|
@@ -1944,39 +4287,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1944
4287
|
let results = json["results"] as? [[String: Any]],
|
|
1945
4288
|
let appInfo = results.first,
|
|
1946
4289
|
let trackId = appInfo["trackId"] as? Int else {
|
|
1947
|
-
|
|
1948
|
-
let fallbackUrlString = "https://apps.apple.com/app/\(bundleId)"
|
|
1949
|
-
guard let fallbackUrl = URL(string: fallbackUrlString) else {
|
|
1950
|
-
call.reject("Failed to find app in App Store and fallback URL is invalid")
|
|
1951
|
-
return
|
|
1952
|
-
}
|
|
1953
|
-
DispatchQueue.main.async {
|
|
1954
|
-
UIApplication.shared.open(fallbackUrl) { success in
|
|
1955
|
-
if success {
|
|
1956
|
-
call.resolve()
|
|
1957
|
-
} else {
|
|
1958
|
-
call.reject("Failed to open App Store")
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
return
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
let appStoreUrl = "https://apps.apple.com/app/id\(trackId)"
|
|
1966
|
-
guard let url = URL(string: appStoreUrl) else {
|
|
1967
|
-
call.reject("Invalid App Store URL")
|
|
4290
|
+
openFallbackAppStorePage()
|
|
1968
4291
|
return
|
|
1969
4292
|
}
|
|
1970
4293
|
|
|
1971
|
-
|
|
1972
|
-
UIApplication.shared.open(url) { success in
|
|
1973
|
-
if success {
|
|
1974
|
-
call.resolve()
|
|
1975
|
-
} else {
|
|
1976
|
-
call.reject("Failed to open App Store")
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
4294
|
+
openAppStorePage(urlString: "https://apps.apple.com/app/id\(trackId)")
|
|
1980
4295
|
}
|
|
1981
4296
|
task.resume()
|
|
1982
4297
|
}
|