craft-native 0.0.219 → 0.0.221
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 +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +6 -1
- package/dist/ios/src/testing.d.ts +8 -0
- package/dist/ios/templates/CraftApp.swift +65 -14
- package/dist/ios/templates/CraftHybrid.swift +2 -0
- package/dist/ios/templates/CraftNativeActions.swift +6 -3
- package/dist/ios/templates/CraftNativeScreen.swift +13 -3
- package/dist/ios/templates/testing/DriverTests.swift.template +13 -0
- package/package.json +1 -1
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.
|
|
3427
|
+
var version = "0.0.221";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.js
CHANGED
|
@@ -137,7 +137,12 @@ function serializeSimulatorSteps(steps) {
|
|
|
137
137
|
arg = step.state;
|
|
138
138
|
else if ("direction" in step)
|
|
139
139
|
arg = step.direction;
|
|
140
|
-
else if ("
|
|
140
|
+
else if ("fromX" in step) {
|
|
141
|
+
const coordinates = [step.fromX, step.fromY, step.toX, step.toY];
|
|
142
|
+
if (coordinates.some((value) => number(value) > 1))
|
|
143
|
+
throw new Error("drag coordinates must be between 0 and 1");
|
|
144
|
+
arg = coordinates.join(" ");
|
|
145
|
+
} else if ("x" in step) {
|
|
141
146
|
if (number(step.x) > 1 || number(step.y) > 1)
|
|
142
147
|
throw new Error("tapxy coordinates must be between 0 and 1");
|
|
143
148
|
arg = `${step.x} ${step.y}`;
|
|
@@ -27,6 +27,14 @@ export type SimulatorStep = {
|
|
|
27
27
|
command: 'swipe';
|
|
28
28
|
direction: 'up' | 'down' | 'left' | 'right';
|
|
29
29
|
}
|
|
30
|
+
/** A slow drag in app-relative coordinates, held before release to avoid a flick. */
|
|
31
|
+
| {
|
|
32
|
+
command: 'drag';
|
|
33
|
+
fromX: number;
|
|
34
|
+
fromY: number;
|
|
35
|
+
toX: number;
|
|
36
|
+
toY: number;
|
|
37
|
+
}
|
|
30
38
|
/** The system back-swipe: a drag in from the left edge (or the right). */
|
|
31
39
|
| {
|
|
32
40
|
command: 'edge';
|
|
@@ -691,12 +691,18 @@ final class CraftChrome: ObservableObject {
|
|
|
691
691
|
}
|
|
692
692
|
/// A destination beside the tab pill, outside the tab stacks and swipe order.
|
|
693
693
|
func updateTabAccessory(_ item: [String: Any]?) {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
694
|
+
let next: Tab?
|
|
695
|
+
if let item, let path = item["path"] as? String,
|
|
696
|
+
path.hasPrefix("/"), !path.hasPrefix("//"),
|
|
697
|
+
let title = item["title"] as? String {
|
|
698
|
+
next = Tab(id: path, title: title, symbol: item["symbol"] as? String ?? "message",
|
|
699
|
+
badge: (item["badge"] as? String).flatMap { $0.isEmpty ? nil : String($0.prefix(8)) },
|
|
700
|
+
accessibilityLabel: item["accessibilityLabel"] as? String)
|
|
701
|
+
} else {
|
|
702
|
+
next = nil
|
|
703
|
+
}
|
|
704
|
+
guard tabAccessory != next else { return }
|
|
705
|
+
tabAccessory = next
|
|
700
706
|
}
|
|
701
707
|
|
|
702
708
|
func setHiddenByScreen(_ hidden: Bool) {
|
|
@@ -936,6 +942,7 @@ struct CraftTabBarView: View {
|
|
|
936
942
|
.foregroundStyle(Color.primary)
|
|
937
943
|
.frame(width: 62, height: 62)
|
|
938
944
|
}
|
|
945
|
+
.contentShape(Rectangle())
|
|
939
946
|
.overlay(alignment: .topTrailing) {
|
|
940
947
|
if let badge = accessory.badge {
|
|
941
948
|
Text(badge)
|
|
@@ -1098,23 +1105,58 @@ struct CraftSplashView: View {
|
|
|
1098
1105
|
}
|
|
1099
1106
|
|
|
1100
1107
|
// MARK: - Deep Link Manager
|
|
1108
|
+
/// Launch intent settles when UIKit delivers its URL or completes activation,
|
|
1109
|
+
/// independently of when a page or native screen registers its listeners.
|
|
1110
|
+
final class CraftInitialLinkState {
|
|
1111
|
+
private(set) var url: URL?
|
|
1112
|
+
private(set) var settled = false
|
|
1113
|
+
private var readers: [(URL?) -> Void] = []
|
|
1114
|
+
|
|
1115
|
+
func receive(_ url: URL) {
|
|
1116
|
+
guard !settled else { return }
|
|
1117
|
+
self.url = url
|
|
1118
|
+
settle()
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
func settle() {
|
|
1122
|
+
guard !settled else { return }
|
|
1123
|
+
settled = true
|
|
1124
|
+
let waiting = readers
|
|
1125
|
+
readers.removeAll()
|
|
1126
|
+
for reader in waiting { reader(url) }
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
func resolve(_ completion: @escaping (URL?) -> Void) {
|
|
1130
|
+
if settled { completion(url) }
|
|
1131
|
+
else { readers.append(completion) }
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1101
1135
|
class DeepLinkManager {
|
|
1102
1136
|
static let shared = DeepLinkManager()
|
|
1103
1137
|
|
|
1104
|
-
private
|
|
1138
|
+
private let launch = CraftInitialLinkState()
|
|
1139
|
+
private var initialURL: URL? { launch.url }
|
|
1140
|
+
private var activationObservers: [NSObjectProtocol] = []
|
|
1105
1141
|
// Every link that arrived while no page could receive it, in order. This
|
|
1106
1142
|
// was a single slot, so a second link before the bridge was ready replaced
|
|
1107
1143
|
// the first; on a cold start, that could be the link that launched the app.
|
|
1108
1144
|
private var pendingURLs: [URL] = []
|
|
1109
1145
|
private weak var webView: WKWebView?
|
|
1110
1146
|
private var isReady = false
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
// is no answer to getInitialURL.
|
|
1147
|
+
// Replay bookkeeping only. A native screen can become ready before UIKit
|
|
1148
|
+
// delivers the launch URL; listener readiness cannot classify that URL.
|
|
1114
1149
|
private var hasBeenReady = false
|
|
1115
1150
|
private var nativeListeners: [UUID: (URL, Bool) -> Void] = [:]
|
|
1116
1151
|
|
|
1117
|
-
private init() {
|
|
1152
|
+
private init() {
|
|
1153
|
+
for name in [UIApplication.didBecomeActiveNotification, UIApplication.didEnterBackgroundNotification] {
|
|
1154
|
+
activationObservers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
|
|
1155
|
+
self?.launch.settle()
|
|
1156
|
+
})
|
|
1157
|
+
}
|
|
1158
|
+
if UIApplication.shared.applicationState == .active { launch.settle() }
|
|
1159
|
+
}
|
|
1118
1160
|
|
|
1119
1161
|
func setWebView(_ webView: WKWebView) {
|
|
1120
1162
|
self.webView = webView
|
|
@@ -1139,9 +1181,7 @@ class DeepLinkManager {
|
|
|
1139
1181
|
}
|
|
1140
1182
|
|
|
1141
1183
|
func handleURL(_ url: URL) {
|
|
1142
|
-
|
|
1143
|
-
initialURL = url
|
|
1144
|
-
}
|
|
1184
|
+
launch.receive(url)
|
|
1145
1185
|
|
|
1146
1186
|
// A hybrid app opens a native path's screen itself, and tells the
|
|
1147
1187
|
// native screens that listen; every other link is the page's. An
|
|
@@ -1172,6 +1212,17 @@ class DeepLinkManager {
|
|
|
1172
1212
|
return initialURL
|
|
1173
1213
|
}
|
|
1174
1214
|
|
|
1215
|
+
/// A foreground scene can draw before SwiftUI delivers its launch activity.
|
|
1216
|
+
/// A headless background launch has no scene or foreground URL to await.
|
|
1217
|
+
func resolveInitialURL(_ completion: @escaping (URL?) -> Void) {
|
|
1218
|
+
let application = UIApplication.shared
|
|
1219
|
+
if application.applicationState == .active ||
|
|
1220
|
+
(application.applicationState == .background && application.connectedScenes.isEmpty) {
|
|
1221
|
+
launch.settle()
|
|
1222
|
+
}
|
|
1223
|
+
launch.resolve(completion)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1175
1226
|
@discardableResult
|
|
1176
1227
|
func addNativeListener(_ listener: @escaping (URL, Bool) -> Void) -> UUID {
|
|
1177
1228
|
let token = UUID()
|
|
@@ -1243,6 +1243,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1243
1243
|
target.view.isHidden = true
|
|
1244
1244
|
target.view.frame = self.view.bounds
|
|
1245
1245
|
current.view.frame = self.view.bounds
|
|
1246
|
+
(current.topViewController as? CraftNativeScreenController)?.screenVisibilityChanged(true)
|
|
1246
1247
|
if target.topViewController is CraftHybridWebSlot,
|
|
1247
1248
|
current.topViewController is CraftNativeScreenController {
|
|
1248
1249
|
self.parkWeb()
|
|
@@ -1272,6 +1273,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1272
1273
|
recognizer.isEnabled = true
|
|
1273
1274
|
}
|
|
1274
1275
|
CraftChrome.shared.cancelTabSwipe(restoringSelection: restoringSelection)
|
|
1276
|
+
(visibleStack?.topViewController as? CraftNativeScreenController)?.screenVisibilityChanged(true)
|
|
1275
1277
|
if visibleStack?.topViewController is CraftNativeScreenController { parkWeb() }
|
|
1276
1278
|
}
|
|
1277
1279
|
|
|
@@ -258,7 +258,10 @@ enum CraftNativeActions {
|
|
|
258
258
|
sync = .success(currentAppState())
|
|
259
259
|
case ("DeepLinks", "getInitialURL"):
|
|
260
260
|
if config.enableDeepLinks {
|
|
261
|
-
|
|
261
|
+
DeepLinkManager.shared.resolveInitialURL { url in
|
|
262
|
+
complete(requestToken, .success(claimInitialDeepLink(url).map(deepLinkData) ?? NSNull()), completion)
|
|
263
|
+
}
|
|
264
|
+
return
|
|
262
265
|
} else {
|
|
263
266
|
sync = failure("CAPABILITY_DISABLED", "Deep links are disabled")
|
|
264
267
|
}
|
|
@@ -809,12 +812,12 @@ enum CraftNativeActions {
|
|
|
809
812
|
else { task.resume() }
|
|
810
813
|
}
|
|
811
814
|
|
|
812
|
-
private static func claimInitialDeepLink() -> URL? {
|
|
815
|
+
private static func claimInitialDeepLink(_ url: URL?) -> URL? {
|
|
813
816
|
cancellationLock.lock()
|
|
814
817
|
defer { cancellationLock.unlock() }
|
|
815
818
|
guard !initialDeepLinkClaimed else { return nil }
|
|
816
819
|
initialDeepLinkClaimed = true
|
|
817
|
-
return
|
|
820
|
+
return url
|
|
818
821
|
}
|
|
819
822
|
|
|
820
823
|
private static func complete(
|
|
@@ -4128,8 +4128,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4128
4128
|
applyNavigationBarAppearance()
|
|
4129
4129
|
// Each screen says whether it wants the tab bar, so one that hid it
|
|
4130
4130
|
// never leaves it hidden for the next.
|
|
4131
|
-
|
|
4132
|
-
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
4131
|
+
restoreShellChrome()
|
|
4133
4132
|
refreshTraitDefaults()
|
|
4134
4133
|
}
|
|
4135
4134
|
|
|
@@ -4176,13 +4175,24 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4176
4175
|
}
|
|
4177
4176
|
}
|
|
4178
4177
|
|
|
4178
|
+
private func restoreShellChrome() {
|
|
4179
|
+
guard !isAppScript else { return }
|
|
4180
|
+
CraftChrome.shared.setHiddenByScreen(tabBarHidden)
|
|
4181
|
+
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4179
4184
|
/// The screen came on screen or went off it: a screen pushed over it was
|
|
4180
4185
|
/// popped, or the hybrid shell switched to or away from its tab (which
|
|
4181
4186
|
/// hides a stack rather than moving it, so UIKit says nothing). The first
|
|
4182
4187
|
/// appearance is the screen's mount, not news; every later one is
|
|
4183
4188
|
/// `SCREEN_FOCUS { focused: true }`, and going away is `focused: false`.
|
|
4184
4189
|
func screenVisibilityChanged(_ visible: Bool) {
|
|
4185
|
-
guard !runtimeDisposed
|
|
4190
|
+
guard !runtimeDisposed else { return }
|
|
4191
|
+
// Hidden tab stacks receive no UIKit appearance callback. A cancelled
|
|
4192
|
+
// preview also restores a screen that never lost focus, so restore its
|
|
4193
|
+
// chrome even when no new SCREEN_FOCUS event is needed.
|
|
4194
|
+
if visible { restoreShellChrome() }
|
|
4195
|
+
guard visible != reportedVisible else { return }
|
|
4186
4196
|
reportedVisible = visible
|
|
4187
4197
|
if visible, !hasAppeared {
|
|
4188
4198
|
hasAppeared = true
|
|
@@ -37,6 +37,7 @@ import XCTest
|
|
|
37
37
|
/// tab <name> tap a tab bar button
|
|
38
38
|
/// scrollto <text> swipes up (at most 8 times) until text is hittable
|
|
39
39
|
/// swipe up|down|left|right
|
|
40
|
+
/// drag x1 y1 x2 y2 slow app-relative drag, held 0.3s before release
|
|
40
41
|
/// audit <name> accessibility audit (element descriptions, Dynamic Type, contrast); logs each issue
|
|
41
42
|
/// unlabelled logs every hittable button without a label (FAIL if any)
|
|
42
43
|
/// dismiss [label] a system prompt's button (default Not Now: "Save Password?")
|
|
@@ -367,6 +368,18 @@ final class DriverTests: XCTestCase {
|
|
|
367
368
|
case "left": app.swipeLeft()
|
|
368
369
|
default: app.swipeRight()
|
|
369
370
|
}
|
|
371
|
+
case "drag":
|
|
372
|
+
let parts = arg.split(separator: " ")
|
|
373
|
+
let coordinates = parts.compactMap { Double($0) }
|
|
374
|
+
guard parts.count == 4, coordinates.count == 4,
|
|
375
|
+
coordinates.allSatisfy({ $0.isFinite && (0...1).contains($0) }) else {
|
|
376
|
+
check(false, "drag needs four coordinates between 0 and 1")
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
let start = app.coordinate(withNormalizedOffset: CGVector(dx: CGFloat(coordinates[0]), dy: CGFloat(coordinates[1])))
|
|
380
|
+
let end = app.coordinate(withNormalizedOffset: CGVector(dx: CGFloat(coordinates[2]), dy: CGFloat(coordinates[3])))
|
|
381
|
+
start.press(forDuration: 0.05, thenDragTo: end, withVelocity: .slow, thenHoldForDuration: 0.3)
|
|
382
|
+
note("drag \(arg): ok")
|
|
370
383
|
case "edge":
|
|
371
384
|
// The system back-swipe: a drag in from the left edge (or the
|
|
372
385
|
// right one with `edge right`), which a centred swipe never is.
|