@capgo/capacitor-updater 8.47.3 → 8.47.5
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/README.md +4 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +312 -42
- package/dist/docs.json +16 -0
- package/dist/esm/definitions.d.ts +9 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +372 -50
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +5 -3
- package/package.json +1 -1
|
@@ -79,7 +79,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
79
79
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
80
80
|
]
|
|
81
81
|
public var implementation = CapgoUpdater()
|
|
82
|
-
private let pluginVersion: String = "8.47.
|
|
82
|
+
private let pluginVersion: String = "8.47.5"
|
|
83
83
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
84
84
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
85
85
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -101,7 +101,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
101
101
|
private let previewPreviousShakeChannelSelectorDefaultsKey = "CapacitorUpdater.previewPreviousShakeChannelSelector"
|
|
102
102
|
private let previewPreviousNextBundleDefaultsKey = "CapacitorUpdater.previewPreviousNextBundle"
|
|
103
103
|
private let previewPreviousAppIdDefaultsKey = "CapacitorUpdater.previewPreviousAppId"
|
|
104
|
+
private let previewPreviousDefaultChannelDefaultsKey = "CapacitorUpdater.previewPreviousDefaultChannel"
|
|
105
|
+
private let previewPreviousDefaultChannelWasSetDefaultsKey = "CapacitorUpdater.previewPreviousDefaultChannelWasSet"
|
|
104
106
|
private let previewAppIdDefaultsKey = "CapacitorUpdater.previewAppId"
|
|
107
|
+
private let previewPayloadUrlDefaultsKey = "CapacitorUpdater.previewPayloadUrl"
|
|
108
|
+
private let previewSessionAlertPendingDefaultsKey = "CapacitorUpdater.previewSessionAlertPending"
|
|
109
|
+
private let previewDeepLinkScheme = "capgo"
|
|
110
|
+
private let previewDeepLinkRootComponent = "preview"
|
|
111
|
+
private let previewDeepLinkChannelComponent = "channel"
|
|
112
|
+
private let previewDeepLinkBundleComponent = "bundle"
|
|
113
|
+
private let previewPathSeparator = Character(UnicodeScalar(UInt8(47)))
|
|
105
114
|
// Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
|
|
106
115
|
private var updateUrl = ""
|
|
107
116
|
private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
|
@@ -155,6 +164,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
155
164
|
public var shakeChannelSelectorEnabled = false
|
|
156
165
|
public var previewSessionEnabled = false
|
|
157
166
|
private var previewSessionAlertPending = false
|
|
167
|
+
private var isLeavingPreviewForIncomingLink = false
|
|
158
168
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
159
169
|
|
|
160
170
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
@@ -230,6 +240,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
230
240
|
previewSessionEnabled = allowPreview && storedPreviewSessionEnabled
|
|
231
241
|
implementation.previewSession = previewSessionEnabled
|
|
232
242
|
if previewSessionEnabled {
|
|
243
|
+
previewSessionAlertPending = UserDefaults.standard.object(forKey: previewSessionAlertPendingDefaultsKey) as? Bool ?? true
|
|
233
244
|
shakeMenuEnabled = true
|
|
234
245
|
shakeChannelSelectorEnabled = false
|
|
235
246
|
}
|
|
@@ -312,6 +323,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
312
323
|
if nativeBuildVersionChanged {
|
|
313
324
|
self.clearPreviewSessionForNativeBuildChange()
|
|
314
325
|
}
|
|
326
|
+
self.leavePreviewSessionForLaunchURLIfNeeded()
|
|
315
327
|
|
|
316
328
|
if resetWhenUpdate {
|
|
317
329
|
let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
|
|
@@ -329,11 +341,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
329
341
|
logger.error("unable to force reload, the plugin might fallback to the builtin version")
|
|
330
342
|
}
|
|
331
343
|
|
|
332
|
-
|
|
333
|
-
nc.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
334
|
-
nc.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
335
|
-
nc.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil)
|
|
336
|
-
nc.addObserver(self, selector: #selector(appDidReceiveMemoryWarning), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
|
|
344
|
+
self.registerNotificationObservers()
|
|
337
345
|
|
|
338
346
|
// Check for 'kill' delay condition on app launch
|
|
339
347
|
// This handles cases where the app was killed (willTerminateNotification is not reliable for system kills)
|
|
@@ -341,6 +349,47 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
341
349
|
|
|
342
350
|
self.appMovedToForeground()
|
|
343
351
|
self.checkForUpdateAfterDelay()
|
|
352
|
+
self.showPreviewSessionNoticeIfNeeded()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private func registerNotificationObservers() {
|
|
356
|
+
let notificationCenter = NotificationCenter.default
|
|
357
|
+
notificationCenter.addObserver(
|
|
358
|
+
self,
|
|
359
|
+
selector: #selector(appMovedToBackground),
|
|
360
|
+
name: UIApplication.didEnterBackgroundNotification,
|
|
361
|
+
object: nil
|
|
362
|
+
)
|
|
363
|
+
notificationCenter.addObserver(
|
|
364
|
+
self,
|
|
365
|
+
selector: #selector(appMovedToForeground),
|
|
366
|
+
name: UIApplication.willEnterForegroundNotification,
|
|
367
|
+
object: nil
|
|
368
|
+
)
|
|
369
|
+
notificationCenter.addObserver(
|
|
370
|
+
self,
|
|
371
|
+
selector: #selector(appWillTerminate),
|
|
372
|
+
name: UIApplication.willTerminateNotification,
|
|
373
|
+
object: nil
|
|
374
|
+
)
|
|
375
|
+
notificationCenter.addObserver(
|
|
376
|
+
self,
|
|
377
|
+
selector: #selector(appDidReceiveMemoryWarning),
|
|
378
|
+
name: UIApplication.didReceiveMemoryWarningNotification,
|
|
379
|
+
object: nil
|
|
380
|
+
)
|
|
381
|
+
notificationCenter.addObserver(
|
|
382
|
+
self,
|
|
383
|
+
selector: #selector(handleOpenURLForPreviewSession(notification:)),
|
|
384
|
+
name: Notification.Name.capacitorOpenURL,
|
|
385
|
+
object: nil
|
|
386
|
+
)
|
|
387
|
+
notificationCenter.addObserver(
|
|
388
|
+
self,
|
|
389
|
+
selector: #selector(handleOpenURLForPreviewSession(notification:)),
|
|
390
|
+
name: Notification.Name.capacitorOpenUniversalLink,
|
|
391
|
+
object: nil
|
|
392
|
+
)
|
|
344
393
|
}
|
|
345
394
|
|
|
346
395
|
private func syncKeepUrlPathFlag(enabled: Bool) {
|
|
@@ -749,6 +798,62 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
749
798
|
return manifestEntries
|
|
750
799
|
}
|
|
751
800
|
|
|
801
|
+
private struct PreviewPayload: Decodable {
|
|
802
|
+
let version: String?
|
|
803
|
+
let url: String?
|
|
804
|
+
let checksum: String?
|
|
805
|
+
let sessionKey: String?
|
|
806
|
+
let manifest: [ManifestEntry]?
|
|
807
|
+
let message: String?
|
|
808
|
+
let error: String?
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private func makePreviewError(_ message: String) -> NSError {
|
|
812
|
+
NSError(domain: "CapacitorUpdaterPreview", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private func downloadBundle(urlString: String, version: String, sessionKey: String, checksum rawChecksum: String, manifestEntries: [ManifestEntry]?) throws -> BundleInfo {
|
|
816
|
+
guard let url = URL(string: urlString) else {
|
|
817
|
+
throw makePreviewError("Invalid download URL")
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
var checksum = rawChecksum
|
|
821
|
+
let next: BundleInfo
|
|
822
|
+
if let manifestEntries = manifestEntries {
|
|
823
|
+
next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
|
|
824
|
+
} else {
|
|
825
|
+
next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if self.implementation.publicKey != "" && checksum == "" {
|
|
829
|
+
self.logger.error("Public key present but no checksum provided")
|
|
830
|
+
self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
|
|
831
|
+
let id = next.getId()
|
|
832
|
+
let resDel = self.implementation.delete(id: id)
|
|
833
|
+
if !resDel {
|
|
834
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
835
|
+
}
|
|
836
|
+
throw ObjectSavableError.checksum
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
840
|
+
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
841
|
+
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
842
|
+
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
843
|
+
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
844
|
+
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
845
|
+
let id = next.getId()
|
|
846
|
+
let resDel = self.implementation.delete(id: id)
|
|
847
|
+
if !resDel {
|
|
848
|
+
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
849
|
+
}
|
|
850
|
+
throw ObjectSavableError.checksum
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
854
|
+
return next
|
|
855
|
+
}
|
|
856
|
+
|
|
752
857
|
@objc func download(_ call: CAPPluginCall) {
|
|
753
858
|
guard let urlString = call.getString("url") else {
|
|
754
859
|
logger.error("Download called without url")
|
|
@@ -762,57 +867,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
762
867
|
}
|
|
763
868
|
|
|
764
869
|
let sessionKey = call.getString("sessionKey", "")
|
|
765
|
-
|
|
870
|
+
let checksum = call.getString("checksum", "")
|
|
766
871
|
let manifestArray = call.getArray("manifest")
|
|
767
|
-
|
|
768
|
-
logger.info("Downloading \(String(describing: url))")
|
|
872
|
+
logger.info("Downloading \(urlString)")
|
|
769
873
|
self.saveCallForAsyncHandling(call)
|
|
770
874
|
self.runBackgroundDownloadWork {
|
|
771
875
|
do {
|
|
772
|
-
let next
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if self.implementation.publicKey != "" && checksum == "" {
|
|
780
|
-
self.logger.error("Public key present but no checksum provided")
|
|
781
|
-
self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
|
|
782
|
-
let id = next.getId()
|
|
783
|
-
let resDel = self.implementation.delete(id: id)
|
|
784
|
-
if !resDel {
|
|
785
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
786
|
-
}
|
|
787
|
-
throw ObjectSavableError.checksum
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
|
|
791
|
-
CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
|
|
792
|
-
CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
|
|
793
|
-
if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
|
|
794
|
-
self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
|
|
795
|
-
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
796
|
-
let id = next.getId()
|
|
797
|
-
let resDel = self.implementation.delete(id: id)
|
|
798
|
-
if !resDel {
|
|
799
|
-
self.logger.error("Delete failed, id \(id) doesn't exist")
|
|
800
|
-
}
|
|
801
|
-
throw ObjectSavableError.checksum
|
|
802
|
-
} else {
|
|
803
|
-
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
804
|
-
}
|
|
876
|
+
let next = try self.downloadBundle(
|
|
877
|
+
urlString: urlString,
|
|
878
|
+
version: version,
|
|
879
|
+
sessionKey: sessionKey,
|
|
880
|
+
checksum: checksum,
|
|
881
|
+
manifestEntries: self.manifestEntries(from: manifestArray)
|
|
882
|
+
)
|
|
805
883
|
var updateAvailablePayload: JSObject = [:]
|
|
806
884
|
updateAvailablePayload["bundle"] = self.bundlePayload(next)
|
|
807
885
|
self.notifyListenersOnMain("updateAvailable", data: updateAvailablePayload)
|
|
808
886
|
self.resolveCall(call, data: next.toJSON())
|
|
809
887
|
} catch {
|
|
810
|
-
self.logger.error("Failed to download from: \(
|
|
888
|
+
self.logger.error("Failed to download from: \(urlString) \(error.localizedDescription)")
|
|
811
889
|
var downloadFailedPayload: JSObject = [:]
|
|
812
890
|
downloadFailedPayload["version"] = version
|
|
813
891
|
self.notifyListenersOnMain("downloadFailed", data: downloadFailedPayload)
|
|
814
892
|
self.implementation.sendStats(action: "download_fail")
|
|
815
|
-
self.rejectCall(call, message: "Failed to download from: \(
|
|
893
|
+
self.rejectCall(call, message: "Failed to download from: \(urlString) - \(error.localizedDescription)")
|
|
816
894
|
}
|
|
817
895
|
}
|
|
818
896
|
}
|
|
@@ -989,6 +1067,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
989
1067
|
return
|
|
990
1068
|
}
|
|
991
1069
|
let previewAppId = self.normalizedPreviewAppId(call.getString("appId"))
|
|
1070
|
+
let rawPayloadUrl = call.getString("payloadUrl")
|
|
1071
|
+
let previewPayloadUrl = self.normalizedPreviewPayloadUrl(rawPayloadUrl)
|
|
1072
|
+
if let rawPayloadUrl = rawPayloadUrl, !rawPayloadUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, previewPayloadUrl == nil {
|
|
1073
|
+
logger.error("startPreviewSession called with invalid payloadUrl")
|
|
1074
|
+
call.reject("Invalid preview payloadUrl")
|
|
1075
|
+
return
|
|
1076
|
+
}
|
|
992
1077
|
|
|
993
1078
|
if !self.previewSessionEnabled {
|
|
994
1079
|
let current = self.implementation.getCurrentBundle()
|
|
@@ -1007,6 +1092,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1007
1092
|
}
|
|
1008
1093
|
|
|
1009
1094
|
UserDefaults.standard.set(self.implementation.appId, forKey: self.previewPreviousAppIdDefaultsKey)
|
|
1095
|
+
if let previousDefaultChannel = UserDefaults.standard.object(forKey: self.defaultChannelDefaultsKey) as? String {
|
|
1096
|
+
UserDefaults.standard.set(previousDefaultChannel, forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1097
|
+
UserDefaults.standard.set(true, forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1098
|
+
} else {
|
|
1099
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1100
|
+
UserDefaults.standard.set(false, forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1101
|
+
}
|
|
1010
1102
|
UserDefaults.standard.set(self.shakeMenuEnabled, forKey: self.previewPreviousShakeMenuDefaultsKey)
|
|
1011
1103
|
UserDefaults.standard.set(self.shakeChannelSelectorEnabled, forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
|
|
1012
1104
|
logger.info("Preview session started with fallback bundle: \(current.toString())")
|
|
@@ -1018,39 +1110,94 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1018
1110
|
logger.info("Preview session using appId: \(previewAppId)")
|
|
1019
1111
|
}
|
|
1020
1112
|
|
|
1113
|
+
if let previewPayloadUrl = previewPayloadUrl {
|
|
1114
|
+
UserDefaults.standard.set(previewPayloadUrl.absoluteString, forKey: self.previewPayloadUrlDefaultsKey)
|
|
1115
|
+
logger.info("Preview session using payload URL")
|
|
1116
|
+
} else {
|
|
1117
|
+
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1021
1120
|
self.previewSessionEnabled = true
|
|
1022
1121
|
self.previewSessionAlertPending = true
|
|
1023
1122
|
self.implementation.previewSession = true
|
|
1024
1123
|
self.shakeMenuEnabled = true
|
|
1025
1124
|
self.shakeChannelSelectorEnabled = false
|
|
1026
1125
|
UserDefaults.standard.set(true, forKey: self.previewSessionDefaultsKey)
|
|
1126
|
+
UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
1027
1127
|
UserDefaults.standard.synchronize()
|
|
1028
1128
|
call.resolve()
|
|
1029
1129
|
}
|
|
1030
1130
|
|
|
1031
1131
|
func leavePreviewSessionFromShakeMenu() -> Bool {
|
|
1032
1132
|
let previewBundle = self.implementation.getCurrentBundle()
|
|
1033
|
-
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
1034
1133
|
|
|
1035
1134
|
let didReset = self.resetToPreviewFallbackBundle()
|
|
1036
1135
|
guard didReset else {
|
|
1037
1136
|
return false
|
|
1038
1137
|
}
|
|
1039
1138
|
|
|
1040
|
-
_ = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
|
|
1041
1139
|
let previewFallbackBundle = self.implementation.getPreviewFallbackBundle()
|
|
1042
1140
|
self.endPreviewSession()
|
|
1043
1141
|
let restoredNextBundle = self.implementation.getNextBundle()
|
|
1142
|
+
self.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle: previewFallbackBundle, restoredNextBundle: restoredNextBundle)
|
|
1143
|
+
return true
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
private func leavePreviewSessionForLaunchURLIfNeeded() {
|
|
1147
|
+
guard self.previewSessionEnabled,
|
|
1148
|
+
!self.isLeavingPreviewForIncomingLink,
|
|
1149
|
+
let launchUrl = ApplicationDelegateProxy.shared.lastURL,
|
|
1150
|
+
self.isPreviewDeepLink(launchUrl) else {
|
|
1151
|
+
return
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
self.isLeavingPreviewForIncomingLink = true
|
|
1155
|
+
logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load")
|
|
1156
|
+
if !self.leavePreviewSessionWithoutReload() {
|
|
1157
|
+
logger.error("Could not leave preview session before initial preview deeplink routing")
|
|
1158
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
private func leavePreviewSessionWithoutReload() -> Bool {
|
|
1163
|
+
let previewBundle = self.implementation.getCurrentBundle()
|
|
1164
|
+
guard let previewFallbackBundle = self.implementation.getPreviewFallbackBundle(), !previewFallbackBundle.isErrorStatus() else {
|
|
1165
|
+
logger.error("No preview fallback bundle available")
|
|
1166
|
+
return false
|
|
1167
|
+
}
|
|
1168
|
+
guard self.implementation.canSet(bundle: previewFallbackBundle) else {
|
|
1169
|
+
logger.error("Preview fallback bundle is not installable")
|
|
1170
|
+
return false
|
|
1171
|
+
}
|
|
1172
|
+
guard self.implementation.stagePreviewFallbackReload(bundle: previewFallbackBundle) else {
|
|
1173
|
+
logger.error("Could not stage preview fallback bundle")
|
|
1174
|
+
return false
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
self.endPreviewSession()
|
|
1178
|
+
let restoredNextBundle = self.implementation.getNextBundle()
|
|
1179
|
+
self.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle: previewFallbackBundle, restoredNextBundle: restoredNextBundle)
|
|
1180
|
+
return true
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
private func deletePreviewBundleIfUnused(
|
|
1184
|
+
_ previewBundle: BundleInfo,
|
|
1185
|
+
previewFallbackBundle: BundleInfo?,
|
|
1186
|
+
restoredNextBundle: BundleInfo?
|
|
1187
|
+
) {
|
|
1044
1188
|
if !previewBundle.isBuiltin() &&
|
|
1045
1189
|
previewFallbackBundle?.getId() != previewBundle.getId() &&
|
|
1046
1190
|
restoredNextBundle?.getId() != previewBundle.getId() {
|
|
1047
1191
|
_ = self.implementation.delete(id: previewBundle.getId(), removeInfo: false)
|
|
1048
1192
|
}
|
|
1049
|
-
return true
|
|
1050
1193
|
}
|
|
1051
1194
|
|
|
1052
1195
|
func reloadPreviewSessionFromShakeMenu() -> Bool {
|
|
1053
|
-
self.
|
|
1196
|
+
if let payloadUrl = self.storedPreviewPayloadUrl() {
|
|
1197
|
+
return self.refreshPreviewSessionFromPayloadUrl(payloadUrl)
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return self._reload()
|
|
1054
1201
|
}
|
|
1055
1202
|
|
|
1056
1203
|
func hasActivePreviewSession() -> Bool {
|
|
@@ -1088,9 +1235,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1088
1235
|
?? getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
1089
1236
|
self.restorePreviewPreviousNextBundle()
|
|
1090
1237
|
self.restorePreviewPreviousAppId()
|
|
1238
|
+
self.restorePreviewPreviousDefaultChannel()
|
|
1091
1239
|
|
|
1092
1240
|
self.previewSessionEnabled = false
|
|
1093
1241
|
self.previewSessionAlertPending = false
|
|
1242
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
1094
1243
|
self.implementation.previewSession = false
|
|
1095
1244
|
self.shakeMenuEnabled = previousShakeMenuEnabled
|
|
1096
1245
|
self.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled
|
|
@@ -1117,8 +1266,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1117
1266
|
|
|
1118
1267
|
self.restorePreviewPreviousNextBundle()
|
|
1119
1268
|
self.restorePreviewPreviousAppId()
|
|
1269
|
+
self.restorePreviewPreviousDefaultChannel()
|
|
1120
1270
|
self.previewSessionEnabled = false
|
|
1121
1271
|
self.previewSessionAlertPending = false
|
|
1272
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
1122
1273
|
self.implementation.previewSession = false
|
|
1123
1274
|
self.shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
1124
1275
|
self.shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
@@ -1132,7 +1283,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1132
1283
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
|
|
1133
1284
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
1134
1285
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousAppIdDefaultsKey)
|
|
1286
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1287
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1135
1288
|
UserDefaults.standard.removeObject(forKey: self.previewAppIdDefaultsKey)
|
|
1289
|
+
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
1290
|
+
UserDefaults.standard.removeObject(forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
1136
1291
|
UserDefaults.standard.synchronize()
|
|
1137
1292
|
}
|
|
1138
1293
|
|
|
@@ -1145,6 +1300,23 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1145
1300
|
logger.info("Restored appId after preview: \(previousAppId)")
|
|
1146
1301
|
}
|
|
1147
1302
|
|
|
1303
|
+
private func restorePreviewPreviousDefaultChannel() {
|
|
1304
|
+
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
1305
|
+
let hadPreviousDefaultChannel = UserDefaults.standard.object(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey) as? Bool ?? false
|
|
1306
|
+
|
|
1307
|
+
guard hadPreviousDefaultChannel,
|
|
1308
|
+
let previousDefaultChannel = UserDefaults.standard.string(forKey: self.previewPreviousDefaultChannelDefaultsKey) else {
|
|
1309
|
+
UserDefaults.standard.removeObject(forKey: self.defaultChannelDefaultsKey)
|
|
1310
|
+
self.implementation.defaultChannel = configDefaultChannel
|
|
1311
|
+
logger.info("Restored defaultChannel after preview to config value")
|
|
1312
|
+
return
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
UserDefaults.standard.set(previousDefaultChannel, forKey: self.defaultChannelDefaultsKey)
|
|
1316
|
+
self.implementation.defaultChannel = previousDefaultChannel
|
|
1317
|
+
logger.info("Restored defaultChannel after preview")
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1148
1320
|
private func normalizedPreviewAppId(_ rawAppId: String?) -> String? {
|
|
1149
1321
|
guard let rawAppId else {
|
|
1150
1322
|
return nil
|
|
@@ -1163,6 +1335,148 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1163
1335
|
return appId
|
|
1164
1336
|
}
|
|
1165
1337
|
|
|
1338
|
+
private func normalizedPreviewPayloadUrl(_ rawPayloadUrl: String?) -> URL? {
|
|
1339
|
+
guard let rawPayloadUrl else {
|
|
1340
|
+
return nil
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
let payloadUrl = rawPayloadUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1344
|
+
guard !payloadUrl.isEmpty,
|
|
1345
|
+
let url = URL(string: payloadUrl),
|
|
1346
|
+
url.scheme == "https" || url.scheme == "http" else {
|
|
1347
|
+
return nil
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return url
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
private func storedPreviewPayloadUrl() -> URL? {
|
|
1354
|
+
normalizedPreviewPayloadUrl(UserDefaults.standard.string(forKey: self.previewPayloadUrlDefaultsKey))
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
private func previewPath(from url: URL) -> String {
|
|
1358
|
+
if url.scheme == self.previewDeepLinkScheme {
|
|
1359
|
+
var components: [String] = []
|
|
1360
|
+
if let host = url.host, !host.isEmpty {
|
|
1361
|
+
components.append(host)
|
|
1362
|
+
}
|
|
1363
|
+
components.append(contentsOf: url.path.split(separator: self.previewPathSeparator).map(String.init))
|
|
1364
|
+
return self.normalizedPreviewPath(components)
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return url.path
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private func normalizedPreviewPath(_ components: [String]) -> String {
|
|
1371
|
+
let separator = String(self.previewPathSeparator)
|
|
1372
|
+
return separator + components.filter { !$0.isEmpty }.joined(separator: separator)
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
private func previewDeepLinkPath(_ leafComponent: String) -> String {
|
|
1376
|
+
self.normalizedPreviewPath([self.previewDeepLinkRootComponent, leafComponent])
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
private func isPreviewDeepLink(_ url: URL) -> Bool {
|
|
1380
|
+
let path = self.previewPath(from: url)
|
|
1381
|
+
return path == self.previewDeepLinkPath(self.previewDeepLinkChannelComponent) ||
|
|
1382
|
+
path == self.previewDeepLinkPath(self.previewDeepLinkBundleComponent)
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
@objc private func handleOpenURLForPreviewSession(notification: NSNotification) {
|
|
1386
|
+
let rawUrl = (notification.object as? [String: Any])?["url"]
|
|
1387
|
+
let url = rawUrl as? URL ?? (rawUrl as? NSURL).map { $0 as URL }
|
|
1388
|
+
guard self.previewSessionEnabled,
|
|
1389
|
+
!self.isLeavingPreviewForIncomingLink,
|
|
1390
|
+
let url,
|
|
1391
|
+
self.isPreviewDeepLink(url) else {
|
|
1392
|
+
return
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
self.isLeavingPreviewForIncomingLink = true
|
|
1396
|
+
logger.info("Preview deeplink received while preview session is active; restoring fallback before routing")
|
|
1397
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1398
|
+
let didLeave = self.leavePreviewSessionFromShakeMenu()
|
|
1399
|
+
if !didLeave {
|
|
1400
|
+
self.logger.error("Could not leave preview session before routing incoming preview deeplink")
|
|
1401
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
private func fetchPreviewPayload(_ payloadUrl: URL) throws -> PreviewPayload {
|
|
1407
|
+
var request = URLRequest(url: payloadUrl)
|
|
1408
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
1409
|
+
|
|
1410
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
1411
|
+
var responseData: Data?
|
|
1412
|
+
var response: URLResponse?
|
|
1413
|
+
var responseError: Error?
|
|
1414
|
+
|
|
1415
|
+
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
|
|
1416
|
+
responseData = data
|
|
1417
|
+
response = urlResponse
|
|
1418
|
+
responseError = error
|
|
1419
|
+
semaphore.signal()
|
|
1420
|
+
}.resume()
|
|
1421
|
+
|
|
1422
|
+
if semaphore.wait(timeout: .now() + 60) == .timedOut {
|
|
1423
|
+
throw makePreviewError("Preview payload request timed out")
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if let responseError = responseError {
|
|
1427
|
+
throw responseError
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
let data = responseData ?? Data()
|
|
1431
|
+
if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
|
|
1432
|
+
if let payload = try? JSONDecoder().decode(PreviewPayload.self, from: data) {
|
|
1433
|
+
throw makePreviewError(payload.message ?? payload.error ?? "Preview payload request failed with HTTP \(httpResponse.statusCode)")
|
|
1434
|
+
}
|
|
1435
|
+
let message = String(data: data, encoding: .utf8) ?? "Preview payload request failed with HTTP \(httpResponse.statusCode)"
|
|
1436
|
+
throw makePreviewError(message)
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
return try JSONDecoder().decode(PreviewPayload.self, from: data)
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private func refreshPreviewSessionFromPayloadUrl(_ payloadUrl: URL) -> Bool {
|
|
1443
|
+
do {
|
|
1444
|
+
let payload = try self.fetchPreviewPayload(payloadUrl)
|
|
1445
|
+
guard let version = payload.version, !version.isEmpty else {
|
|
1446
|
+
throw makePreviewError("Preview payload is missing a version")
|
|
1447
|
+
}
|
|
1448
|
+
guard payload.url != nil || payload.manifest?.isEmpty == false else {
|
|
1449
|
+
throw makePreviewError("Preview payload is missing download information")
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
let current = self.implementation.getCurrentBundle()
|
|
1453
|
+
if current.getVersionName() == version {
|
|
1454
|
+
self.logger.info("Preview payload unchanged, reloading current bundle")
|
|
1455
|
+
return self._reload()
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
let next = try self.downloadBundle(
|
|
1459
|
+
// Fallback URL is only provided when payload.url is missing; when manifestEntries is present,
|
|
1460
|
+
// downloadBundle routes through downloadManifest and ignores urlString.
|
|
1461
|
+
urlString: payload.url ?? "https://404.capgo.app/no.zip",
|
|
1462
|
+
version: version,
|
|
1463
|
+
sessionKey: payload.sessionKey ?? "",
|
|
1464
|
+
checksum: payload.checksum ?? "",
|
|
1465
|
+
manifestEntries: payload.manifest
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
guard self.implementation.set(id: next.getId()) else {
|
|
1469
|
+
throw makePreviewError("Downloaded preview bundle cannot be applied")
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
self.notifyBundleSet(next)
|
|
1473
|
+
return self._reload()
|
|
1474
|
+
} catch {
|
|
1475
|
+
self.logger.error("Could not refresh preview session: \(error.localizedDescription)")
|
|
1476
|
+
return false
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1166
1480
|
private func clearPreviewSessionForNativeBuildChange() {
|
|
1167
1481
|
guard self.previewSessionEnabled || self.implementation.getPreviewFallbackBundle() != nil else {
|
|
1168
1482
|
return
|
|
@@ -1170,14 +1484,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1170
1484
|
logger.info("Native build changed; clearing preview session state")
|
|
1171
1485
|
self.previewSessionEnabled = false
|
|
1172
1486
|
self.previewSessionAlertPending = false
|
|
1487
|
+
self.isLeavingPreviewForIncomingLink = false
|
|
1173
1488
|
self.implementation.previewSession = false
|
|
1174
1489
|
self.shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
1175
1490
|
self.shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
1176
1491
|
self.restorePreviewPreviousAppId()
|
|
1492
|
+
self.restorePreviewPreviousDefaultChannel()
|
|
1177
1493
|
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
1178
1494
|
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
1179
|
-
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
1180
|
-
_ = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
|
|
1181
1495
|
self.clearPreviewSessionPreferences()
|
|
1182
1496
|
}
|
|
1183
1497
|
|
|
@@ -1198,6 +1512,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1198
1512
|
return
|
|
1199
1513
|
}
|
|
1200
1514
|
self.previewSessionAlertPending = false
|
|
1515
|
+
UserDefaults.standard.set(false, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
1516
|
+
UserDefaults.standard.synchronize()
|
|
1201
1517
|
|
|
1202
1518
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(600)) {
|
|
1203
1519
|
guard self.previewSessionEnabled else {
|
|
@@ -1206,6 +1522,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1206
1522
|
if let topVC = UIApplication.topViewController(),
|
|
1207
1523
|
topVC.isKind(of: UIAlertController.self) {
|
|
1208
1524
|
self.previewSessionAlertPending = true
|
|
1525
|
+
UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
1526
|
+
UserDefaults.standard.synchronize()
|
|
1209
1527
|
return
|
|
1210
1528
|
}
|
|
1211
1529
|
|
|
@@ -1217,6 +1535,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1217
1535
|
alert.addAction(UIAlertAction(title: "Got it", style: .default))
|
|
1218
1536
|
if let topVC = UIApplication.topViewController() {
|
|
1219
1537
|
topVC.present(alert, animated: true)
|
|
1538
|
+
} else {
|
|
1539
|
+
self.previewSessionAlertPending = true
|
|
1540
|
+
UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
1541
|
+
UserDefaults.standard.synchronize()
|
|
1220
1542
|
}
|
|
1221
1543
|
}
|
|
1222
1544
|
}
|
|
@@ -29,7 +29,7 @@ extension UIWindow {
|
|
|
29
29
|
// Find the CapacitorUpdaterPlugin instance
|
|
30
30
|
guard let bridgeViewController = rootViewController as? CAPBridgeViewController,
|
|
31
31
|
let bridge = bridgeViewController.bridge,
|
|
32
|
-
let plugin = bridge.plugin(withName: "
|
|
32
|
+
let plugin = bridge.plugin(withName: "CapacitorUpdater") as? CapacitorUpdaterPlugin else {
|
|
33
33
|
return
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -81,9 +81,11 @@ extension UIWindow {
|
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
alertShake.addAction(UIAlertAction(title: reloadButtonTitle, style: .default) { _ in
|
|
84
|
-
DispatchQueue.
|
|
84
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
85
85
|
if !plugin.reloadPreviewSessionFromShakeMenu() {
|
|
86
|
-
|
|
86
|
+
DispatchQueue.main.async {
|
|
87
|
+
self.showError(message: "Could not reload the test app.", plugin: plugin)
|
|
88
|
+
}
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
})
|