@craft-native/ios 0.0.243 → 0.0.245
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/index.d.ts +8 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +13 -1
- package/templates/CraftHybrid.swift +41 -2
- package/templates/CraftNativeActions.swift +63 -5
- package/templates/CraftNativeMutation.swift +4 -0
- package/templates/CraftNativeScreen.swift +91 -17
- package/templates/testing/DriverTests.swift.template +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -253,6 +253,14 @@ export interface CraftConfig {
|
|
|
253
253
|
* `craft.secureStorage.getSync('auth.token')`. Turns on secure storage.
|
|
254
254
|
*/
|
|
255
255
|
shareStorage?: Record<string, string>;
|
|
256
|
+
/**
|
|
257
|
+
* The secure-storage key the signed-in account's token lives under
|
|
258
|
+
* (default `auth.token`). `craft.session` follows it: a change of it, made
|
|
259
|
+
* by any native screen or by the page through a `shareStorage` key, is
|
|
260
|
+
* `SESSION_CHANGED { key, token, previous }` on every native screen and a
|
|
261
|
+
* `craftsession` event on the page.
|
|
262
|
+
*/
|
|
263
|
+
sessionTokenKey?: string;
|
|
256
264
|
/**
|
|
257
265
|
* Crash and error reporting to bughq (CraftCrashReporter.swift): crashes
|
|
258
266
|
* and hangs (signal handlers, NSException, MetricKit), uncaught JavaScript
|
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -670,6 +670,7 @@ final class CraftChrome: ObservableObject {
|
|
|
670
670
|
|
|
671
671
|
@Published var tabs: [Tab] = []
|
|
672
672
|
@Published var tabAccessory: Tab?
|
|
673
|
+
private weak var tabAccessoryOwner: AnyObject?
|
|
673
674
|
@Published var selected: String?
|
|
674
675
|
let tabSwipeProgress = CraftTabSwipeProgress()
|
|
675
676
|
private(set) var tabSwipePosition: CGFloat? {
|
|
@@ -771,7 +772,7 @@ final class CraftChrome: ObservableObject {
|
|
|
771
772
|
tabs[index] = next
|
|
772
773
|
}
|
|
773
774
|
/// A destination beside the tab pill, outside the tab stacks and swipe order.
|
|
774
|
-
func updateTabAccessory(_ item: [String: Any]?) {
|
|
775
|
+
func updateTabAccessory(_ item: [String: Any]?, owner: AnyObject? = nil) {
|
|
775
776
|
let next: Tab?
|
|
776
777
|
if let item, let path = item["path"] as? String,
|
|
777
778
|
path.hasPrefix("/"), !path.hasPrefix("//"),
|
|
@@ -782,10 +783,17 @@ final class CraftChrome: ObservableObject {
|
|
|
782
783
|
} else {
|
|
783
784
|
next = nil
|
|
784
785
|
}
|
|
786
|
+
tabAccessoryOwner = owner
|
|
785
787
|
guard tabAccessory != next else { return }
|
|
786
788
|
tabAccessory = next
|
|
787
789
|
}
|
|
788
790
|
|
|
791
|
+
/// A retained or prepared screen cannot clear another screen's chrome.
|
|
792
|
+
func clearTabAccessory(ownedBy owner: AnyObject) {
|
|
793
|
+
guard tabAccessoryOwner === owner else { return }
|
|
794
|
+
updateTabAccessory(nil)
|
|
795
|
+
}
|
|
796
|
+
|
|
789
797
|
func setHiddenByScreen(_ hidden: Bool) {
|
|
790
798
|
guard hiddenByScreen != hidden else { return }
|
|
791
799
|
hiddenByScreen = hidden
|
|
@@ -1611,6 +1619,10 @@ struct CraftConfig: Codable {
|
|
|
1611
1619
|
/// Hybrid: web-storage keys the page's own code writes, mirrored into the
|
|
1612
1620
|
/// Keychain under another name for native screens (`{"auth_token": "auth.token"}`).
|
|
1613
1621
|
var shareStorage: [String: String]? = nil
|
|
1622
|
+
/// The secure-storage key the signed-in account's token lives under
|
|
1623
|
+
/// (`auth.token` unless set): `craft.session` follows it, and a change of
|
|
1624
|
+
/// it is `SESSION_CHANGED` on every screen (CraftSession).
|
|
1625
|
+
var sessionTokenKey: String? = nil
|
|
1614
1626
|
/// The bundle route of the app script: a screen-less script that runs for
|
|
1615
1627
|
/// the app's whole life and hears the app's events (CraftHybrid.swift).
|
|
1616
1628
|
var nativeApp: String? = nil
|
|
@@ -488,12 +488,16 @@ final class CraftHybrid: NSObject {
|
|
|
488
488
|
func pageScript() -> String {
|
|
489
489
|
let routeList = (config.nativeScreens ?? [:]).map { ["pattern": $0.key, "screen": $0.value] }
|
|
490
490
|
let share = Array((config.shareStorage ?? [:]).keys).sorted()
|
|
491
|
+
// The page's key for the sign-in token, when it shares one.
|
|
492
|
+
let tokenKey = CraftSession.tokenKey(config)
|
|
493
|
+
let session = (config.shareStorage ?? [:]).filter { $0.value == tokenKey }.map(\.key).sorted().first
|
|
491
494
|
func json(_ value: Any) -> String {
|
|
492
495
|
(try? JSONSerialization.data(withJSONObject: value)).flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
|
|
493
496
|
}
|
|
494
497
|
return Self.pageScriptTemplate
|
|
495
498
|
.replacingOccurrences(of: "__CRAFT_HYBRID_ROUTES__", with: json(routeList))
|
|
496
499
|
.replacingOccurrences(of: "__CRAFT_HYBRID_SHARE__", with: json(share))
|
|
500
|
+
.replacingOccurrences(of: "__CRAFT_HYBRID_SESSION__", with: session.map { json([$0]) } ?? "[]")
|
|
497
501
|
}
|
|
498
502
|
|
|
499
503
|
// CRAFT_HYBRID_PAGE_SCRIPT_START
|
|
@@ -691,13 +695,46 @@ final class CraftHybrid: NSObject {
|
|
|
691
695
|
|
|
692
696
|
// Web storage the config shares with native screens (`shareStorage`).
|
|
693
697
|
var shared = {};
|
|
694
|
-
|
|
698
|
+
// The page's key for the sign-in token (`sessionTokenKey`), if shared.
|
|
699
|
+
var SESSION = (__CRAFT_HYBRID_SESSION__)[0] || null;
|
|
700
|
+
var sessionListeners = [];
|
|
701
|
+
function stored(key) {
|
|
695
702
|
var value = null;
|
|
696
703
|
try { value = window.sessionStorage.getItem(key); } catch (e) {}
|
|
697
704
|
try { if (value === null) value = window.localStorage.getItem(key); } catch (e) {}
|
|
705
|
+
return value;
|
|
706
|
+
}
|
|
707
|
+
function sessionToken(value) { return typeof value === 'string' && value !== '' ? value : null; }
|
|
708
|
+
function share(key) {
|
|
709
|
+
var value = stored(key);
|
|
698
710
|
if (shared[key] === value) return;
|
|
711
|
+
var known = Object.prototype.hasOwnProperty.call(shared, key);
|
|
712
|
+
var before = shared[key];
|
|
699
713
|
shared[key] = value;
|
|
700
714
|
post({ type: 'shareStorage', key: key, value: value });
|
|
715
|
+
// A sign-in, sign-out or another account, made here or on a native
|
|
716
|
+
// screen: `craftsession { key, token, previous }` and craft.session.
|
|
717
|
+
if (key === SESSION && known && sessionToken(before) !== sessionToken(value)) {
|
|
718
|
+
var change = { key: key, token: sessionToken(value), previous: sessionToken(before) };
|
|
719
|
+
sessionListeners.slice().forEach(function (callback) {
|
|
720
|
+
try { callback(change.token, change.previous); } catch (e) { console.error(e); }
|
|
721
|
+
});
|
|
722
|
+
try { window.dispatchEvent(new CustomEvent('craftsession', { detail: change })); } catch (e) {}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (SESSION && window.craft && !window.craft.session) {
|
|
726
|
+
window.craft.session = {
|
|
727
|
+
key: SESSION,
|
|
728
|
+
token: function () { return sessionToken(stored(SESSION)); },
|
|
729
|
+
onChange: function (callback) {
|
|
730
|
+
if (typeof callback !== 'function') throw new TypeError('craft.session.onChange needs a function');
|
|
731
|
+
sessionListeners.push(callback);
|
|
732
|
+
return function () {
|
|
733
|
+
var index = sessionListeners.indexOf(callback);
|
|
734
|
+
if (index >= 0) sessionListeners.splice(index, 1);
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
};
|
|
701
738
|
}
|
|
702
739
|
if (SHARE.length && window.Storage) {
|
|
703
740
|
var proto = window.Storage.prototype;
|
|
@@ -808,7 +845,9 @@ final class CraftHybrid: NSObject {
|
|
|
808
845
|
guard let nativeKey = config.shareStorage?[key] else { return }
|
|
809
846
|
// The page's own write, or its echo of one a native screen made.
|
|
810
847
|
pendingShared.removeValue(forKey: key)
|
|
811
|
-
|
|
848
|
+
// The page's sign-in or sign-out moves the account for every
|
|
849
|
+
// screen; an echo writes what is already there and moves nothing.
|
|
850
|
+
CraftSession.observe(key: nativeKey, config: config) { CraftHybridKeychain.set(nativeKey, value: value) }
|
|
812
851
|
case .navigated(let path, let direction, let depth, let byHost):
|
|
813
852
|
pagePath = path
|
|
814
853
|
pageDepth = depth
|
|
@@ -205,11 +205,21 @@ enum CraftNativeActions {
|
|
|
205
205
|
complete(requestToken, failure("CAPABILITY_DISABLED", "Secure storage is disabled"), completion)
|
|
206
206
|
return
|
|
207
207
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
let write = { () -> Result<Any, CraftNativeActionError> in
|
|
209
|
+
let result = secureStorage(method: method, args: args)
|
|
210
|
+
// A key the web page shares (`shareStorage`): the page gets it too,
|
|
211
|
+
// so a sign-in on a native screen signs the page in.
|
|
212
|
+
if case .success = result, ["set", "remove", "delete"].contains(method), let key = args.first as? String {
|
|
213
|
+
CraftHybrid.shared.nativeWroteShared(nativeKey: key, value: method == "set" ? args.count > 1 ? args[1] as? String : nil : nil)
|
|
214
|
+
}
|
|
215
|
+
return result
|
|
216
|
+
}
|
|
217
|
+
// A write that moves the sign-in token is the account's change:
|
|
218
|
+
// every screen hears it (CraftSession). Reads skip the check.
|
|
219
|
+
if ["set", "remove", "delete", "clear"].contains(method) {
|
|
220
|
+
sync = CraftSession.observe(key: method == "clear" ? nil : args.first as? String, config: config, write)
|
|
221
|
+
} else {
|
|
222
|
+
sync = write()
|
|
213
223
|
}
|
|
214
224
|
case ("Biometrics", "isAvailable"):
|
|
215
225
|
sync = config.enableBiometric ? .success(biometricContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)) : failure("CAPABILITY_DISABLED", "Biometric authentication is disabled")
|
|
@@ -1423,6 +1433,54 @@ extension CraftNativeActions {
|
|
|
1423
1433
|
}
|
|
1424
1434
|
}
|
|
1425
1435
|
|
|
1436
|
+
/// The signed-in account: the token kept in secure storage under
|
|
1437
|
+
/// `sessionTokenKey` (default `auth.token`). Whoever moves it, a native
|
|
1438
|
+
/// screen's `craft.secureStorage`, the page's own sign-in through a
|
|
1439
|
+
/// `shareStorage` key, or a sign-out's clear, posts `changed` once with
|
|
1440
|
+
/// `key`, `token` and `previous` (NSNull when signed out), after the Keychain
|
|
1441
|
+
/// holds the new value. Each native screen hands it to its script as
|
|
1442
|
+
/// `SESSION_CHANGED` and to `craft.session.onChange` (CraftNativeScreen).
|
|
1443
|
+
enum CraftSession {
|
|
1444
|
+
static let changed = Notification.Name("craftSessionChanged")
|
|
1445
|
+
static let defaultKey = "auth.token"
|
|
1446
|
+
/// One write and its before and after at a time, so two quick writes
|
|
1447
|
+
/// from different queues are two changes, never one reported twice.
|
|
1448
|
+
private static let lock = NSLock()
|
|
1449
|
+
|
|
1450
|
+
static func tokenKey(_ config: CraftConfig) -> String {
|
|
1451
|
+
guard let key = config.sessionTokenKey, !key.isEmpty else { return defaultKey }
|
|
1452
|
+
return key
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/// The token now, as `craft.secureStorage.getSync` reads it; nil when
|
|
1456
|
+
/// signed out (an empty token is no account either).
|
|
1457
|
+
static func token(_ config: CraftConfig) -> String? {
|
|
1458
|
+
guard config.enableSecureStorage,
|
|
1459
|
+
let value = CraftNativeActions.webSecureValue(forKey: tokenKey(config)), !value.isEmpty else { return nil }
|
|
1460
|
+
return value
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/// Run `write`, and post `changed` when it moved the token. `key` is the
|
|
1464
|
+
/// key written; nil for a clear, which can remove every key.
|
|
1465
|
+
static func observe<T>(key: String?, config: CraftConfig, _ write: () -> T) -> T {
|
|
1466
|
+
let name = tokenKey(config)
|
|
1467
|
+
guard key == nil || key == name else { return write() }
|
|
1468
|
+
lock.lock()
|
|
1469
|
+
let before = token(config)
|
|
1470
|
+
let result = write()
|
|
1471
|
+
let after = token(config)
|
|
1472
|
+
lock.unlock()
|
|
1473
|
+
if before != after { post(key: name, token: after, previous: before) }
|
|
1474
|
+
return result
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
static func post(key: String, token: String?, previous: String?) {
|
|
1478
|
+
let info: [String: Any] = ["key": key, "token": token ?? NSNull(), "previous": previous ?? NSNull()]
|
|
1479
|
+
let send = { NotificationCenter.default.post(name: changed, object: nil, userInfo: info) }
|
|
1480
|
+
if Thread.isMainThread { send() } else { DispatchQueue.main.async(execute: send) }
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1426
1484
|
/// Last-known data for a screen's first frame: one JSON file per name in
|
|
1427
1485
|
/// `Application Support/craft-snapshots/<name>.json`. A web page writes them
|
|
1428
1486
|
/// (`snapshots.set` in the mobile API) and a native screen reads them
|
|
@@ -29,6 +29,7 @@ struct CraftNativeMutationResult {
|
|
|
29
29
|
final class CraftNativeMutationDocument {
|
|
30
30
|
private struct Node {
|
|
31
31
|
let id: String
|
|
32
|
+
let key: String?
|
|
32
33
|
var type: String
|
|
33
34
|
var props: [String: Any]
|
|
34
35
|
var style: [String: Any]
|
|
@@ -177,6 +178,7 @@ final class CraftNativeMutationDocument {
|
|
|
177
178
|
}
|
|
178
179
|
nodes[id] = Node(
|
|
179
180
|
id: id,
|
|
181
|
+
key: value["key"] as? String,
|
|
180
182
|
type: type,
|
|
181
183
|
props: dictionary(value["props"]),
|
|
182
184
|
style: dictionary(value["style"]),
|
|
@@ -330,6 +332,7 @@ final class CraftNativeMutationDocument {
|
|
|
330
332
|
"style": node.style,
|
|
331
333
|
"events": node.events,
|
|
332
334
|
]
|
|
335
|
+
if let key = node.key { value["key"] = key }
|
|
333
336
|
let text: [Any] = node.text
|
|
334
337
|
let children: [Any] = node.children.compactMap { materialize($0, nodes: nodes) }
|
|
335
338
|
value["children"] = text + children
|
|
@@ -350,6 +353,7 @@ final class CraftNativeMutationDocument {
|
|
|
350
353
|
let children = value["children"] as? [Any] ?? []
|
|
351
354
|
nodes[id] = Node(
|
|
352
355
|
id: id,
|
|
356
|
+
key: value["key"] as? String,
|
|
353
357
|
type: value["type"] as? String ?? "View",
|
|
354
358
|
props: props,
|
|
355
359
|
style: dictionary(value["style"]),
|
|
@@ -1435,21 +1435,47 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1435
1435
|
|
|
1436
1436
|
private func configureMarkerView(_ view: MKAnnotationView, id: String) {
|
|
1437
1437
|
if let bearing = markerBearings[id] {
|
|
1438
|
-
let
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
// the contrasting route arrow when displayed over map content.
|
|
1442
|
-
view.image = UIImage(systemName: "arrow.up.circle.fill", withConfiguration: configuration)?
|
|
1443
|
-
.withRenderingMode(.alwaysOriginal)
|
|
1438
|
+
let color = (markerPinColors[id] ?? .systemGreen).resolvedColor(with: traitCollection)
|
|
1439
|
+
view.tintColor = color
|
|
1440
|
+
view.image = Self.markerImage(color: color, direction: true)
|
|
1444
1441
|
view.transform = CGAffineTransform(rotationAngle: CGFloat((bearing - camera.heading) * .pi / 180))
|
|
1445
1442
|
} else {
|
|
1446
|
-
|
|
1447
|
-
view.
|
|
1448
|
-
|
|
1443
|
+
let color = (markerPinColors[id] ?? .systemRed).resolvedColor(with: traitCollection)
|
|
1444
|
+
view.tintColor = color
|
|
1445
|
+
view.image = Self.markerImage(color: color, direction: false)
|
|
1449
1446
|
view.transform = .identity
|
|
1450
1447
|
}
|
|
1451
1448
|
}
|
|
1452
1449
|
|
|
1450
|
+
private static let markerImages: NSCache<NSString, UIImage> = {
|
|
1451
|
+
let cache = NSCache<NSString, UIImage>()
|
|
1452
|
+
cache.countLimit = 64
|
|
1453
|
+
return cache
|
|
1454
|
+
}()
|
|
1455
|
+
|
|
1456
|
+
private static func markerImage(color: UIColor, direction: Bool) -> UIImage {
|
|
1457
|
+
let key = "\(direction ? "direction" : "pin"):\(color.description)" as NSString
|
|
1458
|
+
if let cached = markerImages.object(forKey: key) { return cached }
|
|
1459
|
+
let size = direction ? CGSize(width: 20, height: 24) : CGSize(width: 28, height: 28)
|
|
1460
|
+
// Explicit native drawing preserves the light backing independently
|
|
1461
|
+
// of MapKit's tint and SF Symbols' multicolor rendering preferences.
|
|
1462
|
+
let image = UIGraphicsImageRenderer(size: size).image { _ in
|
|
1463
|
+
let border = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size).insetBy(dx: 1, dy: 1),
|
|
1464
|
+
cornerRadius: (size.width - 2) / 2)
|
|
1465
|
+
UIColor.white.setFill()
|
|
1466
|
+
border.fill()
|
|
1467
|
+
color.setStroke()
|
|
1468
|
+
border.lineWidth = 1.5
|
|
1469
|
+
border.stroke()
|
|
1470
|
+
let symbol = UIImage(systemName: direction ? "arrow.up" : "mappin",
|
|
1471
|
+
withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold))?
|
|
1472
|
+
.withTintColor(color, renderingMode: .alwaysOriginal)
|
|
1473
|
+
symbol?.draw(in: direction ? CGRect(x: 5, y: 6, width: 10, height: 12) : CGRect(x: 7, y: 5, width: 14, height: 18))
|
|
1474
|
+
}.withRenderingMode(.alwaysOriginal)
|
|
1475
|
+
markerImages.setObject(image, forKey: key)
|
|
1476
|
+
return image
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1453
1479
|
override func layoutSubviews() {
|
|
1454
1480
|
super.layoutSubviews()
|
|
1455
1481
|
if tileTemplate != nil {
|
|
@@ -4621,10 +4647,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4621
4647
|
super.viewWillDisappear(animated)
|
|
4622
4648
|
screenVisibilityChanged(false)
|
|
4623
4649
|
stopCelebrations(in: view)
|
|
4624
|
-
//
|
|
4625
|
-
//
|
|
4626
|
-
|
|
4627
|
-
CraftChrome.shared.updateTabAccessory(nil)
|
|
4650
|
+
// Release only this screen's accessory. A retained or prepared screen
|
|
4651
|
+
// can disappear after the destination has already published its own.
|
|
4652
|
+
CraftChrome.shared.clearTabAccessory(ownedBy: self)
|
|
4628
4653
|
// Popped (or its stack dismissed) while the bar was away: it comes
|
|
4629
4654
|
// back for whatever shows next, a web page included.
|
|
4630
4655
|
if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
|
|
@@ -4669,7 +4694,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4669
4694
|
private func restoreShellChrome() {
|
|
4670
4695
|
guard !isAppScript else { return }
|
|
4671
4696
|
CraftChrome.shared.setHiddenByScreen(tabBarHidden)
|
|
4672
|
-
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
4697
|
+
CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
|
|
4673
4698
|
}
|
|
4674
4699
|
|
|
4675
4700
|
/// The screen came on screen or went off it: a screen pushed over it was
|
|
@@ -4830,6 +4855,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4830
4855
|
let appURL = Self.jsonString(config.devServerURL ?? "")
|
|
4831
4856
|
let appVersion = Self.jsonString(info?["CFBundleShortVersionString"] as? String ?? "")
|
|
4832
4857
|
let appBuild = Self.jsonString(info?["CFBundleVersion"] as? String ?? "")
|
|
4858
|
+
let sessionTokenKey = Self.jsonString(CraftSession.tokenKey(config))
|
|
4833
4859
|
jsContext.evaluateScript("""
|
|
4834
4860
|
// JavaScriptCore has no DOM window, but browser-safe SDK bridges
|
|
4835
4861
|
// address the host through `window.craft`. Keep native screens
|
|
@@ -4848,6 +4874,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4848
4874
|
appURL: \(appURL),
|
|
4849
4875
|
appVersion: \(appVersion),
|
|
4850
4876
|
appBuild: \(appBuild),
|
|
4877
|
+
sessionTokenKey: \(sessionTokenKey),
|
|
4851
4878
|
postMessage: function(message) { craftNativePostMessage(message); },
|
|
4852
4879
|
onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
|
|
4853
4880
|
};
|
|
@@ -5249,6 +5276,33 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5249
5276
|
return value === undefined ? null : value;
|
|
5250
5277
|
}
|
|
5251
5278
|
});
|
|
5279
|
+
// The signed-in account (CraftSession): the token under
|
|
5280
|
+
// `sessionTokenKey`, and each change of it, which the host
|
|
5281
|
+
// reports through `__craftSessionChanged` after the Keychain
|
|
5282
|
+
// holds it, beside the `SESSION_CHANGED` bridge message.
|
|
5283
|
+
var sessionListeners = [];
|
|
5284
|
+
var sessionKey = globalThis.__stxNativeBridge && __stxNativeBridge.sessionTokenKey || 'auth.token';
|
|
5285
|
+
function sessionToken() {
|
|
5286
|
+
var value = craftNativeSecureGet(String(sessionKey));
|
|
5287
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
5288
|
+
}
|
|
5289
|
+
globalThis.__craftSessionChanged = function(token, previous) {
|
|
5290
|
+
sessionListeners.slice().forEach(function(callback) {
|
|
5291
|
+
try { callback(token, previous); } catch (error) { console.error('[session] ' + (error && error.stack || error)); }
|
|
5292
|
+
});
|
|
5293
|
+
};
|
|
5294
|
+
keep('session', {
|
|
5295
|
+
key: sessionKey,
|
|
5296
|
+
token: sessionToken,
|
|
5297
|
+
onChange: function(callback) {
|
|
5298
|
+
if (typeof callback !== 'function') throw new TypeError('craft.session.onChange needs a function');
|
|
5299
|
+
sessionListeners.push(callback);
|
|
5300
|
+
return function() {
|
|
5301
|
+
var index = sessionListeners.indexOf(callback);
|
|
5302
|
+
if (index >= 0) sessionListeners.splice(index, 1);
|
|
5303
|
+
};
|
|
5304
|
+
}
|
|
5305
|
+
});
|
|
5252
5306
|
keep('navigation', {
|
|
5253
5307
|
setOptions: function(options) { craftNativeSetOptions(JSON.stringify(options || {})); }
|
|
5254
5308
|
});
|
|
@@ -5340,7 +5394,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5340
5394
|
if options.keys.contains("tabAccessory") {
|
|
5341
5395
|
tabAccessory = options["tabAccessory"] as? [String: Any]
|
|
5342
5396
|
if navigationController?.topViewController === self, view.window != nil {
|
|
5343
|
-
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
5397
|
+
CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
|
|
5344
5398
|
}
|
|
5345
5399
|
}
|
|
5346
5400
|
if options.keys.contains("title") {
|
|
@@ -5714,6 +5768,22 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5714
5768
|
guard let name = notification.userInfo?["name"] as? String else { return }
|
|
5715
5769
|
self?.send(type: "SNAPSHOT_CHANGED", payload: ["name": name])
|
|
5716
5770
|
})
|
|
5771
|
+
if config.enableSecureStorage {
|
|
5772
|
+
// A sign-in, sign-out or another account, made on any screen or
|
|
5773
|
+
// the page (CraftSession): `SESSION_CHANGED { key, token,
|
|
5774
|
+
// previous }` for the runtime, then craft.session's listeners.
|
|
5775
|
+
lifecycleObservers.append(NotificationCenter.default.addObserver(
|
|
5776
|
+
forName: CraftSession.changed,
|
|
5777
|
+
object: nil,
|
|
5778
|
+
queue: .main
|
|
5779
|
+
) { [weak self] notification in
|
|
5780
|
+
guard let self, let info = notification.userInfo, let key = info["key"] as? String else { return }
|
|
5781
|
+
let token = info["token"] as? String
|
|
5782
|
+
let previous = info["previous"] as? String
|
|
5783
|
+
self.send(type: "SESSION_CHANGED", payload: ["key": key, "token": token ?? NSNull(), "previous": previous ?? NSNull()])
|
|
5784
|
+
self.jsContext.objectForKeyedSubscript("__craftSessionChanged")?.call(withArguments: [token ?? NSNull(), previous ?? NSNull()])
|
|
5785
|
+
})
|
|
5786
|
+
}
|
|
5717
5787
|
if config.enablePushNotifications {
|
|
5718
5788
|
// A new device token (iOS hands one out after a reinstall or a
|
|
5719
5789
|
// restore): every screen's `craft.push.onToken` hears it.
|
|
@@ -6865,7 +6935,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6865
6935
|
|
|
6866
6936
|
private func explicitKey(_ node: [String: Any], props: [String: Any]) -> String? {
|
|
6867
6937
|
if let authored = authoredKey(node, props: props) { return authored }
|
|
6868
|
-
|
|
6938
|
+
// Repeated test selectors are valid for list rows. Retain each row by
|
|
6939
|
+
// its unique protocol ID before considering legacy selector identity.
|
|
6940
|
+
return [node["id"], props["testID"]]
|
|
6941
|
+
.compactMap { $0 as? String }
|
|
6942
|
+
.first { !$0.isEmpty }
|
|
6869
6943
|
}
|
|
6870
6944
|
|
|
6871
6945
|
/// The key a node's author gave it. A wrapper of one child (a
|
|
@@ -6877,7 +6951,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6877
6951
|
private func authoredKey(_ node: [String: Any], props: [String: Any]) -> String? {
|
|
6878
6952
|
// The current @stacksjs/stx native compiler emits key in props; accept the IR
|
|
6879
6953
|
// field too so keyed children keep working when the compiler adopts it.
|
|
6880
|
-
let direct = [node["key"], props["key"]
|
|
6954
|
+
let direct = [node["key"], props["key"]]
|
|
6881
6955
|
.compactMap { $0 as? String }
|
|
6882
6956
|
.first { !$0.isEmpty }
|
|
6883
6957
|
if let direct { return direct }
|
|
@@ -448,7 +448,11 @@ final class DriverTests: XCTestCase {
|
|
|
448
448
|
var found = element(containing: arg)
|
|
449
449
|
var swipes = 0
|
|
450
450
|
while (found == nil || !(found!.isHittable)) && swipes < 8 {
|
|
451
|
-
app.
|
|
451
|
+
if let target = found, !target.frame.isEmpty, target.frame.midY < app.frame.midY {
|
|
452
|
+
app.swipeDown()
|
|
453
|
+
} else {
|
|
454
|
+
app.swipeUp()
|
|
455
|
+
}
|
|
452
456
|
swipes += 1
|
|
453
457
|
found = element(containing: arg)
|
|
454
458
|
}
|