@capgo/capacitor-native-navigation 8.0.15 → 8.0.17

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.
@@ -44,9 +44,11 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
44
44
  private var tabBar: UITabBar?
45
45
  private var tabBarController: NativeNavigationTabController?
46
46
  private var tabViewControllers: [UIViewController] = []
47
+ private weak var systemTabRootContainer: UIView?
47
48
  private weak var originalWebViewSuperview: UIView?
48
49
  private var originalWebViewIndex: Int?
49
50
  private var originalWebViewAutoresizingMask: UIView.AutoresizingMask?
51
+ private var liftedWebViewOverlays: [NativeNavigationWeakView] = []
50
52
  private var isWebViewHostedInSystemTabController = false
51
53
  private var navbarHeight: CGFloat = 44
52
54
  private var tabbarHeight: CGFloat = 64
@@ -437,6 +439,13 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
437
439
  notifyTabSelect(index: item.tag)
438
440
  }
439
441
 
442
+ public func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
443
+ if usesSystemLiquidGlass {
444
+ hostWebView(in: viewController)
445
+ }
446
+ return true
447
+ }
448
+
440
449
  public func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
441
450
  guard !suppressTabSelectEvent else {
442
451
  hostWebViewInSelectedSystemTab()
@@ -552,43 +561,108 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
552
561
  controller.view.isHidden = !tabbarVisible
553
562
 
554
563
  if let parent = bridge?.viewController {
564
+ let containerView = systemTabHostingContainerView(in: parent)
555
565
  parent.addChild(controller)
556
- insertSystemTabControllerView(controller.view, in: parent.view)
566
+ insertSystemTabControllerView(controller.view, in: containerView)
557
567
  controller.didMove(toParent: parent)
558
568
  }
559
569
 
560
570
  self.tabBarController = controller
561
571
  self.tabBar = controller.tabBar
572
+ liftWebViewOverlaysAboveSystemTabs()
562
573
  hostWebViewInSelectedSystemTab()
563
574
  return controller.tabBar
564
575
  }
565
576
 
577
+ private func systemTabHostingContainerView(in parent: UIViewController) -> UIView {
578
+ if let systemTabRootContainer = systemTabRootContainer {
579
+ return systemTabRootContainer
580
+ }
581
+
582
+ guard let webView = webView,
583
+ parent.view === webView else {
584
+ return parent.view
585
+ }
586
+
587
+ let previousSuperview = webView.superview
588
+ let previousIndex = previousSuperview?.subviews.firstIndex(of: webView)
589
+ let previousFrame = webView.frame
590
+ let previousAutoresizingMask = webView.autoresizingMask
591
+ let container = UIView(frame: previousFrame)
592
+ container.backgroundColor = nativeNavigationFallbackBackground(for: webView)
593
+ container.isOpaque = true
594
+ container.autoresizingMask = previousAutoresizingMask.isEmpty ? [.flexibleWidth, .flexibleHeight] : previousAutoresizingMask
595
+
596
+ if let previousSuperview = previousSuperview {
597
+ previousSuperview.insertSubview(container, at: min(previousIndex ?? previousSuperview.subviews.count, previousSuperview.subviews.count))
598
+ }
599
+
600
+ parent.view = container
601
+ container.addSubview(webView)
602
+ webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
603
+ webView.frame = container.bounds
604
+ moveNativeChrome(from: webView, to: container)
605
+
606
+ systemTabRootContainer = container
607
+ originalWebViewSuperview = container
608
+ originalWebViewIndex = 0
609
+ originalWebViewAutoresizingMask = webView.autoresizingMask
610
+ liftWebViewOverlaysAboveSystemTabs()
611
+ return container
612
+ }
613
+
614
+ private func moveNativeChrome(from webView: UIView, to container: UIView) {
615
+ if let navContainer = navContainer,
616
+ navContainer.superview === webView {
617
+ container.addSubview(navContainer)
618
+ }
619
+ }
620
+
566
621
  private func applySystemTabBarItems(_ items: [UITabBarItem], selectedIndex: Int?, animated: Bool) {
567
622
  guard let tabBarController = tabBarController else {
568
623
  return
569
624
  }
570
625
 
571
626
  let previousSelectedIndex = tabBarController.selectedIndex
572
- let controllers = items.map { item -> UIViewController in
573
- let controller = NativeNavigationTabContentController()
574
- controller.tabBarItem = item
575
- return controller
576
- }
627
+ let controllers = systemTabContentControllers(for: items)
628
+ let currentControllers = tabBarController.viewControllers ?? []
629
+ let shouldUpdateControllers = currentControllers.count != controllers.count
630
+ || zip(currentControllers, controllers).contains { currentController, nextController in
631
+ currentController !== nextController
632
+ }
577
633
  let shouldAnimate = animated && tabBarController.viewControllers?.count == controllers.count
578
634
 
579
635
  suppressTabSelectEvent = true
580
- tabBarController.setViewControllers(controllers, animated: shouldAnimate)
636
+ if shouldUpdateControllers {
637
+ tabBarController.setViewControllers(controllers, animated: shouldAnimate)
638
+ }
581
639
  if !controllers.isEmpty {
582
640
  let fallbackIndex = selectedIndex ?? previousSelectedIndex
583
641
  let index = min(max(fallbackIndex, 0), controllers.count - 1)
642
+ hostWebView(in: controllers[index])
584
643
  tabBarController.selectedIndex = index
585
644
  }
586
- hostWebViewInSelectedSystemTab()
587
645
  suppressTabSelectEvent = false
588
646
 
589
647
  tabViewControllers = controllers
590
648
  }
591
649
 
650
+ private func systemTabContentControllers(for items: [UITabBarItem]) -> [UIViewController] {
651
+ let existingControllers = tabViewControllers.compactMap { $0 as? NativeNavigationTabContentController }
652
+ if existingControllers.count == items.count {
653
+ zip(existingControllers, items).forEach { controller, item in
654
+ controller.tabBarItem = item
655
+ }
656
+ return existingControllers
657
+ }
658
+
659
+ return items.map { item -> UIViewController in
660
+ let controller = NativeNavigationTabContentController()
661
+ controller.tabBarItem = item
662
+ return controller
663
+ }
664
+ }
665
+
592
666
  private func setSystemTabBarHidden(_ hidden: Bool) {
593
667
  guard let tabBarController = tabBarController else {
594
668
  return
@@ -617,6 +691,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
617
691
  tabBarController?.view.isHidden = false
618
692
  if usesSystemLiquidGlass {
619
693
  setSystemTabBarHidden(false)
694
+ liftWebViewOverlaysAboveSystemTabs()
620
695
  hostWebViewInSelectedSystemTab()
621
696
  } else {
622
697
  tabBar.isHidden = false
@@ -671,17 +746,53 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
671
746
  }
672
747
 
673
748
  private func hostWebViewInSelectedSystemTab() {
749
+ hostWebView(in: tabBarController?.selectedViewController)
750
+ }
751
+
752
+ private func hostWebView(in viewController: UIViewController?) {
674
753
  guard usesSystemLiquidGlass,
675
754
  let webView = webView,
676
- let selectedController = tabBarController?.selectedViewController as? NativeNavigationTabContentController else {
755
+ let selectedController = viewController as? NativeNavigationTabContentController else {
677
756
  return
678
757
  }
679
758
 
759
+ liftWebViewOverlaysAboveSystemTabs()
680
760
  captureOriginalWebViewPlacementIfNeeded(webView)
681
- clearHostedWebViews(matching: webView, except: selectedController)
682
- selectedController.host(webView: webView)
761
+ clearHostedWebViews(matching: webView, except: selectedController, preservingSnapshots: true)
762
+ guard selectedController.host(webView: webView) else {
763
+ isWebViewHostedInSystemTabController = false
764
+ return
765
+ }
683
766
  clearHostedWebViews(matching: webView, except: selectedController)
684
767
  isWebViewHostedInSystemTabController = true
768
+ bringLiftedWebViewOverlaysToFront()
769
+ }
770
+
771
+ private func liftWebViewOverlaysAboveSystemTabs() {
772
+ guard usesSystemLiquidGlass,
773
+ let webView = webView,
774
+ let container = systemTabRootContainer else {
775
+ return
776
+ }
777
+
778
+ nativeNavigationLiftWebViewOverlaySubviews(
779
+ from: webView,
780
+ to: container,
781
+ tracking: &liftedWebViewOverlays,
782
+ excluding: [navContainer, tabContainer, tabBarController?.view]
783
+ )
784
+ }
785
+
786
+ private func bringLiftedWebViewOverlaysToFront() {
787
+ guard let container = systemTabRootContainer else {
788
+ return
789
+ }
790
+
791
+ liftedWebViewOverlays = liftedWebViewOverlays.filter { $0.value != nil }
792
+ liftedWebViewOverlays
793
+ .compactMap(\.value)
794
+ .filter { $0.superview === container }
795
+ .forEach { container.bringSubviewToFront($0) }
685
796
  }
686
797
 
687
798
  private func restoreWebViewFromSystemTabController() {
@@ -700,11 +811,15 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
700
811
  isWebViewHostedInSystemTabController = false
701
812
  }
702
813
 
703
- private func clearHostedWebViews(matching webView: UIView, except owner: NativeNavigationTabContentController? = nil) {
814
+ private func clearHostedWebViews(
815
+ matching webView: UIView,
816
+ except owner: NativeNavigationTabContentController? = nil,
817
+ preservingSnapshots: Bool = false
818
+ ) {
704
819
  tabViewControllers
705
820
  .compactMap { $0 as? NativeNavigationTabContentController }
706
821
  .filter { $0 !== owner }
707
- .forEach { $0.clearHostedWebView(ifMatching: webView) }
822
+ .forEach { $0.clearHostedWebView(ifMatching: webView, preservingSnapshot: preservingSnapshots) }
708
823
  }
709
824
 
710
825
  private func makeBarButtonItems(_ rawItems: [[String: Any]], placement: String) -> [UIBarButtonItem] {
@@ -900,12 +1015,12 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
900
1015
 
901
1016
  private func transitionSnapshotView(from webView: UIView, sourceRect: CGRect?) -> UIView {
902
1017
  guard let sourceRect = sourceRect else {
903
- return webView.snapshotView(afterScreenUpdates: false) ?? UIView(frame: webView.frame)
1018
+ return webView.snapshotView(afterScreenUpdates: false) ?? nativeNavigationSnapshotPlaceholder(for: webView)
904
1019
  }
905
1020
 
906
1021
  let cropRect = sourceRect.intersection(webView.bounds)
907
1022
  guard cropRect.width > 0, cropRect.height > 0 else {
908
- return webView.snapshotView(afterScreenUpdates: false) ?? UIView(frame: webView.frame)
1023
+ return webView.snapshotView(afterScreenUpdates: false) ?? nativeNavigationSnapshotPlaceholder(for: webView)
909
1024
  }
910
1025
 
911
1026
  let renderer = UIGraphicsImageRenderer(bounds: webView.bounds)
@@ -921,7 +1036,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
921
1036
  ).integral
922
1037
 
923
1038
  guard let croppedImage = image.cgImage?.cropping(to: scaledCropRect) else {
924
- return webView.snapshotView(afterScreenUpdates: false) ?? UIView(frame: webView.frame)
1039
+ return webView.snapshotView(afterScreenUpdates: false) ?? nativeNavigationSnapshotPlaceholder(for: webView)
925
1040
  }
926
1041
 
927
1042
  let imageView = UIImageView(image: UIImage(cgImage: croppedImage, scale: scale, orientation: image.imageOrientation))
@@ -1142,6 +1257,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
1142
1257
  if let navContainer = navContainer {
1143
1258
  bridge?.viewController?.view.bringSubviewToFront(navContainer)
1144
1259
  }
1260
+ bringLiftedWebViewOverlaysToFront()
1145
1261
  return
1146
1262
  }
1147
1263
 
@@ -1367,22 +1483,82 @@ private final class NativeNavigationBar: UINavigationBar {
1367
1483
  }
1368
1484
  }
1369
1485
 
1370
- private final class NativeNavigationTabController: UITabBarController {
1486
+ final class NativeNavigationWeakView {
1487
+ weak var value: UIView?
1488
+
1489
+ init(_ value: UIView) {
1490
+ self.value = value
1491
+ }
1492
+ }
1493
+
1494
+ func nativeNavigationLiftWebViewOverlaySubviews(
1495
+ from webView: UIView,
1496
+ to container: UIView,
1497
+ tracking liftedOverlays: inout [NativeNavigationWeakView],
1498
+ excluding excludedViews: [UIView?] = []
1499
+ ) {
1500
+ webView.subviews
1501
+ .filter { nativeNavigationShouldLiftWebViewOverlay($0, excluding: excludedViews) }
1502
+ .forEach { overlay in
1503
+ let frame = overlay.convert(overlay.bounds, to: container)
1504
+ let hadParentConstraints = nativeNavigationDeactivateParentConstraints(in: webView, involving: overlay)
1505
+ overlay.removeFromSuperview()
1506
+ overlay.frame = frame
1507
+ if hadParentConstraints {
1508
+ overlay.translatesAutoresizingMaskIntoConstraints = true
1509
+ }
1510
+ overlay.autoresizingMask = overlay.autoresizingMask.isEmpty
1511
+ ? [.flexibleWidth, .flexibleHeight]
1512
+ : overlay.autoresizingMask
1513
+ container.addSubview(overlay)
1514
+ liftedOverlays.append(NativeNavigationWeakView(overlay))
1515
+ }
1516
+
1517
+ liftedOverlays = liftedOverlays.filter { $0.value != nil }
1518
+ liftedOverlays
1519
+ .compactMap(\.value)
1520
+ .filter { $0.superview === container }
1521
+ .forEach { container.bringSubviewToFront($0) }
1522
+ }
1523
+
1524
+ func nativeNavigationShouldLiftWebViewOverlay(_ view: UIView, excluding excludedViews: [UIView?] = []) -> Bool {
1525
+ if excludedViews.contains(where: { $0 === view }) {
1526
+ return false
1527
+ }
1528
+
1529
+ if view is UIScrollView {
1530
+ return false
1531
+ }
1532
+
1533
+ let className = NSStringFromClass(type(of: view))
1534
+ return !className.contains("WK")
1535
+ }
1536
+
1537
+ private func nativeNavigationDeactivateParentConstraints(in parent: UIView, involving view: UIView) -> Bool {
1538
+ let constraints = parent.constraints.filter { constraint in
1539
+ constraint.firstItem === view || constraint.secondItem === view
1540
+ }
1541
+ NSLayoutConstraint.deactivate(constraints)
1542
+ return !constraints.isEmpty
1543
+ }
1544
+
1545
+ final class NativeNavigationTabController: UITabBarController {
1371
1546
  override func viewDidLoad() {
1372
1547
  super.viewDidLoad()
1373
- view.backgroundColor = .clear
1374
- view.isOpaque = false
1548
+ view.backgroundColor = .systemBackground
1549
+ view.isOpaque = true
1375
1550
  tabBar.isTranslucent = true
1376
1551
  }
1377
1552
  }
1378
1553
 
1379
- private final class NativeNavigationTabContentController: UIViewController {
1554
+ final class NativeNavigationTabContentController: UIViewController {
1380
1555
  private weak var hostedWebView: UIView?
1556
+ private var snapshotPlaceholder: UIView?
1381
1557
 
1382
1558
  override func loadView() {
1383
1559
  let view = UIView()
1384
- view.backgroundColor = .clear
1385
- view.isOpaque = false
1560
+ view.backgroundColor = .systemBackground
1561
+ view.isOpaque = true
1386
1562
  self.view = view
1387
1563
  }
1388
1564
 
@@ -1395,26 +1571,58 @@ private final class NativeNavigationTabContentController: UIViewController {
1395
1571
  hostedWebView?.frame = view.bounds
1396
1572
  }
1397
1573
 
1398
- func clearHostedWebView(ifMatching webView: UIView? = nil) {
1574
+ func clearHostedWebView(ifMatching webView: UIView? = nil, preservingSnapshot: Bool = false) {
1399
1575
  guard webView == nil || hostedWebView === webView else {
1400
1576
  return
1401
1577
  }
1578
+
1579
+ if preservingSnapshot, let hostedWebView = hostedWebView, hostedWebView.superview === view {
1580
+ let placeholder = hostedWebView.snapshotView(afterScreenUpdates: false) ?? nativeNavigationSnapshotPlaceholder(for: hostedWebView)
1581
+ placeholder.frame = hostedWebView.frame
1582
+ placeholder.autoresizingMask = [.flexibleWidth, .flexibleHeight]
1583
+ snapshotPlaceholder?.removeFromSuperview()
1584
+ view.insertSubview(placeholder, belowSubview: hostedWebView)
1585
+ snapshotPlaceholder = placeholder
1586
+ }
1587
+
1402
1588
  hostedWebView = nil
1403
1589
  }
1404
1590
 
1405
- func host(webView: UIView) {
1406
- if hostedWebView !== webView {
1407
- hostedWebView = webView
1591
+ @discardableResult
1592
+ func host(webView: UIView) -> Bool {
1593
+ guard view !== webView, !view.isDescendant(of: webView) else {
1594
+ hostedWebView = nil
1595
+ return false
1408
1596
  }
1597
+
1598
+ snapshotPlaceholder?.removeFromSuperview()
1599
+ snapshotPlaceholder = nil
1600
+ hostedWebView = webView
1409
1601
  if webView.superview !== view {
1410
1602
  webView.removeFromSuperview()
1411
1603
  view.addSubview(webView)
1412
1604
  }
1413
1605
  webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
1414
1606
  webView.frame = view.bounds
1607
+ return true
1415
1608
  }
1416
1609
  }
1417
1610
 
1611
+ private func nativeNavigationFallbackBackground(for view: UIView) -> UIColor {
1612
+ if let color = view.backgroundColor,
1613
+ color.cgColor.alpha > 0 {
1614
+ return color
1615
+ }
1616
+ return .systemBackground
1617
+ }
1618
+
1619
+ private func nativeNavigationSnapshotPlaceholder(for view: UIView) -> UIView {
1620
+ let placeholder = UIView(frame: view.frame)
1621
+ placeholder.backgroundColor = nativeNavigationFallbackBackground(for: view)
1622
+ placeholder.isOpaque = true
1623
+ return placeholder
1624
+ }
1625
+
1418
1626
  private final class SVGIconRenderer: NSObject, XMLParserDelegate {
1419
1627
  private let context: CGContext
1420
1628
  private var styleStack = [SVGRenderStyle()]
@@ -1,19 +1,92 @@
1
1
  import XCTest
2
+ import UIKit
2
3
  @testable import NativeNavigationPlugin
3
4
 
4
5
  class NativeNavigationTests: XCTestCase {
5
- func testEcho() {
6
- let implementation = NativeNavigation()
7
- let value = "Hello, World!"
8
- let result = implementation.echo(value)
9
-
10
- XCTAssertEqual(value, result)
11
- }
12
-
13
6
  func testGetPluginVersion() {
14
7
  let implementation = NativeNavigation()
15
8
  let result = implementation.getPluginVersion()
16
9
 
17
10
  XCTAssertEqual("native", result)
18
11
  }
12
+
13
+ func testTabContentControllerHostsWebView() {
14
+ let webView = UIView()
15
+ let originalContainer = UIView()
16
+ let controller = NativeNavigationTabContentController()
17
+ _ = controller.view
18
+
19
+ originalContainer.addSubview(webView)
20
+
21
+ XCTAssertTrue(controller.host(webView: webView))
22
+ XCTAssertEqual(webView.superview, controller.view)
23
+ XCTAssertEqual(webView.frame, controller.view.bounds)
24
+ }
25
+
26
+ func testTabContentControllerKeepsSnapshotPlaceholderWhenWebViewMoves() {
27
+ let webView = UIView()
28
+ let firstController = NativeNavigationTabContentController()
29
+ let secondController = NativeNavigationTabContentController()
30
+ _ = firstController.view
31
+ _ = secondController.view
32
+
33
+ firstController.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
34
+ secondController.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
35
+ webView.backgroundColor = .systemBackground
36
+
37
+ XCTAssertTrue(firstController.host(webView: webView))
38
+ XCTAssertEqual(firstController.view.subviews.count, 1)
39
+
40
+ firstController.clearHostedWebView(ifMatching: webView, preservingSnapshot: true)
41
+ XCTAssertEqual(firstController.view.subviews.count, 2)
42
+
43
+ XCTAssertTrue(secondController.host(webView: webView))
44
+ XCTAssertEqual(webView.superview, secondController.view)
45
+ XCTAssertEqual(firstController.view.subviews.count, 1)
46
+ XCTAssertFalse(firstController.view.subviews.contains(webView))
47
+
48
+ XCTAssertTrue(firstController.host(webView: webView))
49
+ XCTAssertEqual(webView.superview, firstController.view)
50
+ XCTAssertEqual(firstController.view.subviews.count, 1)
51
+ XCTAssertTrue(firstController.view.subviews.first === webView)
52
+ }
53
+
54
+ func testLiftWebViewOverlaySubviewsMovesSplashOverlayAboveContainerContent() {
55
+ let webView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
56
+ let container = UIView(frame: webView.frame)
57
+ let tabControllerView = UIView(frame: webView.frame)
58
+ let scrollView = UIScrollView(frame: webView.bounds)
59
+ let splashOverlay = UIView(frame: webView.bounds)
60
+ var liftedOverlays: [NativeNavigationWeakView] = []
61
+
62
+ container.addSubview(webView)
63
+ container.addSubview(tabControllerView)
64
+ webView.addSubview(scrollView)
65
+ webView.addSubview(splashOverlay)
66
+
67
+ nativeNavigationLiftWebViewOverlaySubviews(
68
+ from: webView,
69
+ to: container,
70
+ tracking: &liftedOverlays,
71
+ excluding: [tabControllerView]
72
+ )
73
+
74
+ XCTAssertEqual(scrollView.superview, webView)
75
+ XCTAssertEqual(splashOverlay.superview, container)
76
+ XCTAssertTrue(container.subviews.last === splashOverlay)
77
+ XCTAssertEqual(liftedOverlays.count, 1)
78
+ XCTAssertTrue(liftedOverlays.first?.value === splashOverlay)
79
+ }
80
+
81
+ func testTabContentControllerRejectsLayerCycle() {
82
+ let webView = UIView()
83
+ let controller = NativeNavigationTabContentController()
84
+ _ = controller.view
85
+
86
+ webView.addSubview(controller.view)
87
+
88
+ XCTAssertFalse(controller.host(webView: webView))
89
+ XCTAssertNil(webView.superview)
90
+ XCTAssertEqual(controller.view.superview, webView)
91
+ }
19
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-native-navigation",
3
- "version": "8.0.15",
3
+ "version": "8.0.17",
4
4
  "description": "Capacitor plugin for native navbar, tabbar, and route transition chrome over a WebView.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -26,6 +26,9 @@
26
26
  "url": "https://github.com/Cap-go/capacitor-native-navigation/issues"
27
27
  },
28
28
  "homepage": "https://capgo.app/docs/plugins/native-navigation/",
29
+ "engines": {
30
+ "node": ">=22.0.0"
31
+ },
29
32
  "keywords": [
30
33
  "capacitor",
31
34
  "native-navigation",