@capgo/capacitor-updater 8.45.11 → 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.
@@ -47,8 +47,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
47
47
  CAPPluginMethod(name: "setMultiDelay", returnType: CAPPluginReturnPromise),
48
48
  CAPPluginMethod(name: "cancelDelay", returnType: CAPPluginReturnPromise),
49
49
  CAPPluginMethod(name: "getLatest", returnType: CAPPluginReturnPromise),
50
+ CAPPluginMethod(name: "triggerUpdateCheck", returnType: CAPPluginReturnPromise),
50
51
  CAPPluginMethod(name: "setChannel", returnType: CAPPluginReturnPromise),
51
52
  CAPPluginMethod(name: "unsetChannel", returnType: CAPPluginReturnPromise),
53
+ CAPPluginMethod(name: "reportWebViewError", returnType: CAPPluginReturnPromise),
52
54
  CAPPluginMethod(name: "getChannel", returnType: CAPPluginReturnPromise),
53
55
  CAPPluginMethod(name: "listChannels", returnType: CAPPluginReturnPromise),
54
56
  CAPPluginMethod(name: "setCustomId", returnType: CAPPluginReturnPromise),
@@ -74,7 +76,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
74
76
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
75
77
  ]
76
78
  public var implementation = CapgoUpdater()
77
- private let pluginVersion: String = "8.45.11"
79
+ private let pluginVersion: String = "8.46.0"
78
80
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
79
81
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
80
82
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -130,6 +132,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
130
132
  private var persistModifyUrl = false
131
133
  private var allowManualBundleError = false
132
134
  private var keepUrlPathFlagLastValue: Bool?
135
+ private var appHealthTracker: AppHealthTracker?
136
+ private var webViewStatsReporter: WebViewStatsReporter?
133
137
  public var shakeMenuEnabled = false
134
138
  public var shakeChannelSelectorEnabled = false
135
139
  let semaphoreReady = DispatchSemaphore(value: 0)
@@ -145,6 +149,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
145
149
  } else {
146
150
  logger.error("Failed to get webView for logging")
147
151
  }
152
+ let webViewStatsReporter = WebViewStatsReporter(implementation: implementation)
153
+ self.webViewStatsReporter = webViewStatsReporter
154
+ webViewStatsReporter.install(on: self.bridge?.webView)
148
155
  #if targetEnvironment(simulator)
149
156
  logger.info("::::: SIMULATOR :::::")
150
157
  logger.info("Application directory: \(NSHomeDirectory())")
@@ -225,12 +232,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
225
232
  resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
226
233
  shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
227
234
  shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
228
- let periodCheckDelayValue = getConfig().getInt("periodCheckDelay", 0)
229
- if periodCheckDelayValue >= 0 && periodCheckDelayValue > 600 {
230
- periodCheckDelay = 600
231
- } else {
232
- periodCheckDelay = periodCheckDelayValue
233
- }
235
+ periodCheckDelay = Self.normalizedPeriodCheckDelaySeconds(getConfig().getInt("periodCheckDelay", 0))
234
236
 
235
237
  implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
236
238
  implementation.notifyDownloadRaw = notifyDownload
@@ -289,6 +291,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
289
291
  implementation.defaultChannel = getConfig().getString("defaultChannel", "")!
290
292
  }
291
293
  self.implementation.autoReset()
294
+ let appHealthTracker = AppHealthTracker(implementation: self.implementation)
295
+ self.appHealthTracker = appHealthTracker
296
+ appHealthTracker.reportPreviousUncleanForegroundExit()
297
+ appHealthTracker.startSession()
292
298
 
293
299
  // Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
294
300
  self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
@@ -312,6 +318,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
312
318
  let nc = NotificationCenter.default
313
319
  nc.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
314
320
  nc.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
321
+ nc.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil)
322
+ nc.addObserver(self, selector: #selector(appDidReceiveMemoryWarning), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
315
323
 
316
324
  // Check for 'kill' delay condition on app launch
317
325
  // This handles cases where the app was killed (willTerminateNotification is not reliable for system kills)
@@ -368,6 +376,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
368
376
  }
369
377
  }
370
378
 
379
+ @objc private func appWillTerminate() {
380
+ appHealthTracker?.markForeground(false)
381
+ }
382
+
383
+ @objc private func appDidReceiveMemoryWarning() {
384
+ appHealthTracker?.reportMemoryWarning()
385
+ }
386
+
387
+ @objc func reportWebViewError(_ call: CAPPluginCall) {
388
+ guard let webViewStatsReporter = webViewStatsReporter else {
389
+ call.resolve()
390
+ return
391
+ }
392
+ webViewStatsReporter.reportError(call)
393
+ }
394
+
371
395
  private func initialLoad() -> Bool {
372
396
  guard let bridge = self.bridge else { return false }
373
397
  if keepUrlPathAfterReload {
@@ -1028,6 +1052,27 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1028
1052
  }
1029
1053
  }
1030
1054
 
1055
+ public func triggerBackgroundUpdateCheck() -> String {
1056
+ guard !self.updateUrl.isEmpty, URL(string: self.updateUrl) != nil else {
1057
+ logger.error("Error no url or wrong format")
1058
+ return "unavailable"
1059
+ }
1060
+ if self.isDownloadStuckOrTimedOut() {
1061
+ logger.info("Download already in progress, skipping duplicate download request")
1062
+ return "already_running"
1063
+ }
1064
+ self.backgroundDownload()
1065
+ return "queued"
1066
+ }
1067
+
1068
+ @objc func triggerUpdateCheck(_ call: CAPPluginCall) {
1069
+ let status = self.triggerBackgroundUpdateCheck()
1070
+ call.resolve([
1071
+ "status": status,
1072
+ "queued": status == "queued"
1073
+ ])
1074
+ }
1075
+
1031
1076
  @objc func unsetChannel(_ call: CAPPluginCall) {
1032
1077
  let triggerAutoUpdate = call.getBool("triggerAutoUpdate", false)
1033
1078
  self.saveCallForAsyncHandling(call)
@@ -1719,6 +1764,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1719
1764
  plannedDirectUpdate && directUpdateMode == "onLaunch"
1720
1765
  }
1721
1766
 
1767
+ static func normalizedPeriodCheckDelaySeconds(_ value: Int) -> Int {
1768
+ guard value > 0 else {
1769
+ return 0
1770
+ }
1771
+ return max(600, value)
1772
+ }
1773
+
1722
1774
  private func getOnLaunchDirectUpdateUsed() -> Bool {
1723
1775
  self.onLaunchDirectUpdateStateLock.lock()
1724
1776
  defer { self.onLaunchDirectUpdateStateLock.unlock() }
@@ -1769,13 +1821,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1769
1821
  self.notifyListeners("majorAvailable", data: payload)
1770
1822
  }
1771
1823
 
1772
- private func updateResponseKind(kind: String?) -> String {
1824
+ static func normalizedUpdateResponseKind(kind: String?) -> String {
1773
1825
  if let kind, ["up_to_date", "blocked", "failed"].contains(kind) {
1774
1826
  return kind
1775
1827
  }
1776
1828
  return "failed"
1777
1829
  }
1778
1830
 
1831
+ private func updateResponseKind(kind: String?) -> String {
1832
+ Self.normalizedUpdateResponseKind(kind: kind)
1833
+ }
1834
+
1779
1835
  private func endBackgroundDownloadAfterLatestError(
1780
1836
  backendError: String,
1781
1837
  res: AppVersion,
@@ -2153,6 +2209,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2153
2209
  }
2154
2210
 
2155
2211
  @objc func appMovedToForeground() {
2212
+ appHealthTracker?.markForeground(true)
2156
2213
  let current: BundleInfo = self.implementation.getCurrentBundle()
2157
2214
  self.implementation.sendStats(action: "app_moved_to_foreground", versionName: current.getVersionName())
2158
2215
  self.delayUpdateUtils.checkCancelDelay(source: .foreground)
@@ -2219,6 +2276,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2219
2276
  @objc func appMovedToBackground() {
2220
2277
  // Reset timeout flag at start of each background cycle
2221
2278
  self.autoSplashscreenTimedOut = false
2279
+ appHealthTracker?.markForeground(false)
2222
2280
 
2223
2281
  let current: BundleInfo = self.implementation.getCurrentBundle()
2224
2282
  self.implementation.sendStats(action: "app_moved_to_background", versionName: current.getVersionName())
@@ -2256,6 +2256,14 @@ import UIKit
2256
2256
  }()
2257
2257
 
2258
2258
  func sendStats(action: String, versionName: String? = nil, oldVersionName: String? = "") {
2259
+ sendStatsWithMetadata(action: action, versionName: versionName, oldVersionName: oldVersionName, metadata: nil)
2260
+ }
2261
+
2262
+ func sendStats(action: String, versionName: String?, oldVersionName: String?, metadata: [String: String]) {
2263
+ sendStatsWithMetadata(action: action, versionName: versionName, oldVersionName: oldVersionName, metadata: metadata)
2264
+ }
2265
+
2266
+ private func sendStatsWithMetadata(action: String, versionName: String?, oldVersionName: String?, metadata: [String: String]?) {
2259
2267
  // Check if rate limit was exceeded
2260
2268
  if CapgoUpdater.rateLimitExceeded {
2261
2269
  logger.debug("Skipping sendStats due to rate limit (429). Stats will resume after app restart.")
@@ -2286,6 +2294,7 @@ import UIKit
2286
2294
  channel: info.channel,
2287
2295
  defaultChannel: info.defaultChannel,
2288
2296
  key_id: info.key_id,
2297
+ metadata: metadata,
2289
2298
  timestamp: Int64(Date().timeIntervalSince1970 * 1000)
2290
2299
  )
2291
2300
 
@@ -183,6 +183,7 @@ struct StatsEvent: Codable {
183
183
  let channel: String?
184
184
  let defaultChannel: String?
185
185
  let key_id: String?
186
+ let metadata: [String: String]?
186
187
  let timestamp: Int64
187
188
  }
188
189
  // swiftlint:enable identifier_name
@@ -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.11",
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,6 +45,15 @@
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 ..",