@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.
- package/Package.swift +1 -1
- package/README.md +114 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +703 -60
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +85 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +27 -3
- package/dist/docs.json +213 -5
- package/dist/esm/definitions.d.ts +128 -19
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +4 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +4 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +4 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AppHealthTracker.swift +82 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +353 -100
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +681 -300
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +32 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +20 -3
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +12 -2
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
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",
|