@craft-native/ios 0.0.134 → 0.0.136

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/README.md CHANGED
@@ -505,6 +505,13 @@ await window.craft.restorePurchases();
505
505
  window.craft.setKeepAwake(true); // Prevent screen dimming
506
506
  window.craft.setKeepAwake(false); // Allow screen dimming
507
507
 
508
+ // Screen brightness, 0 to 1: always available (no config flag). It is the
509
+ // device's backlight, so put it back: `brightness` from 'craft-native/mobile'
510
+ // remembers the person's level, restores it, and releases it while the app is
511
+ // in the background.
512
+ const level = await window.craft.getBrightness(); // 0.62
513
+ await window.craft.setBrightness(0.1); // resolves with what the screen reports
514
+
508
515
  // Speech: short spoken cues that duck the person's music instead of stopping
509
516
  // it, and play with the silent switch on. Always available (no config flag).
510
517
  // Resolves when the utterance ends: true if spoken, false if stopped or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.134",
3
+ "version": "0.0.136",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -535,8 +535,25 @@ final class CraftChrome: ObservableObject {
535
535
  private var occupied: CGFloat = 0
536
536
  private var pendingHide: DispatchWorkItem?
537
537
  private var splashDeadline: DispatchWorkItem?
538
+ /// The keyboard is up and covers the tab bar, so the page need not leave
539
+ /// room for it: the web view ends at the keyboard (CraftHybrid's webHost).
540
+ private var keyboardUp = false
538
541
 
539
- private init() {}
542
+ private init() {
543
+ let center = NotificationCenter.default
544
+ center.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { [weak self] _ in
545
+ self?.keyboardChanged(up: true)
546
+ }
547
+ center.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { [weak self] _ in
548
+ self?.keyboardChanged(up: false)
549
+ }
550
+ }
551
+
552
+ private func keyboardChanged(up: Bool) {
553
+ guard up != keyboardUp else { return }
554
+ keyboardUp = up
555
+ publishLayout()
556
+ }
540
557
 
541
558
  /// The tab bar from `tabs` in craft.config.json, there before the page
542
559
  /// loads; the page's own `tabBar` message replaces it later. `splash`
@@ -685,7 +702,7 @@ final class CraftChrome: ObservableObject {
685
702
  /// The bar's height as `--craft-tab-bar-height`, so the page leaves room
686
703
  /// for it, and as an event for anything that measures.
687
704
  func publishLayout() {
688
- let height = tabBarVisible ? Int(occupied) : 0
705
+ let height = tabBarVisible && !keyboardUp ? Int(occupied) : 0
689
706
  CraftHybrid.shared.tabBarCovers(CGFloat(height))
690
707
  let script = "document.documentElement.style.setProperty('--craft-tab-bar-height','\(height)px');window.dispatchEvent(new CustomEvent('craftTabBarLayout',{detail:{height:\(height)}}));"
691
708
  DispatchQueue.main.async { [weak self] in self?.webView?.evaluateJavaScript(script, completionHandler: nil) }
@@ -2140,6 +2157,15 @@ struct CraftWebView: UIViewRepresentable {
2140
2157
  } else {
2141
2158
  rejectCallback(callbackId, error: "Keep awake is disabled", code: "CAPABILITY_DISABLED")
2142
2159
  }
2160
+ // MARK: - Screen Brightness
2161
+ case "getBrightness":
2162
+ getBrightness(callbackId: callbackId)
2163
+ case "setBrightness":
2164
+ if let level = (body["level"] as? NSNumber)?.doubleValue, level.isFinite {
2165
+ setBrightness(level, callbackId: callbackId)
2166
+ } else {
2167
+ rejectCallback(callbackId, error: "setBrightness was called without the values it needs", code: "INVALID_ARGUMENT")
2168
+ }
2143
2169
  // MARK: - Orientation Lock
2144
2170
  case "lockOrientation":
2145
2171
  if config.enableOrientationLock {
@@ -3102,6 +3128,7 @@ struct CraftWebView: UIViewRepresentable {
3102
3128
  orientationLock: \(config.enableOrientationLock),
3103
3129
  deepLinks: \(config.enableDeepLinks),
3104
3130
  flashlight: true,
3131
+ brightness: true,
3105
3132
  speech: true,
3106
3133
  network: true,
3107
3134
  deviceInfo: true,
@@ -4041,6 +4068,25 @@ struct CraftWebView: UIViewRepresentable {
4041
4068
  });
4042
4069
  },
4043
4070
 
4071
+ // Screen brightness, 0 to 1. Each resolves with the level the
4072
+ // screen then reports.
4073
+ getBrightness: function() {
4074
+ var self = this;
4075
+ var id = 'cb_' + (++this._callbackId);
4076
+ window.webkit.messageHandlers.craft.postMessage({action: 'getBrightness', callbackId: id});
4077
+ return new Promise(function(resolve, reject) {
4078
+ self._callbacks[id] = {resolve: resolve, reject: reject};
4079
+ });
4080
+ },
4081
+ setBrightness: function(level) {
4082
+ var self = this;
4083
+ var id = 'cb_' + (++this._callbackId);
4084
+ window.webkit.messageHandlers.craft.postMessage({action: 'setBrightness', level: level, callbackId: id});
4085
+ return new Promise(function(resolve, reject) {
4086
+ self._callbacks[id] = {resolve: resolve, reject: reject};
4087
+ });
4088
+ },
4089
+
4044
4090
  // Speech synthesis. speak() settles when the utterance ends:
4045
4091
  // true when it was spoken, false when something stopped it.
4046
4092
  // No timeout, because a long sentence at a slow rate can
@@ -6236,6 +6282,25 @@ struct CraftWebView: UIViewRepresentable {
6236
6282
  }
6237
6283
  }
6238
6284
 
6285
+ // MARK: - Screen Brightness
6286
+ //
6287
+ // The device's backlight, 0 to 1. It is system-wide, not the app's:
6288
+ // a page that dims it reads it first and puts it back - when it is
6289
+ // done, and when the app leaves the foreground (@stacksjs/mobile's
6290
+ // screen API does both off the page's visibility).
6291
+ private func getBrightness(callbackId: String?) {
6292
+ DispatchQueue.main.async {
6293
+ self.resolveCallback(callbackId, result: Double(UIScreen.main.brightness))
6294
+ }
6295
+ }
6296
+
6297
+ private func setBrightness(_ level: Double, callbackId: String?) {
6298
+ DispatchQueue.main.async {
6299
+ UIScreen.main.brightness = CGFloat(min(max(level, 0), 1))
6300
+ self.resolveCallback(callbackId, result: Double(UIScreen.main.brightness))
6301
+ }
6302
+ }
6303
+
6239
6304
  // MARK: - Speech Synthesis
6240
6305
  //
6241
6306
  // Native rather than the page's own `speechSynthesis`, which in a
@@ -539,6 +539,14 @@ final class CraftHybrid: NSObject {
539
539
  }, function () { arriving = null; finish(); });
540
540
  });
541
541
  },
542
+ // Put the page at `path` in place of where it is, without a step in
543
+ // its history: the shell is back at a native screen and the page
544
+ // behind it had no history left to go back through (it reloaded).
545
+ reset: function (path) {
546
+ if (!(navigate && current && current.__craftHybrid)) { location.replace(path); return Promise.resolve(false); }
547
+ hostNav = path;
548
+ return Promise.resolve(navigate(path, { instant: true, replace: true }));
549
+ },
542
550
  setBase: function (value) { base = typeof value === 'number' ? value : null; },
543
551
  // Back to `target` in the page's stack, after the shell popped the
544
552
  // entries above it.
@@ -667,6 +675,11 @@ final class CraftHybrid: NSObject {
667
675
  webView?.evaluateJavaScript("window.__craftHybrid && window.__craftHybrid.setBase(\(value))", completionHandler: nil)
668
676
  }
669
677
 
678
+ func pageReset(to path: String) {
679
+ guard let data = try? JSONSerialization.data(withJSONObject: [path]), let json = String(data: data, encoding: .utf8) else { return }
680
+ webView?.evaluateJavaScript("window.__craftHybrid && window.__craftHybrid.reset && window.__craftHybrid.reset(\(json)[0])", completionHandler: nil)
681
+ }
682
+
670
683
  func pageBack(to depth: Int) {
671
684
  webView?.evaluateJavaScript("window.__craftHybrid && window.__craftHybrid.backTo(\(max(0, depth)))", completionHandler: nil)
672
685
  }
@@ -752,7 +765,10 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
752
765
 
753
766
  init(config: CraftConfig) {
754
767
  self.config = config
755
- webHost = UIHostingController(rootView: AnyView(CraftWebView(config: config).ignoresSafeArea()))
768
+ // Edge to edge, but not under the keyboard: the web view ends at its
769
+ // top. Under it, WebKit scrolled the page's layout viewport to show a
770
+ // focused field and carried every sticky header off the screen.
771
+ webHost = UIHostingController(rootView: AnyView(CraftWebView(config: config).ignoresSafeArea(.container)))
756
772
  super.init(nibName: nil, bundle: nil)
757
773
  hybrid.controller = self
758
774
  }
@@ -784,7 +800,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
784
800
  guard !webAttached else { return }
785
801
  webAttached = true
786
802
  webHost.view.backgroundColor = config.resolvedBackgroundColor
787
- if #available(iOS 16.4, *) { webHost.safeAreaRegions = [] }
803
+ if #available(iOS 16.4, *) { webHost.safeAreaRegions = [.keyboard] }
788
804
  if let slot = visibleStack?.topViewController as? CraftHybridWebSlot, slot.isViewLoaded {
789
805
  moveWeb(to: slot, in: slot.view)
790
806
  } else {
@@ -1109,6 +1125,16 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1109
1125
  if let depth = removed.compactMap({ ($0 as? CraftHybridWebSlot)?.depth }).min() {
1110
1126
  hybrid.pageBack(to: depth - 1)
1111
1127
  }
1128
+ // Back at a tab's native root while the page, with no history left
1129
+ // to go back through (it reloaded on the screen just popped), still
1130
+ // shows that screen: it moves to the root's path. Left there, the
1131
+ // next screen opened from the root was pushed on top of it and named
1132
+ // it in its Back button ("Me" instead of "Today").
1133
+ if !removed.isEmpty, !(viewController is CraftHybridWebSlot), navigationController.viewControllers.first === viewController,
1134
+ let rootPath = nativePaths[ObjectIdentifier(viewController)], !rootPath.contains(":"), hybrid.pageDepth == 0,
1135
+ let pagePath = hybrid.pagePath, CraftHybridRoutes.normalize(pagePath) != CraftHybridRoutes.normalize(rootPath) {
1136
+ hybrid.pageReset(to: rootPath)
1137
+ }
1112
1138
  guard navigationController === visibleStack else { return }
1113
1139
  if let slot = viewController as? CraftHybridWebSlot { adoptWeb(into: slot) } else { parkWeb() }
1114
1140
  }