craft-native 0.0.244 → 0.0.246

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.
@@ -205,11 +205,21 @@ enum CraftNativeActions {
205
205
  complete(requestToken, failure("CAPABILITY_DISABLED", "Secure storage is disabled"), completion)
206
206
  return
207
207
  }
208
- sync = secureStorage(method: method, args: args)
209
- // A key the web page shares (`shareStorage`): the page gets it too,
210
- // so a sign-in on a native screen signs the page in.
211
- if case .success = sync, ["set", "remove", "delete"].contains(method), let key = args.first as? String {
212
- CraftHybrid.shared.nativeWroteShared(nativeKey: key, value: method == "set" ? args.count > 1 ? args[1] as? String : nil : nil)
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
@@ -52,11 +52,19 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
52
52
  var onRecycle: ((String) -> Void)?
53
53
  override func prepareForReuse() {
54
54
  super.prepareForReuse()
55
- releaseHostedView()
55
+ recycle()
56
+ }
57
+
58
+ /// The row this cell showed is gone; its views stay in the cell, and
59
+ /// the next row it shows reuses them when it has the same shape.
60
+ func recycle() {
61
+ if let identity = representedIdentity { onRecycle?(identity) }
62
+ representedIdentity = nil
63
+ onRecycle = nil
56
64
  }
57
65
 
58
66
  func host(_ view: UIView, identity: String) {
59
- if representedIdentity != identity { releaseHostedView() }
67
+ if let shown = representedIdentity, shown != identity { recycle() }
60
68
  representedIdentity = identity
61
69
  guard hostedView !== view else { return }
62
70
  hostedView?.removeFromSuperview()
@@ -72,11 +80,9 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
72
80
  }
73
81
 
74
82
  func releaseHostedView() {
75
- if let identity = representedIdentity { onRecycle?(identity) }
83
+ recycle()
76
84
  hostedView?.removeFromSuperview()
77
85
  hostedView = nil
78
- representedIdentity = nil
79
- onRecycle = nil
80
86
  }
81
87
 
82
88
  override func preferredLayoutAttributesFitting(
@@ -354,7 +360,8 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
354
360
 
355
361
  @objc private func refreshTriggered() {
356
362
  refreshHandler?()
357
- DispatchQueue.main.async { [weak self] in
363
+ // Once the script has answered the pull, on its own thread or not.
364
+ CraftNativeScriptThread.afterScript { [weak self] in
358
365
  guard let self, !self.refreshingProp, self.nativeRefreshControl.isRefreshing else { return }
359
366
  self.nativeRefreshControl.endRefreshing()
360
367
  }