@capgo/capacitor-updater 8.45.10 → 8.46.0

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.
@@ -134,6 +134,35 @@ struct InfoObject: Codable {
134
134
  var defaultChannel: String?
135
135
  var key_id: String?
136
136
  }
137
+
138
+ extension InfoObject {
139
+ func toParameters() -> [String: Any] {
140
+ var parameters: [String: Any] = [:]
141
+ func set(_ key: String, _ value: Any?) {
142
+ guard let value = value else {
143
+ return
144
+ }
145
+ parameters[key] = value
146
+ }
147
+ set("platform", platform)
148
+ set("device_id", device_id)
149
+ set("app_id", app_id)
150
+ set("custom_id", custom_id)
151
+ set("version_build", version_build)
152
+ set("version_code", version_code)
153
+ set("version_os", version_os)
154
+ set("version_name", version_name)
155
+ set("old_version_name", old_version_name)
156
+ set("plugin_version", plugin_version)
157
+ set("is_emulator", is_emulator)
158
+ set("is_prod", is_prod)
159
+ set("action", action)
160
+ set("channel", channel)
161
+ set("defaultChannel", defaultChannel)
162
+ set("key_id", key_id)
163
+ return parameters
164
+ }
165
+ }
137
166
  // swiftlint:enable identifier_name
138
167
 
139
168
  // swiftlint:disable identifier_name
@@ -154,6 +183,7 @@ struct StatsEvent: Codable {
154
183
  let channel: String?
155
184
  let defaultChannel: String?
156
185
  let key_id: String?
186
+ let metadata: [String: String]?
157
187
  let timestamp: Int64
158
188
  }
159
189
  // swiftlint:enable identifier_name
@@ -186,6 +216,7 @@ struct AppVersionDec: Decodable {
186
216
  let url: String?
187
217
  let message: String?
188
218
  let error: String?
219
+ let kind: String?
189
220
  let session_key: String?
190
221
  let major: Bool?
191
222
  let breaking: Bool?
@@ -203,6 +234,7 @@ public class AppVersion: NSObject {
203
234
  var url: String = ""
204
235
  var message: String?
205
236
  var error: String?
237
+ var kind: String?
206
238
  var sessionKey: String?
207
239
  var major: Bool?
208
240
  var breaking: Bool?
@@ -308,19 +308,36 @@ extension UIWindow {
308
308
  }
309
309
 
310
310
  let latest = updater.getLatest(url: updateUrl, channel: name)
311
+ let latestKind = latest.kind
312
+
313
+ let detail = [latest.message, latest.error, latestKind]
314
+ .compactMap { value in
315
+ guard let value, !value.isEmpty else { return nil }
316
+ return value
317
+ }
318
+ .first ?? "server did not provide a message"
311
319
 
312
320
  // Handle update errors first (before "no new version" check)
313
- if let error = latest.error, !error.isEmpty && error != "no_new_version_available" {
321
+ if latestKind == "failed" || (latest.error?.isEmpty == false && latestKind != "up_to_date" && latestKind != "blocked") {
322
+ DispatchQueue.main.async {
323
+ progressAlert.dismiss(animated: true) {
324
+ self.showError(message: "Channel set to \(name). Update check failed: \(detail)", plugin: plugin)
325
+ }
326
+ }
327
+ return
328
+ }
329
+
330
+ if latestKind == "blocked" {
314
331
  DispatchQueue.main.async {
315
332
  progressAlert.dismiss(animated: true) {
316
- self.showError(message: "Channel set to \(name). Update check failed: \(error)", plugin: plugin)
333
+ self.showError(message: "Channel set to \(name). Update check blocked: \(detail)", plugin: plugin)
317
334
  }
318
335
  }
319
336
  return
320
337
  }
321
338
 
322
339
  // Check if there's an actual update available
323
- if latest.error == "no_new_version_available" || latest.url.isEmpty {
340
+ if latestKind == "up_to_date" || latest.url.isEmpty {
324
341
  DispatchQueue.main.async {
325
342
  progressAlert.dismiss(animated: true) {
326
343
  self.showSuccess(message: "Channel set to \(name). Already on latest version.", plugin: plugin)
@@ -0,0 +1,276 @@
1
+ import Capacitor
2
+ import Foundation
3
+ import WebKit
4
+
5
+ final class WebViewStatsReporter {
6
+ static let script = """
7
+ (function(){
8
+ if(window.__capgoWebViewErrorReporterInstalled){return;}
9
+ window.__capgoWebViewErrorReporterInstalled=true;
10
+ var maxReports=20,sentReports=0,queue=[],seen={};
11
+ var sessionKey='CapacitorUpdater.webViewSession';
12
+ var sessionId=String(Date.now())+'-'+Math.random().toString(36).slice(2);
13
+ function s(value){
14
+ try{
15
+ if(value===undefined){return '';}
16
+ if(value===null){return 'null';}
17
+ if(typeof value==='string'){return value;}
18
+ if(value&&typeof value.message==='string'){return value.message;}
19
+ return String(value);
20
+ }catch(_){return '';}
21
+ }
22
+ function stack(value){
23
+ try{return value&&value.stack?String(value.stack):'';}catch(_){return '';}
24
+ }
25
+ function updater(){
26
+ var cap=window.Capacitor;
27
+ if(!cap||!cap.Plugins){return null;}
28
+ return cap.Plugins.CapacitorUpdater||null;
29
+ }
30
+ function flush(){
31
+ var plugin=updater();
32
+ if(!plugin||typeof plugin.reportWebViewError!=='function'){return false;}
33
+ while(queue.length){
34
+ var payload=queue.shift();
35
+ try{
36
+ var result=plugin.reportWebViewError(payload);
37
+ if(result&&typeof result.catch==='function'){result.catch(function(){});}
38
+ }catch(_){}
39
+ }
40
+ return true;
41
+ }
42
+ var retries=0;
43
+ function scheduleFlush(){
44
+ if(flush()){return;}
45
+ if(retries++<40){setTimeout(scheduleFlush,250);}
46
+ }
47
+ function send(payload){
48
+ try{
49
+ if(sentReports>=maxReports){return;}
50
+ payload.href=payload.href||location.href||'';
51
+ payload.user_agent=navigator.userAgent||'';
52
+ payload.session_id=sessionId;
53
+ var key=[payload.type,payload.message,payload.source,payload.line,payload.column,payload.tag_name].join('|');
54
+ if(seen[key]){return;}
55
+ seen[key]=true;
56
+ sentReports+=1;
57
+ queue.push(payload);
58
+ scheduleFlush();
59
+ }catch(_){}
60
+ }
61
+ function readSession(){
62
+ try{return JSON.parse(localStorage.getItem(sessionKey)||'null')||null;}catch(_){return null;}
63
+ }
64
+ function writeSession(active){
65
+ try{
66
+ localStorage.setItem(sessionKey,JSON.stringify({
67
+ id:sessionId,
68
+ active:active,
69
+ href:location.href||'',
70
+ started_at:window.__capgoWebViewSessionStartedAt,
71
+ updated_at:String(Date.now())
72
+ }));
73
+ }catch(_){}
74
+ }
75
+ window.__capgoWebViewSessionStartedAt=String(Date.now());
76
+ var previous=readSession();
77
+ if(previous&&previous.active){
78
+ send({
79
+ type:'webview_unclean_restart',
80
+ message:'WebView restarted without a clean page unload',
81
+ previous_session_id:s(previous.id),
82
+ previous_href:s(previous.href),
83
+ previous_started_at:s(previous.started_at),
84
+ previous_updated_at:s(previous.updated_at)
85
+ });
86
+ }
87
+ writeSession(true);
88
+ setInterval(function(){writeSession(true);},15000);
89
+ function markClean(){writeSession(false);}
90
+ window.addEventListener('pagehide',markClean,true);
91
+ window.addEventListener('beforeunload',markClean,true);
92
+ window.addEventListener('error',function(event){
93
+ var target=event&&event.target;
94
+ if(target&&target!==window&&(target.src||target.href)){
95
+ send({
96
+ type:'resource_error',
97
+ message:'Resource failed to load',
98
+ source:s(target.src||target.href),
99
+ tag_name:s(target.tagName)
100
+ });
101
+ return;
102
+ }
103
+ send({
104
+ type:'javascript_error',
105
+ message:s((event&&event.message)||(event&&event.error)),
106
+ source:s(event&&event.filename),
107
+ line:s(event&&event.lineno),
108
+ column:s(event&&event.colno),
109
+ stack:stack(event&&event.error)
110
+ });
111
+ },true);
112
+ window.addEventListener('unhandledrejection',function(event){
113
+ var reason=event&&event.reason;
114
+ send({type:'unhandled_rejection',message:s(reason),stack:stack(reason)});
115
+ },true);
116
+ document.addEventListener('securitypolicyviolation',function(event){
117
+ send({
118
+ type:'security_policy_violation',
119
+ message:s(event&&event.violatedDirective),
120
+ source:s(event&&event.blockedURI)
121
+ });
122
+ },true);
123
+ document.addEventListener('deviceready',scheduleFlush,false);
124
+ setTimeout(scheduleFlush,0);
125
+ })();
126
+ """
127
+
128
+ private let implementation: CapgoUpdater
129
+ private var installed = false
130
+
131
+ init(implementation: CapgoUpdater) {
132
+ self.implementation = implementation
133
+ }
134
+
135
+ func install(on webView: WKWebView?) {
136
+ guard !installed else {
137
+ return
138
+ }
139
+ guard let webView else {
140
+ return
141
+ }
142
+
143
+ installed = true
144
+ let userScript = WKUserScript(source: Self.script, injectionTime: .atDocumentStart, forMainFrameOnly: true)
145
+ webView.configuration.userContentController.addUserScript(userScript)
146
+ webView.evaluateJavaScript(Self.script, completionHandler: nil)
147
+ }
148
+
149
+ func reportError(_ call: CAPPluginCall) {
150
+ let errorType = call.getString("type") ?? "javascript_error"
151
+ let current = implementation.getCurrentBundle()
152
+ implementation.sendStats(
153
+ action: Self.statsAction(for: errorType),
154
+ versionName: current.getVersionName(),
155
+ oldVersionName: "",
156
+ metadata: Self.buildMetadata([
157
+ "type": errorType,
158
+ "message": call.getString("message"),
159
+ "source": call.getString("source"),
160
+ "line": call.getString("line") ?? call.getString("lineno"),
161
+ "column": call.getString("column") ?? call.getString("colno"),
162
+ "stack": call.getString("stack"),
163
+ "tag_name": call.getString("tag_name"),
164
+ "href": call.getString("href"),
165
+ "user_agent": call.getString("user_agent"),
166
+ "session_id": call.getString("session_id"),
167
+ "previous_session_id": call.getString("previous_session_id"),
168
+ "previous_href": call.getString("previous_href"),
169
+ "previous_started_at": call.getString("previous_started_at"),
170
+ "previous_updated_at": call.getString("previous_updated_at")
171
+ ])
172
+ )
173
+ call.resolve()
174
+ }
175
+
176
+ static func statsAction(for type: String) -> String {
177
+ switch type {
178
+ case "unhandled_rejection":
179
+ return "webview_unhandled_rejection"
180
+ case "resource_error":
181
+ return "webview_resource_error"
182
+ case "security_policy_violation":
183
+ return "webview_security_policy_violation"
184
+ case "webview_unclean_restart":
185
+ return "webview_unclean_restart"
186
+ case "render_process_gone":
187
+ return "webview_render_process_gone"
188
+ case "web_content_process_terminated":
189
+ return "webview_content_process_terminated"
190
+ case "javascript_error":
191
+ return "webview_javascript_error"
192
+ default:
193
+ return "webview_javascript_error"
194
+ }
195
+ }
196
+
197
+ static func buildMetadata(_ values: [String: String?]) -> [String: String] {
198
+ var metadata: [String: String] = [:]
199
+ put(&metadata, key: "error_type", value: payloadValue(values, "type") ?? "javascript_error", maxLength: 64)
200
+ put(&metadata, key: "message", value: payloadValue(values, "message"), maxLength: 1_024)
201
+ put(&metadata, key: "source", value: sanitizeUrl(payloadValue(values, "source")), maxLength: 512)
202
+ put(&metadata, key: "line", value: payloadValue(values, "line"), maxLength: 32)
203
+ put(&metadata, key: "column", value: payloadValue(values, "column"), maxLength: 32)
204
+ put(&metadata, key: "stack", value: payloadValue(values, "stack"), maxLength: 2_048)
205
+ put(&metadata, key: "tag_name", value: payloadValue(values, "tag_name"), maxLength: 64)
206
+ put(&metadata, key: "href", value: sanitizeUrl(payloadValue(values, "href")), maxLength: 512)
207
+ put(&metadata, key: "user_agent", value: payloadValue(values, "user_agent"), maxLength: 256)
208
+ put(&metadata, key: "session_id", value: payloadValue(values, "session_id"), maxLength: 128)
209
+ put(&metadata, key: "previous_session_id", value: payloadValue(values, "previous_session_id"), maxLength: 128)
210
+ put(&metadata, key: "previous_href", value: sanitizeUrl(payloadValue(values, "previous_href")), maxLength: 512)
211
+ put(&metadata, key: "previous_started_at", value: payloadValue(values, "previous_started_at"), maxLength: 64)
212
+ put(&metadata, key: "previous_updated_at", value: payloadValue(values, "previous_updated_at"), maxLength: 64)
213
+ return metadata
214
+ }
215
+
216
+ private static func payloadValue(_ values: [String: String?], _ key: String) -> String? {
217
+ guard let value = values[key] else {
218
+ return nil
219
+ }
220
+ return value
221
+ }
222
+
223
+ static func sanitizeUrl(_ value: String?) -> String? {
224
+ guard let value = value, !value.isEmpty else {
225
+ return value
226
+ }
227
+
228
+ if var components = URLComponents(string: value), components.scheme != nil, components.host != nil {
229
+ components.user = nil
230
+ components.password = nil
231
+ components.query = nil
232
+ components.fragment = nil
233
+ components.path = sanitizeUrlPath(components.path)
234
+ return components.string ?? stripUrlQueryAndFragment(value)
235
+ }
236
+
237
+ return stripUrlQueryAndFragment(value)
238
+ }
239
+
240
+ private static func sanitizeUrlPath(_ path: String) -> String {
241
+ guard !path.isEmpty else {
242
+ return path
243
+ }
244
+
245
+ return path
246
+ .split(separator: "/", omittingEmptySubsequences: false)
247
+ .map { isSensitiveUrlPathSegment(String($0)) ? "redacted" : String($0) }
248
+ .joined(separator: "/")
249
+ }
250
+
251
+ private static func isSensitiveUrlPathSegment(_ segment: String) -> Bool {
252
+ segment.range(of: #"^[0-9]{6,}$"#, options: .regularExpression) != nil ||
253
+ segment.range(of: #"^[0-9a-fA-F]{16,}$"#, options: .regularExpression) != nil ||
254
+ segment.range(
255
+ of: #"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"#,
256
+ options: .regularExpression
257
+ ) != nil
258
+ }
259
+
260
+ private static func stripUrlQueryAndFragment(_ value: String) -> String {
261
+ let queryIndex = value.firstIndex(of: "?")
262
+ let fragmentIndex = value.firstIndex(of: "#")
263
+ let endIndexes = [queryIndex, fragmentIndex].compactMap { $0 }
264
+ guard let endIndex = endIndexes.min() else {
265
+ return value
266
+ }
267
+ return String(value[..<endIndex])
268
+ }
269
+
270
+ private static func put(_ metadata: inout [String: String], key: String, value: String?, maxLength: Int) {
271
+ guard let value = value, !value.isEmpty else {
272
+ return
273
+ }
274
+ metadata[key] = String(value.prefix(maxLength))
275
+ }
276
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.45.10",
3
+ "version": "8.46.0",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",
@@ -45,12 +45,22 @@
45
45
  "verify:ios": "xcodebuild -scheme CapgoCapacitorUpdater -destination generic/platform=iOS",
46
46
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
47
47
  "verify:web": "bun run build",
48
+ "native:compile": "./scripts/native-platform.sh compile all",
49
+ "native:compile:ios": "./scripts/native-platform.sh compile ios",
50
+ "native:compile:android": "./scripts/native-platform.sh compile android",
51
+ "native:test": "./scripts/native-platform.sh test all",
52
+ "native:test:ios": "./scripts/native-platform.sh test ios",
53
+ "native:test:android": "./scripts/native-platform.sh test android",
54
+ "native:contract": "bun run native:contract:ios && bun run native:contract:android",
55
+ "native:contract:ios": "./scripts/test-ios.sh -only-testing:CapacitorUpdaterPluginTests/NativeContractTests",
56
+ "native:contract:android": "cd android && ./gradlew testDebugUnitTest --tests ee.forgr.capacitor_updater.NativeContractTest && cd ..",
48
57
  "test": "bun run test:ios && bun run test:android",
49
58
  "test:ios": "./scripts/test-ios.sh",
50
59
  "test:android": "cd android && ./gradlew test && cd ..",
51
60
  "test:maestro": "./scripts/maestro/run-android-live-update.sh",
52
61
  "test:maestro:android": "./scripts/test-maestro-android.sh",
53
- "test:maestro:ios": "./scripts/test-maestro-ios.sh",
62
+ "test:maestro:ios": "./scripts/maestro/run-ios-live-update.sh",
63
+ "test:maestro:ios:smoke": "./scripts/test-maestro-ios.sh",
54
64
  "lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint",
55
65
  "fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format",
56
66
  "eslint": "eslint . --ext .ts",