@capgo/capacitor-updater 7.45.10 → 7.50.2
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 +510 -92
- package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3192 -777
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +798 -299
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +46 -28
- 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 +359 -25
- package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
- package/dist/docs.json +1283 -169
- package/dist/esm/definitions.d.ts +621 -44
- 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 +2116 -223
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +997 -332
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +78 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +418 -36
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +15 -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 = "7.
|
|
88
|
+
private let pluginVersion: String = "7.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,6 +148,10 @@ 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
|
|
105
155
|
private let splashscreenPluginName = "SplashScreen"
|
|
106
156
|
private let splashscreenRetryDelayMilliseconds = 100
|
|
107
157
|
private let splashscreenMaxRetries = 20
|
|
@@ -118,7 +168,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
118
168
|
private let onLaunchDirectUpdateStateLock = NSLock()
|
|
119
169
|
private var downloadInProgress = false
|
|
120
170
|
private var downloadStartTime: Date?
|
|
121
|
-
private let downloadTimeout: TimeInterval =
|
|
171
|
+
private let downloadTimeout: TimeInterval = 600 // 10 minute timeout
|
|
122
172
|
|
|
123
173
|
// Lock to ensure cleanup completes before downloads start
|
|
124
174
|
private let cleanupLock = NSLock()
|
|
@@ -127,9 +177,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
127
177
|
private var persistCustomId = false
|
|
128
178
|
private var persistModifyUrl = false
|
|
129
179
|
private var allowManualBundleError = false
|
|
180
|
+
private var allowPreview = false
|
|
130
181
|
private var keepUrlPathFlagLastValue: Bool?
|
|
182
|
+
private var appHealthTracker: AppHealthTracker?
|
|
183
|
+
private var webViewStatsReporter: WebViewStatsReporter?
|
|
131
184
|
public var shakeMenuEnabled = false
|
|
132
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?
|
|
133
193
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
134
194
|
|
|
135
195
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
@@ -143,6 +203,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
143
203
|
} else {
|
|
144
204
|
logger.error("Failed to get webView for logging")
|
|
145
205
|
}
|
|
206
|
+
let webViewStatsReporter = WebViewStatsReporter(implementation: implementation)
|
|
207
|
+
self.webViewStatsReporter = webViewStatsReporter
|
|
208
|
+
webViewStatsReporter.install(on: self.bridge?.webView)
|
|
146
209
|
#if targetEnvironment(simulator)
|
|
147
210
|
logger.info("::::: SIMULATOR :::::")
|
|
148
211
|
logger.info("Application directory: \(NSHomeDirectory())")
|
|
@@ -162,6 +225,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
162
225
|
}
|
|
163
226
|
persistModifyUrl = getConfig().getBoolean("persistModifyUrl", false)
|
|
164
227
|
allowManualBundleError = getConfig().getBoolean("allowManualBundleError", false)
|
|
228
|
+
allowPreview = getConfig().getBoolean("allowPreview", false)
|
|
165
229
|
logger.info("init for device \(self.implementation.deviceID)")
|
|
166
230
|
guard let versionName = getConfig().getString("version", Bundle.main.versionName) else {
|
|
167
231
|
logger.error("Cannot get version name")
|
|
@@ -181,33 +245,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
181
245
|
keepUrlPathAfterReload = getConfig().getBoolean("keepUrlPathAfterReload", false)
|
|
182
246
|
syncKeepUrlPathFlag(enabled: keepUrlPathAfterReload)
|
|
183
247
|
|
|
184
|
-
// Handle directUpdate configuration - support string values and backward compatibility
|
|
185
|
-
if let directUpdateString = getConfig().getString("directUpdate") {
|
|
186
|
-
// Handle backward compatibility for boolean true
|
|
187
|
-
if directUpdateString == "true" {
|
|
188
|
-
directUpdateMode = "always"
|
|
189
|
-
directUpdate = true
|
|
190
|
-
} else {
|
|
191
|
-
directUpdateMode = directUpdateString
|
|
192
|
-
directUpdate = directUpdateString == "always" || directUpdateString == "atInstall" || directUpdateString == "onLaunch"
|
|
193
|
-
// Validate directUpdate value
|
|
194
|
-
if directUpdateString != "false" && directUpdateString != "always" && directUpdateString != "atInstall" && directUpdateString != "onLaunch" {
|
|
195
|
-
logger.error("Invalid directUpdate value: \"\(directUpdateString)\". Supported values are: \"false\", \"true\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\".")
|
|
196
|
-
directUpdateMode = "false"
|
|
197
|
-
directUpdate = false
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
let directUpdateBool = getConfig().getBoolean("directUpdate", false)
|
|
202
|
-
if directUpdateBool {
|
|
203
|
-
directUpdateMode = "always" // backward compatibility: true = always
|
|
204
|
-
directUpdate = true
|
|
205
|
-
} else {
|
|
206
|
-
directUpdateMode = "false"
|
|
207
|
-
directUpdate = false
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
248
|
autoSplashscreen = getConfig().getBoolean("autoSplashscreen", false)
|
|
212
249
|
autoSplashscreenLoader = getConfig().getBoolean("autoSplashscreenLoader", false)
|
|
213
250
|
let splashscreenTimeoutValue = getConfig().getInt("autoSplashscreenTimeout", 10000)
|
|
@@ -217,23 +254,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
217
254
|
updateUrl = storedUpdateUrl
|
|
218
255
|
logger.info("Loaded persisted updateUrl")
|
|
219
256
|
}
|
|
220
|
-
|
|
257
|
+
configureAutoUpdateModeFromConfig()
|
|
221
258
|
appReadyTimeout = max(1000, getConfig().getInt("appReadyTimeout", 10000)) // Minimum 1 second
|
|
222
259
|
implementation.timeout = Double(getConfig().getInt("responseTimeout", 20))
|
|
223
260
|
resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
|
|
224
261
|
shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
225
262
|
shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
273
|
+
}
|
|
274
|
+
syncShakeMenuGestureRecognizer()
|
|
275
|
+
periodCheckDelay = Self.normalizedPeriodCheckDelaySeconds(getConfig().getInt("periodCheckDelay", 0))
|
|
232
276
|
|
|
233
277
|
implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
|
|
234
278
|
implementation.notifyDownloadRaw = notifyDownload
|
|
235
279
|
implementation.notifyListeners = { [weak self] eventName, data in
|
|
236
|
-
|
|
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
|
+
}
|
|
237
290
|
}
|
|
238
291
|
implementation.pluginVersion = self.pluginVersion
|
|
239
292
|
|
|
@@ -256,6 +309,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
256
309
|
// crash the app on purpose it should not happen
|
|
257
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")
|
|
258
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
|
+
}
|
|
259
321
|
logger.info("appId \(implementation.appId)")
|
|
260
322
|
implementation.statsUrl = getConfig().getString("statsUrl", CapacitorUpdaterPlugin.statsUrlDefault)!
|
|
261
323
|
implementation.channelUrl = getConfig().getString("channelUrl", CapacitorUpdaterPlugin.channelUrlDefault)!
|
|
@@ -278,14 +340,24 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
278
340
|
implementation.defaultChannel = getConfig().getString("defaultChannel", "")!
|
|
279
341
|
}
|
|
280
342
|
self.implementation.autoReset()
|
|
343
|
+
let appHealthTracker = AppHealthTracker(implementation: self.implementation)
|
|
344
|
+
self.appHealthTracker = appHealthTracker
|
|
345
|
+
appHealthTracker.reportPreviousUncleanForegroundExit()
|
|
346
|
+
appHealthTracker.startSession()
|
|
281
347
|
|
|
282
348
|
// Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
|
|
283
349
|
self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
|
|
350
|
+
let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
|
|
351
|
+
if nativeBuildVersionChanged {
|
|
352
|
+
self.clearPreviewSessionForNativeBuildChange()
|
|
353
|
+
}
|
|
354
|
+
self.leavePreviewSessionForLaunchURLIfNeeded()
|
|
284
355
|
|
|
285
356
|
if resetWhenUpdate {
|
|
286
357
|
let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
|
|
287
358
|
self.cleanupObsoleteVersions(didResetCurrentBundle: didResetCurrentBundle)
|
|
288
359
|
}
|
|
360
|
+
self.reportNativeVersionStatsIfChanged()
|
|
289
361
|
|
|
290
362
|
// Load the server
|
|
291
363
|
// This is very much swift specific, android does not do that
|
|
@@ -298,9 +370,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
298
370
|
logger.error("unable to force reload, the plugin might fallback to the builtin version")
|
|
299
371
|
}
|
|
300
372
|
|
|
301
|
-
|
|
302
|
-
nc.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
303
|
-
nc.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
373
|
+
self.registerNotificationObservers()
|
|
304
374
|
|
|
305
375
|
// Check for 'kill' delay condition on app launch
|
|
306
376
|
// This handles cases where the app was killed (willTerminateNotification is not reliable for system kills)
|
|
@@ -308,6 +378,47 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
308
378
|
|
|
309
379
|
self.appMovedToForeground()
|
|
310
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
|
+
)
|
|
311
422
|
}
|
|
312
423
|
|
|
313
424
|
private func syncKeepUrlPathFlag(enabled: Bool) {
|
|
@@ -357,6 +468,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
357
468
|
}
|
|
358
469
|
}
|
|
359
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
|
+
|
|
360
487
|
private func initialLoad() -> Bool {
|
|
361
488
|
guard let bridge = self.bridge else { return false }
|
|
362
489
|
if keepUrlPathAfterReload {
|
|
@@ -409,6 +536,86 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
409
536
|
return previous != "0" && self.currentBuildVersion != previous
|
|
410
537
|
}
|
|
411
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
|
+
|
|
412
619
|
@discardableResult
|
|
413
620
|
func resetCurrentBundleForNativeBuildChangeIfNeeded() -> Bool {
|
|
414
621
|
let previous = self.storedNativeBuildVersion()
|
|
@@ -522,11 +729,88 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
522
729
|
logger.info("Cleanup finished, proceeding with download")
|
|
523
730
|
}
|
|
524
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
|
+
|
|
525
804
|
@objc func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false, bundle: BundleInfo? = nil) {
|
|
526
805
|
let bundleInfo = bundle ?? self.implementation.getBundleInfo(id: id)
|
|
527
|
-
|
|
806
|
+
var downloadPayload: JSObject = [:]
|
|
807
|
+
downloadPayload["percent"] = percent
|
|
808
|
+
downloadPayload["bundle"] = bundlePayload(bundleInfo)
|
|
809
|
+
self.notifyListenersOnMain("download", data: downloadPayload)
|
|
528
810
|
if percent == 100 {
|
|
529
|
-
|
|
811
|
+
var downloadCompletePayload: JSObject = [:]
|
|
812
|
+
downloadCompletePayload["bundle"] = bundlePayload(bundleInfo)
|
|
813
|
+
self.notifyListenersOnMain("downloadComplete", data: downloadCompletePayload)
|
|
530
814
|
self.implementation.sendStats(action: "download_complete", versionName: bundleInfo.getVersionName())
|
|
531
815
|
} else if percent.isMultiple(of: 10) || ignoreMultipleOfTen {
|
|
532
816
|
self.implementation.sendStats(action: "download_\(percent)", versionName: bundleInfo.getVersionName())
|
|
@@ -606,6 +890,299 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
606
890
|
call.resolve(["version": self.pluginVersion])
|
|
607
891
|
}
|
|
608
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
|
+
|
|
609
1186
|
@objc func download(_ call: CAPPluginCall) {
|
|
610
1187
|
guard let urlString = call.getString("url") else {
|
|
611
1188
|
logger.error("Download called without url")
|
|
@@ -619,64 +1196,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
619
1196
|
}
|
|
620
1197
|
|
|
621
1198
|
let sessionKey = call.getString("sessionKey", "")
|
|
622
|
-
|
|
1199
|
+
let checksum = call.getString("checksum", "")
|
|
623
1200
|
let manifestArray = call.getArray("manifest")
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1201
|
+
logger.info("Downloading \(urlString)")
|
|
1202
|
+
self.saveCallForAsyncHandling(call)
|
|
1203
|
+
self.runBackgroundDownloadWork {
|
|
627
1204
|
do {
|
|
628
|
-
let next
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
manifestEntries.append(entry)
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
|
|
643
|
-
} else {
|
|
644
|
-
next = try self.implementation.download(url: url!, version: version, sessionKey: sessionKey)
|
|
645
|
-
}
|
|
646
|
-
// If public key is present but no checksum provided, refuse installation
|
|
647
|
-
if self.implementation.publicKey != "" && checksum == "" {
|
|
648
|
-
self.logger.error("Public key present but no checksum provided")
|
|
649
|
-
self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
|
|
650
|
-
let id = next.getId()
|
|
651
|
-
let resDel = self.implementation.delete(id: id)
|
|
652
|
-
if !resDel {
|
|
653
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
654
|
-
}
|
|
655
|
-
throw ObjectSavableError.checksum
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
659
|
-
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
660
|
-
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
661
|
-
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
662
|
-
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
663
|
-
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
664
|
-
let id = next.getId()
|
|
665
|
-
let resDel = self.implementation.delete(id: id)
|
|
666
|
-
if !resDel {
|
|
667
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
668
|
-
}
|
|
669
|
-
throw ObjectSavableError.checksum
|
|
670
|
-
} else {
|
|
671
|
-
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
672
|
-
}
|
|
673
|
-
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
674
|
-
call.resolve(next.toJSON())
|
|
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())
|
|
675
1216
|
} catch {
|
|
676
|
-
self.logger.error("Failed to download from: \(
|
|
677
|
-
|
|
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)
|
|
678
1221
|
self.implementation.sendStats(action: "download_fail")
|
|
679
|
-
|
|
1222
|
+
self.rejectCall(call, message: "Failed to download from: \(urlString) - \(error.localizedDescription)")
|
|
680
1223
|
}
|
|
681
1224
|
}
|
|
682
1225
|
}
|
|
@@ -768,11 +1311,37 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
768
1311
|
}
|
|
769
1312
|
}
|
|
770
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
|
+
|
|
771
1337
|
@objc func reload(_ call: CAPPluginCall) {
|
|
772
1338
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
773
1339
|
let next: BundleInfo? = self.implementation.getNextBundle()
|
|
774
1340
|
|
|
775
|
-
if
|
|
1341
|
+
if !self.isPreviewSessionStateActive(),
|
|
1342
|
+
let next = next,
|
|
1343
|
+
!next.isErrorStatus(),
|
|
1344
|
+
next.getId() != current.getId() {
|
|
776
1345
|
let previousState = self.implementation.captureResetState()
|
|
777
1346
|
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
778
1347
|
logger.info("Applying pending bundle before reload: \(next.toString())")
|
|
@@ -791,6 +1360,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
791
1360
|
}
|
|
792
1361
|
self.notifyBundleSet(next)
|
|
793
1362
|
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
1363
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
794
1364
|
call.resolve()
|
|
795
1365
|
return
|
|
796
1366
|
}
|
|
@@ -802,6 +1372,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
802
1372
|
}
|
|
803
1373
|
|
|
804
1374
|
if self._reload() {
|
|
1375
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
805
1376
|
call.resolve()
|
|
806
1377
|
} else {
|
|
807
1378
|
logger.error("Reload failed")
|
|
@@ -809,14 +1380,36 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
809
1380
|
}
|
|
810
1381
|
}
|
|
811
1382
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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) {
|
|
820
1413
|
logger.error("Set next version failed. id \(id) does not exist.")
|
|
821
1414
|
call.reject("Set next version failed. id \(id) does not exist.")
|
|
822
1415
|
} else {
|
|
@@ -835,11 +1428,851 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
835
1428
|
if !res {
|
|
836
1429
|
logger.info("Bundle successfully set to: \(id) ")
|
|
837
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()
|
|
838
1441
|
} else if !self._reload() {
|
|
839
1442
|
call.reject("Reload failed after setting bundle \(id)")
|
|
840
1443
|
} else {
|
|
841
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 {
|
|
842
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
|
|
2067
|
+
}
|
|
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
|
|
2074
|
+
}
|
|
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)
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
return url.path
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
private func normalizedPreviewPath(_ components: [String]) -> String {
|
|
2097
|
+
let separator = String(self.previewPathSeparator)
|
|
2098
|
+
return separator + components.filter { !$0.isEmpty }.joined(separator: separator)
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
private func previewDeepLinkPath(_ leafComponent: String) -> String {
|
|
2102
|
+
self.normalizedPreviewPath([self.previewDeepLinkRootComponent, leafComponent])
|
|
2103
|
+
}
|
|
2104
|
+
|
|
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)
|
|
2109
|
+
}
|
|
2110
|
+
|
|
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 {
|
|
2118
|
+
return
|
|
2119
|
+
}
|
|
2120
|
+
|
|
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")
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
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")
|
|
2152
|
+
}
|
|
2153
|
+
|
|
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)")
|
|
2162
|
+
}
|
|
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")
|
|
2175
|
+
}
|
|
2176
|
+
guard payload.url != nil || payload.manifest?.isEmpty == false else {
|
|
2177
|
+
throw makePreviewError("Preview payload is missing download information")
|
|
2178
|
+
}
|
|
2179
|
+
|
|
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()
|
|
2184
|
+
}
|
|
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
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
private func clearPreviewSessionForNativeBuildChange() {
|
|
2210
|
+
guard self.previewSessionEnabled || self.implementation.getPreviewFallbackBundle() != nil || !self.previewSessions().isEmpty else {
|
|
2211
|
+
return
|
|
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()
|
|
2229
|
+
}
|
|
2230
|
+
|
|
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)
|
|
2235
|
+
return
|
|
2236
|
+
}
|
|
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)
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
private func showPreviewSessionNoticeIfNeeded() {
|
|
2244
|
+
guard self.previewSessionEnabled && self.previewSessionAlertPending else {
|
|
2245
|
+
return
|
|
2246
|
+
}
|
|
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
|
+
}
|
|
843
2276
|
}
|
|
844
2277
|
}
|
|
845
2278
|
|
|
@@ -902,45 +2335,132 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
902
2335
|
|
|
903
2336
|
@objc func getLatest(_ call: CAPPluginCall) {
|
|
904
2337
|
let channel = call.getString("channel")
|
|
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)
|
|
905
2346
|
runGetLatestWork {
|
|
906
|
-
let res = self.implementation.getLatest(
|
|
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
|
+
}
|
|
907
2355
|
if let error = res.error, !error.isEmpty {
|
|
908
2356
|
let responseKind = self.updateResponseKind(kind: res.kind)
|
|
909
2357
|
res.kind = responseKind
|
|
2358
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
|
|
910
2359
|
if responseKind == "failed" {
|
|
911
|
-
|
|
2360
|
+
self.rejectCall(call, message: error)
|
|
912
2361
|
} else {
|
|
913
2362
|
if res.version.isEmpty {
|
|
914
2363
|
res.version = self.implementation.getCurrentBundle().getVersionName()
|
|
915
2364
|
}
|
|
916
|
-
|
|
2365
|
+
self.resolveCall(call, data: res.toDict())
|
|
917
2366
|
}
|
|
918
2367
|
} else if let kind = res.kind, !kind.isEmpty {
|
|
919
2368
|
let responseKind = self.updateResponseKind(kind: kind)
|
|
920
2369
|
res.kind = responseKind
|
|
2370
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
|
|
921
2371
|
if responseKind != "failed" {
|
|
922
2372
|
if res.version.isEmpty {
|
|
923
2373
|
res.version = self.implementation.getCurrentBundle().getVersionName()
|
|
924
2374
|
}
|
|
925
|
-
|
|
2375
|
+
self.resolveCall(call, data: res.toDict())
|
|
926
2376
|
} else {
|
|
927
|
-
|
|
2377
|
+
self.rejectCall(call, message: res.message ?? "server did not provide a message")
|
|
928
2378
|
}
|
|
929
2379
|
} else if let message = res.message, !message.isEmpty {
|
|
930
|
-
|
|
2380
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
|
|
2381
|
+
self.rejectCall(call, message: message)
|
|
931
2382
|
} else {
|
|
932
|
-
|
|
2383
|
+
self.resolveCall(call, data: res.toDict())
|
|
933
2384
|
}
|
|
934
2385
|
}
|
|
935
2386
|
}
|
|
936
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
|
+
|
|
937
2456
|
@objc func unsetChannel(_ call: CAPPluginCall) {
|
|
938
2457
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate", false)
|
|
939
|
-
|
|
2458
|
+
self.saveCallForAsyncHandling(call)
|
|
2459
|
+
DispatchQueue.global(qos: .utility).async {
|
|
940
2460
|
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
941
2461
|
let res = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
|
|
942
2462
|
if res.error != "" {
|
|
943
|
-
|
|
2463
|
+
self.rejectCall(call, message: res.error, code: "UNSETCHANNEL_FAILED", data: [
|
|
944
2464
|
"message": res.error,
|
|
945
2465
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
946
2466
|
])
|
|
@@ -954,7 +2474,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
954
2474
|
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
955
2475
|
}
|
|
956
2476
|
}
|
|
957
|
-
|
|
2477
|
+
self.resolveCall(call, data: res.toDict())
|
|
958
2478
|
}
|
|
959
2479
|
}
|
|
960
2480
|
}
|
|
@@ -969,17 +2489,24 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
969
2489
|
return
|
|
970
2490
|
}
|
|
971
2491
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate") ?? false
|
|
972
|
-
|
|
973
|
-
|
|
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
|
+
)
|
|
974
2501
|
if res.error != "" {
|
|
975
2502
|
// Fire channelPrivate event if channel doesn't allow self-assignment
|
|
976
2503
|
if res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed") {
|
|
977
|
-
self.
|
|
2504
|
+
self.notifyListenersOnMain("channelPrivate", data: [
|
|
978
2505
|
"channel": channel,
|
|
979
2506
|
"message": res.error
|
|
980
2507
|
])
|
|
981
2508
|
}
|
|
982
|
-
|
|
2509
|
+
self.rejectCall(call, message: res.error, code: "SETCHANNEL_FAILED", data: [
|
|
983
2510
|
"message": res.error,
|
|
984
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"
|
|
985
2512
|
])
|
|
@@ -993,35 +2520,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
993
2520
|
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
994
2521
|
}
|
|
995
2522
|
}
|
|
996
|
-
|
|
2523
|
+
self.resolveCall(call, data: res.toDict())
|
|
997
2524
|
}
|
|
998
2525
|
}
|
|
999
2526
|
}
|
|
1000
2527
|
|
|
1001
2528
|
@objc func getChannel(_ call: CAPPluginCall) {
|
|
1002
|
-
|
|
1003
|
-
|
|
2529
|
+
self.saveCallForAsyncHandling(call)
|
|
2530
|
+
DispatchQueue.global(qos: .utility).async {
|
|
2531
|
+
let res = self.implementation.getChannel(defaultChannelKey: self.defaultChannelDefaultsKey)
|
|
1004
2532
|
if res.error != "" {
|
|
1005
|
-
|
|
2533
|
+
self.rejectCall(call, message: res.error, code: "GETCHANNEL_FAILED", data: [
|
|
1006
2534
|
"message": res.error,
|
|
1007
2535
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
1008
2536
|
])
|
|
1009
2537
|
} else {
|
|
1010
|
-
|
|
2538
|
+
self.resolveCall(call, data: res.toDict())
|
|
1011
2539
|
}
|
|
1012
2540
|
}
|
|
1013
2541
|
}
|
|
1014
2542
|
|
|
1015
2543
|
@objc func listChannels(_ call: CAPPluginCall) {
|
|
1016
|
-
|
|
2544
|
+
self.saveCallForAsyncHandling(call)
|
|
2545
|
+
DispatchQueue.global(qos: .utility).async {
|
|
1017
2546
|
let res = self.implementation.listChannels()
|
|
1018
2547
|
if res.error != "" {
|
|
1019
|
-
|
|
2548
|
+
self.rejectCall(call, message: res.error, code: "LISTCHANNELS_FAILED", data: [
|
|
1020
2549
|
"message": res.error,
|
|
1021
2550
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
1022
2551
|
])
|
|
1023
2552
|
} else {
|
|
1024
|
-
|
|
2553
|
+
var payload: JSObject = [:]
|
|
2554
|
+
payload["channels"] = res.channels
|
|
2555
|
+
self.resolveCall(call, data: payload)
|
|
1025
2556
|
}
|
|
1026
2557
|
}
|
|
1027
2558
|
}
|
|
@@ -1148,6 +2679,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1148
2679
|
let bundle = self.implementation.getCurrentBundle()
|
|
1149
2680
|
self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
|
|
1150
2681
|
logger.info("Current bundle loaded successfully. [notifyAppReady was called] \(bundle.toString())")
|
|
2682
|
+
self.clearIncomingPreviewTransition()
|
|
2683
|
+
self.hidePreviewTransitionLoader(reason: "notify-app-ready")
|
|
2684
|
+
|
|
1151
2685
|
call.resolve(["bundle": bundle.toJSON()])
|
|
1152
2686
|
}
|
|
1153
2687
|
|
|
@@ -1197,6 +2731,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1197
2731
|
// Note: _checkCancelDelay method has been moved to DelayUpdateUtils class
|
|
1198
2732
|
|
|
1199
2733
|
private func _isAutoUpdateEnabled() -> Bool {
|
|
2734
|
+
if self.isPreviewSessionStateActive() {
|
|
2735
|
+
return false
|
|
2736
|
+
}
|
|
1200
2737
|
let instanceDescriptor = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor()
|
|
1201
2738
|
if instanceDescriptor?.serverURL != nil {
|
|
1202
2739
|
logger.warn("AutoUpdate is automatic disabled when serverUrl is set.")
|
|
@@ -1234,10 +2771,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1234
2771
|
logger.info("Built-in bundle is active. We skip the check for notifyAppReady.")
|
|
1235
2772
|
return
|
|
1236
2773
|
}
|
|
2774
|
+
if self.isPreviewSessionStateActive() {
|
|
2775
|
+
logger.info("Preview session is active. We skip the check for notifyAppReady.")
|
|
2776
|
+
return
|
|
2777
|
+
}
|
|
1237
2778
|
|
|
1238
2779
|
logger.info("Current bundle is: \(current.toString())")
|
|
1239
2780
|
|
|
1240
|
-
if BundleStatus.SUCCESS.
|
|
2781
|
+
if BundleStatus.SUCCESS.storedValue != current.getStatus() {
|
|
1241
2782
|
logger.error("notifyAppReady was not called, roll back current bundle: \(current.toString())")
|
|
1242
2783
|
logger.error("Did you forget to call 'notifyAppReady()' in your Capacitor App code?")
|
|
1243
2784
|
self.notifyListeners("updateFailed", data: [
|
|
@@ -1286,6 +2827,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1286
2827
|
if self.autoSplashscreen {
|
|
1287
2828
|
self.hideSplashscreen()
|
|
1288
2829
|
}
|
|
2830
|
+
self.hidePreviewTransitionLoader(reason: "app-ready")
|
|
1289
2831
|
}
|
|
1290
2832
|
}
|
|
1291
2833
|
|
|
@@ -1453,6 +2995,51 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1453
2995
|
}
|
|
1454
2996
|
}
|
|
1455
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
|
|
3011
|
+
} else {
|
|
3012
|
+
indicatorStyle = .whiteLarge
|
|
3013
|
+
}
|
|
3014
|
+
|
|
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
|
+
])
|
|
3041
|
+
}
|
|
3042
|
+
|
|
1456
3043
|
private func addSplashscreenLoaderIfNeeded() {
|
|
1457
3044
|
guard self.autoSplashscreenLoader else {
|
|
1458
3045
|
return
|
|
@@ -1467,40 +3054,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1467
3054
|
return
|
|
1468
3055
|
}
|
|
1469
3056
|
|
|
1470
|
-
let
|
|
1471
|
-
container.translatesAutoresizingMaskIntoConstraints = false
|
|
1472
|
-
container.backgroundColor = UIColor.clear
|
|
1473
|
-
container.isUserInteractionEnabled = false
|
|
1474
|
-
|
|
1475
|
-
let indicatorStyle: UIActivityIndicatorView.Style
|
|
3057
|
+
let indicatorColor: UIColor?
|
|
1476
3058
|
if #available(iOS 13.0, *) {
|
|
1477
|
-
|
|
3059
|
+
indicatorColor = UIColor.label
|
|
1478
3060
|
} else {
|
|
1479
|
-
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
let indicator = UIActivityIndicatorView(style: indicatorStyle)
|
|
1483
|
-
indicator.translatesAutoresizingMaskIntoConstraints = false
|
|
1484
|
-
indicator.hidesWhenStopped = false
|
|
1485
|
-
if #available(iOS 13.0, *) {
|
|
1486
|
-
indicator.color = UIColor.label
|
|
3061
|
+
indicatorColor = nil
|
|
1487
3062
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
container.topAnchor.constraint(equalTo: rootView.topAnchor),
|
|
1497
|
-
container.bottomAnchor.constraint(equalTo: rootView.bottomAnchor),
|
|
1498
|
-
indicator.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
|
1499
|
-
indicator.centerYAnchor.constraint(equalTo: container.centerYAnchor)
|
|
1500
|
-
])
|
|
1501
|
-
|
|
1502
|
-
self.splashscreenLoaderContainer = container
|
|
1503
|
-
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
|
|
1504
3071
|
}
|
|
1505
3072
|
|
|
1506
3073
|
if Thread.isMainThread {
|
|
@@ -1529,6 +3096,97 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1529
3096
|
}
|
|
1530
3097
|
}
|
|
1531
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
|
+
|
|
1532
3190
|
private func scheduleSplashscreenTimeout() {
|
|
1533
3191
|
guard self.autoSplashscreenTimeout > 0 else {
|
|
1534
3192
|
return
|
|
@@ -1588,6 +3246,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1588
3246
|
}
|
|
1589
3247
|
|
|
1590
3248
|
private func shouldUseDirectUpdate() -> Bool {
|
|
3249
|
+
if !self.autoUpdate || self.autoUpdateMode == Self.autoUpdateModeOnlyDownload {
|
|
3250
|
+
return false
|
|
3251
|
+
}
|
|
1591
3252
|
if self.autoSplashscreenTimedOut {
|
|
1592
3253
|
return false
|
|
1593
3254
|
}
|
|
@@ -1614,10 +3275,132 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1614
3275
|
}
|
|
1615
3276
|
}
|
|
1616
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
|
+
|
|
1617
3393
|
static func shouldConsumeOnLaunchDirectUpdate(directUpdateMode: String, plannedDirectUpdate: Bool) -> Bool {
|
|
1618
3394
|
plannedDirectUpdate && directUpdateMode == "onLaunch"
|
|
1619
3395
|
}
|
|
1620
3396
|
|
|
3397
|
+
static func normalizedPeriodCheckDelaySeconds(_ value: Int) -> Int {
|
|
3398
|
+
guard value > 0 else {
|
|
3399
|
+
return 0
|
|
3400
|
+
}
|
|
3401
|
+
return max(600, value)
|
|
3402
|
+
}
|
|
3403
|
+
|
|
1621
3404
|
private func getOnLaunchDirectUpdateUsed() -> Bool {
|
|
1622
3405
|
self.onLaunchDirectUpdateStateLock.lock()
|
|
1623
3406
|
defer { self.onLaunchDirectUpdateStateLock.unlock() }
|
|
@@ -1640,6 +3423,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1640
3423
|
|
|
1641
3424
|
func configureDirectUpdateModeForTesting(_ directUpdateMode: String, onLaunchDirectUpdateUsed: Bool = false) {
|
|
1642
3425
|
self.directUpdateMode = directUpdateMode
|
|
3426
|
+
self.autoUpdateMode = Self.autoUpdateModeForLegacyDirectUpdateMode(directUpdateMode)
|
|
3427
|
+
self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
|
|
3428
|
+
self.directUpdate = Self.isDirectUpdateMode(self.directUpdateMode)
|
|
1643
3429
|
self.setOnLaunchDirectUpdateUsed(onLaunchDirectUpdateUsed)
|
|
1644
3430
|
}
|
|
1645
3431
|
|
|
@@ -1647,6 +3433,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1647
3433
|
self.updateUrl = updateUrl
|
|
1648
3434
|
}
|
|
1649
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
|
+
|
|
1650
3443
|
func setCurrentBuildVersionForTesting(_ currentBuildVersion: String) {
|
|
1651
3444
|
self.currentBuildVersion = currentBuildVersion
|
|
1652
3445
|
}
|
|
@@ -1668,13 +3461,32 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1668
3461
|
self.notifyListeners("majorAvailable", data: payload)
|
|
1669
3462
|
}
|
|
1670
3463
|
|
|
1671
|
-
private func
|
|
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 {
|
|
1672
3480
|
if let kind, ["up_to_date", "blocked", "failed"].contains(kind) {
|
|
1673
3481
|
return kind
|
|
1674
3482
|
}
|
|
1675
3483
|
return "failed"
|
|
1676
3484
|
}
|
|
1677
3485
|
|
|
3486
|
+
private func updateResponseKind(kind: String?) -> String {
|
|
3487
|
+
Self.normalizedUpdateResponseKind(kind: kind)
|
|
3488
|
+
}
|
|
3489
|
+
|
|
1678
3490
|
private func endBackgroundDownloadAfterLatestError(
|
|
1679
3491
|
backendError: String,
|
|
1680
3492
|
res: AppVersion,
|
|
@@ -1694,6 +3506,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1694
3506
|
"version": latestVersionName,
|
|
1695
3507
|
"bundle": current.toJSON()
|
|
1696
3508
|
])
|
|
3509
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
|
|
1697
3510
|
|
|
1698
3511
|
if responseKind == "up_to_date" {
|
|
1699
3512
|
self.logger.info("No new version available")
|
|
@@ -1722,7 +3535,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1722
3535
|
plannedDirectUpdate: Bool = false,
|
|
1723
3536
|
failureAction: String = "download_fail",
|
|
1724
3537
|
failureEvent: String = "downloadFailed",
|
|
1725
|
-
sendStats: Bool = true
|
|
3538
|
+
sendStats: Bool = true,
|
|
3539
|
+
notifyNoNeedUpdate: Bool = true
|
|
1726
3540
|
) {
|
|
1727
3541
|
// Clear download in progress flag - this is called at the end of every download attempt
|
|
1728
3542
|
// whether it succeeds, fails, or is skipped (e.g., already up to date)
|
|
@@ -1739,7 +3553,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1739
3553
|
}
|
|
1740
3554
|
self.notifyListeners(failureEvent, data: ["version": latestVersionName])
|
|
1741
3555
|
}
|
|
1742
|
-
|
|
3556
|
+
if notifyNoNeedUpdate {
|
|
3557
|
+
self.notifyListeners("noNeedUpdate", data: ["bundle": current.toJSON()])
|
|
3558
|
+
}
|
|
1743
3559
|
self.sendReadyToJs(current: current, msg: msg)
|
|
1744
3560
|
logger.info("endBackGroundTaskWithNotif \(msg) current: \(current.getVersionName()) latestVersionName: \(latestVersionName)")
|
|
1745
3561
|
self.endBackGroundTask()
|
|
@@ -1767,8 +3583,31 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1767
3583
|
return true
|
|
1768
3584
|
}
|
|
1769
3585
|
|
|
3586
|
+
private func clearDownloadInProgressState() {
|
|
3587
|
+
downloadLock.lock()
|
|
3588
|
+
defer { downloadLock.unlock() }
|
|
3589
|
+
downloadInProgress = false
|
|
3590
|
+
downloadStartTime = nil
|
|
3591
|
+
}
|
|
3592
|
+
|
|
1770
3593
|
func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
|
|
1771
|
-
|
|
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
|
+
}
|
|
1772
3611
|
}
|
|
1773
3612
|
|
|
1774
3613
|
func runGetLatestWork(_ work: @escaping () -> Void) {
|
|
@@ -1776,6 +3615,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1776
3615
|
}
|
|
1777
3616
|
|
|
1778
3617
|
func backgroundDownload() {
|
|
3618
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3619
|
+
return
|
|
3620
|
+
}
|
|
1779
3621
|
// Set download in progress flag (thread-safe)
|
|
1780
3622
|
downloadLock.lock()
|
|
1781
3623
|
downloadInProgress = true
|
|
@@ -1783,7 +3625,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1783
3625
|
downloadLock.unlock()
|
|
1784
3626
|
|
|
1785
3627
|
let plannedDirectUpdate = self.shouldUseDirectUpdate()
|
|
1786
|
-
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
|
+
}
|
|
1787
3636
|
guard let url = URL(string: self.updateUrl) else {
|
|
1788
3637
|
logger.error("Error no url or wrong format")
|
|
1789
3638
|
// Clear the flag if we return early
|
|
@@ -1797,13 +3646,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1797
3646
|
self.runBackgroundDownloadWork {
|
|
1798
3647
|
// Wait for cleanup to complete before starting download
|
|
1799
3648
|
self.waitForCleanupIfNeeded()
|
|
1800
|
-
self.
|
|
1801
|
-
|
|
1802
|
-
|
|
3649
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3650
|
+
self.clearDownloadInProgressState()
|
|
3651
|
+
return
|
|
1803
3652
|
}
|
|
3653
|
+
self.beginDownloadBackgroundTask()
|
|
1804
3654
|
self.logger.info("Check for update via \(self.updateUrl)")
|
|
1805
3655
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
1806
3656
|
let current = self.implementation.getCurrentBundle()
|
|
3657
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3658
|
+
self.clearDownloadInProgressState()
|
|
3659
|
+
self.endBackGroundTask()
|
|
3660
|
+
return
|
|
3661
|
+
}
|
|
1807
3662
|
|
|
1808
3663
|
// Handle network errors and other failures first
|
|
1809
3664
|
let backendError = res.error ?? ""
|
|
@@ -1830,7 +3685,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1830
3685
|
error: false,
|
|
1831
3686
|
plannedDirectUpdate: plannedDirectUpdate
|
|
1832
3687
|
)
|
|
1833
|
-
} else {
|
|
3688
|
+
} else if self.shouldAutoSetNextBundle() {
|
|
1834
3689
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
1835
3690
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will apply later.")
|
|
1836
3691
|
}
|
|
@@ -1843,21 +3698,37 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1843
3698
|
error: false,
|
|
1844
3699
|
plannedDirectUpdate: plannedDirectUpdate
|
|
1845
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
|
+
)
|
|
1846
3716
|
}
|
|
1847
3717
|
return
|
|
1848
3718
|
}
|
|
1849
3719
|
let sessionKey = res.sessionKey ?? ""
|
|
3720
|
+
let latestVersionName = res.version
|
|
1850
3721
|
guard let downloadUrl = URL(string: res.url) else {
|
|
3722
|
+
self.notifyBreakingEventsIfNeeded(response: res, version: latestVersionName)
|
|
1851
3723
|
self.logger.error("Error no url or wrong format")
|
|
1852
3724
|
self.endBackGroundTaskWithNotif(
|
|
1853
3725
|
msg: "Error no url or wrong format",
|
|
1854
|
-
latestVersionName:
|
|
3726
|
+
latestVersionName: latestVersionName,
|
|
1855
3727
|
current: current,
|
|
1856
3728
|
plannedDirectUpdate: plannedDirectUpdate
|
|
1857
3729
|
)
|
|
1858
3730
|
return
|
|
1859
3731
|
}
|
|
1860
|
-
let latestVersionName = res.version
|
|
1861
3732
|
if latestVersionName != "" && current.getVersionName() != latestVersionName {
|
|
1862
3733
|
do {
|
|
1863
3734
|
self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
|
|
@@ -1899,6 +3770,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1899
3770
|
)
|
|
1900
3771
|
return
|
|
1901
3772
|
}
|
|
3773
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3774
|
+
self.clearDownloadInProgressState()
|
|
3775
|
+
self.endBackGroundTask()
|
|
3776
|
+
return
|
|
3777
|
+
}
|
|
1902
3778
|
res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
|
|
1903
3779
|
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
1904
3780
|
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
|
|
@@ -1918,6 +3794,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1918
3794
|
)
|
|
1919
3795
|
return
|
|
1920
3796
|
}
|
|
3797
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3798
|
+
self.clearDownloadInProgressState()
|
|
3799
|
+
self.endBackGroundTask()
|
|
3800
|
+
return
|
|
3801
|
+
}
|
|
1921
3802
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
1922
3803
|
if directUpdateAllowed {
|
|
1923
3804
|
let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
|
|
@@ -1937,7 +3818,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1937
3818
|
)
|
|
1938
3819
|
return
|
|
1939
3820
|
}
|
|
1940
|
-
if self.
|
|
3821
|
+
if self.applyDownloadedBundleForDirectUpdate(next) {
|
|
1941
3822
|
self.notifyBundleSet(next)
|
|
1942
3823
|
self.endBackGroundTaskWithNotif(
|
|
1943
3824
|
msg: "update installed",
|
|
@@ -1947,14 +3828,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1947
3828
|
plannedDirectUpdate: plannedDirectUpdate
|
|
1948
3829
|
)
|
|
1949
3830
|
} else {
|
|
3831
|
+
_ = self.implementation.setNextBundle(next: next.getId())
|
|
3832
|
+
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
1950
3833
|
self.endBackGroundTaskWithNotif(
|
|
1951
|
-
msg: "
|
|
3834
|
+
msg: "Direct update reload failed, update will install next background",
|
|
1952
3835
|
latestVersionName: latestVersionName,
|
|
1953
|
-
current:
|
|
3836
|
+
current: self.implementation.getCurrentBundle(),
|
|
3837
|
+
error: false,
|
|
1954
3838
|
plannedDirectUpdate: plannedDirectUpdate
|
|
1955
3839
|
)
|
|
1956
3840
|
}
|
|
1957
|
-
} else {
|
|
3841
|
+
} else if self.shouldAutoSetNextBundle() {
|
|
1958
3842
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
1959
3843
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
1960
3844
|
}
|
|
@@ -1967,6 +3851,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1967
3851
|
error: false,
|
|
1968
3852
|
plannedDirectUpdate: plannedDirectUpdate
|
|
1969
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
|
+
)
|
|
1970
3865
|
}
|
|
1971
3866
|
return
|
|
1972
3867
|
} catch {
|
|
@@ -1995,6 +3890,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1995
3890
|
}
|
|
1996
3891
|
|
|
1997
3892
|
private func installNext() {
|
|
3893
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3894
|
+
return
|
|
3895
|
+
}
|
|
1998
3896
|
let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
|
|
1999
3897
|
let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
|
|
2000
3898
|
let kind: String = obj.value(forKey: "kind") as! String
|
|
@@ -2039,6 +3937,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2039
3937
|
}
|
|
2040
3938
|
|
|
2041
3939
|
@objc func appMovedToForeground() {
|
|
3940
|
+
appHealthTracker?.markForeground(true)
|
|
2042
3941
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
2043
3942
|
self.implementation.sendStats(action: "app_moved_to_foreground", versionName: current.getVersionName())
|
|
2044
3943
|
self.delayUpdateUtils.checkCancelDelay(source: .foreground)
|
|
@@ -2084,9 +3983,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2084
3983
|
timer.invalidate()
|
|
2085
3984
|
return
|
|
2086
3985
|
}
|
|
2087
|
-
DispatchQueue.global(qos: .
|
|
3986
|
+
DispatchQueue.global(qos: .utility).async {
|
|
3987
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3988
|
+
return
|
|
3989
|
+
}
|
|
2088
3990
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
2089
3991
|
let current = self.implementation.getCurrentBundle()
|
|
3992
|
+
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3993
|
+
return
|
|
3994
|
+
}
|
|
2090
3995
|
|
|
2091
3996
|
if res.version != current.getVersionName() {
|
|
2092
3997
|
self.logger.info("New version found: \(res.version)")
|
|
@@ -2105,6 +4010,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2105
4010
|
@objc func appMovedToBackground() {
|
|
2106
4011
|
// Reset timeout flag at start of each background cycle
|
|
2107
4012
|
self.autoSplashscreenTimedOut = false
|
|
4013
|
+
appHealthTracker?.markForeground(false)
|
|
2108
4014
|
|
|
2109
4015
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
2110
4016
|
self.implementation.sendStats(action: "app_moved_to_background", versionName: current.getVersionName())
|
|
@@ -2171,13 +4077,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2171
4077
|
}
|
|
2172
4078
|
|
|
2173
4079
|
self.shakeMenuEnabled = enabled
|
|
2174
|
-
|
|
4080
|
+
self.syncShakeMenuGestureRecognizer()
|
|
4081
|
+
logger.info("Shake menu \(enabled ? "enabled" : "disabled") with \(self.shakeMenuGesture) gesture")
|
|
2175
4082
|
call.resolve()
|
|
2176
4083
|
}
|
|
2177
4084
|
|
|
2178
4085
|
@objc func isShakeMenuEnabled(_ call: CAPPluginCall) {
|
|
2179
4086
|
call.resolve([
|
|
2180
|
-
"enabled": self.shakeMenuEnabled
|
|
4087
|
+
"enabled": self.shakeMenuEnabled,
|
|
4088
|
+
"gesture": self.shakeMenuGesture
|
|
2181
4089
|
])
|
|
2182
4090
|
}
|
|
2183
4091
|
|
|
@@ -2189,6 +4097,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2189
4097
|
}
|
|
2190
4098
|
|
|
2191
4099
|
self.shakeChannelSelectorEnabled = enabled
|
|
4100
|
+
self.syncShakeMenuGestureRecognizer()
|
|
2192
4101
|
logger.info("Shake channel selector \(enabled ? "enabled" : "disabled")")
|
|
2193
4102
|
call.resolve()
|
|
2194
4103
|
}
|
|
@@ -2236,29 +4145,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2236
4145
|
|
|
2237
4146
|
logger.info("Getting App Store update info for \(bundleId) in country \(country)")
|
|
2238
4147
|
|
|
4148
|
+
self.saveCallForAsyncHandling(call)
|
|
2239
4149
|
DispatchQueue.global(qos: .background).async {
|
|
2240
4150
|
let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=\(country)"
|
|
2241
4151
|
guard let url = URL(string: urlString) else {
|
|
2242
|
-
|
|
4152
|
+
self.rejectCall(call, message: "Invalid URL for App Store lookup")
|
|
2243
4153
|
return
|
|
2244
4154
|
}
|
|
2245
4155
|
|
|
2246
4156
|
let task = URLSession.shared.dataTask(with: url) { data, _, error in
|
|
2247
4157
|
if let error = error {
|
|
2248
4158
|
self.logger.error("App Store lookup failed: \(error.localizedDescription)")
|
|
2249
|
-
|
|
4159
|
+
self.rejectCall(call, message: "App Store lookup failed: \(error.localizedDescription)")
|
|
2250
4160
|
return
|
|
2251
4161
|
}
|
|
2252
4162
|
|
|
2253
4163
|
guard let data = data else {
|
|
2254
|
-
|
|
4164
|
+
self.rejectCall(call, message: "No data received from App Store")
|
|
2255
4165
|
return
|
|
2256
4166
|
}
|
|
2257
4167
|
|
|
2258
4168
|
do {
|
|
2259
4169
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
2260
4170
|
let resultCount = json["resultCount"] as? Int else {
|
|
2261
|
-
|
|
4171
|
+
self.rejectCall(call, message: "Invalid response from App Store")
|
|
2262
4172
|
return
|
|
2263
4173
|
}
|
|
2264
4174
|
|
|
@@ -2315,10 +4225,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2315
4225
|
self.logger.info("App not found in App Store for bundleId: \(bundleId)")
|
|
2316
4226
|
}
|
|
2317
4227
|
|
|
2318
|
-
|
|
4228
|
+
self.resolveCall(call, data: result)
|
|
2319
4229
|
} catch {
|
|
2320
4230
|
self.logger.error("Failed to parse App Store response: \(error.localizedDescription)")
|
|
2321
|
-
|
|
4231
|
+
self.rejectCall(call, message: "Failed to parse App Store response: \(error.localizedDescription)")
|
|
2322
4232
|
}
|
|
2323
4233
|
}
|
|
2324
4234
|
task.resume()
|
|
@@ -2327,37 +4237,48 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2327
4237
|
|
|
2328
4238
|
@objc func openAppStore(_ call: CAPPluginCall) {
|
|
2329
4239
|
let appId = call.getString("appId")
|
|
4240
|
+
let bundleId = implementation.appId
|
|
4241
|
+
self.saveCallForAsyncHandling(call)
|
|
2330
4242
|
|
|
2331
|
-
|
|
2332
|
-
// Open App Store with provided app ID
|
|
2333
|
-
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") {
|
|
2334
4244
|
guard let url = URL(string: urlString) else {
|
|
2335
|
-
|
|
4245
|
+
self.rejectCall(call, message: invalidMessage)
|
|
2336
4246
|
return
|
|
2337
4247
|
}
|
|
2338
4248
|
DispatchQueue.main.async {
|
|
2339
4249
|
UIApplication.shared.open(url) { success in
|
|
2340
4250
|
if success {
|
|
2341
|
-
|
|
4251
|
+
self.resolveCall(call)
|
|
2342
4252
|
} else {
|
|
2343
|
-
|
|
4253
|
+
self.rejectCall(call, message: failureMessage)
|
|
2344
4254
|
}
|
|
2345
4255
|
}
|
|
2346
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)")
|
|
2347
4269
|
} else {
|
|
2348
|
-
// Look up app ID using bundle identifier
|
|
2349
|
-
let bundleId = implementation.appId
|
|
2350
4270
|
let lookupUrl = "https://itunes.apple.com/lookup?bundleId=\(bundleId)"
|
|
2351
4271
|
|
|
2352
4272
|
DispatchQueue.global(qos: .background).async {
|
|
2353
4273
|
guard let url = URL(string: lookupUrl) else {
|
|
2354
|
-
|
|
4274
|
+
openFallbackAppStorePage()
|
|
2355
4275
|
return
|
|
2356
4276
|
}
|
|
2357
4277
|
|
|
2358
4278
|
let task = URLSession.shared.dataTask(with: url) { data, _, error in
|
|
2359
4279
|
if let error = error {
|
|
2360
|
-
|
|
4280
|
+
self.logger.error("App Store lookup failed: \(error.localizedDescription)")
|
|
4281
|
+
openFallbackAppStorePage()
|
|
2361
4282
|
return
|
|
2362
4283
|
}
|
|
2363
4284
|
|
|
@@ -2366,39 +4287,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2366
4287
|
let results = json["results"] as? [[String: Any]],
|
|
2367
4288
|
let appInfo = results.first,
|
|
2368
4289
|
let trackId = appInfo["trackId"] as? Int else {
|
|
2369
|
-
|
|
2370
|
-
let fallbackUrlString = "https://apps.apple.com/app/\(bundleId)"
|
|
2371
|
-
guard let fallbackUrl = URL(string: fallbackUrlString) else {
|
|
2372
|
-
call.reject("Failed to find app in App Store and fallback URL is invalid")
|
|
2373
|
-
return
|
|
2374
|
-
}
|
|
2375
|
-
DispatchQueue.main.async {
|
|
2376
|
-
UIApplication.shared.open(fallbackUrl) { success in
|
|
2377
|
-
if success {
|
|
2378
|
-
call.resolve()
|
|
2379
|
-
} else {
|
|
2380
|
-
call.reject("Failed to open App Store")
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
return
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
let appStoreUrl = "https://apps.apple.com/app/id\(trackId)"
|
|
2388
|
-
guard let url = URL(string: appStoreUrl) else {
|
|
2389
|
-
call.reject("Invalid App Store URL")
|
|
4290
|
+
openFallbackAppStorePage()
|
|
2390
4291
|
return
|
|
2391
4292
|
}
|
|
2392
4293
|
|
|
2393
|
-
|
|
2394
|
-
UIApplication.shared.open(url) { success in
|
|
2395
|
-
if success {
|
|
2396
|
-
call.resolve()
|
|
2397
|
-
} else {
|
|
2398
|
-
call.reject("Failed to open App Store")
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
4294
|
+
openAppStorePage(urlString: "https://apps.apple.com/app/id\(trackId)")
|
|
2402
4295
|
}
|
|
2403
4296
|
task.resume()
|
|
2404
4297
|
}
|