expo-router 7.0.0-canary-20251127-587bc53 → 7.0.0-canary-20251205-a1dedc6

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.
Files changed (177) hide show
  1. package/android/build.gradle +2 -2
  2. package/build/fork/getStateFromPath-forks.d.ts +5 -10
  3. package/build/fork/getStateFromPath-forks.d.ts.map +1 -1
  4. package/build/fork/getStateFromPath-forks.js +14 -13
  5. package/build/fork/getStateFromPath-forks.js.map +1 -1
  6. package/build/fork/getStateFromPath.js +3 -3
  7. package/build/fork/getStateFromPath.js.map +1 -1
  8. package/build/fork/native-stack/createNativeStackNavigator.d.ts.map +1 -1
  9. package/build/fork/native-stack/createNativeStackNavigator.js +12 -3
  10. package/build/fork/native-stack/createNativeStackNavigator.js.map +1 -1
  11. package/build/fork/native-stack/descriptors-context.d.ts +8 -0
  12. package/build/fork/native-stack/descriptors-context.d.ts.map +1 -0
  13. package/build/fork/native-stack/descriptors-context.js +6 -0
  14. package/build/fork/native-stack/descriptors-context.js.map +1 -0
  15. package/build/internal/utils.d.ts +1 -0
  16. package/build/internal/utils.d.ts.map +1 -1
  17. package/build/internal/utils.js +3 -1
  18. package/build/internal/utils.js.map +1 -1
  19. package/build/layouts/StackClient.d.ts.map +1 -1
  20. package/build/layouts/StackClient.js +34 -1
  21. package/build/layouts/StackClient.js.map +1 -1
  22. package/build/link/ExpoLink.d.ts.map +1 -1
  23. package/build/link/ExpoLink.js +12 -4
  24. package/build/link/ExpoLink.js.map +1 -1
  25. package/build/link/Link.d.ts +2 -0
  26. package/build/link/Link.d.ts.map +1 -1
  27. package/build/link/Link.js +2 -0
  28. package/build/link/Link.js.map +1 -1
  29. package/build/link/LinkWithPreview.d.ts +6 -1
  30. package/build/link/LinkWithPreview.d.ts.map +1 -1
  31. package/build/link/LinkWithPreview.js +6 -5
  32. package/build/link/LinkWithPreview.js.map +1 -1
  33. package/build/link/elements.d.ts +8 -0
  34. package/build/link/elements.d.ts.map +1 -1
  35. package/build/link/elements.js +7 -2
  36. package/build/link/elements.js.map +1 -1
  37. package/build/link/preview/native.d.ts +17 -0
  38. package/build/link/preview/native.d.ts.map +1 -1
  39. package/build/link/preview/native.js +22 -0
  40. package/build/link/preview/native.js.map +1 -1
  41. package/build/link/zoom/ZoomTransitionEnabler.d.ts +5 -0
  42. package/build/link/zoom/ZoomTransitionEnabler.d.ts.map +1 -0
  43. package/build/link/zoom/ZoomTransitionEnabler.ios.d.ts +5 -0
  44. package/build/link/zoom/ZoomTransitionEnabler.ios.d.ts.map +1 -0
  45. package/build/link/zoom/ZoomTransitionEnabler.ios.js +44 -0
  46. package/build/link/zoom/ZoomTransitionEnabler.ios.js.map +1 -0
  47. package/build/link/zoom/ZoomTransitionEnabler.js +13 -0
  48. package/build/link/zoom/ZoomTransitionEnabler.js.map +1 -0
  49. package/build/link/zoom/ZoomTransitionEnabler.types.d.ts +4 -0
  50. package/build/link/zoom/ZoomTransitionEnabler.types.d.ts.map +1 -0
  51. package/build/link/zoom/ZoomTransitionEnabler.types.js +3 -0
  52. package/build/link/zoom/ZoomTransitionEnabler.types.js.map +1 -0
  53. package/build/link/zoom/link-apple-zoom.d.ts +23 -0
  54. package/build/link/zoom/link-apple-zoom.d.ts.map +1 -0
  55. package/build/link/zoom/link-apple-zoom.js +40 -0
  56. package/build/link/zoom/link-apple-zoom.js.map +1 -0
  57. package/build/link/zoom/useZoomTransitionPrimitives.d.ts +6 -0
  58. package/build/link/zoom/useZoomTransitionPrimitives.d.ts.map +1 -0
  59. package/build/link/zoom/useZoomTransitionPrimitives.ios.d.ts +11 -0
  60. package/build/link/zoom/useZoomTransitionPrimitives.ios.d.ts.map +1 -0
  61. package/build/link/zoom/useZoomTransitionPrimitives.ios.js +66 -0
  62. package/build/link/zoom/useZoomTransitionPrimitives.ios.js.map +1 -0
  63. package/build/link/zoom/useZoomTransitionPrimitives.js +9 -0
  64. package/build/link/zoom/useZoomTransitionPrimitives.js.map +1 -0
  65. package/build/link/zoom/zoom-transition-context.d.ts +7 -0
  66. package/build/link/zoom/zoom-transition-context.d.ts.map +1 -0
  67. package/build/link/zoom/zoom-transition-context.js +6 -0
  68. package/build/link/zoom/zoom-transition-context.js.map +1 -0
  69. package/build/native-tabs/NativeBottomTabsNavigator.d.ts +3 -15
  70. package/build/native-tabs/NativeBottomTabsNavigator.d.ts.map +1 -1
  71. package/build/native-tabs/NativeBottomTabsNavigator.js +10 -2
  72. package/build/native-tabs/NativeBottomTabsNavigator.js.map +1 -1
  73. package/build/native-tabs/NativeBottomTabsRouter.d.ts.map +1 -1
  74. package/build/native-tabs/NativeBottomTabsRouter.js +12 -1
  75. package/build/native-tabs/NativeBottomTabsRouter.js.map +1 -1
  76. package/build/native-tabs/NativeTabTrigger.d.ts +1 -1
  77. package/build/native-tabs/NativeTabTrigger.d.ts.map +1 -1
  78. package/build/native-tabs/NativeTabTrigger.js +3 -1
  79. package/build/native-tabs/NativeTabTrigger.js.map +1 -1
  80. package/build/native-tabs/NativeTabs.d.ts +6 -3
  81. package/build/native-tabs/NativeTabs.d.ts.map +1 -1
  82. package/build/native-tabs/NativeTabs.js +7 -2
  83. package/build/native-tabs/NativeTabs.js.map +1 -1
  84. package/build/native-tabs/NativeTabsView.d.ts.map +1 -1
  85. package/build/native-tabs/NativeTabsView.js +26 -3
  86. package/build/native-tabs/NativeTabsView.js.map +1 -1
  87. package/build/native-tabs/common/elements.d.ts +25 -0
  88. package/build/native-tabs/common/elements.d.ts.map +1 -1
  89. package/build/native-tabs/common/elements.js +26 -1
  90. package/build/native-tabs/common/elements.js.map +1 -1
  91. package/build/native-tabs/hooks.d.ts +45 -0
  92. package/build/native-tabs/hooks.d.ts.map +1 -0
  93. package/build/native-tabs/hooks.js +57 -0
  94. package/build/native-tabs/hooks.js.map +1 -0
  95. package/build/native-tabs/types.d.ts +36 -2
  96. package/build/native-tabs/types.d.ts.map +1 -1
  97. package/build/native-tabs/types.js.map +1 -1
  98. package/build/native-tabs/utils/bottomAccessory.d.ts +9 -0
  99. package/build/native-tabs/utils/bottomAccessory.d.ts.map +1 -0
  100. package/build/native-tabs/utils/bottomAccessory.js +17 -0
  101. package/build/native-tabs/utils/bottomAccessory.js.map +1 -0
  102. package/build/navigationParams.d.ts +3 -1
  103. package/build/navigationParams.d.ts.map +1 -1
  104. package/build/navigationParams.js +5 -1
  105. package/build/navigationParams.js.map +1 -1
  106. package/build/toolbar/elements.d.ts +24 -0
  107. package/build/toolbar/elements.d.ts.map +1 -0
  108. package/build/toolbar/elements.js +36 -0
  109. package/build/toolbar/elements.js.map +1 -0
  110. package/build/toolbar/index.d.ts +9 -0
  111. package/build/toolbar/index.d.ts.map +1 -0
  112. package/build/toolbar/index.js +12 -0
  113. package/build/toolbar/index.js.map +1 -0
  114. package/build/toolbar/native.d.ts +4 -0
  115. package/build/toolbar/native.d.ts.map +1 -0
  116. package/build/toolbar/native.ios.d.ts +4 -0
  117. package/build/toolbar/native.ios.d.ts.map +1 -0
  118. package/build/toolbar/native.ios.js +21 -0
  119. package/build/toolbar/native.ios.js.map +1 -0
  120. package/build/toolbar/native.js +11 -0
  121. package/build/toolbar/native.js.map +1 -0
  122. package/build/toolbar/native.types.d.ts +30 -0
  123. package/build/toolbar/native.types.d.ts.map +1 -0
  124. package/build/toolbar/native.types.js +3 -0
  125. package/build/toolbar/native.types.js.map +1 -0
  126. package/build/useScreens.d.ts.map +1 -1
  127. package/build/useScreens.js +13 -5
  128. package/build/useScreens.js.map +1 -1
  129. package/build/views/Sitemap.js +3 -2
  130. package/build/views/Sitemap.js.map +1 -1
  131. package/expo-module.config.json +2 -2
  132. package/ios/LinkPreview/LinkPreviewNativeActionView.swift +1 -0
  133. package/ios/LinkPreview/LinkPreviewNativeModule.swift +49 -1
  134. package/ios/LinkPreview/LinkPreviewNativeNavigation.swift +132 -70
  135. package/ios/LinkPreview/LinkPreviewNativeView.swift +28 -14
  136. package/ios/LinkPreview/LinkZoomTransition.swift +227 -0
  137. package/ios/Toolbar/RouterToolbarHostView.swift +140 -0
  138. package/ios/Toolbar/RouterToolbarItemView.swift +171 -0
  139. package/ios/Toolbar/RouterToolbarModule.swift +102 -0
  140. package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.module → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.module} +7 -7
  141. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.module.md5 +1 -0
  142. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.module.sha1 +1 -0
  143. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.module.sha256 +1 -0
  144. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.module.sha512 +1 -0
  145. package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.pom → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.pom} +1 -1
  146. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.pom.md5 +1 -0
  147. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.pom.sha1 +1 -0
  148. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.pom.sha256 +1 -0
  149. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.pom.sha512 +1 -0
  150. package/local-maven-repo/expo/modules/router/expo.modules.router/maven-metadata.xml +4 -4
  151. package/local-maven-repo/expo/modules/router/expo.modules.router/maven-metadata.xml.md5 +1 -1
  152. package/local-maven-repo/expo/modules/router/expo.modules.router/maven-metadata.xml.sha1 +1 -1
  153. package/local-maven-repo/expo/modules/router/expo.modules.router/maven-metadata.xml.sha256 +1 -1
  154. package/local-maven-repo/expo/modules/router/expo.modules.router/maven-metadata.xml.sha512 +1 -1
  155. package/package.json +14 -12
  156. package/unstable-toolbar.d.ts +1 -0
  157. package/unstable-toolbar.js +1 -0
  158. package/ios/LinkPreview/LinkPreviewNativeNavigation.h +0 -50
  159. package/ios/LinkPreview/LinkPreviewNativeNavigation.mm +0 -164
  160. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.module.md5 +0 -1
  161. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.module.sha1 +0 -1
  162. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.module.sha256 +0 -1
  163. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.module.sha512 +0 -1
  164. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.pom.md5 +0 -1
  165. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.pom.sha1 +0 -1
  166. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.pom.sha256 +0 -1
  167. package/local-maven-repo/expo/modules/router/expo.modules.router/7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.pom.sha512 +0 -1
  168. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53-sources.jar → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6-sources.jar} +0 -0
  169. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53-sources.jar.md5 → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6-sources.jar.md5} +0 -0
  170. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53-sources.jar.sha1 → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6-sources.jar.sha1} +0 -0
  171. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53-sources.jar.sha256 → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6-sources.jar.sha256} +0 -0
  172. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53-sources.jar.sha512 → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6-sources.jar.sha512} +0 -0
  173. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.aar → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.aar} +0 -0
  174. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.aar.md5 → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.aar.md5} +0 -0
  175. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.aar.sha1 → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.aar.sha1} +0 -0
  176. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.aar.sha256 → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.aar.sha256} +0 -0
  177. /package/local-maven-repo/expo/modules/router/expo.modules.router/{7.0.0-canary-20251127-587bc53/expo.modules.router-7.0.0-canary-20251127-587bc53.aar.sha512 → 7.0.0-canary-20251205-a1dedc6/expo.modules.router-7.0.0-canary-20251205-a1dedc6.aar.sha512} +0 -0
@@ -1,11 +1,11 @@
1
1
  import ExpoModulesCore
2
+ import RNScreens
2
3
 
3
4
  class NativeLinkPreviewView: ExpoView, UIContextMenuInteractionDelegate,
4
- LinkPreviewModalDismissible, LinkPreviewMenuUpdatable
5
- {
5
+ RNSDismissibleModalProtocol, LinkPreviewMenuUpdatable {
6
6
  private var preview: NativeLinkPreviewContentView?
7
7
  private var interaction: UIContextMenuInteraction?
8
- private var directChild: UIView?
8
+ var directChild: UIView?
9
9
  var nextScreenId: String? {
10
10
  didSet {
11
11
  performUpdateOfPreloadedView()
@@ -18,7 +18,9 @@ class NativeLinkPreviewView: ExpoView, UIContextMenuInteractionDelegate,
18
18
  }
19
19
  private var actions: [LinkPreviewNativeActionView] = []
20
20
 
21
- private let linkPreviewNativeNavigation = LinkPreviewNativeNavigation()
21
+ private lazy var linkPreviewNativeNavigation: LinkPreviewNativeNavigation = {
22
+ return LinkPreviewNativeNavigation(logger: appContext?.jsLogger)
23
+ }()
22
24
 
23
25
  let onPreviewTapped = EventDispatcher()
24
26
  let onPreviewTappedAnimationCompleted = EventDispatcher()
@@ -67,7 +69,11 @@ class NativeLinkPreviewView: ExpoView, UIContextMenuInteractionDelegate,
67
69
  }
68
70
  directChild = childComponentView
69
71
  if let interaction = self.interaction {
70
- childComponentView.addInteraction(interaction)
72
+ if let indirectTrigger = childComponentView as? LinkPreviewIndirectTriggerProtocol {
73
+ indirectTrigger.indirectTrigger?.addInteraction(interaction)
74
+ } else {
75
+ childComponentView.addInteraction(interaction)
76
+ }
71
77
  }
72
78
  super.mountChildComponentView(childComponentView, index: index)
73
79
  }
@@ -89,7 +95,11 @@ class NativeLinkPreviewView: ExpoView, UIContextMenuInteractionDelegate,
89
95
  return
90
96
  }
91
97
  if let interaction = self.interaction {
92
- directChild.removeInteraction(interaction)
98
+ if let indirectTrigger = directChild as? LinkPreviewIndirectTriggerProtocol {
99
+ indirectTrigger.indirectTrigger?.removeInteraction(interaction)
100
+ } else {
101
+ directChild.removeInteraction(interaction)
102
+ }
93
103
  }
94
104
  super.unmountChildComponentView(child, index: index)
95
105
  } else {
@@ -124,16 +134,16 @@ class NativeLinkPreviewView: ExpoView, UIContextMenuInteractionDelegate,
124
134
  configuration: UIContextMenuConfiguration,
125
135
  highlightPreviewForItemWithIdentifier identifier: any NSCopying
126
136
  ) -> UITargetedPreview? {
127
- if let superview = self.superview {
128
- if let directChild = self.directChild {
129
- let target = UIPreviewTarget(
130
- container: superview, center: self.convert(directChild.center, to: superview))
137
+ if let superview, let directChild {
138
+ let triggerView: UIView =
139
+ (directChild as? LinkPreviewIndirectTriggerProtocol)?.indirectTrigger ?? directChild
140
+ let target = UIPreviewTarget(
141
+ container: superview, center: self.convert(triggerView.center, to: superview))
131
142
 
132
- let parameters = UIPreviewParameters()
133
- parameters.backgroundColor = .clear
143
+ let parameters = UIPreviewParameters()
144
+ parameters.backgroundColor = .clear
134
145
 
135
- return UITargetedPreview(view: directChild, parameters: parameters, target: target)
136
- }
146
+ return UITargetedPreview(view: triggerView, parameters: parameters, target: target)
137
147
  }
138
148
  return nil
139
149
  }
@@ -232,3 +242,7 @@ class PreviewViewController: UIViewController {
232
242
  linkPreviewNativePreview.setInitialSize(bounds: self.view.bounds)
233
243
  }
234
244
  }
245
+
246
+ protocol LinkPreviewIndirectTriggerProtocol {
247
+ var indirectTrigger: UIView? { get }
248
+ }
@@ -0,0 +1,227 @@
1
+ import ExpoModulesCore
2
+ import RNScreens
3
+ import UIKit
4
+
5
+ class LinkSourceInfo {
6
+ let alignment: CGRect?
7
+ weak var view: UIView?
8
+
9
+ init(alignment: CGRect?, view: UIView) {
10
+ self.alignment = alignment
11
+ self.view = view
12
+ }
13
+ }
14
+
15
+ class LinkZoomTransitionsSourceRepository {
16
+ private var sources: [String: LinkSourceInfo] = [:]
17
+ private let lock = NSLock()
18
+
19
+ init() {}
20
+
21
+ func registerSource(
22
+ identifier: String,
23
+ source: LinkSourceInfo
24
+ ) {
25
+ lock.lock()
26
+ defer { lock.unlock() }
27
+ if sources[identifier] != nil {
28
+ print(
29
+ "[expo-router] LinkPreviewZoomTransitionSource with identifier \(identifier) is already registered. Overwriting the existing source."
30
+ )
31
+ }
32
+ if !identifier.isEmpty {
33
+ sources[identifier] = source
34
+ }
35
+ }
36
+
37
+ func unregisterSource(identifier: String) {
38
+ lock.lock()
39
+ defer { lock.unlock() }
40
+ sources.removeValue(forKey: identifier)
41
+ }
42
+
43
+ func getSource(identifier: String) -> LinkSourceInfo? {
44
+ lock.lock()
45
+ defer { lock.unlock() }
46
+ return sources[identifier]
47
+ }
48
+
49
+ func updateIdentifier(
50
+ oldIdentifier: String,
51
+ newIdentifier: String
52
+ ) {
53
+ lock.lock()
54
+ defer { lock.unlock() }
55
+ if let source = sources[oldIdentifier] {
56
+ if !newIdentifier.isEmpty {
57
+ sources[newIdentifier] = source
58
+ }
59
+ sources.removeValue(forKey: oldIdentifier)
60
+ }
61
+ }
62
+
63
+ func updateAlignment(
64
+ identifier: String,
65
+ alignment: CGRect?
66
+ ) {
67
+ lock.lock()
68
+ defer { lock.unlock() }
69
+ if let source = sources[identifier], let view = source.view, !identifier.isEmpty {
70
+ sources[identifier] = LinkSourceInfo(
71
+ alignment: alignment,
72
+ view: view
73
+ )
74
+ }
75
+ }
76
+ }
77
+
78
+ class LinkZoomTransitionSource: LinkZoomExpoView, LinkPreviewIndirectTriggerProtocol {
79
+ var child: UIView?
80
+
81
+ var indirectTrigger: UIView? {
82
+ return child
83
+ }
84
+
85
+ var alignment: CGRect? {
86
+ didSet {
87
+ // Update alignment info in the repository
88
+ if child != nil {
89
+ repository?.updateAlignment(
90
+ identifier: identifier,
91
+ alignment: alignment
92
+ )
93
+ }
94
+ }
95
+ }
96
+
97
+ var identifier: String = "" {
98
+ didSet {
99
+ guard identifier != oldValue else { return }
100
+ if let child {
101
+ if oldValue.isEmpty {
102
+ repository?.registerSource(
103
+ identifier: identifier,
104
+ source: LinkSourceInfo(alignment: alignment, view: child)
105
+ )
106
+ } else {
107
+ repository?.updateIdentifier(
108
+ oldIdentifier: oldValue,
109
+ newIdentifier: identifier
110
+ )
111
+ }
112
+ } else {
113
+ repository?.unregisterSource(
114
+ identifier: oldValue
115
+ )
116
+ }
117
+ }
118
+ }
119
+
120
+ override func mountChildComponentView(
121
+ _ childComponentView: UIView,
122
+ index: Int
123
+ ) {
124
+ if child != nil {
125
+ print(
126
+ "[expo-router] LinkZoomTransitionSource can only have one child view."
127
+ )
128
+ return
129
+ }
130
+ child = childComponentView
131
+ repository?.registerSource(
132
+ identifier: identifier,
133
+ source: LinkSourceInfo(alignment: alignment, view: childComponentView)
134
+ )
135
+ super.mountChildComponentView(childComponentView, index: index)
136
+ }
137
+
138
+ override func unmountChildComponentView(_ child: UIView, index: Int) {
139
+ if child == self.child {
140
+ self.child = nil
141
+ repository?.unregisterSource(
142
+ identifier: identifier
143
+ )
144
+ }
145
+ super.unmountChildComponentView(child, index: index)
146
+ }
147
+ }
148
+
149
+ class LinkZoomTransitionEnabler: LinkZoomExpoView {
150
+ var zoomTransitionSourceIdentifier: String = ""
151
+ var isPreventingInteractiveDismissal: Bool = false
152
+
153
+ override func didMoveToSuperview() {
154
+ super.didMoveToSuperview()
155
+ if superview != nil {
156
+ // Need to run this async. Otherwise the view has no view controller yet
157
+ DispatchQueue.main.async {
158
+ self.setupZoomTransition()
159
+ }
160
+ }
161
+ }
162
+
163
+ private func setupZoomTransition() {
164
+ if self.zoomTransitionSourceIdentifier.isEmpty {
165
+ print("[expo-router] No zoomTransitionSourceIdentifier passed to LinkZoomTransitionEnabler")
166
+ return
167
+ }
168
+ if let controller = self.findViewController() {
169
+ if #available(iOS 18.0, *) {
170
+ let options = UIViewController.Transition.ZoomOptions()
171
+
172
+ options.alignmentRectProvider = { _ in
173
+ let sourceInfo = self.repository?.getSource(
174
+ identifier: self.zoomTransitionSourceIdentifier)
175
+ return sourceInfo?.alignment
176
+ }
177
+ options.interactiveDismissShouldBegin = { _ in
178
+ !self.isPreventingInteractiveDismissal
179
+ }
180
+ controller.preferredTransition = .zoom(options: options) { _ in
181
+ let sourceInfo = self.repository?.getSource(
182
+ identifier: self.zoomTransitionSourceIdentifier)
183
+ var view: UIView? = sourceInfo?.view
184
+ if let linkPreviewView = view as? NativeLinkPreviewView {
185
+ view = linkPreviewView.directChild
186
+ }
187
+ guard let view else {
188
+ print(
189
+ "[expo-router] No source view found for identifier \(self.zoomTransitionSourceIdentifier) to enable zoom transition"
190
+ )
191
+ return nil
192
+ }
193
+ return view
194
+ }
195
+ return
196
+ }
197
+ } else {
198
+ print("[expo-router] No navigation controller found to enable zoom transition")
199
+ }
200
+ }
201
+
202
+ private func findViewController() -> RNSScreen? {
203
+ var responder: UIResponder? = self
204
+ while let r = responder {
205
+ if let r = r as? RNSScreen {
206
+ return r
207
+ }
208
+ responder = r.next
209
+ }
210
+ return nil
211
+ }
212
+ }
213
+
214
+ class LinkZoomExpoView: ExpoView {
215
+ var module: LinkPreviewNativeModule? {
216
+ return appContext?.moduleRegistry.get(moduleWithName: LinkPreviewNativeModule.moduleName)
217
+ as? LinkPreviewNativeModule
218
+ }
219
+
220
+ var repository: LinkZoomTransitionsSourceRepository? {
221
+ guard let module else {
222
+ print("[expo-router] LinkPreviewNativeModule not loaded")
223
+ return nil
224
+ }
225
+ return module.zoomSourceRepository
226
+ }
227
+ }
@@ -0,0 +1,140 @@
1
+ import ExpoModulesCore
2
+ import RNScreens
3
+ import UIKit
4
+
5
+ class RouterToolbarHostView: ExpoView, LinkPreviewMenuUpdatable {
6
+ // Mutable map of toolbar items
7
+ var toolbarItemsArray: [String] = []
8
+ var toolbarItemsMap: [String: RouterToolbarItemView] = [:]
9
+ var menuItemsMap: [String: LinkPreviewNativeActionView] = [:]
10
+
11
+ required init(appContext: AppContext? = nil) {
12
+ super.init(appContext: appContext)
13
+ }
14
+
15
+ private func addRouterToolbarItemAtIndex(
16
+ _ item: RouterToolbarItemView,
17
+ index: Int
18
+ ) {
19
+ let identifier = item.identifier
20
+ toolbarItemsArray.insert(identifier, at: index)
21
+ toolbarItemsMap[identifier] = item
22
+ item.host = self
23
+ }
24
+
25
+ private func addMenuToolbarItemAtIndex(
26
+ _ item: LinkPreviewNativeActionView,
27
+ index: Int
28
+ ) {
29
+ let identifier = item.identifier
30
+ toolbarItemsArray.insert(identifier, at: index)
31
+ menuItemsMap[identifier] = item
32
+ }
33
+
34
+ private func removeToolbarItemWithId(_ id: String) {
35
+ if let index = toolbarItemsArray.firstIndex(of: id) {
36
+ toolbarItemsArray.remove(at: index)
37
+ toolbarItemsMap.removeValue(forKey: id)
38
+ menuItemsMap.removeValue(forKey: id)
39
+ }
40
+ }
41
+
42
+ func updateToolbarItem(withId id: String) {
43
+ if let controller = self.findViewController() {
44
+ let index = toolbarItemsArray.firstIndex(of: id)
45
+ if let index = index, let item = toolbarItemsMap[id] {
46
+ controller.toolbarItems?[index] = item.barButtonItem
47
+ }
48
+ }
49
+ }
50
+
51
+ func updateToolbarItems() {
52
+ if let controller = self.findViewController() {
53
+ if #available(iOS 18.0, *) {
54
+ controller.setToolbarItems(
55
+ toolbarItemsArray.compactMap {
56
+ if let item = toolbarItemsMap[$0] {
57
+ return item.barButtonItem
58
+ }
59
+ if let menu = menuItemsMap[$0] {
60
+ return UIBarButtonItem(
61
+ title: menu.title,
62
+ image: menu.icon.flatMap { UIImage(systemName: $0) },
63
+ primaryAction: nil,
64
+ menu: menu.uiAction as? UIMenu
65
+ )
66
+ }
67
+ print(
68
+ "[expo-router] Warning: Could not find toolbar item or menu for identifier \($0)"
69
+ )
70
+ return nil
71
+ }, animated: true)
72
+ controller.navigationController?.setToolbarHidden(
73
+ false, animated: true)
74
+ return
75
+ }
76
+ } else {
77
+ print(
78
+ "[expo-router] Warning: Could not find owning UIViewController for RouterToolbarHostView")
79
+ }
80
+ }
81
+
82
+ override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
83
+ if let toolbarItem = childComponentView as? RouterToolbarItemView {
84
+ if toolbarItem.identifier.isEmpty {
85
+ print("[expo-router] RouterToolbarItemView identifier is empty")
86
+ return
87
+ }
88
+ addRouterToolbarItemAtIndex(toolbarItem, index: index)
89
+ } else if let menu = childComponentView as? LinkPreviewNativeActionView {
90
+ addMenuToolbarItemAtIndex(menu, index: index)
91
+ } else {
92
+ print(
93
+ "ExpoRouter: Unknown child component view (\(childComponentView)) mounted to RouterToolbarHost"
94
+ )
95
+ }
96
+ updateToolbarItems()
97
+ }
98
+
99
+ override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
100
+ if let toolbarItem = childComponentView as? RouterToolbarItemView {
101
+ if toolbarItem.identifier.isEmpty {
102
+ print("[expo-router] RouterToolbarItemView identifier is empty")
103
+ return
104
+ }
105
+ removeToolbarItemWithId(toolbarItem.identifier)
106
+ } else if let menu = childComponentView as? LinkPreviewNativeActionView {
107
+ if menu.identifier.isEmpty {
108
+ print("[expo-router] Menu identifier is empty")
109
+ return
110
+ }
111
+ removeToolbarItemWithId(menu.identifier)
112
+ } else {
113
+ print(
114
+ "ExpoRouter: Unknown child component view (\(childComponentView)) unmounted from RouterToolbarHost"
115
+ )
116
+ }
117
+ updateToolbarItems()
118
+ }
119
+
120
+ override func didMoveToWindow() {
121
+ super.didMoveToWindow()
122
+ // Update toolbar items when the view is added to the window
123
+ updateToolbarItems()
124
+ }
125
+
126
+ func updateMenu() {
127
+ updateToolbarItems()
128
+ }
129
+
130
+ private func findViewController() -> RNSScreen? {
131
+ var responder: UIResponder? = self
132
+ while let r = responder {
133
+ if let r = r as? RNSScreen {
134
+ return r
135
+ }
136
+ responder = r.next
137
+ }
138
+ return nil
139
+ }
140
+ }
@@ -0,0 +1,171 @@
1
+ import ExpoModulesCore
2
+ import React
3
+ import UIKit
4
+
5
+ class RouterToolbarItemView: ExpoView {
6
+ var identifier: String = ""
7
+ @ReactiveProp var type: ItemType?
8
+ @ReactiveProp var title: String?
9
+ @ReactiveProp var systemImageName: String?
10
+ @ReactiveProp var customView: UIView?
11
+ @ReactiveProp var customTintColor: UIColor?
12
+ @ReactiveProp var hidesSharedBackground: Bool = false
13
+ @ReactiveProp var sharesBackground: Bool = true
14
+ @ReactiveProp var barButtonItemStyle: UIBarButtonItem.Style?
15
+ @ReactiveProp var width: Double?
16
+ // Using "routerHidden" to avoid conflict with UIView's "isHidden"
17
+ @ReactiveProp var routerHidden: Bool = false
18
+ @ReactiveProp var selected: Bool = false
19
+ @ReactiveProp var possibleTitles: Set<String>?
20
+ @ReactiveProp var badgeConfiguration: BadgeConfiguration?
21
+
22
+ var host: RouterToolbarHostView?
23
+
24
+ let onSelected = EventDispatcher()
25
+
26
+ func performUpdate() {
27
+ self.host?.updateToolbarItem(withId: self.identifier)
28
+ }
29
+
30
+ @objc func handleAction() {
31
+ onSelected()
32
+ }
33
+
34
+ var barButtonItem: UIBarButtonItem {
35
+ var item = UIBarButtonItem()
36
+ if let customView {
37
+ item = UIBarButtonItem(customView: customView)
38
+ } else if type == .fluidSpacer {
39
+ item = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
40
+ } else if type == .fixedSpacer {
41
+ item = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
42
+ } else {
43
+ if let title {
44
+ item.title = title
45
+ }
46
+ item.possibleTitles = possibleTitles
47
+ if let systemImageName {
48
+ item.image = UIImage(systemName: systemImageName)
49
+ }
50
+ if let tintColor = customTintColor {
51
+ item.tintColor = tintColor
52
+ }
53
+ }
54
+ if #available(iOS 26.0, *) {
55
+ item.hidesSharedBackground = hidesSharedBackground
56
+ item.sharesBackground = sharesBackground
57
+ }
58
+ if let barButtonItemStyle {
59
+ item.style = barButtonItemStyle
60
+ }
61
+ item.target = self
62
+ item.action = #selector(handleAction)
63
+ if let width = width {
64
+ item.width = CGFloat(width)
65
+ }
66
+ if #available(iOS 16.0, *) {
67
+ item.isHidden = routerHidden
68
+ }
69
+ item.isSelected = selected
70
+ if #available(iOS 26.0, *) {
71
+ if let badgeConfig = badgeConfiguration {
72
+ var badge = UIBarButtonItem.Badge.indicator()
73
+ if let value = badgeConfig.value {
74
+ badge = .string(value)
75
+ }
76
+ if let backgroundColor = badgeConfig.backgroundColor {
77
+ badge.backgroundColor = backgroundColor
78
+ }
79
+ if let foregroundColor = badgeConfig.color {
80
+ badge.foregroundColor = foregroundColor
81
+ }
82
+ if badgeConfig.fontFamily != nil || badgeConfig.fontSize != nil
83
+ || badgeConfig.fontWeight != nil {
84
+ let font = RCTFont.update(
85
+ nil,
86
+ withFamily: badgeConfig.fontFamily,
87
+ size: badgeConfig.fontSize != nil ? NSNumber(value: badgeConfig.fontSize!) : nil,
88
+ weight: badgeConfig.fontWeight,
89
+ style: nil,
90
+ variant: nil,
91
+ scaleMultiplier: 1.0)
92
+ badge.font = font
93
+ }
94
+ // TODO: Find out why this does not work
95
+ item.badge = badge
96
+ }
97
+ }
98
+
99
+ return item
100
+ }
101
+
102
+ required init(appContext: AppContext? = nil) {
103
+ super.init(appContext: appContext)
104
+ }
105
+
106
+ override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
107
+ if customView != nil {
108
+ print(
109
+ "[expo-router] Warning: RouterToolbarItemView can only have one child view"
110
+ )
111
+ return
112
+ }
113
+ customView = childComponentView
114
+ performUpdate()
115
+ }
116
+
117
+ override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
118
+ if customView == childComponentView {
119
+ childComponentView.removeFromSuperview()
120
+ customView = nil
121
+ performUpdate()
122
+ }
123
+ }
124
+ }
125
+
126
+ enum ItemType: String, Enumerable {
127
+ case normal
128
+ case fixedSpacer
129
+ case fluidSpacer
130
+ }
131
+
132
+ struct BadgeConfiguration: Equatable {
133
+ var value: String?
134
+ var backgroundColor: UIColor?
135
+ var color: UIColor?
136
+ var fontFamily: String?
137
+ var fontSize: Double?
138
+ var fontWeight: String?
139
+ }
140
+
141
+ @propertyWrapper
142
+ struct ReactiveProp<Value: Equatable> {
143
+ private var value: Value
144
+
145
+ init(wrappedValue: Value) {
146
+ self.value = wrappedValue
147
+ }
148
+
149
+ static subscript<EnclosingSelf: RouterToolbarItemView>(
150
+ _enclosingInstance instance: EnclosingSelf,
151
+ wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
152
+ storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, ReactiveProp<Value>>
153
+ ) -> Value {
154
+ get {
155
+ instance[keyPath: storageKeyPath].value
156
+ }
157
+ set {
158
+ let oldValue = instance[keyPath: storageKeyPath].value
159
+ if oldValue != newValue {
160
+ instance[keyPath: storageKeyPath].value = newValue
161
+ instance.performUpdate()
162
+ }
163
+ }
164
+ }
165
+
166
+ @available(*, unavailable, message: "Use the enclosing instance subscript.")
167
+ var wrappedValue: Value {
168
+ get { fatalError() }
169
+ set { fatalError() }
170
+ }
171
+ }