craft-native 0.0.214 → 0.0.216

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/dist/cli.js CHANGED
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3424
3424
  `);
3425
3425
  }
3426
3426
  // package.json
3427
- var version = "0.0.214";
3427
+ var version = "0.0.216";
3428
3428
 
3429
3429
  // bin/cli.ts
3430
3430
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -309,7 +309,7 @@ function craftBinaryNotFoundMessage(triedPath) {
309
309
  `);
310
310
  }
311
311
  // package.json
312
- var version = "0.0.214";
312
+ var version = "0.0.216";
313
313
 
314
314
  // src/sdk-version.ts
315
315
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ function craftBinaryNotFoundMessage(triedPath) {
57
57
  `);
58
58
  }
59
59
  // package.json
60
- var version = "0.0.214";
60
+ var version = "0.0.216";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -156,8 +156,11 @@ function serializeSimulatorSteps(steps) {
156
156
  }
157
157
  } else if ("name" in step)
158
158
  arg = token(step.name);
159
- else if ("url" in step)
159
+ else if ("url" in step) {
160
+ if (step.command === "universallink" && new URL(step.url).protocol !== "https:")
161
+ throw new Error("Universal Links require an HTTPS URL");
160
162
  arg = step.url;
163
+ }
161
164
  if (/[\r\n]/.test(arg))
162
165
  throw new Error("Simulator arguments cannot contain newlines");
163
166
  return `${step.command}${arg ? ` ${arg}` : ""}`;
@@ -60,6 +60,11 @@ export type SimulatorStep = {
60
60
  command: 'openurl';
61
61
  url: string;
62
62
  }
63
+ /** Use Apple's universalLinksOnly API, allowing association downloads up to 60 seconds. */
64
+ | {
65
+ command: 'universallink';
66
+ url: string;
67
+ }
63
68
  /** Runs `step` only when the element with this identifier is there (a screen shown the first time). */
64
69
  | {
65
70
  command: 'ifid';
@@ -308,7 +308,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
308
308
 
309
309
  func setContentContainerStyle(_ raw: [String: Any]?) {
310
310
  func nonNegative(_ value: Any?) -> CGFloat? {
311
- guard !(value is Bool), let number = value as? NSNumber else { return nil }
311
+ guard let number = craftNativeNumber(value) else { return nil }
312
312
  let points = CGFloat(truncating: number)
313
313
  guard points.isFinite, points >= 0 else { return nil }
314
314
  return points
@@ -1,4 +1,5 @@
1
1
  import Foundation
2
+ import CoreFoundation
2
3
  import AVFoundation
3
4
  import AVKit
4
5
  import CryptoKit
@@ -12,32 +13,41 @@ import UIKit
12
13
 
13
14
  private let craftNativeCapabilityTimeoutMilliseconds = 30_000
14
15
 
16
+ /// Foundation's `NSNumber(0/1) is Bool` is true even for JSON numbers.
17
+ /// Check the Core Foundation identity so real booleans stay invalid numeric
18
+ /// props while zero lengths, flex factors and opacity 1 remain numbers.
19
+ func craftNativeNumber(_ value: Any?) -> NSNumber? {
20
+ guard let number = value as? NSNumber,
21
+ CFGetTypeID(number) != CFBooleanGetTypeID() else { return nil }
22
+ return number
23
+ }
24
+
15
25
  /// A finite number from a prop, for the views outside the screen controller,
16
26
  /// which has its own: the keyboard-avoiding view and SVG text read theirs
17
27
  /// with it, and without it the template did not compile.
18
28
  private func number(_ value: Any?) -> CGFloat? {
19
29
  // JavaScript booleans bridge to NSNumber on Apple platforms; they are
20
30
  // invalid numeric styles and must stay unset like Android's Number cast.
21
- guard !(value is Bool), let number = value as? NSNumber else { return nil }
31
+ guard let number = craftNativeNumber(value) else { return nil }
22
32
  let value = CGFloat(truncating: number)
23
33
  return value.isFinite ? value : nil
24
34
  }
25
35
 
26
36
  private func craftNativeFontWeight(_ value: Any?) -> String? {
27
37
  if let value = value as? String { return value }
28
- guard !(value is Bool), let number = value as? NSNumber, number.doubleValue.isFinite else { return nil }
38
+ guard let number = craftNativeNumber(value), number.doubleValue.isFinite else { return nil }
29
39
  let integer = number.doubleValue.rounded()
30
40
  return integer == number.doubleValue ? String(Int(integer)) : nil
31
41
  }
32
42
 
33
43
  private func craftNativePickerValue(_ value: Any?) -> String? {
34
44
  if let value = value as? String, !value.isEmpty { return value }
35
- if !(value is Bool), let value = value as? NSNumber, value.doubleValue.isFinite { return String(value.doubleValue) }
45
+ if let value = craftNativeNumber(value), value.doubleValue.isFinite { return String(value.doubleValue) }
36
46
  return nil
37
47
  }
38
48
 
39
49
  private func craftNativeGridColumnCount(_ value: Any?) -> Int {
40
- if !(value is Bool), let number = value as? NSNumber { return max(1, number.intValue) }
50
+ if let number = craftNativeNumber(value) { return max(1, number.intValue) }
41
51
  guard let text = value as? String else { return 1 }
42
52
  let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
43
53
  if trimmed.hasPrefix("repeat(") {
@@ -1387,7 +1397,7 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
1387
1397
  }
1388
1398
 
1389
1399
  private static func numeric(_ value: Any?) -> Double? {
1390
- guard !(value is Bool), let number = value as? NSNumber else { return nil }
1400
+ guard let number = craftNativeNumber(value) else { return nil }
1391
1401
  let value = number.doubleValue
1392
1402
  return value.isFinite ? value : nil
1393
1403
  }
@@ -1458,7 +1468,7 @@ private enum CraftNativeLength: Equatable {
1458
1468
  init?(_ value: Any?) {
1459
1469
  // JavaScript booleans can bridge to NSNumber on Apple platforms;
1460
1470
  // they are invalid CSS lengths and must stay unset like Android.
1461
- if !(value is Bool), let number = value as? NSNumber {
1471
+ if let number = craftNativeNumber(value) {
1462
1472
  let points = CGFloat(truncating: number)
1463
1473
  guard points.isFinite else { return nil }
1464
1474
  self = .points(points)
@@ -1516,7 +1526,7 @@ private struct CraftNativeLayoutStyle {
1516
1526
 
1517
1527
  init(_ raw: [String: Any]) {
1518
1528
  func number(_ value: Any?) -> CGFloat? {
1519
- guard !(value is Bool), let number = value as? NSNumber else { return nil }
1529
+ guard let number = craftNativeNumber(value) else { return nil }
1520
1530
  let value = CGFloat(truncating: number)
1521
1531
  return value.isFinite ? value : nil
1522
1532
  }
@@ -5000,7 +5010,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5000
5010
  else if let symbol = spec["symbol"] as? String { button.setImage(UIImage(systemName: symbol, withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal) }
5001
5011
  button.accessibilityIdentifier = spec["id"] as? String
5002
5012
  button.accessibilityLabel = spec["accessibilityLabel"] as? String
5003
- if !(spec["badge"] is Bool), let value = spec["badge"] as? NSNumber, value.intValue > 0 {
5013
+ if let value = craftNativeNumber(spec["badge"]), value.intValue > 0 {
5004
5014
  let dot = spec["badgeStyle"] as? String == "dot"
5005
5015
  let size: CGFloat = dot ? 9 : 18
5006
5016
  // The fill is a tinted symbol, not a background colour: on iOS 26
@@ -7658,7 +7668,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
7658
7668
  /// Countdown values use seconds when numeric, or `HH:mm[:ss]` text.
7659
7669
  static func countdownDuration(_ value: Any?) -> TimeInterval? {
7660
7670
  let maximum: TimeInterval = 23 * 3600 + 59 * 60 + 59
7661
- if let number = value as? NSNumber, !(value is Bool) {
7671
+ if let number = craftNativeNumber(value) {
7662
7672
  let seconds = number.doubleValue
7663
7673
  guard seconds.isFinite else { return nil }
7664
7674
  return min(maximum, max(0, seconds))
@@ -7679,7 +7689,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
7679
7689
  /// time), `2026-10-12T07:30` (local), a full ISO 8601 string, or
7680
7690
  /// milliseconds since 1970. Nil for anything else.
7681
7691
  static func pickerDate(_ value: Any?) -> Date? {
7682
- if let number = value as? NSNumber, !(value is Bool) {
7692
+ if let number = craftNativeNumber(value) {
7683
7693
  let milliseconds = number.doubleValue
7684
7694
  guard milliseconds.isFinite else { return nil }
7685
7695
  return Date(timeIntervalSince1970: milliseconds / 1000)
@@ -7851,7 +7861,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
7851
7861
  }
7852
7862
 
7853
7863
  private func number(_ value: Any?) -> CGFloat? {
7854
- guard !(value is Bool), let number = value as? NSNumber else { return nil }
7864
+ guard let number = craftNativeNumber(value) else { return nil }
7855
7865
  let value = CGFloat(truncating: number)
7856
7866
  return value.isFinite ? value : nil
7857
7867
  }
@@ -14,6 +14,7 @@ import XCTest
14
14
  /// delay <ms> every proxied request waits this long (0 = off)
15
15
  /// exec <hook> runs the switch's HOOKS/<hook>.sh on the Mac (restart the API, …)
16
16
  /// openurl <url> opens a link through the system, answering "Open in the app?"
17
+ /// universallink <https-url> waits up to 60s for an OS-verified link; never opens a browser
17
18
  /// shot <name> screenshot → shots/<name>.png
18
19
  /// tree <name> accessibility tree → shots/<name>.txt
19
20
  /// tap <text> first hittable element whose label contains text
@@ -253,6 +254,21 @@ final class DriverTests: XCTestCase {
253
254
  answerOpenPrompt()
254
255
  note("openurl \(arg)")
255
256
  }
257
+ case "universallink":
258
+ guard let url = URL(string: arg), url.scheme == "https" else {
259
+ check(false, "universallink requires an HTTPS URL")
260
+ return
261
+ }
262
+ let host = XCUIApplication(bundleIdentifier: "{{BUNDLE_PREFIX}}.host")
263
+ host.launchArguments = ["--craft-universal-link", url.absoluteString]
264
+ host.launch()
265
+ let opened = app.wait(for: .runningForeground, timeout: 75)
266
+ if !opened {
267
+ post("/save?name=universal-link-host.txt", Data(host.debugDescription.utf8))
268
+ post("/save?name=universal-link-app.txt", Data(app.debugDescription.utf8))
269
+ }
270
+ check(opened, "universallink \(arg) opened the target app")
271
+ host.terminate()
256
272
  case "shot":
257
273
  let shot = XCUIScreen.main.screenshot()
258
274
  post("/save?name=\(arg).png", shot.pngRepresentation)
@@ -1,6 +1,36 @@
1
1
  import SwiftUI
2
+ import UIKit
2
3
 
3
4
  @main
4
5
  struct HostApp: App {
5
- var body: some Scene { WindowGroup { Text("Craft simulator driver") } }
6
+ @State private var status = "Craft simulator driver"
7
+
8
+ var body: some Scene {
9
+ WindowGroup {
10
+ Text(status).accessibilityIdentifier("craft-driver-status")
11
+ .task { await openUniversalLinkIfRequested() }
12
+ }
13
+ }
14
+
15
+ @MainActor
16
+ private func openUniversalLinkIfRequested() async {
17
+ let arguments = ProcessInfo.processInfo.arguments
18
+ guard let index = arguments.firstIndex(of: "--craft-universal-link"),
19
+ arguments.indices.contains(index + 1),
20
+ let url = URL(string: arguments[index + 1]), url.scheme == "https" else { return }
21
+ // Installation returns before iOS finishes downloading the AASA from
22
+ // Apple's CDN. Ask the public API until the OS accepts the association;
23
+ // universalLinksOnly prevents a failed attempt from opening Safari.
24
+ let deadline = Date().addingTimeInterval(60)
25
+ while Date() < deadline && !Task.isCancelled {
26
+ let opened = await UIApplication.shared.open(url, options: [.universalLinksOnly: true])
27
+ if opened {
28
+ status = "Universal Link accepted"
29
+ return
30
+ }
31
+ status = "Waiting for Universal Link association"
32
+ try? await Task.sleep(nanoseconds: 1_000_000_000)
33
+ }
34
+ status = "Universal Link was not accepted within 60 seconds"
35
+ }
6
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.214",
3
+ "version": "0.0.216",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",