craft-native 0.0.175 → 0.0.177

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.
Files changed (34) hide show
  1. package/dist/android/src/index.d.ts +19 -0
  2. package/dist/android/src/index.js +19 -2
  3. package/dist/android/templates/AndroidManifest.xml.template +1 -0
  4. package/dist/android/templates/CraftBridge.kt.template +51 -0
  5. package/dist/android/templates/CraftCrashReporter.kt.template +544 -0
  6. package/dist/android/templates/CraftNative.kt.template +17 -0
  7. package/dist/android/templates/CraftNativeFlatList.kt.template +20 -9
  8. package/dist/android/templates/CraftNativeMutation.kt.template +4 -1
  9. package/dist/android/templates/CraftNativeServices.kt.template +31 -1
  10. package/dist/android/templates/MainActivityNative.kt.template +986 -304
  11. package/dist/android/templates/build.gradle.kts.app.template +4 -2
  12. package/dist/api/mobile.d.ts +31 -0
  13. package/dist/cli.js +1 -1
  14. package/dist/components/index.d.ts +203 -2
  15. package/dist/index.cjs +1 -1
  16. package/dist/index.d.cts +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/ios/src/index.d.ts +19 -0
  20. package/dist/ios/src/index.js +7 -1
  21. package/dist/ios/src/testing.d.ts +5 -0
  22. package/dist/ios/templates/CraftApp.swift +51 -1
  23. package/dist/ios/templates/CraftCrashReporter.swift +1010 -0
  24. package/dist/ios/templates/CraftHybrid.swift +10 -0
  25. package/dist/ios/templates/CraftNativeActions.swift +49 -1
  26. package/dist/ios/templates/CraftNativeFlatList.swift +30 -28
  27. package/dist/ios/templates/CraftNativeMutation.swift +1 -1
  28. package/dist/ios/templates/CraftNativeScreen.swift +559 -86
  29. package/dist/ios/templates/project.yml.template +1 -0
  30. package/dist/ios/templates/testing/DriverTests.swift.template +8 -0
  31. package/dist/macos/templates/CraftNativeMutation.swift +1 -1
  32. package/dist/macos/templates/native_layout.zig +389 -17
  33. package/dist/mobile.js +66 -0
  34. package/package.json +1 -1
@@ -421,6 +421,16 @@ final class CraftHybrid: NSObject {
421
421
  #endif
422
422
  }
423
423
 
424
+ #if DEBUG
425
+ /// The crash reporter's debug-only test (`-CraftCrashTest js`): an
426
+ /// uncaught error thrown in the app script, as a real one would be.
427
+ func debugThrowInAppScript(_ message: String) {
428
+ if appScript == nil { startAppScript() }
429
+ let literal = (try? JSONSerialization.data(withJSONObject: [message])).flatMap { String(data: $0, encoding: .utf8) } ?? "[\"test\"]"
430
+ appScript?.evaluateScript("(function crashReporterTest() { throw new TypeError(\(literal)[0]); })()")
431
+ }
432
+ #endif
433
+
424
434
  /// The shell is up: what waited for it.
425
435
  func shellReady() {
426
436
  guard let controller else { return }
@@ -82,6 +82,8 @@ enum CraftNativeActions {
82
82
  // which the native Video element plays in place of the address.
83
83
  values.append("media")
84
84
  if config.enableCamera { values.append("camera") }
85
+ // craft.crashReporter: the app's crash and error reporter (bughq).
86
+ if config.crashReporting?.key.isEmpty == false { values.append("crashReporter") }
85
87
  return values
86
88
  }
87
89
 
@@ -108,6 +110,32 @@ enum CraftNativeActions {
108
110
  case "clipboardRead":
109
111
  guard config.enableClipboard else { return failure("CAPABILITY_DISABLED", "Clipboard is disabled") }
110
112
  return .success(UIPasteboard.general.string ?? "")
113
+ // Crash reporting (CraftCrashReporter.swift): on when the config names
114
+ // a bughq key. Each answers at once; delivery is the outbox's job.
115
+ case "crashReporterCapture":
116
+ guard CraftCrashReporter.shared.enabled else { return failure("CAPABILITY_DISABLED", "Crash reporting is not configured") }
117
+ guard let event = body["event"] as? [String: Any] else {
118
+ return failure("INVALID_ARGUMENT", "crashReporterCapture was called without the values it needs")
119
+ }
120
+ CraftCrashReporter.shared.capture(event)
121
+ return .success(true)
122
+ case "crashReporterSetUser":
123
+ guard CraftCrashReporter.shared.enabled else { return failure("CAPABILITY_DISABLED", "Crash reporting is not configured") }
124
+ CraftCrashReporter.shared.setUser(body["id"] as? String)
125
+ return .success(true)
126
+ case "crashReporterAddBreadcrumb":
127
+ guard CraftCrashReporter.shared.enabled else { return failure("CAPABILITY_DISABLED", "Crash reporting is not configured") }
128
+ guard let crumb = body["breadcrumb"] as? [String: Any] else {
129
+ return failure("INVALID_ARGUMENT", "crashReporterAddBreadcrumb was called without the values it needs")
130
+ }
131
+ CraftCrashReporter.shared.addBreadcrumb(crumb)
132
+ return .success(true)
133
+ case "crashReporterFlush":
134
+ guard CraftCrashReporter.shared.enabled else { return failure("CAPABILITY_DISABLED", "Crash reporting is not configured") }
135
+ CraftCrashReporter.shared.requestFlush()
136
+ return .success(true)
137
+ case "crashReporterContext":
138
+ return .success(CraftCrashReporter.shared.context())
111
139
  default:
112
140
  return nil
113
141
  }
@@ -134,6 +162,16 @@ enum CraftNativeActions {
134
162
  switch (module, method) {
135
163
  case ("Device", "getInfo"):
136
164
  sync = perform(action: "getDeviceInfo", body: [:], config: config)
165
+ case ("CrashReporter", "capture"):
166
+ sync = perform(action: "crashReporterCapture", body: ["event": args.first ?? NSNull()], config: config)
167
+ case ("CrashReporter", "setUser"):
168
+ sync = perform(action: "crashReporterSetUser", body: ["id": args.first ?? NSNull()], config: config)
169
+ case ("CrashReporter", "addBreadcrumb"):
170
+ sync = perform(action: "crashReporterAddBreadcrumb", body: ["breadcrumb": args.first ?? NSNull()], config: config)
171
+ case ("CrashReporter", "flush"):
172
+ sync = perform(action: "crashReporterFlush", body: [:], config: config)
173
+ case ("CrashReporter", "context"):
174
+ sync = perform(action: "crashReporterContext", body: [:], config: config)
137
175
  case ("Haptics", "impact"):
138
176
  sync = perform(action: "haptic", body: ["style": args.first ?? NSNull()], config: config)
139
177
  case ("Haptics", "notification"):
@@ -575,9 +613,15 @@ enum CraftNativeActions {
575
613
  cancellationLock.unlock()
576
614
  if let error = error {
577
615
  let code = (error as? URLError)?.code == .timedOut ? "TIMEOUT" : "NETWORK_ERROR"
616
+ if (error as? URLError)?.code != .cancelled {
617
+ CraftCrashReporter.shared.requestFailed(method: method, url: url, status: nil, error: code)
618
+ }
578
619
  complete(requestToken, failure(code, error.localizedDescription), completion)
579
620
  return
580
621
  }
622
+ if let http = response as? HTTPURLResponse, http.statusCode >= 400 {
623
+ CraftCrashReporter.shared.requestFailed(method: method, url: url, status: http.statusCode, error: nil)
624
+ }
581
625
  guard let http = response as? HTTPURLResponse else {
582
626
  complete(requestToken, failure("NETWORK_ERROR", "fetch received a response that is not HTTP"), completion)
583
627
  return
@@ -1236,13 +1280,17 @@ final class CraftNativePasskeys: NSObject, ASAuthorizationControllerDelegate, AS
1236
1280
  guard let request = pending.removeValue(forKey: key) else { return }
1237
1281
  controllers.removeValue(forKey: request.requestToken)
1238
1282
  if let registration = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
1283
+ guard let attestationObject = registration.rawAttestationObject else {
1284
+ request.completion(.failure(CraftNativeActionError(code: "PASSKEY_FAILED", message: "Apple returned a registration without an attestation object")))
1285
+ return
1286
+ }
1239
1287
  request.completion(.success([
1240
1288
  "id": Self.base64url(registration.credentialID),
1241
1289
  "rawId": Self.base64url(registration.credentialID),
1242
1290
  "type": "public-key",
1243
1291
  "response": [
1244
1292
  "clientDataJSON": Self.base64url(registration.rawClientDataJSON),
1245
- "attestationObject": Self.base64url(registration.rawAttestationObject),
1293
+ "attestationObject": Self.base64url(attestationObject),
1246
1294
  ],
1247
1295
  ]))
1248
1296
  return
@@ -219,15 +219,20 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
219
219
  }
220
220
 
221
221
  func setContentContainerStyle(_ raw: [String: Any]?) {
222
- func number(_ value: Any?) -> CGFloat? { (value as? NSNumber).map { CGFloat(truncating: $0) } }
223
- let padding = number(raw?["padding"]) ?? 0
224
- let horizontal = number(raw?["paddingHorizontal"]) ?? padding
225
- let vertical = number(raw?["paddingVertical"]) ?? padding
222
+ func nonNegative(_ value: Any?) -> CGFloat? {
223
+ guard let number = value as? NSNumber else { return nil }
224
+ let points = CGFloat(truncating: number)
225
+ guard points.isFinite, points >= 0 else { return nil }
226
+ return points
227
+ }
228
+ let padding = nonNegative(raw?["padding"]) ?? 0
229
+ let horizontal = nonNegative(raw?["paddingHorizontal"]) ?? padding
230
+ let vertical = nonNegative(raw?["paddingVertical"]) ?? padding
226
231
  contentInset = UIEdgeInsets(
227
- top: number(raw?["paddingTop"]) ?? vertical,
228
- left: number(raw?["paddingLeft"]) ?? horizontal,
229
- bottom: number(raw?["paddingBottom"]) ?? vertical,
230
- right: number(raw?["paddingRight"]) ?? horizontal
232
+ top: nonNegative(raw?["paddingTop"]) ?? vertical,
233
+ left: nonNegative(raw?["paddingLeft"]) ?? horizontal,
234
+ bottom: nonNegative(raw?["paddingBottom"]) ?? vertical,
235
+ right: nonNegative(raw?["paddingRight"]) ?? horizontal
231
236
  )
232
237
  scrollIndicatorInsets = contentInset
233
238
  collectionViewLayout.invalidateLayout()
@@ -265,14 +270,24 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
265
270
 
266
271
  @objc private func keyboardTap() {
267
272
  guard keyboardShouldPersistTaps != "always" else { return }
268
- if keyboardShouldPersistTaps == "handled" {
269
- let point = keyboardTapGesture.location(in: self)
270
- var hit = hitTest(point, with: nil)
271
- while let view = hit, view !== self {
272
- if view is UIControl || !(view.gestureRecognizers ?? []).isEmpty { return }
273
- hit = view.superview
274
- }
273
+ let point = keyboardTapGesture.location(in: self)
274
+ var hit = hitTest(point, with: nil)
275
+ var onControl = false
276
+ var onButton = false
277
+ while let view = hit, view !== self {
278
+ if view is UIButton { onButton = true }
279
+ if view is UIControl || !(view.gestureRecognizers ?? []).isEmpty { onControl = true; break }
280
+ hit = view.superview
275
281
  }
282
+ if keyboardShouldPersistTaps == "handled", onControl { return }
283
+ // A button keeps the keyboard: its press gives the focus straight
284
+ // back. Ending editing first only let the keyboard's inset go while
285
+ // the finger was down, the button moved out from under it, and the
286
+ // press was lost.
287
+ if onButton { return }
288
+ // Otherwise the keyboard goes; after a tap on nothing, for good (the
289
+ // next render must not bring the last input back).
290
+ if !onControl { craftScreenController?.forgetFocusedInput() }
276
291
  window?.endEditing(true)
277
292
  }
278
293
 
@@ -443,19 +458,6 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
443
458
  onMomentumScrollEnd?(scrollView)
444
459
  }
445
460
 
446
- func collectionView(
447
- _ collectionView: UICollectionView,
448
- layout collectionViewLayout: UICollectionViewLayout,
449
- sizeForItemAt indexPath: IndexPath
450
- ) -> CGSize {
451
- guard items.indices.contains(indexPath.item) else { return CGSize(width: bounds.width, height: 44) }
452
- let item = items[indexPath.item]
453
- if flowLayout.scrollDirection == .horizontal {
454
- return CGSize(width: 44, height: itemExtent(isChrome: item.isChrome))
455
- }
456
- return CGSize(width: itemExtent(isChrome: item.isChrome), height: 44)
457
- }
458
-
459
461
  private func evaluateEndReached() {
460
462
  guard !isApplyingSnapshot, pendingApply == nil,
461
463
  let endReached = endReached,
@@ -305,7 +305,7 @@ final class CraftNativeMutationDocument {
305
305
  static let containerTypes: Set<String> = [
306
306
  "View", "SafeAreaView", "ScrollView", "KeyboardAvoidingView", "Modal", "Pressable",
307
307
  "TouchableOpacity", "TouchableHighlight", "ImageBackground", "FlatList", "SectionList",
308
- "Fragment", "Svg", "G",
308
+ "Fragment", "Svg", "G", "Map",
309
309
  ]
310
310
  private static let containerTypeKeys = Set(containerTypes.map { $0.lowercased() })
311
311