@capgo/capacitor-updater 6.43.5 → 6.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 +1 -1
- 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 -354
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +905 -283
- 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 -1
- package/dist/esm/web.js +59 -1
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +59 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +59 -1
- 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 -416
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1191 -363
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
- 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 +18 -3
|
@@ -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),
|
|
@@ -47,8 +54,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
47
54
|
CAPPluginMethod(name: "setMultiDelay", returnType: CAPPluginReturnPromise),
|
|
48
55
|
CAPPluginMethod(name: "cancelDelay", returnType: CAPPluginReturnPromise),
|
|
49
56
|
CAPPluginMethod(name: "getLatest", returnType: CAPPluginReturnPromise),
|
|
57
|
+
CAPPluginMethod(name: "getMissingBundleFiles", returnType: CAPPluginReturnPromise),
|
|
58
|
+
CAPPluginMethod(name: "getBundleDownloadSize", returnType: CAPPluginReturnPromise),
|
|
59
|
+
CAPPluginMethod(name: "triggerUpdateCheck", returnType: CAPPluginReturnPromise),
|
|
50
60
|
CAPPluginMethod(name: "setChannel", returnType: CAPPluginReturnPromise),
|
|
51
61
|
CAPPluginMethod(name: "unsetChannel", returnType: CAPPluginReturnPromise),
|
|
62
|
+
CAPPluginMethod(name: "reportWebViewError", returnType: CAPPluginReturnPromise),
|
|
52
63
|
CAPPluginMethod(name: "getChannel", returnType: CAPPluginReturnPromise),
|
|
53
64
|
CAPPluginMethod(name: "listChannels", returnType: CAPPluginReturnPromise),
|
|
54
65
|
CAPPluginMethod(name: "setCustomId", returnType: CAPPluginReturnPromise),
|
|
@@ -64,6 +75,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
64
75
|
CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise),
|
|
65
76
|
CAPPluginMethod(name: "setShakeChannelSelector", returnType: CAPPluginReturnPromise),
|
|
66
77
|
CAPPluginMethod(name: "isShakeChannelSelectorEnabled", returnType: CAPPluginReturnPromise),
|
|
78
|
+
CAPPluginMethod(name: "getAppId", returnType: CAPPluginReturnPromise),
|
|
79
|
+
CAPPluginMethod(name: "setAppId", returnType: CAPPluginReturnPromise),
|
|
67
80
|
// App Store update methods
|
|
68
81
|
CAPPluginMethod(name: "getAppUpdateInfo", returnType: CAPPluginReturnPromise),
|
|
69
82
|
CAPPluginMethod(name: "openAppStore", returnType: CAPPluginReturnPromise),
|
|
@@ -72,10 +85,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
72
85
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
73
86
|
]
|
|
74
87
|
public var implementation = CapgoUpdater()
|
|
75
|
-
private let pluginVersion: String = "
|
|
88
|
+
private let pluginVersion: String = "6.50.1"
|
|
76
89
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
77
90
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
78
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
|
|
79
101
|
private let keepUrlPathFlagKey = "__capgo_keep_url_path_after_reload"
|
|
80
102
|
private let customIdDefaultsKey = "CapacitorUpdater.customId"
|
|
81
103
|
private let updateUrlDefaultsKey = "CapacitorUpdater.updateUrl"
|
|
@@ -83,12 +105,36 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
83
105
|
private let channelUrlDefaultsKey = "CapacitorUpdater.channelUrl"
|
|
84
106
|
private let defaultChannelDefaultsKey = "CapacitorUpdater.defaultChannel"
|
|
85
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)))
|
|
86
131
|
// Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
|
|
87
132
|
private var updateUrl = ""
|
|
88
133
|
private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
|
89
134
|
private var currentVersionNative: Version = "0.0.0"
|
|
90
135
|
private var currentBuildVersion: String = "0"
|
|
91
136
|
private var autoUpdate = false
|
|
137
|
+
private var autoUpdateMode = CapacitorUpdaterPlugin.autoUpdateModeOff
|
|
92
138
|
private var appReadyTimeout = 10000
|
|
93
139
|
private var appReadyCheck: DispatchWorkItem?
|
|
94
140
|
private var resetWhenUpdate = true
|
|
@@ -102,7 +148,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
102
148
|
private var autoSplashscreenTimeoutWorkItem: DispatchWorkItem?
|
|
103
149
|
private var splashscreenLoaderView: UIActivityIndicatorView?
|
|
104
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
|
|
105
158
|
private var autoSplashscreenTimedOut = false
|
|
159
|
+
private var splashscreenInvocationToken = 0
|
|
106
160
|
private var autoDeleteFailed = false
|
|
107
161
|
private var autoDeletePrevious = false
|
|
108
162
|
var allowSetDefaultChannel = true
|
|
@@ -111,9 +165,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
111
165
|
private var taskRunning = false
|
|
112
166
|
private var periodCheckDelay = 0
|
|
113
167
|
private let downloadLock = NSLock()
|
|
168
|
+
private let onLaunchDirectUpdateStateLock = NSLock()
|
|
114
169
|
private var downloadInProgress = false
|
|
115
170
|
private var downloadStartTime: Date?
|
|
116
|
-
private let downloadTimeout: TimeInterval =
|
|
171
|
+
private let downloadTimeout: TimeInterval = 600 // 10 minute timeout
|
|
117
172
|
|
|
118
173
|
// Lock to ensure cleanup completes before downloads start
|
|
119
174
|
private let cleanupLock = NSLock()
|
|
@@ -122,9 +177,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
122
177
|
private var persistCustomId = false
|
|
123
178
|
private var persistModifyUrl = false
|
|
124
179
|
private var allowManualBundleError = false
|
|
180
|
+
private var allowPreview = false
|
|
125
181
|
private var keepUrlPathFlagLastValue: Bool?
|
|
182
|
+
private var appHealthTracker: AppHealthTracker?
|
|
183
|
+
private var webViewStatsReporter: WebViewStatsReporter?
|
|
126
184
|
public var shakeMenuEnabled = false
|
|
127
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?
|
|
128
193
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
129
194
|
|
|
130
195
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
@@ -138,6 +203,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
138
203
|
} else {
|
|
139
204
|
logger.error("Failed to get webView for logging")
|
|
140
205
|
}
|
|
206
|
+
let webViewStatsReporter = WebViewStatsReporter(implementation: implementation)
|
|
207
|
+
self.webViewStatsReporter = webViewStatsReporter
|
|
208
|
+
webViewStatsReporter.install(on: self.bridge?.webView)
|
|
141
209
|
#if targetEnvironment(simulator)
|
|
142
210
|
logger.info("::::: SIMULATOR :::::")
|
|
143
211
|
logger.info("Application directory: \(NSHomeDirectory())")
|
|
@@ -157,6 +225,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
157
225
|
}
|
|
158
226
|
persistModifyUrl = getConfig().getBoolean("persistModifyUrl", false)
|
|
159
227
|
allowManualBundleError = getConfig().getBoolean("allowManualBundleError", false)
|
|
228
|
+
allowPreview = getConfig().getBoolean("allowPreview", false)
|
|
160
229
|
logger.info("init for device \(self.implementation.deviceID)")
|
|
161
230
|
guard let versionName = getConfig().getString("version", Bundle.main.versionName) else {
|
|
162
231
|
logger.error("Cannot get version name")
|
|
@@ -176,33 +245,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
176
245
|
keepUrlPathAfterReload = getConfig().getBoolean("keepUrlPathAfterReload", false)
|
|
177
246
|
syncKeepUrlPathFlag(enabled: keepUrlPathAfterReload)
|
|
178
247
|
|
|
179
|
-
// Handle directUpdate configuration - support string values and backward compatibility
|
|
180
|
-
if let directUpdateString = getConfig().getString("directUpdate") {
|
|
181
|
-
// Handle backward compatibility for boolean true
|
|
182
|
-
if directUpdateString == "true" {
|
|
183
|
-
directUpdateMode = "always"
|
|
184
|
-
directUpdate = true
|
|
185
|
-
} else {
|
|
186
|
-
directUpdateMode = directUpdateString
|
|
187
|
-
directUpdate = directUpdateString == "always" || directUpdateString == "atInstall" || directUpdateString == "onLaunch"
|
|
188
|
-
// Validate directUpdate value
|
|
189
|
-
if directUpdateString != "false" && directUpdateString != "always" && directUpdateString != "atInstall" && directUpdateString != "onLaunch" {
|
|
190
|
-
logger.error("Invalid directUpdate value: \"\(directUpdateString)\". Supported values are: \"false\", \"true\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\".")
|
|
191
|
-
directUpdateMode = "false"
|
|
192
|
-
directUpdate = false
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
let directUpdateBool = getConfig().getBoolean("directUpdate", false)
|
|
197
|
-
if directUpdateBool {
|
|
198
|
-
directUpdateMode = "always" // backward compatibility: true = always
|
|
199
|
-
directUpdate = true
|
|
200
|
-
} else {
|
|
201
|
-
directUpdateMode = "false"
|
|
202
|
-
directUpdate = false
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
248
|
autoSplashscreen = getConfig().getBoolean("autoSplashscreen", false)
|
|
207
249
|
autoSplashscreenLoader = getConfig().getBoolean("autoSplashscreenLoader", false)
|
|
208
250
|
let splashscreenTimeoutValue = getConfig().getInt("autoSplashscreenTimeout", 10000)
|
|
@@ -212,21 +254,40 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
212
254
|
updateUrl = storedUpdateUrl
|
|
213
255
|
logger.info("Loaded persisted updateUrl")
|
|
214
256
|
}
|
|
215
|
-
|
|
257
|
+
configureAutoUpdateModeFromConfig()
|
|
216
258
|
appReadyTimeout = max(1000, getConfig().getInt("appReadyTimeout", 10000)) // Minimum 1 second
|
|
217
259
|
implementation.timeout = Double(getConfig().getInt("responseTimeout", 20))
|
|
218
260
|
resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
|
|
219
261
|
shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
220
262
|
shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
226
273
|
}
|
|
274
|
+
syncShakeMenuGestureRecognizer()
|
|
275
|
+
periodCheckDelay = Self.normalizedPeriodCheckDelaySeconds(getConfig().getInt("periodCheckDelay", 0))
|
|
227
276
|
|
|
228
277
|
implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
|
|
229
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
|
+
}
|
|
230
291
|
implementation.pluginVersion = self.pluginVersion
|
|
231
292
|
|
|
232
293
|
// Set logger for shared classes
|
|
@@ -248,6 +309,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
248
309
|
// crash the app on purpose it should not happen
|
|
249
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")
|
|
250
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
|
+
}
|
|
251
321
|
logger.info("appId \(implementation.appId)")
|
|
252
322
|
implementation.statsUrl = getConfig().getString("statsUrl", CapacitorUpdaterPlugin.statsUrlDefault)!
|
|
253
323
|
implementation.channelUrl = getConfig().getString("channelUrl", CapacitorUpdaterPlugin.channelUrlDefault)!
|
|
@@ -270,13 +340,24 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
270
340
|
implementation.defaultChannel = getConfig().getString("defaultChannel", "")!
|
|
271
341
|
}
|
|
272
342
|
self.implementation.autoReset()
|
|
343
|
+
let appHealthTracker = AppHealthTracker(implementation: self.implementation)
|
|
344
|
+
self.appHealthTracker = appHealthTracker
|
|
345
|
+
appHealthTracker.reportPreviousUncleanForegroundExit()
|
|
346
|
+
appHealthTracker.startSession()
|
|
273
347
|
|
|
274
|
-
// Check if app was recently installed/updated BEFORE
|
|
348
|
+
// Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
|
|
275
349
|
self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
|
|
350
|
+
let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
|
|
351
|
+
if nativeBuildVersionChanged {
|
|
352
|
+
self.clearPreviewSessionForNativeBuildChange()
|
|
353
|
+
}
|
|
354
|
+
self.leavePreviewSessionForLaunchURLIfNeeded()
|
|
276
355
|
|
|
277
356
|
if resetWhenUpdate {
|
|
278
|
-
self.
|
|
357
|
+
let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
|
|
358
|
+
self.cleanupObsoleteVersions(didResetCurrentBundle: didResetCurrentBundle)
|
|
279
359
|
}
|
|
360
|
+
self.reportNativeVersionStatsIfChanged()
|
|
280
361
|
|
|
281
362
|
// Load the server
|
|
282
363
|
// This is very much swift specific, android does not do that
|
|
@@ -289,9 +370,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
289
370
|
logger.error("unable to force reload, the plugin might fallback to the builtin version")
|
|
290
371
|
}
|
|
291
372
|
|
|
292
|
-
|
|
293
|
-
nc.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
294
|
-
nc.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
373
|
+
self.registerNotificationObservers()
|
|
295
374
|
|
|
296
375
|
// Check for 'kill' delay condition on app launch
|
|
297
376
|
// This handles cases where the app was killed (willTerminateNotification is not reliable for system kills)
|
|
@@ -299,6 +378,47 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
299
378
|
|
|
300
379
|
self.appMovedToForeground()
|
|
301
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
|
+
)
|
|
302
422
|
}
|
|
303
423
|
|
|
304
424
|
private func syncKeepUrlPathFlag(enabled: Bool) {
|
|
@@ -348,6 +468,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
348
468
|
}
|
|
349
469
|
}
|
|
350
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
|
+
|
|
351
487
|
private func initialLoad() -> Bool {
|
|
352
488
|
guard let bridge = self.bridge else { return false }
|
|
353
489
|
if keepUrlPathAfterReload {
|
|
@@ -391,7 +527,109 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
391
527
|
semaphoreReady.signal()
|
|
392
528
|
}
|
|
393
529
|
|
|
394
|
-
|
|
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) {
|
|
395
633
|
cleanupThread = Thread {
|
|
396
634
|
self.cleanupLock.lock()
|
|
397
635
|
defer {
|
|
@@ -426,9 +664,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
426
664
|
// 1. Write "LatestVersionNative" - this fixes the part 1 of this bug
|
|
427
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
|
|
428
666
|
|
|
429
|
-
let previous =
|
|
667
|
+
let previous = self.storedNativeBuildVersion()
|
|
430
668
|
if previous != "0" && self.currentBuildVersion != previous {
|
|
431
|
-
|
|
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
|
+
}
|
|
432
673
|
let res = self.implementation.list()
|
|
433
674
|
for version in res {
|
|
434
675
|
// Check if thread was cancelled
|
|
@@ -488,11 +729,88 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
488
729
|
logger.info("Cleanup finished, proceeding with download")
|
|
489
730
|
}
|
|
490
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
|
+
|
|
491
804
|
@objc func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false, bundle: BundleInfo? = nil) {
|
|
492
805
|
let bundleInfo = bundle ?? self.implementation.getBundleInfo(id: id)
|
|
493
|
-
|
|
806
|
+
var downloadPayload: JSObject = [:]
|
|
807
|
+
downloadPayload["percent"] = percent
|
|
808
|
+
downloadPayload["bundle"] = bundlePayload(bundleInfo)
|
|
809
|
+
self.notifyListenersOnMain("download", data: downloadPayload)
|
|
494
810
|
if percent == 100 {
|
|
495
|
-
|
|
811
|
+
var downloadCompletePayload: JSObject = [:]
|
|
812
|
+
downloadCompletePayload["bundle"] = bundlePayload(bundleInfo)
|
|
813
|
+
self.notifyListenersOnMain("downloadComplete", data: downloadCompletePayload)
|
|
496
814
|
self.implementation.sendStats(action: "download_complete", versionName: bundleInfo.getVersionName())
|
|
497
815
|
} else if percent.isMultiple(of: 10) || ignoreMultipleOfTen {
|
|
498
816
|
self.implementation.sendStats(action: "download_\(percent)", versionName: bundleInfo.getVersionName())
|
|
@@ -541,206 +859,1420 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
541
859
|
call.resolve()
|
|
542
860
|
}
|
|
543
861
|
|
|
544
|
-
@objc func setChannelUrl(_ call: CAPPluginCall) {
|
|
545
|
-
if !getConfig().getBoolean("allowModifyUrl", false) {
|
|
546
|
-
logger.error("setChannelUrl called without allowModifyUrl")
|
|
547
|
-
call.reject("setChannelUrl called without allowModifyUrl set allowModifyUrl in your config to true to allow it")
|
|
548
|
-
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
|
|
549
2067
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
|
554
2074
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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)
|
|
559
2091
|
}
|
|
560
|
-
|
|
2092
|
+
|
|
2093
|
+
return url.path
|
|
561
2094
|
}
|
|
562
2095
|
|
|
563
|
-
|
|
564
|
-
|
|
2096
|
+
private func normalizedPreviewPath(_ components: [String]) -> String {
|
|
2097
|
+
let separator = String(self.previewPathSeparator)
|
|
2098
|
+
return separator + components.filter { !$0.isEmpty }.joined(separator: separator)
|
|
565
2099
|
}
|
|
566
2100
|
|
|
567
|
-
|
|
568
|
-
|
|
2101
|
+
private func previewDeepLinkPath(_ leafComponent: String) -> String {
|
|
2102
|
+
self.normalizedPreviewPath([self.previewDeepLinkRootComponent, leafComponent])
|
|
569
2103
|
}
|
|
570
2104
|
|
|
571
|
-
|
|
572
|
-
|
|
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)
|
|
573
2109
|
}
|
|
574
2110
|
|
|
575
|
-
@objc func
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
logger.error("Download called without version")
|
|
583
|
-
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 {
|
|
584
2118
|
return
|
|
585
2119
|
}
|
|
586
2120
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
// Convert JSArray to [ManifestEntry]
|
|
597
|
-
var manifestEntries: [ManifestEntry] = []
|
|
598
|
-
for item in manifestArray {
|
|
599
|
-
if let manifestDict = item as? [String: Any] {
|
|
600
|
-
let entry = ManifestEntry(
|
|
601
|
-
file_name: manifestDict["file_name"] as? String,
|
|
602
|
-
file_hash: manifestDict["file_hash"] as? String,
|
|
603
|
-
download_url: manifestDict["download_url"] as? String
|
|
604
|
-
)
|
|
605
|
-
manifestEntries.append(entry)
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
|
|
609
|
-
} else {
|
|
610
|
-
next = try self.implementation.download(url: url!, version: version, sessionKey: sessionKey)
|
|
611
|
-
}
|
|
612
|
-
// If public key is present but no checksum provided, refuse installation
|
|
613
|
-
if self.implementation.publicKey != "" && checksum == "" {
|
|
614
|
-
self.logger.error("Public key present but no checksum provided")
|
|
615
|
-
self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
|
|
616
|
-
let id = next.getId()
|
|
617
|
-
let resDel = self.implementation.delete(id: id)
|
|
618
|
-
if !resDel {
|
|
619
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
620
|
-
}
|
|
621
|
-
throw ObjectSavableError.checksum
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
625
|
-
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
626
|
-
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
627
|
-
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
628
|
-
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
629
|
-
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
630
|
-
let id = next.getId()
|
|
631
|
-
let resDel = self.implementation.delete(id: id)
|
|
632
|
-
if !resDel {
|
|
633
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
634
|
-
}
|
|
635
|
-
throw ObjectSavableError.checksum
|
|
636
|
-
} else {
|
|
637
|
-
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
638
|
-
}
|
|
639
|
-
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
640
|
-
call.resolve(next.toJSON())
|
|
641
|
-
} catch {
|
|
642
|
-
self.logger.error("Failed to download from: \(String(describing: url)) \(error.localizedDescription)")
|
|
643
|
-
self.notifyListeners("downloadFailed", data: ["version": version])
|
|
644
|
-
self.implementation.sendStats(action: "download_fail")
|
|
645
|
-
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")
|
|
646
2130
|
}
|
|
647
2131
|
}
|
|
648
2132
|
}
|
|
649
2133
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
let
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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")
|
|
659
2152
|
}
|
|
660
|
-
logger.info("Reloading \(id)")
|
|
661
2153
|
|
|
662
|
-
let
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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)")
|
|
666
2162
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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")
|
|
670
2175
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
capBridge.setServerBasePath(dest.path)
|
|
674
|
-
var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
|
|
675
|
-
urlComponents.path = currentURL.path
|
|
676
|
-
urlComponents.query = currentURL.query
|
|
677
|
-
urlComponents.fragment = currentURL.fragment
|
|
678
|
-
if let finalUrl = urlComponents.url {
|
|
679
|
-
_ = vc.webView?.load(URLRequest(url: finalUrl))
|
|
680
|
-
} else {
|
|
681
|
-
self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
|
|
682
|
-
vc.setServerBasePath(path: dest.path)
|
|
683
|
-
}
|
|
684
|
-
} else {
|
|
685
|
-
self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
|
|
686
|
-
vc.setServerBasePath(path: dest.path)
|
|
687
|
-
}
|
|
688
|
-
} else {
|
|
689
|
-
vc.setServerBasePath(path: dest.path)
|
|
2176
|
+
guard payload.url != nil || payload.manifest?.isEmpty == false else {
|
|
2177
|
+
throw makePreviewError("Preview payload is missing download information")
|
|
690
2178
|
}
|
|
691
|
-
self.checkAppReady()
|
|
692
|
-
self.notifyListeners("appReloaded", data: [:])
|
|
693
|
-
return true
|
|
694
|
-
}
|
|
695
2179
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
DispatchQueue.main.sync {
|
|
701
|
-
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()
|
|
702
2184
|
}
|
|
703
|
-
|
|
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
|
|
704
2206
|
}
|
|
705
2207
|
}
|
|
706
2208
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
} else {
|
|
711
|
-
logger.error("Reload failed")
|
|
712
|
-
call.reject("Reload failed")
|
|
2209
|
+
private func clearPreviewSessionForNativeBuildChange() {
|
|
2210
|
+
guard self.previewSessionEnabled || self.implementation.getPreviewFallbackBundle() != nil || !self.previewSessions().isEmpty else {
|
|
2211
|
+
return
|
|
713
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()
|
|
714
2229
|
}
|
|
715
2230
|
|
|
716
|
-
|
|
717
|
-
guard let
|
|
718
|
-
|
|
719
|
-
|
|
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)
|
|
720
2235
|
return
|
|
721
2236
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
call.reject("Set next version failed. id \(id) does not exist.")
|
|
726
|
-
} else {
|
|
727
|
-
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)
|
|
728
2240
|
}
|
|
729
2241
|
}
|
|
730
2242
|
|
|
731
|
-
|
|
732
|
-
guard
|
|
733
|
-
logger.error("Set called without id")
|
|
734
|
-
call.reject("Set called without id")
|
|
2243
|
+
private func showPreviewSessionNoticeIfNeeded() {
|
|
2244
|
+
guard self.previewSessionEnabled && self.previewSessionAlertPending else {
|
|
735
2245
|
return
|
|
736
2246
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
+
}
|
|
744
2276
|
}
|
|
745
2277
|
}
|
|
746
2278
|
|
|
@@ -803,25 +2335,132 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
803
2335
|
|
|
804
2336
|
@objc func getLatest(_ call: CAPPluginCall) {
|
|
805
2337
|
let channel = call.getString("channel")
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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)
|
|
812
2382
|
} else {
|
|
813
|
-
|
|
2383
|
+
self.resolveCall(call, data: res.toDict())
|
|
814
2384
|
}
|
|
815
2385
|
}
|
|
816
2386
|
}
|
|
817
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
|
+
|
|
818
2456
|
@objc func unsetChannel(_ call: CAPPluginCall) {
|
|
819
2457
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate", false)
|
|
820
|
-
|
|
2458
|
+
self.saveCallForAsyncHandling(call)
|
|
2459
|
+
DispatchQueue.global(qos: .utility).async {
|
|
821
2460
|
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
822
2461
|
let res = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
|
|
823
2462
|
if res.error != "" {
|
|
824
|
-
|
|
2463
|
+
self.rejectCall(call, message: res.error, code: "UNSETCHANNEL_FAILED", data: [
|
|
825
2464
|
"message": res.error,
|
|
826
2465
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
827
2466
|
])
|
|
@@ -835,7 +2474,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
835
2474
|
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
836
2475
|
}
|
|
837
2476
|
}
|
|
838
|
-
|
|
2477
|
+
self.resolveCall(call, data: res.toDict())
|
|
839
2478
|
}
|
|
840
2479
|
}
|
|
841
2480
|
}
|
|
@@ -850,17 +2489,24 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
850
2489
|
return
|
|
851
2490
|
}
|
|
852
2491
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate") ?? false
|
|
853
|
-
|
|
854
|
-
|
|
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
|
+
)
|
|
855
2501
|
if res.error != "" {
|
|
856
2502
|
// Fire channelPrivate event if channel doesn't allow self-assignment
|
|
857
2503
|
if res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed") {
|
|
858
|
-
self.
|
|
2504
|
+
self.notifyListenersOnMain("channelPrivate", data: [
|
|
859
2505
|
"channel": channel,
|
|
860
2506
|
"message": res.error
|
|
861
2507
|
])
|
|
862
2508
|
}
|
|
863
|
-
|
|
2509
|
+
self.rejectCall(call, message: res.error, code: "SETCHANNEL_FAILED", data: [
|
|
864
2510
|
"message": res.error,
|
|
865
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"
|
|
866
2512
|
])
|
|
@@ -874,35 +2520,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
874
2520
|
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
875
2521
|
}
|
|
876
2522
|
}
|
|
877
|
-
|
|
2523
|
+
self.resolveCall(call, data: res.toDict())
|
|
878
2524
|
}
|
|
879
2525
|
}
|
|
880
2526
|
}
|
|
881
2527
|
|
|
882
2528
|
@objc func getChannel(_ call: CAPPluginCall) {
|
|
883
|
-
|
|
884
|
-
|
|
2529
|
+
self.saveCallForAsyncHandling(call)
|
|
2530
|
+
DispatchQueue.global(qos: .utility).async {
|
|
2531
|
+
let res = self.implementation.getChannel(defaultChannelKey: self.defaultChannelDefaultsKey)
|
|
885
2532
|
if res.error != "" {
|
|
886
|
-
|
|
2533
|
+
self.rejectCall(call, message: res.error, code: "GETCHANNEL_FAILED", data: [
|
|
887
2534
|
"message": res.error,
|
|
888
2535
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
889
2536
|
])
|
|
890
2537
|
} else {
|
|
891
|
-
|
|
2538
|
+
self.resolveCall(call, data: res.toDict())
|
|
892
2539
|
}
|
|
893
2540
|
}
|
|
894
2541
|
}
|
|
895
2542
|
|
|
896
2543
|
@objc func listChannels(_ call: CAPPluginCall) {
|
|
897
|
-
|
|
2544
|
+
self.saveCallForAsyncHandling(call)
|
|
2545
|
+
DispatchQueue.global(qos: .utility).async {
|
|
898
2546
|
let res = self.implementation.listChannels()
|
|
899
2547
|
if res.error != "" {
|
|
900
|
-
|
|
2548
|
+
self.rejectCall(call, message: res.error, code: "LISTCHANNELS_FAILED", data: [
|
|
901
2549
|
"message": res.error,
|
|
902
2550
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
903
2551
|
])
|
|
904
2552
|
} else {
|
|
905
|
-
|
|
2553
|
+
var payload: JSObject = [:]
|
|
2554
|
+
payload["channels"] = res.channels
|
|
2555
|
+
self.resolveCall(call, data: payload)
|
|
906
2556
|
}
|
|
907
2557
|
}
|
|
908
2558
|
}
|
|
@@ -925,32 +2575,90 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
925
2575
|
call.resolve()
|
|
926
2576
|
}
|
|
927
2577
|
|
|
928
|
-
@objc func _reset(toLastSuccessful: Bool) -> Bool {
|
|
929
|
-
|
|
2578
|
+
@objc func _reset(toLastSuccessful: Bool, usePendingBundle: Bool) -> Bool {
|
|
2579
|
+
self.performReset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle, isInternal: false)
|
|
2580
|
+
}
|
|
930
2581
|
|
|
931
|
-
|
|
932
|
-
|
|
2582
|
+
func performReset(toLastSuccessful: Bool, usePendingBundle: Bool, isInternal: Bool) -> Bool {
|
|
2583
|
+
guard self.canPerformResetTransition() else { return false }
|
|
933
2584
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return self.implementation.set(bundle: fallback) && self._reload()
|
|
939
|
-
}
|
|
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()
|
|
940
2589
|
|
|
941
|
-
|
|
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
|
+
}
|
|
942
2617
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
+
}
|
|
946
2638
|
}
|
|
947
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
|
+
}
|
|
948
2650
|
return false
|
|
949
2651
|
}
|
|
950
2652
|
|
|
2653
|
+
func canPerformResetTransition() -> Bool {
|
|
2654
|
+
guard let bridge = self.bridge else { return false }
|
|
2655
|
+
return (bridge.viewController as? CAPBridgeViewController) != nil
|
|
2656
|
+
}
|
|
2657
|
+
|
|
951
2658
|
@objc func reset(_ call: CAPPluginCall) {
|
|
952
2659
|
let toLastSuccessful = call.getBool("toLastSuccessful") ?? false
|
|
953
|
-
|
|
2660
|
+
let usePendingBundle = call.getBool("usePendingBundle") ?? false
|
|
2661
|
+
if self._reset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle) {
|
|
954
2662
|
call.resolve()
|
|
955
2663
|
} else {
|
|
956
2664
|
logger.error("Reset failed")
|
|
@@ -971,6 +2679,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
971
2679
|
let bundle = self.implementation.getCurrentBundle()
|
|
972
2680
|
self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
|
|
973
2681
|
logger.info("Current bundle loaded successfully. [notifyAppReady was called] \(bundle.toString())")
|
|
2682
|
+
self.clearIncomingPreviewTransition()
|
|
2683
|
+
self.hidePreviewTransitionLoader(reason: "notify-app-ready")
|
|
2684
|
+
|
|
974
2685
|
call.resolve(["bundle": bundle.toJSON()])
|
|
975
2686
|
}
|
|
976
2687
|
|
|
@@ -1020,6 +2731,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1020
2731
|
// Note: _checkCancelDelay method has been moved to DelayUpdateUtils class
|
|
1021
2732
|
|
|
1022
2733
|
private func _isAutoUpdateEnabled() -> Bool {
|
|
2734
|
+
if self.isPreviewSessionStateActive() {
|
|
2735
|
+
return false
|
|
2736
|
+
}
|
|
1023
2737
|
let instanceDescriptor = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor()
|
|
1024
2738
|
if instanceDescriptor?.serverURL != nil {
|
|
1025
2739
|
logger.warn("AutoUpdate is automatic disabled when serverUrl is set.")
|
|
@@ -1057,10 +2771,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1057
2771
|
logger.info("Built-in bundle is active. We skip the check for notifyAppReady.")
|
|
1058
2772
|
return
|
|
1059
2773
|
}
|
|
2774
|
+
if self.isPreviewSessionStateActive() {
|
|
2775
|
+
logger.info("Preview session is active. We skip the check for notifyAppReady.")
|
|
2776
|
+
return
|
|
2777
|
+
}
|
|
1060
2778
|
|
|
1061
2779
|
logger.info("Current bundle is: \(current.toString())")
|
|
1062
2780
|
|
|
1063
|
-
if BundleStatus.SUCCESS.
|
|
2781
|
+
if BundleStatus.SUCCESS.storedValue != current.getStatus() {
|
|
1064
2782
|
logger.error("notifyAppReady was not called, roll back current bundle: \(current.toString())")
|
|
1065
2783
|
logger.error("Did you forget to call 'notifyAppReady()' in your Capacitor App code?")
|
|
1066
2784
|
self.notifyListeners("updateFailed", data: [
|
|
@@ -1069,7 +2787,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1069
2787
|
self.persistLastFailedBundle(current)
|
|
1070
2788
|
self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
|
|
1071
2789
|
self.implementation.setError(bundle: current)
|
|
1072
|
-
_ = self.
|
|
2790
|
+
_ = self.performReset(toLastSuccessful: true, usePendingBundle: false, isInternal: true)
|
|
1073
2791
|
if self.autoDeleteFailed && !current.isBuiltin() {
|
|
1074
2792
|
logger.info("Deleting failing bundle: \(current.toString())")
|
|
1075
2793
|
let res = self.implementation.delete(id: current.getId(), removeInfo: false)
|
|
@@ -1094,6 +2812,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1094
2812
|
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
|
|
1095
2813
|
}
|
|
1096
2814
|
|
|
2815
|
+
private func notifyBundleSet(_ bundle: BundleInfo) {
|
|
2816
|
+
self.notifyListeners("set", data: ["bundle": bundle.toJSON()], retainUntilConsumed: true)
|
|
2817
|
+
}
|
|
2818
|
+
|
|
1097
2819
|
func sendReadyToJs(current: BundleInfo, msg: String) {
|
|
1098
2820
|
logger.info("sendReadyToJs")
|
|
1099
2821
|
DispatchQueue.global().async {
|
|
@@ -1105,6 +2827,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1105
2827
|
if self.autoSplashscreen {
|
|
1106
2828
|
self.hideSplashscreen()
|
|
1107
2829
|
}
|
|
2830
|
+
self.hidePreviewTransitionLoader(reason: "app-ready")
|
|
1108
2831
|
}
|
|
1109
2832
|
}
|
|
1110
2833
|
|
|
@@ -1121,32 +2844,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1121
2844
|
private func performHideSplashscreen() {
|
|
1122
2845
|
self.cancelSplashscreenTimeout()
|
|
1123
2846
|
self.removeSplashscreenLoader()
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
self.logger.info("Splashscreen hidden automatically")
|
|
1133
|
-
}, error: { (_) in
|
|
1134
|
-
self.logger.error("Failed to auto-hide splashscreen")
|
|
1135
|
-
})
|
|
1136
|
-
|
|
1137
|
-
// Try to call the SplashScreen hide method directly through the bridge
|
|
1138
|
-
if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
|
|
1139
|
-
// Use runtime method invocation to call hide method
|
|
1140
|
-
let selector = NSSelectorFromString("hide:")
|
|
1141
|
-
if splashScreenPlugin.responds(to: selector) {
|
|
1142
|
-
_ = splashScreenPlugin.perform(selector, with: call)
|
|
1143
|
-
self.logger.info("Called SplashScreen hide method")
|
|
1144
|
-
} else {
|
|
1145
|
-
self.logger.warn("autoSplashscreen: SplashScreen plugin does not respond to hide: method. Make sure @capacitor/splash-screen plugin is properly installed.")
|
|
1146
|
-
}
|
|
1147
|
-
} else {
|
|
1148
|
-
self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
|
|
1149
|
-
}
|
|
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
|
+
)
|
|
1150
2855
|
}
|
|
1151
2856
|
|
|
1152
2857
|
private func showSplashscreen() {
|
|
@@ -1157,40 +2862,182 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1157
2862
|
self.performShowSplashscreen()
|
|
1158
2863
|
}
|
|
1159
2864
|
}
|
|
1160
|
-
}
|
|
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
|
+
}
|
|
1161
2935
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
}
|
|
1165
2947
|
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
+
)
|
|
1168
2958
|
return
|
|
1169
2959
|
}
|
|
1170
2960
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
self.logger.error("Failed to auto-show splashscreen")
|
|
1176
|
-
})
|
|
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
|
+
}
|
|
1177
2965
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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)
|
|
1185
2977
|
} else {
|
|
1186
|
-
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
|
|
1187
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
|
|
1188
3011
|
} else {
|
|
1189
|
-
|
|
3012
|
+
indicatorStyle = .whiteLarge
|
|
1190
3013
|
}
|
|
1191
3014
|
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
+
])
|
|
1194
3041
|
}
|
|
1195
3042
|
|
|
1196
3043
|
private func addSplashscreenLoaderIfNeeded() {
|
|
@@ -1207,40 +3054,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1207
3054
|
return
|
|
1208
3055
|
}
|
|
1209
3056
|
|
|
1210
|
-
let
|
|
1211
|
-
container.translatesAutoresizingMaskIntoConstraints = false
|
|
1212
|
-
container.backgroundColor = UIColor.clear
|
|
1213
|
-
container.isUserInteractionEnabled = false
|
|
1214
|
-
|
|
1215
|
-
let indicatorStyle: UIActivityIndicatorView.Style
|
|
3057
|
+
let indicatorColor: UIColor?
|
|
1216
3058
|
if #available(iOS 13.0, *) {
|
|
1217
|
-
|
|
3059
|
+
indicatorColor = UIColor.label
|
|
1218
3060
|
} else {
|
|
1219
|
-
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
let indicator = UIActivityIndicatorView(style: indicatorStyle)
|
|
1223
|
-
indicator.translatesAutoresizingMaskIntoConstraints = false
|
|
1224
|
-
indicator.hidesWhenStopped = false
|
|
1225
|
-
if #available(iOS 13.0, *) {
|
|
1226
|
-
indicator.color = UIColor.label
|
|
3061
|
+
indicatorColor = nil
|
|
1227
3062
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
container.topAnchor.constraint(equalTo: rootView.topAnchor),
|
|
1237
|
-
container.bottomAnchor.constraint(equalTo: rootView.bottomAnchor),
|
|
1238
|
-
indicator.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
|
1239
|
-
indicator.centerYAnchor.constraint(equalTo: container.centerYAnchor)
|
|
1240
|
-
])
|
|
1241
|
-
|
|
1242
|
-
self.splashscreenLoaderContainer = container
|
|
1243
|
-
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
|
|
1244
3071
|
}
|
|
1245
3072
|
|
|
1246
3073
|
if Thread.isMainThread {
|
|
@@ -1269,6 +3096,97 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1269
3096
|
}
|
|
1270
3097
|
}
|
|
1271
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
|
+
|
|
1272
3190
|
private func scheduleSplashscreenTimeout() {
|
|
1273
3191
|
guard self.autoSplashscreenTimeout > 0 else {
|
|
1274
3192
|
return
|
|
@@ -1328,6 +3246,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1328
3246
|
}
|
|
1329
3247
|
|
|
1330
3248
|
private func shouldUseDirectUpdate() -> Bool {
|
|
3249
|
+
if !self.autoUpdate || self.autoUpdateMode == Self.autoUpdateModeOnlyDownload {
|
|
3250
|
+
return false
|
|
3251
|
+
}
|
|
1331
3252
|
if self.autoSplashscreenTimedOut {
|
|
1332
3253
|
return false
|
|
1333
3254
|
}
|
|
@@ -1344,7 +3265,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1344
3265
|
}
|
|
1345
3266
|
return false
|
|
1346
3267
|
case "onLaunch":
|
|
1347
|
-
if !self.
|
|
3268
|
+
if !self.getOnLaunchDirectUpdateUsed() {
|
|
1348
3269
|
return true
|
|
1349
3270
|
}
|
|
1350
3271
|
return false
|
|
@@ -1354,6 +3275,183 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1354
3275
|
}
|
|
1355
3276
|
}
|
|
1356
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
|
+
|
|
1357
3455
|
private func notifyBreakingEvents(version: String) {
|
|
1358
3456
|
guard !version.isEmpty else {
|
|
1359
3457
|
return
|
|
@@ -1363,14 +3461,82 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1363
3461
|
self.notifyListeners("majorAvailable", data: payload)
|
|
1364
3462
|
}
|
|
1365
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
|
+
|
|
1366
3530
|
func endBackGroundTaskWithNotif(
|
|
1367
3531
|
msg: String,
|
|
1368
3532
|
latestVersionName: String,
|
|
1369
3533
|
current: BundleInfo,
|
|
1370
3534
|
error: Bool = true,
|
|
3535
|
+
plannedDirectUpdate: Bool = false,
|
|
1371
3536
|
failureAction: String = "download_fail",
|
|
1372
3537
|
failureEvent: String = "downloadFailed",
|
|
1373
|
-
sendStats: Bool = true
|
|
3538
|
+
sendStats: Bool = true,
|
|
3539
|
+
notifyNoNeedUpdate: Bool = true
|
|
1374
3540
|
) {
|
|
1375
3541
|
// Clear download in progress flag - this is called at the end of every download attempt
|
|
1376
3542
|
// whether it succeeds, fails, or is skipped (e.g., already up to date)
|
|
@@ -1379,13 +3545,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1379
3545
|
downloadInProgress = false
|
|
1380
3546
|
downloadStartTime = nil
|
|
1381
3547
|
|
|
3548
|
+
self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
|
|
3549
|
+
|
|
1382
3550
|
if error {
|
|
1383
3551
|
if sendStats {
|
|
1384
3552
|
self.implementation.sendStats(action: failureAction, versionName: current.getVersionName())
|
|
1385
3553
|
}
|
|
1386
3554
|
self.notifyListeners(failureEvent, data: ["version": latestVersionName])
|
|
1387
3555
|
}
|
|
1388
|
-
|
|
3556
|
+
if notifyNoNeedUpdate {
|
|
3557
|
+
self.notifyListeners("noNeedUpdate", data: ["bundle": current.toJSON()])
|
|
3558
|
+
}
|
|
1389
3559
|
self.sendReadyToJs(current: current, msg: msg)
|
|
1390
3560
|
logger.info("endBackGroundTaskWithNotif \(msg) current: \(current.getVersionName()) latestVersionName: \(latestVersionName)")
|
|
1391
3561
|
self.endBackGroundTask()
|
|
@@ -1413,7 +3583,41 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1413
3583
|
return true
|
|
1414
3584
|
}
|
|
1415
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
|
+
|
|
1416
3617
|
func backgroundDownload() {
|
|
3618
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3619
|
+
return
|
|
3620
|
+
}
|
|
1417
3621
|
// Set download in progress flag (thread-safe)
|
|
1418
3622
|
downloadLock.lock()
|
|
1419
3623
|
downloadInProgress = true
|
|
@@ -1421,7 +3625,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1421
3625
|
downloadLock.unlock()
|
|
1422
3626
|
|
|
1423
3627
|
let plannedDirectUpdate = self.shouldUseDirectUpdate()
|
|
1424
|
-
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
|
+
}
|
|
1425
3636
|
guard let url = URL(string: self.updateUrl) else {
|
|
1426
3637
|
logger.error("Error no url or wrong format")
|
|
1427
3638
|
// Clear the flag if we return early
|
|
@@ -1432,28 +3643,32 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1432
3643
|
return
|
|
1433
3644
|
}
|
|
1434
3645
|
|
|
1435
|
-
|
|
3646
|
+
self.runBackgroundDownloadWork {
|
|
1436
3647
|
// Wait for cleanup to complete before starting download
|
|
1437
3648
|
self.waitForCleanupIfNeeded()
|
|
1438
|
-
self.
|
|
1439
|
-
|
|
1440
|
-
|
|
3649
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3650
|
+
self.clearDownloadInProgressState()
|
|
3651
|
+
return
|
|
1441
3652
|
}
|
|
3653
|
+
self.beginDownloadBackgroundTask()
|
|
1442
3654
|
self.logger.info("Check for update via \(self.updateUrl)")
|
|
1443
3655
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
1444
3656
|
let current = self.implementation.getCurrentBundle()
|
|
3657
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3658
|
+
self.clearDownloadInProgressState()
|
|
3659
|
+
self.endBackGroundTask()
|
|
3660
|
+
return
|
|
3661
|
+
}
|
|
1445
3662
|
|
|
1446
3663
|
// Handle network errors and other failures first
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
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,
|
|
1454
3670
|
current: current,
|
|
1455
|
-
|
|
1456
|
-
sendStats: !responseIsOk
|
|
3671
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1457
3672
|
)
|
|
1458
3673
|
return
|
|
1459
3674
|
}
|
|
@@ -1462,29 +3677,58 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1462
3677
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
1463
3678
|
if directUpdateAllowed {
|
|
1464
3679
|
self.logger.info("Direct update to builtin version")
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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() {
|
|
1472
3689
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
1473
3690
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will apply later.")
|
|
1474
3691
|
}
|
|
1475
3692
|
self.logger.info("Setting next bundle to builtin")
|
|
1476
3693
|
_ = self.implementation.setNextBundle(next: BundleInfo.ID_BUILTIN)
|
|
1477
|
-
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
|
+
)
|
|
1478
3716
|
}
|
|
1479
3717
|
return
|
|
1480
3718
|
}
|
|
1481
3719
|
let sessionKey = res.sessionKey ?? ""
|
|
3720
|
+
let latestVersionName = res.version
|
|
1482
3721
|
guard let downloadUrl = URL(string: res.url) else {
|
|
3722
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: latestVersionName)
|
|
1483
3723
|
self.logger.error("Error no url or wrong format")
|
|
1484
|
-
self.endBackGroundTaskWithNotif(
|
|
3724
|
+
self.endBackGroundTaskWithNotif(
|
|
3725
|
+
msg: "Error no url or wrong format",
|
|
3726
|
+
latestVersionName: latestVersionName,
|
|
3727
|
+
current: current,
|
|
3728
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3729
|
+
)
|
|
1485
3730
|
return
|
|
1486
3731
|
}
|
|
1487
|
-
let latestVersionName = res.version
|
|
1488
3732
|
if latestVersionName != "" && current.getVersionName() != latestVersionName {
|
|
1489
3733
|
do {
|
|
1490
3734
|
self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
|
|
@@ -1499,6 +3743,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1499
3743
|
self.logger.error("Failed to delete failed bundle: \(nextImpl!.toString())")
|
|
1500
3744
|
}
|
|
1501
3745
|
}
|
|
3746
|
+
self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
|
|
1502
3747
|
if res.manifest != nil {
|
|
1503
3748
|
nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
|
|
1504
3749
|
} else {
|
|
@@ -1507,12 +3752,27 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1507
3752
|
}
|
|
1508
3753
|
guard let next = nextImpl else {
|
|
1509
3754
|
self.logger.error("Error downloading file")
|
|
1510
|
-
self.endBackGroundTaskWithNotif(
|
|
3755
|
+
self.endBackGroundTaskWithNotif(
|
|
3756
|
+
msg: "Error downloading file",
|
|
3757
|
+
latestVersionName: latestVersionName,
|
|
3758
|
+
current: current,
|
|
3759
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3760
|
+
)
|
|
1511
3761
|
return
|
|
1512
3762
|
}
|
|
1513
3763
|
if next.isErrorStatus() {
|
|
1514
3764
|
self.logger.error("Latest bundle already exists and is in error state. Aborting update.")
|
|
1515
|
-
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()
|
|
1516
3776
|
return
|
|
1517
3777
|
}
|
|
1518
3778
|
res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
|
|
@@ -1526,7 +3786,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1526
3786
|
if !resDel {
|
|
1527
3787
|
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
1528
3788
|
}
|
|
1529
|
-
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()
|
|
1530
3800
|
return
|
|
1531
3801
|
}
|
|
1532
3802
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
@@ -1539,40 +3809,90 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1539
3809
|
}
|
|
1540
3810
|
if !delayConditionList.isEmpty {
|
|
1541
3811
|
self.logger.info("Update delayed until delay conditions met")
|
|
1542
|
-
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
|
+
)
|
|
1543
3819
|
return
|
|
1544
3820
|
}
|
|
1545
|
-
if self.
|
|
1546
|
-
self.
|
|
1547
|
-
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
|
+
)
|
|
1548
3840
|
}
|
|
1549
|
-
|
|
1550
|
-
_ = self._reload()
|
|
1551
|
-
self.endBackGroundTaskWithNotif(msg: "update installed", latestVersionName: latestVersionName, current: next, error: false)
|
|
1552
|
-
} else {
|
|
3841
|
+
} else if self.shouldAutoSetNextBundle() {
|
|
1553
3842
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
1554
3843
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
1555
3844
|
}
|
|
1556
3845
|
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
1557
3846
|
_ = self.implementation.setNextBundle(next: next.getId())
|
|
1558
|
-
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
|
+
)
|
|
1559
3865
|
}
|
|
1560
3866
|
return
|
|
1561
3867
|
} catch {
|
|
1562
3868
|
self.logger.error("Error downloading file \(error.localizedDescription)")
|
|
1563
3869
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1564
|
-
self.endBackGroundTaskWithNotif(
|
|
3870
|
+
self.endBackGroundTaskWithNotif(
|
|
3871
|
+
msg: "Error downloading file",
|
|
3872
|
+
latestVersionName: latestVersionName,
|
|
3873
|
+
current: current,
|
|
3874
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
3875
|
+
)
|
|
1565
3876
|
return
|
|
1566
3877
|
}
|
|
1567
3878
|
} else {
|
|
1568
3879
|
self.logger.info("No need to update, \(current.getId()) is the latest bundle.")
|
|
1569
|
-
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
|
+
)
|
|
1570
3887
|
return
|
|
1571
3888
|
}
|
|
1572
3889
|
}
|
|
1573
3890
|
}
|
|
1574
3891
|
|
|
1575
3892
|
private func installNext() {
|
|
3893
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3894
|
+
return
|
|
3895
|
+
}
|
|
1576
3896
|
let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
|
|
1577
3897
|
let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
|
|
1578
3898
|
let kind: String = obj.value(forKey: "kind") as! String
|
|
@@ -1590,6 +3910,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1590
3910
|
logger.info("Next bundle is: \(next!.toString())")
|
|
1591
3911
|
if self.implementation.set(bundle: next!) && self._reload() {
|
|
1592
3912
|
logger.info("Updated to bundle: \(next!.toString())")
|
|
3913
|
+
self.notifyBundleSet(next!)
|
|
1593
3914
|
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
1594
3915
|
} else {
|
|
1595
3916
|
logger.error("Update to bundle: \(next!.toString()) Failed!")
|
|
@@ -1616,6 +3937,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1616
3937
|
}
|
|
1617
3938
|
|
|
1618
3939
|
@objc func appMovedToForeground() {
|
|
3940
|
+
appHealthTracker?.markForeground(true)
|
|
1619
3941
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1620
3942
|
self.implementation.sendStats(action: "app_moved_to_foreground", versionName: current.getVersionName())
|
|
1621
3943
|
self.delayUpdateUtils.checkCancelDelay(source: .foreground)
|
|
@@ -1661,9 +3983,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1661
3983
|
timer.invalidate()
|
|
1662
3984
|
return
|
|
1663
3985
|
}
|
|
1664
|
-
DispatchQueue.global(qos: .
|
|
3986
|
+
DispatchQueue.global(qos: .utility).async {
|
|
3987
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3988
|
+
return
|
|
3989
|
+
}
|
|
1665
3990
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
1666
3991
|
let current = self.implementation.getCurrentBundle()
|
|
3992
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3993
|
+
return
|
|
3994
|
+
}
|
|
1667
3995
|
|
|
1668
3996
|
if res.version != current.getVersionName() {
|
|
1669
3997
|
self.logger.info("New version found: \(res.version)")
|
|
@@ -1682,6 +4010,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1682
4010
|
@objc func appMovedToBackground() {
|
|
1683
4011
|
// Reset timeout flag at start of each background cycle
|
|
1684
4012
|
self.autoSplashscreenTimedOut = false
|
|
4013
|
+
appHealthTracker?.markForeground(false)
|
|
1685
4014
|
|
|
1686
4015
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1687
4016
|
self.implementation.sendStats(action: "app_moved_to_background", versionName: current.getVersionName())
|
|
@@ -1748,13 +4077,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1748
4077
|
}
|
|
1749
4078
|
|
|
1750
4079
|
self.shakeMenuEnabled = enabled
|
|
1751
|
-
|
|
4080
|
+
self.syncShakeMenuGestureRecognizer()
|
|
4081
|
+
logger.info("Shake menu \(enabled ? "enabled" : "disabled") with \(self.shakeMenuGesture) gesture")
|
|
1752
4082
|
call.resolve()
|
|
1753
4083
|
}
|
|
1754
4084
|
|
|
1755
4085
|
@objc func isShakeMenuEnabled(_ call: CAPPluginCall) {
|
|
1756
4086
|
call.resolve([
|
|
1757
|
-
"enabled": self.shakeMenuEnabled
|
|
4087
|
+
"enabled": self.shakeMenuEnabled,
|
|
4088
|
+
"gesture": self.shakeMenuGesture
|
|
1758
4089
|
])
|
|
1759
4090
|
}
|
|
1760
4091
|
|
|
@@ -1766,6 +4097,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1766
4097
|
}
|
|
1767
4098
|
|
|
1768
4099
|
self.shakeChannelSelectorEnabled = enabled
|
|
4100
|
+
self.syncShakeMenuGestureRecognizer()
|
|
1769
4101
|
logger.info("Shake channel selector \(enabled ? "enabled" : "disabled")")
|
|
1770
4102
|
call.resolve()
|
|
1771
4103
|
}
|
|
@@ -1813,29 +4145,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1813
4145
|
|
|
1814
4146
|
logger.info("Getting App Store update info for \(bundleId) in country \(country)")
|
|
1815
4147
|
|
|
4148
|
+
self.saveCallForAsyncHandling(call)
|
|
1816
4149
|
DispatchQueue.global(qos: .background).async {
|
|
1817
4150
|
let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=\(country)"
|
|
1818
4151
|
guard let url = URL(string: urlString) else {
|
|
1819
|
-
|
|
4152
|
+
self.rejectCall(call, message: "Invalid URL for App Store lookup")
|
|
1820
4153
|
return
|
|
1821
4154
|
}
|
|
1822
4155
|
|
|
1823
4156
|
let task = URLSession.shared.dataTask(with: url) { data, _, error in
|
|
1824
4157
|
if let error = error {
|
|
1825
4158
|
self.logger.error("App Store lookup failed: \(error.localizedDescription)")
|
|
1826
|
-
|
|
4159
|
+
self.rejectCall(call, message: "App Store lookup failed: \(error.localizedDescription)")
|
|
1827
4160
|
return
|
|
1828
4161
|
}
|
|
1829
4162
|
|
|
1830
4163
|
guard let data = data else {
|
|
1831
|
-
|
|
4164
|
+
self.rejectCall(call, message: "No data received from App Store")
|
|
1832
4165
|
return
|
|
1833
4166
|
}
|
|
1834
4167
|
|
|
1835
4168
|
do {
|
|
1836
4169
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
1837
4170
|
let resultCount = json["resultCount"] as? Int else {
|
|
1838
|
-
|
|
4171
|
+
self.rejectCall(call, message: "Invalid response from App Store")
|
|
1839
4172
|
return
|
|
1840
4173
|
}
|
|
1841
4174
|
|
|
@@ -1892,10 +4225,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1892
4225
|
self.logger.info("App not found in App Store for bundleId: \(bundleId)")
|
|
1893
4226
|
}
|
|
1894
4227
|
|
|
1895
|
-
|
|
4228
|
+
self.resolveCall(call, data: result)
|
|
1896
4229
|
} catch {
|
|
1897
4230
|
self.logger.error("Failed to parse App Store response: \(error.localizedDescription)")
|
|
1898
|
-
|
|
4231
|
+
self.rejectCall(call, message: "Failed to parse App Store response: \(error.localizedDescription)")
|
|
1899
4232
|
}
|
|
1900
4233
|
}
|
|
1901
4234
|
task.resume()
|
|
@@ -1904,37 +4237,48 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1904
4237
|
|
|
1905
4238
|
@objc func openAppStore(_ call: CAPPluginCall) {
|
|
1906
4239
|
let appId = call.getString("appId")
|
|
4240
|
+
let bundleId = implementation.appId
|
|
4241
|
+
self.saveCallForAsyncHandling(call)
|
|
1907
4242
|
|
|
1908
|
-
|
|
1909
|
-
// Open App Store with provided app ID
|
|
1910
|
-
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") {
|
|
1911
4244
|
guard let url = URL(string: urlString) else {
|
|
1912
|
-
|
|
4245
|
+
self.rejectCall(call, message: invalidMessage)
|
|
1913
4246
|
return
|
|
1914
4247
|
}
|
|
1915
4248
|
DispatchQueue.main.async {
|
|
1916
4249
|
UIApplication.shared.open(url) { success in
|
|
1917
4250
|
if success {
|
|
1918
|
-
|
|
4251
|
+
self.resolveCall(call)
|
|
1919
4252
|
} else {
|
|
1920
|
-
|
|
4253
|
+
self.rejectCall(call, message: failureMessage)
|
|
1921
4254
|
}
|
|
1922
4255
|
}
|
|
1923
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)")
|
|
1924
4269
|
} else {
|
|
1925
|
-
// Look up app ID using bundle identifier
|
|
1926
|
-
let bundleId = implementation.appId
|
|
1927
4270
|
let lookupUrl = "https://itunes.apple.com/lookup?bundleId=\(bundleId)"
|
|
1928
4271
|
|
|
1929
4272
|
DispatchQueue.global(qos: .background).async {
|
|
1930
4273
|
guard let url = URL(string: lookupUrl) else {
|
|
1931
|
-
|
|
4274
|
+
openFallbackAppStorePage()
|
|
1932
4275
|
return
|
|
1933
4276
|
}
|
|
1934
4277
|
|
|
1935
4278
|
let task = URLSession.shared.dataTask(with: url) { data, _, error in
|
|
1936
4279
|
if let error = error {
|
|
1937
|
-
|
|
4280
|
+
self.logger.error("App Store lookup failed: \(error.localizedDescription)")
|
|
4281
|
+
openFallbackAppStorePage()
|
|
1938
4282
|
return
|
|
1939
4283
|
}
|
|
1940
4284
|
|
|
@@ -1943,39 +4287,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1943
4287
|
let results = json["results"] as? [[String: Any]],
|
|
1944
4288
|
let appInfo = results.first,
|
|
1945
4289
|
let trackId = appInfo["trackId"] as? Int else {
|
|
1946
|
-
|
|
1947
|
-
let fallbackUrlString = "https://apps.apple.com/app/\(bundleId)"
|
|
1948
|
-
guard let fallbackUrl = URL(string: fallbackUrlString) else {
|
|
1949
|
-
call.reject("Failed to find app in App Store and fallback URL is invalid")
|
|
1950
|
-
return
|
|
1951
|
-
}
|
|
1952
|
-
DispatchQueue.main.async {
|
|
1953
|
-
UIApplication.shared.open(fallbackUrl) { success in
|
|
1954
|
-
if success {
|
|
1955
|
-
call.resolve()
|
|
1956
|
-
} else {
|
|
1957
|
-
call.reject("Failed to open App Store")
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
return
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
let appStoreUrl = "https://apps.apple.com/app/id\(trackId)"
|
|
1965
|
-
guard let url = URL(string: appStoreUrl) else {
|
|
1966
|
-
call.reject("Invalid App Store URL")
|
|
4290
|
+
openFallbackAppStorePage()
|
|
1967
4291
|
return
|
|
1968
4292
|
}
|
|
1969
4293
|
|
|
1970
|
-
|
|
1971
|
-
UIApplication.shared.open(url) { success in
|
|
1972
|
-
if success {
|
|
1973
|
-
call.resolve()
|
|
1974
|
-
} else {
|
|
1975
|
-
call.reject("Failed to open App Store")
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
4294
|
+
openAppStorePage(urlString: "https://apps.apple.com/app/id\(trackId)")
|
|
1979
4295
|
}
|
|
1980
4296
|
task.resume()
|
|
1981
4297
|
}
|