@capgo/capacitor-updater 6.14.29 → 6.14.36
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 +136 -126
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +51 -8
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +44 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
- package/dist/docs.json +4 -4
- package/dist/esm/definitions.d.ts +14 -4
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +34 -6
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +43 -0
- package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
- package/package.json +2 -3
|
@@ -54,7 +54,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
54
54
|
CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise)
|
|
55
55
|
]
|
|
56
56
|
public var implementation = CapgoUpdater()
|
|
57
|
-
private let PLUGIN_VERSION: String = "6.14.
|
|
57
|
+
private let PLUGIN_VERSION: String = "6.14.31"
|
|
58
58
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
59
59
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
60
60
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -76,6 +76,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
76
76
|
private var directUpdate = false
|
|
77
77
|
private var directUpdateMode: String = "false"
|
|
78
78
|
private var wasRecentlyInstalledOrUpdated = false
|
|
79
|
+
private var onLaunchDirectUpdateUsed = false
|
|
79
80
|
private var autoSplashscreen = false
|
|
80
81
|
private var autoSplashscreenLoader = false
|
|
81
82
|
private var autoSplashscreenTimeout = 10000
|
|
@@ -113,9 +114,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
113
114
|
#endif
|
|
114
115
|
|
|
115
116
|
self.semaphoreUp()
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
UserDefaults.standard.synchronize()
|
|
117
|
+
// Use DeviceIdHelper to get or create device ID that persists across reinstalls
|
|
118
|
+
self.implementation.deviceID = DeviceIdHelper.getOrCreateDeviceId()
|
|
119
119
|
persistCustomId = getConfig().getBoolean("persistCustomId", false)
|
|
120
120
|
if persistCustomId {
|
|
121
121
|
let storedCustomId = UserDefaults.standard.string(forKey: customIdDefaultsKey) ?? ""
|
|
@@ -148,7 +148,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
148
148
|
// Handle directUpdate configuration - support string values and backward compatibility
|
|
149
149
|
if let directUpdateString = getConfig().getString("directUpdate") {
|
|
150
150
|
directUpdateMode = directUpdateString
|
|
151
|
-
directUpdate = directUpdateString == "always" || directUpdateString == "atInstall"
|
|
151
|
+
directUpdate = directUpdateString == "always" || directUpdateString == "atInstall" || directUpdateString == "onLaunch"
|
|
152
|
+
// Validate directUpdate value
|
|
153
|
+
if directUpdateString != "false" && directUpdateString != "always" && directUpdateString != "atInstall" && directUpdateString != "onLaunch" {
|
|
154
|
+
logger.error("Invalid directUpdate value: \"\(directUpdateString)\". Supported values are: false, \"always\", \"atInstall\", \"onLaunch\". Defaulting to false.")
|
|
155
|
+
directUpdateMode = "false"
|
|
156
|
+
directUpdate = false
|
|
157
|
+
}
|
|
152
158
|
} else {
|
|
153
159
|
let directUpdateBool = getConfig().getBoolean("directUpdate", false)
|
|
154
160
|
if directUpdateBool {
|
|
@@ -1196,7 +1202,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1196
1202
|
return true
|
|
1197
1203
|
}
|
|
1198
1204
|
return false
|
|
1205
|
+
case "onLaunch":
|
|
1206
|
+
if !self.onLaunchDirectUpdateUsed {
|
|
1207
|
+
return true
|
|
1208
|
+
}
|
|
1209
|
+
return false
|
|
1199
1210
|
default:
|
|
1211
|
+
logger.error("Invalid directUpdateMode: \"\(self.directUpdateMode)\". Supported values are: \"false\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to false behavior.")
|
|
1200
1212
|
return false
|
|
1201
1213
|
}
|
|
1202
1214
|
}
|
|
@@ -1287,6 +1299,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1287
1299
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
1288
1300
|
if directUpdateAllowed {
|
|
1289
1301
|
self.logger.info("Direct update to builtin version")
|
|
1302
|
+
if self.directUpdateMode == "onLaunch" {
|
|
1303
|
+
self.onLaunchDirectUpdateUsed = true
|
|
1304
|
+
self.directUpdate = false
|
|
1305
|
+
}
|
|
1290
1306
|
_ = self._reset(toLastSuccessful: false)
|
|
1291
1307
|
self.endBackGroundTaskWithNotif(msg: "Updated to builtin version", latestVersionName: res.version, current: self.implementation.getCurrentBundle(), error: false)
|
|
1292
1308
|
} else {
|
|
@@ -1361,6 +1377,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1361
1377
|
self.endBackGroundTaskWithNotif(msg: "Update delayed until delay conditions met", latestVersionName: latestVersionName, current: next, error: false)
|
|
1362
1378
|
return
|
|
1363
1379
|
}
|
|
1380
|
+
if self.directUpdateMode == "onLaunch" {
|
|
1381
|
+
self.onLaunchDirectUpdateUsed = true
|
|
1382
|
+
self.directUpdate = false
|
|
1383
|
+
}
|
|
1364
1384
|
_ = self.implementation.set(bundle: next)
|
|
1365
1385
|
_ = self._reload()
|
|
1366
1386
|
self.endBackGroundTaskWithNotif(msg: "update installed", latestVersionName: latestVersionName, current: next, error: false)
|
|
@@ -1442,6 +1462,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1442
1462
|
if self._isAutoUpdateEnabled() {
|
|
1443
1463
|
self.backgroundDownload()
|
|
1444
1464
|
} else {
|
|
1465
|
+
let instanceDescriptor = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor()
|
|
1466
|
+
if instanceDescriptor?.serverURL != nil {
|
|
1467
|
+
self.implementation.sendStats(action: "blocked_by_server_url", versionName: current.getVersionName())
|
|
1468
|
+
}
|
|
1445
1469
|
logger.info("Auto update is disabled")
|
|
1446
1470
|
self.sendReadyToJs(current: current, msg: "disabled")
|
|
1447
1471
|
}
|
|
@@ -1495,7 +1519,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1495
1519
|
}
|
|
1496
1520
|
|
|
1497
1521
|
if !self.shouldUseDirectUpdate() {
|
|
1498
|
-
|
|
1522
|
+
if self.directUpdateMode == "false" {
|
|
1523
|
+
logger.warn("autoSplashscreen is enabled but directUpdate is not configured for immediate updates. Set directUpdate to 'always' or disable autoSplashscreen.")
|
|
1524
|
+
} else if self.directUpdateMode == "atInstall" || self.directUpdateMode == "onLaunch" {
|
|
1525
|
+
logger.info("autoSplashscreen is enabled but directUpdate is set to \"\(self.directUpdateMode)\". This is normal. Skipping autoSplashscreen logic.")
|
|
1526
|
+
}
|
|
1499
1527
|
canShowSplashscreen = false
|
|
1500
1528
|
}
|
|
1501
1529
|
|
|
@@ -47,6 +47,9 @@ import UIKit
|
|
|
47
47
|
// Flag to track if we received a 429 response - stops requests until app restart
|
|
48
48
|
private static var rateLimitExceeded = false
|
|
49
49
|
|
|
50
|
+
// Flag to track if we've already sent the rate limit statistic - prevents infinite loop
|
|
51
|
+
private static var rateLimitStatisticSent = false
|
|
52
|
+
|
|
50
53
|
private var userAgent: String {
|
|
51
54
|
let safePluginVersion = PLUGIN_VERSION.isEmpty ? "unknown" : PLUGIN_VERSION
|
|
52
55
|
let safeAppId = appId.isEmpty ? "unknown" : appId
|
|
@@ -95,6 +98,12 @@ import UIKit
|
|
|
95
98
|
*/
|
|
96
99
|
private func checkAndHandleRateLimitResponse(statusCode: Int?) -> Bool {
|
|
97
100
|
if statusCode == 429 {
|
|
101
|
+
// Send a statistic about the rate limit BEFORE setting the flag
|
|
102
|
+
// Only send once to prevent infinite loop if the stat request itself gets rate limited
|
|
103
|
+
if !CapgoUpdater.rateLimitExceeded && !CapgoUpdater.rateLimitStatisticSent {
|
|
104
|
+
CapgoUpdater.rateLimitStatisticSent = true
|
|
105
|
+
self.sendRateLimitStatistic()
|
|
106
|
+
}
|
|
98
107
|
CapgoUpdater.rateLimitExceeded = true
|
|
99
108
|
logger.warn("Rate limit exceeded (429). Stopping all stats and channel requests until app restart.")
|
|
100
109
|
return true
|
|
@@ -102,6 +111,40 @@ import UIKit
|
|
|
102
111
|
return false
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Send a synchronous statistic about rate limiting
|
|
116
|
+
*/
|
|
117
|
+
private func sendRateLimitStatistic() {
|
|
118
|
+
guard !statsUrl.isEmpty else {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let current = getCurrentBundle()
|
|
123
|
+
var parameters = createInfoObject()
|
|
124
|
+
parameters.action = "rate_limit_reached"
|
|
125
|
+
parameters.version_name = current.getVersionName()
|
|
126
|
+
parameters.old_version_name = ""
|
|
127
|
+
|
|
128
|
+
// Send synchronously to ensure it goes out before the flag is set
|
|
129
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
130
|
+
self.alamofireSession.request(
|
|
131
|
+
self.statsUrl,
|
|
132
|
+
method: .post,
|
|
133
|
+
parameters: parameters,
|
|
134
|
+
encoder: JSONParameterEncoder.default,
|
|
135
|
+
requestModifier: { $0.timeoutInterval = self.timeout }
|
|
136
|
+
).responseData { response in
|
|
137
|
+
switch response.result {
|
|
138
|
+
case .success:
|
|
139
|
+
self.logger.info("Rate limit statistic sent")
|
|
140
|
+
case let .failure(error):
|
|
141
|
+
self.logger.error("Error sending rate limit statistic: \(error.localizedDescription)")
|
|
142
|
+
}
|
|
143
|
+
semaphore.signal()
|
|
144
|
+
}
|
|
145
|
+
semaphore.wait()
|
|
146
|
+
}
|
|
147
|
+
|
|
105
148
|
// MARK: Private
|
|
106
149
|
private func hasEmbeddedMobileProvision() -> Bool {
|
|
107
150
|
guard Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") == nil else {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Foundation
|
|
8
|
+
import Security
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper class to manage device ID persistence across app installations.
|
|
12
|
+
* Uses iOS Keychain to persist the device ID.
|
|
13
|
+
*/
|
|
14
|
+
class DeviceIdHelper {
|
|
15
|
+
private static let keychainService = "app.capgo.updater"
|
|
16
|
+
private static let keychainAccount = "deviceId"
|
|
17
|
+
private static let legacyDefaultsKey = "appUUID"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets or creates a device ID that persists across reinstalls.
|
|
21
|
+
*
|
|
22
|
+
* This method:
|
|
23
|
+
* 1. First checks for an existing ID in Keychain (persists across reinstalls)
|
|
24
|
+
* 2. Falls back to UserDefaults (for migration from older versions)
|
|
25
|
+
* 3. Generates a new UUID if neither exists
|
|
26
|
+
* 4. Stores the ID in Keychain for future use
|
|
27
|
+
*
|
|
28
|
+
* @return Device ID as a lowercase UUID string
|
|
29
|
+
*/
|
|
30
|
+
static func getOrCreateDeviceId() -> String {
|
|
31
|
+
// Try to get device ID from Keychain first
|
|
32
|
+
if let keychainDeviceId = getDeviceIdFromKeychain() {
|
|
33
|
+
return keychainDeviceId.lowercased()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Migration: Check UserDefaults for existing device ID
|
|
37
|
+
var deviceId = UserDefaults.standard.string(forKey: legacyDefaultsKey)
|
|
38
|
+
|
|
39
|
+
if deviceId == nil || deviceId!.isEmpty {
|
|
40
|
+
// Generate new device ID if none exists
|
|
41
|
+
deviceId = UUID().uuidString
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Ensure lowercase for consistency
|
|
45
|
+
deviceId = deviceId!.lowercased()
|
|
46
|
+
|
|
47
|
+
// Save to Keychain for persistence across reinstalls
|
|
48
|
+
saveDeviceIdToKeychain(deviceId: deviceId!)
|
|
49
|
+
|
|
50
|
+
return deviceId!
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Retrieves the device ID from iOS Keychain.
|
|
55
|
+
*
|
|
56
|
+
* @return Device ID string or nil if not found
|
|
57
|
+
*/
|
|
58
|
+
private static func getDeviceIdFromKeychain() -> String? {
|
|
59
|
+
let query: [String: Any] = [
|
|
60
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
61
|
+
kSecAttrService as String: keychainService,
|
|
62
|
+
kSecAttrAccount as String: keychainAccount,
|
|
63
|
+
kSecReturnData as String: true,
|
|
64
|
+
kSecMatchLimit as String: kSecMatchLimitOne
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
var result: AnyObject?
|
|
68
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
69
|
+
|
|
70
|
+
guard status == errSecSuccess,
|
|
71
|
+
let data = result as? Data,
|
|
72
|
+
let deviceId = String(data: data, encoding: .utf8) else {
|
|
73
|
+
return nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return deviceId
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Saves the device ID to iOS Keychain with appropriate accessibility settings.
|
|
81
|
+
*
|
|
82
|
+
* Uses kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
|
|
83
|
+
* - Data persists across reinstalls
|
|
84
|
+
* - Data is NOT synced to iCloud
|
|
85
|
+
* - Data is accessible after first device unlock
|
|
86
|
+
* - Data stays on this device only (privacy-friendly)
|
|
87
|
+
*
|
|
88
|
+
* @param deviceId The device ID to save
|
|
89
|
+
*/
|
|
90
|
+
private static func saveDeviceIdToKeychain(deviceId: String) {
|
|
91
|
+
guard let data = deviceId.data(using: .utf8) else {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Delete any existing entry first
|
|
96
|
+
let deleteQuery: [String: Any] = [
|
|
97
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
98
|
+
kSecAttrService as String: keychainService,
|
|
99
|
+
kSecAttrAccount as String: keychainAccount
|
|
100
|
+
]
|
|
101
|
+
SecItemDelete(deleteQuery as CFDictionary)
|
|
102
|
+
|
|
103
|
+
// Add new entry with appropriate accessibility
|
|
104
|
+
let addQuery: [String: Any] = [
|
|
105
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
106
|
+
kSecAttrService as String: keychainService,
|
|
107
|
+
kSecAttrAccount as String: keychainAccount,
|
|
108
|
+
kSecValueData as String: data,
|
|
109
|
+
// This ensures data persists across reinstalls but stays on device (not synced)
|
|
110
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
let status = SecItemAdd(addQuery as CFDictionary, nil)
|
|
114
|
+
|
|
115
|
+
if status != errSecSuccess {
|
|
116
|
+
// Log error but don't crash - we'll fall back to UserDefaults on next launch
|
|
117
|
+
print("Failed to save device ID to Keychain: \(status)")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-updater",
|
|
3
|
-
"version": "6.14.
|
|
3
|
+
"version": "6.14.36",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Live update for capacitor apps",
|
|
6
6
|
"main": "dist/plugin.cjs.js",
|
|
@@ -52,8 +52,7 @@
|
|
|
52
52
|
"eslint": "eslint . --ext .ts",
|
|
53
53
|
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
54
54
|
"swiftlint": "node-swiftlint",
|
|
55
|
-
"docgen": "
|
|
56
|
-
"docgen:api": "node scripts/generate-docs.js",
|
|
55
|
+
"docgen": "node scripts/generate-docs.js",
|
|
57
56
|
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
|
|
58
57
|
"clean": "rimraf ./dist",
|
|
59
58
|
"watch": "tsc --watch",
|