@userflow-tech/react-native 0.3.0 → 0.4.0

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 (253) hide show
  1. package/README.md +360 -70
  2. package/docs/anchoring.md +58 -42
  3. package/docs/troubleshooting.md +69 -27
  4. package/expo-plugin/hostDependencyWarnings.d.ts +21 -0
  5. package/expo-plugin/hostDependencyWarnings.d.ts.map +1 -0
  6. package/expo-plugin/hostDependencyWarnings.js +101 -0
  7. package/expo-plugin/hostDependencyWarnings.js.map +1 -0
  8. package/expo-plugin/withUserflow.d.ts.map +1 -1
  9. package/expo-plugin/withUserflow.js +16 -0
  10. package/expo-plugin/withUserflow.js.map +1 -1
  11. package/lib/commonjs/anchors/AnchorRegistry.js +7 -0
  12. package/lib/commonjs/anchors/AnchorRegistry.js.map +1 -1
  13. package/lib/commonjs/anchors/elementConditionEvaluator.js +189 -55
  14. package/lib/commonjs/anchors/elementConditionEvaluator.js.map +1 -1
  15. package/lib/commonjs/anchors/elementTapMonitor.js +92 -0
  16. package/lib/commonjs/anchors/elementTapMonitor.js.map +1 -0
  17. package/lib/commonjs/anchors/elementTargetMatch.js +85 -0
  18. package/lib/commonjs/anchors/elementTargetMatch.js.map +1 -0
  19. package/lib/commonjs/anchors/fiber/findAnchorFiber.js +64 -39
  20. package/lib/commonjs/anchors/fiber/findAnchorFiber.js.map +1 -1
  21. package/lib/commonjs/anchors/fiber/touchTargetFiber.js +105 -0
  22. package/lib/commonjs/anchors/fiber/touchTargetFiber.js.map +1 -0
  23. package/lib/commonjs/anchors/resolveAnchorHost.js +2 -1
  24. package/lib/commonjs/anchors/resolveAnchorHost.js.map +1 -1
  25. package/lib/commonjs/anchors/resolveAutoAnchorHost.js +4 -2
  26. package/lib/commonjs/anchors/resolveAutoAnchorHost.js.map +1 -1
  27. package/lib/commonjs/anchors/resolveElementTargets.js +158 -0
  28. package/lib/commonjs/anchors/resolveElementTargets.js.map +1 -0
  29. package/lib/commonjs/anchors/targetPressMonitor.js +9 -3
  30. package/lib/commonjs/anchors/targetPressMonitor.js.map +1 -1
  31. package/lib/commonjs/anchors/useUserflowAnchor.js +1 -0
  32. package/lib/commonjs/anchors/useUserflowAnchor.js.map +1 -1
  33. package/lib/commonjs/anonymousId.js +16 -7
  34. package/lib/commonjs/anonymousId.js.map +1 -1
  35. package/lib/commonjs/authoring/capturePickerScreenshot.js +9 -1
  36. package/lib/commonjs/authoring/capturePickerScreenshot.js.map +1 -1
  37. package/lib/commonjs/evalJs/getWebView.js +5 -13
  38. package/lib/commonjs/evalJs/getWebView.js.map +1 -1
  39. package/lib/commonjs/hostAppVersion.js +6 -1
  40. package/lib/commonjs/hostAppVersion.js.map +1 -1
  41. package/lib/commonjs/index.js +5 -0
  42. package/lib/commonjs/index.js.map +1 -1
  43. package/lib/commonjs/installCheck.js +183 -0
  44. package/lib/commonjs/installCheck.js.map +1 -0
  45. package/lib/commonjs/nativeModules.js +44 -0
  46. package/lib/commonjs/nativeModules.js.map +1 -0
  47. package/lib/commonjs/network.js +20 -7
  48. package/lib/commonjs/network.js.map +1 -1
  49. package/lib/commonjs/pairing/deviceId.js +14 -11
  50. package/lib/commonjs/pairing/deviceId.js.map +1 -1
  51. package/lib/commonjs/pairing/tokenStorage.js +17 -4
  52. package/lib/commonjs/pairing/tokenStorage.js.map +1 -1
  53. package/lib/commonjs/runtime/TargetPressBridge.js +12 -1
  54. package/lib/commonjs/runtime/TargetPressBridge.js.map +1 -1
  55. package/lib/commonjs/runtime/UserflowProvider.js +6 -3
  56. package/lib/commonjs/runtime/UserflowProvider.js.map +1 -1
  57. package/lib/commonjs/storage/asyncStorage.js +67 -0
  58. package/lib/commonjs/storage/asyncStorage.js.map +1 -0
  59. package/lib/commonjs/surfaces/BannerSurface.js +3 -1
  60. package/lib/commonjs/surfaces/BannerSurface.js.map +1 -1
  61. package/lib/commonjs/surfaces/safeArea.js +74 -28
  62. package/lib/commonjs/surfaces/safeArea.js.map +1 -1
  63. package/lib/commonjs/version.js +1 -1
  64. package/lib/module/anchors/AnchorRegistry.js +7 -0
  65. package/lib/module/anchors/AnchorRegistry.js.map +1 -1
  66. package/lib/module/anchors/elementConditionEvaluator.js +189 -55
  67. package/lib/module/anchors/elementConditionEvaluator.js.map +1 -1
  68. package/lib/module/anchors/elementTapMonitor.js +87 -0
  69. package/lib/module/anchors/elementTapMonitor.js.map +1 -0
  70. package/lib/module/anchors/elementTargetMatch.js +78 -0
  71. package/lib/module/anchors/elementTargetMatch.js.map +1 -0
  72. package/lib/module/anchors/fiber/findAnchorFiber.js +62 -39
  73. package/lib/module/anchors/fiber/findAnchorFiber.js.map +1 -1
  74. package/lib/module/anchors/fiber/touchTargetFiber.js +100 -0
  75. package/lib/module/anchors/fiber/touchTargetFiber.js.map +1 -0
  76. package/lib/module/anchors/resolveAnchorHost.js +2 -1
  77. package/lib/module/anchors/resolveAnchorHost.js.map +1 -1
  78. package/lib/module/anchors/resolveAutoAnchorHost.js +3 -3
  79. package/lib/module/anchors/resolveAutoAnchorHost.js.map +1 -1
  80. package/lib/module/anchors/resolveElementTargets.js +152 -0
  81. package/lib/module/anchors/resolveElementTargets.js.map +1 -0
  82. package/lib/module/anchors/targetPressMonitor.js +8 -3
  83. package/lib/module/anchors/targetPressMonitor.js.map +1 -1
  84. package/lib/module/anchors/useUserflowAnchor.js +1 -0
  85. package/lib/module/anchors/useUserflowAnchor.js.map +1 -1
  86. package/lib/module/anonymousId.js +15 -5
  87. package/lib/module/anonymousId.js.map +1 -1
  88. package/lib/module/authoring/capturePickerScreenshot.js +9 -1
  89. package/lib/module/authoring/capturePickerScreenshot.js.map +1 -1
  90. package/lib/module/evalJs/getWebView.js +5 -13
  91. package/lib/module/evalJs/getWebView.js.map +1 -1
  92. package/lib/module/hostAppVersion.js +6 -1
  93. package/lib/module/hostAppVersion.js.map +1 -1
  94. package/lib/module/index.js +5 -0
  95. package/lib/module/index.js.map +1 -1
  96. package/lib/module/installCheck.js +169 -0
  97. package/lib/module/installCheck.js.map +1 -0
  98. package/lib/module/nativeModules.js +40 -0
  99. package/lib/module/nativeModules.js.map +1 -0
  100. package/lib/module/network.js +20 -7
  101. package/lib/module/network.js.map +1 -1
  102. package/lib/module/pairing/deviceId.js +14 -10
  103. package/lib/module/pairing/deviceId.js.map +1 -1
  104. package/lib/module/pairing/tokenStorage.js +17 -4
  105. package/lib/module/pairing/tokenStorage.js.map +1 -1
  106. package/lib/module/runtime/TargetPressBridge.js +12 -1
  107. package/lib/module/runtime/TargetPressBridge.js.map +1 -1
  108. package/lib/module/runtime/UserflowProvider.js +7 -4
  109. package/lib/module/runtime/UserflowProvider.js.map +1 -1
  110. package/lib/module/storage/asyncStorage.js +61 -0
  111. package/lib/module/storage/asyncStorage.js.map +1 -0
  112. package/lib/module/surfaces/BannerSurface.js +3 -1
  113. package/lib/module/surfaces/BannerSurface.js.map +1 -1
  114. package/lib/module/surfaces/safeArea.js +70 -28
  115. package/lib/module/surfaces/safeArea.js.map +1 -1
  116. package/lib/module/version.js +1 -1
  117. package/lib/typescript/commonjs/src/anchors/AnchorRegistry.d.ts +7 -0
  118. package/lib/typescript/commonjs/src/anchors/AnchorRegistry.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/src/anchors/elementConditionEvaluator.d.ts +12 -16
  120. package/lib/typescript/commonjs/src/anchors/elementConditionEvaluator.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/src/anchors/elementTapMonitor.d.ts +36 -0
  122. package/lib/typescript/commonjs/src/anchors/elementTapMonitor.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/src/anchors/elementTargetMatch.d.ts +20 -0
  124. package/lib/typescript/commonjs/src/anchors/elementTargetMatch.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/src/anchors/fiber/findAnchorFiber.d.ts +17 -5
  126. package/lib/typescript/commonjs/src/anchors/fiber/findAnchorFiber.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/src/anchors/fiber/touchTargetFiber.d.ts +24 -0
  128. package/lib/typescript/commonjs/src/anchors/fiber/touchTargetFiber.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/src/anchors/resolveAnchorHost.d.ts +2 -1
  130. package/lib/typescript/commonjs/src/anchors/resolveAnchorHost.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/src/anchors/resolveAutoAnchorHost.d.ts +10 -1
  132. package/lib/typescript/commonjs/src/anchors/resolveAutoAnchorHost.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/src/anchors/resolveElementTargets.d.ts +39 -0
  134. package/lib/typescript/commonjs/src/anchors/resolveElementTargets.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/src/anchors/targetPressMonitor.d.ts +2 -0
  136. package/lib/typescript/commonjs/src/anchors/targetPressMonitor.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/src/anchors/useUserflowAnchor.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/src/anonymousId.d.ts +2 -0
  139. package/lib/typescript/commonjs/src/anonymousId.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/src/authoring/capturePickerScreenshot.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/src/evalJs/getWebView.d.ts +2 -1
  142. package/lib/typescript/commonjs/src/evalJs/getWebView.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/src/hostAppVersion.d.ts +1 -1
  144. package/lib/typescript/commonjs/src/hostAppVersion.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  146. package/lib/typescript/commonjs/src/installCheck.d.ts +48 -0
  147. package/lib/typescript/commonjs/src/installCheck.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/src/nativeModules.d.ts +13 -0
  149. package/lib/typescript/commonjs/src/nativeModules.d.ts.map +1 -0
  150. package/lib/typescript/commonjs/src/network.d.ts.map +1 -1
  151. package/lib/typescript/commonjs/src/pairing/deviceId.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/src/pairing/tokenStorage.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/src/runtime/TargetPressBridge.d.ts +2 -1
  154. package/lib/typescript/commonjs/src/runtime/TargetPressBridge.d.ts.map +1 -1
  155. package/lib/typescript/commonjs/src/runtime/UserflowProvider.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/src/storage/asyncStorage.d.ts +15 -0
  157. package/lib/typescript/commonjs/src/storage/asyncStorage.d.ts.map +1 -0
  158. package/lib/typescript/commonjs/src/surfaces/BannerSurface.d.ts.map +1 -1
  159. package/lib/typescript/commonjs/src/surfaces/safeArea.d.ts +6 -2
  160. package/lib/typescript/commonjs/src/surfaces/safeArea.d.ts.map +1 -1
  161. package/lib/typescript/commonjs/src/version.d.ts +1 -1
  162. package/lib/typescript/module/src/anchors/AnchorRegistry.d.ts +7 -0
  163. package/lib/typescript/module/src/anchors/AnchorRegistry.d.ts.map +1 -1
  164. package/lib/typescript/module/src/anchors/elementConditionEvaluator.d.ts +12 -16
  165. package/lib/typescript/module/src/anchors/elementConditionEvaluator.d.ts.map +1 -1
  166. package/lib/typescript/module/src/anchors/elementTapMonitor.d.ts +36 -0
  167. package/lib/typescript/module/src/anchors/elementTapMonitor.d.ts.map +1 -0
  168. package/lib/typescript/module/src/anchors/elementTargetMatch.d.ts +20 -0
  169. package/lib/typescript/module/src/anchors/elementTargetMatch.d.ts.map +1 -0
  170. package/lib/typescript/module/src/anchors/fiber/findAnchorFiber.d.ts +17 -5
  171. package/lib/typescript/module/src/anchors/fiber/findAnchorFiber.d.ts.map +1 -1
  172. package/lib/typescript/module/src/anchors/fiber/touchTargetFiber.d.ts +24 -0
  173. package/lib/typescript/module/src/anchors/fiber/touchTargetFiber.d.ts.map +1 -0
  174. package/lib/typescript/module/src/anchors/resolveAnchorHost.d.ts +2 -1
  175. package/lib/typescript/module/src/anchors/resolveAnchorHost.d.ts.map +1 -1
  176. package/lib/typescript/module/src/anchors/resolveAutoAnchorHost.d.ts +10 -1
  177. package/lib/typescript/module/src/anchors/resolveAutoAnchorHost.d.ts.map +1 -1
  178. package/lib/typescript/module/src/anchors/resolveElementTargets.d.ts +39 -0
  179. package/lib/typescript/module/src/anchors/resolveElementTargets.d.ts.map +1 -0
  180. package/lib/typescript/module/src/anchors/targetPressMonitor.d.ts +2 -0
  181. package/lib/typescript/module/src/anchors/targetPressMonitor.d.ts.map +1 -1
  182. package/lib/typescript/module/src/anchors/useUserflowAnchor.d.ts.map +1 -1
  183. package/lib/typescript/module/src/anonymousId.d.ts +2 -0
  184. package/lib/typescript/module/src/anonymousId.d.ts.map +1 -1
  185. package/lib/typescript/module/src/authoring/capturePickerScreenshot.d.ts.map +1 -1
  186. package/lib/typescript/module/src/evalJs/getWebView.d.ts +2 -1
  187. package/lib/typescript/module/src/evalJs/getWebView.d.ts.map +1 -1
  188. package/lib/typescript/module/src/hostAppVersion.d.ts +1 -1
  189. package/lib/typescript/module/src/hostAppVersion.d.ts.map +1 -1
  190. package/lib/typescript/module/src/index.d.ts.map +1 -1
  191. package/lib/typescript/module/src/installCheck.d.ts +48 -0
  192. package/lib/typescript/module/src/installCheck.d.ts.map +1 -0
  193. package/lib/typescript/module/src/nativeModules.d.ts +13 -0
  194. package/lib/typescript/module/src/nativeModules.d.ts.map +1 -0
  195. package/lib/typescript/module/src/network.d.ts.map +1 -1
  196. package/lib/typescript/module/src/pairing/deviceId.d.ts.map +1 -1
  197. package/lib/typescript/module/src/pairing/tokenStorage.d.ts.map +1 -1
  198. package/lib/typescript/module/src/runtime/TargetPressBridge.d.ts +2 -1
  199. package/lib/typescript/module/src/runtime/TargetPressBridge.d.ts.map +1 -1
  200. package/lib/typescript/module/src/runtime/UserflowProvider.d.ts.map +1 -1
  201. package/lib/typescript/module/src/storage/asyncStorage.d.ts +15 -0
  202. package/lib/typescript/module/src/storage/asyncStorage.d.ts.map +1 -0
  203. package/lib/typescript/module/src/surfaces/BannerSurface.d.ts.map +1 -1
  204. package/lib/typescript/module/src/surfaces/safeArea.d.ts +6 -2
  205. package/lib/typescript/module/src/surfaces/safeArea.d.ts.map +1 -1
  206. package/lib/typescript/module/src/version.d.ts +1 -1
  207. package/package.json +25 -8
  208. package/src/anchors/AnchorRegistry.ts +12 -0
  209. package/src/anchors/elementConditionEvaluator.ts +235 -70
  210. package/src/anchors/elementTapMonitor.ts +95 -0
  211. package/src/anchors/elementTargetMatch.ts +90 -0
  212. package/src/anchors/fiber/findAnchorFiber.ts +66 -35
  213. package/src/anchors/fiber/touchTargetFiber.ts +125 -0
  214. package/src/anchors/resolveAnchorHost.ts +2 -1
  215. package/src/anchors/resolveAutoAnchorHost.ts +3 -3
  216. package/src/anchors/resolveElementTargets.ts +165 -0
  217. package/src/anchors/targetPressMonitor.ts +12 -3
  218. package/src/anchors/useUserflowAnchor.ts +1 -0
  219. package/src/anonymousId.ts +19 -8
  220. package/src/authoring/capturePickerScreenshot.ts +10 -1
  221. package/src/evalJs/getWebView.ts +6 -13
  222. package/src/hostAppVersion.ts +7 -1
  223. package/src/index.ts +10 -0
  224. package/src/installCheck.ts +225 -0
  225. package/src/nativeModules.ts +48 -0
  226. package/src/network.ts +27 -11
  227. package/src/pairing/deviceId.ts +14 -11
  228. package/src/pairing/tokenStorage.ts +23 -7
  229. package/src/runtime/TargetPressBridge.tsx +17 -1
  230. package/src/runtime/UserflowProvider.tsx +11 -4
  231. package/src/storage/asyncStorage.ts +88 -0
  232. package/src/surfaces/BannerSurface.tsx +7 -5
  233. package/src/surfaces/safeArea.ts +85 -40
  234. package/src/version.ts +1 -1
  235. package/vendor/core/dist/constants.d.ts +2 -0
  236. package/vendor/core/dist/constants.d.ts.map +1 -1
  237. package/vendor/core/dist/constants.js +2 -0
  238. package/vendor/core/dist/constants.js.map +1 -1
  239. package/vendor/core/dist/engine/ClientConditionEngine.d.ts +20 -1
  240. package/vendor/core/dist/engine/ClientConditionEngine.d.ts.map +1 -1
  241. package/vendor/core/dist/engine/ClientConditionEngine.js +13 -10
  242. package/vendor/core/dist/engine/ClientConditionEngine.js.map +1 -1
  243. package/vendor/core/dist/engine/UserflowCore.d.ts.map +1 -1
  244. package/vendor/core/dist/engine/UserflowCore.js +21 -2
  245. package/vendor/core/dist/engine/UserflowCore.js.map +1 -1
  246. package/vendor/core/dist/index.d.ts +1 -1
  247. package/vendor/core/dist/index.d.ts.map +1 -1
  248. package/vendor/core/dist/index.js.map +1 -1
  249. package/vendor/core/dist/version.d.ts +1 -1
  250. package/vendor/core/dist/version.js +1 -1
  251. package/vendor/core/package.json +1 -1
  252. package/vendor/protocol/package.json +1 -1
  253. package/docs/webview-custom-banner-poc.md +0 -64
@@ -19,35 +19,65 @@ const MAX_NODES = 60_000;
19
19
  /** One production warning per process — truncation must not look like "missing". */
20
20
  let maxNodesWarned = false;
21
21
 
22
- export type AnchorPropTier = 'testID' | 'nativeID' | 'accessibilityLabel';
22
+ export type AnchorPropTier = 'userflowId' | 'testID' | 'nativeID' | 'accessibilityLabel';
23
23
 
24
- const TIER_ORDER: AnchorPropTier[] = ['testID', 'nativeID', 'accessibilityLabel'];
24
+ /** Props an anchor id can sit on, strongest first. */
25
+ export const ANCHOR_PROP_TIERS: readonly AnchorPropTier[] = [
26
+ 'userflowId',
27
+ 'testID',
28
+ 'nativeID',
29
+ 'accessibilityLabel',
30
+ ];
25
31
 
26
32
  export type FindAnchorFiberResult =
27
33
  | { status: 'found'; fiber: AnyFiber; tier: AnchorPropTier }
28
34
  | { status: 'ambiguous'; tier: AnchorPropTier }
29
35
  | { status: 'missing' };
30
36
 
37
+ /** Every collapsed host of the winning tier. Duplicates stay in the list. */
38
+ export type FindAnchorFibersResult =
39
+ | { status: 'found'; fibers: AnyFiber[]; tier: AnchorPropTier }
40
+ | { status: 'missing' };
41
+
31
42
  /**
32
43
  * Bounded DFS under `rootFiber` looking for fibers whose memoizedProps match
33
- * `id` on testID, then nativeID, then accessibilityLabel. Uses the highest
34
- * tier that has any match so a testID hit is never made ambiguous by an
35
- * unrelated accessibilityLabel.
44
+ * `id` on userflowId, then testID, then nativeID, then accessibilityLabel.
45
+ * Uses the highest tier that has any match so a testID hit is never made
46
+ * ambiguous by an unrelated accessibilityLabel.
36
47
  *
37
48
  * A composite match resolves to the host beneath it that carries the same id
38
49
  * (e.g. a tab item that wraps its button in a View and forwards testID to the
39
50
  * button), else to its shallowest host descendant. Multiple matches on a single
40
51
  * root-to-leaf chain (e.g. Pressable forwarding testID to its inner View)
41
- * collapse to the outermost host. Disjoint matches fail closed as ambiguous.
52
+ * collapse to the outermost host. Disjoint matches stay in the list returned by
53
+ * {@link findAnchorFibers}; {@link findAnchorFiber} fails closed on those.
42
54
  *
43
55
  * The walk never follows `rootFiber.sibling` so the Userflow overlay (a sibling
44
56
  * of {@link FiberScanRoot} in UserflowProvider) cannot self-match SDK testIDs.
45
57
  */
46
- export function findAnchorFiber(rootFiber: AnyFiber, id: string): FindAnchorFiberResult {
58
+ export function findAnchorFibers(rootFiber: AnyFiber, id: string): FindAnchorFibersResult {
47
59
  if (!rootFiber || !id) return { status: 'missing' };
60
+ const won = winningTier(rootFiber, id);
61
+ if (!won) return { status: 'missing' };
62
+ return { status: 'found', fibers: won.hosts, tier: won.tier };
63
+ }
64
+
65
+ export function findAnchorFiber(rootFiber: AnyFiber, id: string): FindAnchorFiberResult {
66
+ const many = findAnchorFibers(rootFiber, id);
67
+ if (many.status === 'missing') return { status: 'missing' };
68
+ if (many.fibers.length === 1) {
69
+ return { status: 'found', fiber: many.fibers[0]!, tier: many.tier };
70
+ }
71
+ return { status: 'ambiguous', tier: many.tier };
72
+ }
48
73
 
74
+ function winningTier(
75
+ rootFiber: AnyFiber,
76
+ id: string,
77
+ ): { tier: AnchorPropTier; hosts: AnyFiber[] } | null {
49
78
  try {
50
79
  const byTier: Record<AnchorPropTier, AnyFiber[]> = {
80
+ userflowId: [],
51
81
  testID: [],
52
82
  nativeID: [],
53
83
  accessibilityLabel: [],
@@ -58,6 +88,9 @@ export function findAnchorFiber(rootFiber: AnyFiber, id: string): FindAnchorFibe
58
88
  (fiber) => {
59
89
  const props = getMemoizedProps(fiber);
60
90
  if (!props) return;
91
+ if (typeof props.userflowId === 'string' && props.userflowId === id) {
92
+ byTier.userflowId.push(fiber);
93
+ }
61
94
  if (typeof props.testID === 'string' && props.testID === id) {
62
95
  byTier.testID.push(fiber);
63
96
  }
@@ -72,44 +105,42 @@ export function findAnchorFiber(rootFiber: AnyFiber, id: string): FindAnchorFibe
72
105
  );
73
106
 
74
107
  if (truncated || visited >= MAX_NODES) {
75
- // Truncation would otherwise look exactly like "the anchor isn't there".
76
- // eslint-disable-next-line no-console -- actionable: the anchor may exist but was not reached
77
- const warn = (message: string): void => console.warn(message);
78
- if (__DEV__) {
79
- warn(
80
- `[Userflow] Anchor scan for "${id}" stopped after ${MAX_NODES} nodes. ` +
81
- `The element may exist but was not reached. Register it explicitly with ` +
82
- `useUserflowAnchor("${id}") to skip the scan.`,
83
- );
84
- } else if (!maxNodesWarned) {
85
- maxNodesWarned = true;
86
- warn(
87
- `[Userflow] Anchor scan stopped after ${MAX_NODES} nodes; some anchors may ` +
88
- `be missed. Prefer useUserflowAnchor for deep or dense trees.`,
89
- );
90
- }
108
+ warnScanTruncated(id);
91
109
  }
92
110
 
93
- for (const tier of TIER_ORDER) {
111
+ for (const tier of ANCHOR_PROP_TIERS) {
94
112
  const matches = byTier[tier];
95
113
  if (matches.length === 0) continue;
96
114
 
97
115
  const hosts = uniqueHosts(matches);
98
116
  if (hosts.length === 0) continue;
99
- if (hosts.length === 1) {
100
- return { status: 'found', fiber: hosts[0]!, tier };
101
- }
102
-
103
- const collapsed = collapseToChains(hosts);
104
- if (collapsed.length === 1) {
105
- return { status: 'found', fiber: collapsed[0]!, tier };
106
- }
107
- return { status: 'ambiguous', tier };
117
+ return { tier, hosts: collapseToChains(hosts) };
108
118
  }
109
119
 
110
- return { status: 'missing' };
120
+ return null;
111
121
  } catch {
112
- return { status: 'missing' };
122
+ return null;
123
+ }
124
+ }
125
+
126
+ function warnScanTruncated(id: string): void {
127
+ // Truncation would otherwise look exactly like "the anchor isn't there".
128
+ // eslint-disable-next-line no-console -- actionable: the anchor may exist but was not reached
129
+ const warn = (message: string): void => console.warn(message);
130
+ if (__DEV__) {
131
+ warn(
132
+ `[Userflow] Anchor scan for "${id}" stopped after ${MAX_NODES} nodes. ` +
133
+ `The element may exist but was not reached. Register it explicitly with ` +
134
+ `useUserflowAnchor("${id}") to skip the scan.`,
135
+ );
136
+ return;
137
+ }
138
+ if (!maxNodesWarned) {
139
+ maxNodesWarned = true;
140
+ warn(
141
+ `[Userflow] Anchor scan stopped after ${MAX_NODES} nodes; some anchors may ` +
142
+ `be missed. Prefer useUserflowAnchor for deep or dense trees.`,
143
+ );
113
144
  }
114
145
  }
115
146
 
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Host fiber under a touch, resolved synchronously from the responder event.
3
+ *
4
+ * Feature-detected and never throws. A renderer that does not expose any of
5
+ * these pointers degrades to "no target", so a click condition simply does not
6
+ * latch.
7
+ *
8
+ * Order:
9
+ * 1. `event._targetInst` (React's fiber for the touched component)
10
+ * 2. `event.target` public instance via {@link lookupHostFiber}
11
+ * 3. The touch's native tag (`changedTouches[].target`, else `nativeEvent.target`)
12
+ * via the scan-root host index
13
+ */
14
+ import type { GestureResponderEvent } from 'react-native';
15
+
16
+ import {
17
+ type AnyFiber,
18
+ isHostFiber,
19
+ isReactFiber,
20
+ nativeTagFromHostFiber,
21
+ resolveCurrentFiber,
22
+ } from './fiberInternals';
23
+ import { nearestHostDescendant } from './findAnchorFiber';
24
+ import { createLazyHostFiberIndex, lookupHostFiber } from './indexHostFibers';
25
+
26
+ export function touchTargetFiber(
27
+ event: GestureResponderEvent,
28
+ identifier?: number | string,
29
+ ): AnyFiber | null {
30
+ try {
31
+ const tag = nativeTagForTouch(event, identifier);
32
+
33
+ const fromInst = hostFromFiber(readTargetInst(event));
34
+ if (fromInst && tagMatches(fromInst, tag)) return fromInst;
35
+
36
+ const fromInstance = hostFromInstance(event.target);
37
+ if (fromInstance && tagMatches(fromInstance, tag)) return fromInstance;
38
+
39
+ if (tag === undefined) return null;
40
+ // The event-level fibers belong to another finger's view, so only the
41
+ // index can name this one. A miss must not fall back to theirs.
42
+ return createLazyHostFiberIndex()().byNativeTag.get(tag) ?? null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Fabric sends one touch event per touched view, and each lists every finger
50
+ * that changed. True when `identifier`'s finger is on this event's own view,
51
+ * so a caller can record each finger once, from its own event, instead of
52
+ * overwriting it from another finger's. Unreadable tags count as this event's.
53
+ */
54
+ export function touchBelongsToEvent(
55
+ event: GestureResponderEvent,
56
+ identifier: number | string,
57
+ ): boolean {
58
+ try {
59
+ const tag = nativeTagForTouch(event, identifier);
60
+ if (tag === undefined) return true;
61
+ const own = hostFromFiber(readTargetInst(event)) ?? hostFromInstance(event.target);
62
+ return own == null || tagMatches(own, tag);
63
+ } catch {
64
+ return true;
65
+ }
66
+ }
67
+
68
+ function readTargetInst(event: GestureResponderEvent): unknown {
69
+ return (event as { _targetInst?: unknown })._targetInst;
70
+ }
71
+
72
+ function nativeTagForTouch(
73
+ event: GestureResponderEvent,
74
+ identifier: number | string | undefined,
75
+ ): number | undefined {
76
+ const native = event.nativeEvent as {
77
+ target?: unknown;
78
+ changedTouches?: Array<{ identifier?: number | string; target?: unknown }>;
79
+ };
80
+ if (identifier != null && native.changedTouches) {
81
+ const touch = native.changedTouches.find((entry) => entry.identifier === identifier);
82
+ const touchTag = asTag(touch?.target);
83
+ if (touchTag !== undefined) return touchTag;
84
+ }
85
+ return asTag(native.target);
86
+ }
87
+
88
+ function asTag(value: unknown): number | undefined {
89
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
90
+ }
91
+
92
+ /**
93
+ * Accept a host whose tag we cannot read (test renderer, older Paper). Reject
94
+ * only when both sides have a tag and they differ — that event-level fiber
95
+ * belongs to a different finger.
96
+ */
97
+ function tagMatches(fiber: AnyFiber, tag: number | undefined): boolean {
98
+ if (tag === undefined) return true;
99
+ const fiberTag = nativeTagFromHostFiber(fiber);
100
+ if (fiberTag === undefined) return true;
101
+ return fiberTag === tag;
102
+ }
103
+
104
+ function hostFromFiber(value: unknown): AnyFiber | null {
105
+ if (!isReactFiber(value)) return null;
106
+ try {
107
+ const current = resolveCurrentFiber(value);
108
+ if (isHostFiber(current)) return current;
109
+ return nearestHostDescendant(current);
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ function hostFromInstance(value: unknown): AnyFiber | null {
116
+ if (!value || typeof value !== 'object') return null;
117
+ try {
118
+ const fiber = lookupHostFiber(createLazyHostFiberIndex(), value);
119
+ if (!fiber) return null;
120
+ if (isHostFiber(fiber)) return fiber;
121
+ return nearestHostDescendant(fiber);
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
@@ -43,7 +43,8 @@ export function invalidateFiberAnchor(id: string): void {
43
43
  *
44
44
  * Tier 0: {@link anchorRegistry} (`useUserflowAnchor`) — higher priority, fails
45
45
  * closed on ambiguous registry keys.
46
- * Tier 1+: fiber scan for `testID`, then `nativeID`, then `accessibilityLabel`.
46
+ * Tier 1+: fiber scan for `userflowId`, then `testID`, then `nativeID`, then
47
+ * `accessibilityLabel`.
47
48
  *
48
49
  * Synchronous (the walk is pure JS; only measurement is async via the returned
49
50
  * host). Caches the resolved fiber per id and revalidates cheaply before reuse.
@@ -75,7 +75,7 @@ function fiberFromInstance(inst: AutoAnchorInstance): AnyFiber | null {
75
75
  }
76
76
 
77
77
  /** Authoritative id that the existing fiber / manual resolver can look up. */
78
- function exactIdFromFingerprint(fp: MobileFingerprint): string | null {
78
+ export function exactIdFromFingerprint(fp: MobileFingerprint): string | null {
79
79
  if (fp.userflowId) return fp.userflowId;
80
80
  if (fp.testID) return fp.testID;
81
81
  if (fp.nativeID && !fp.nativeID.startsWith('uf_')) return fp.nativeID;
@@ -116,7 +116,7 @@ function mapMatch(match: MatchResult): AutoAnchorResolution {
116
116
  * scored and the unfiltered pool still has a hit, that hit is on a covered
117
117
  * screen — `absent`, matching MANUAL covered-screen semantics.
118
118
  */
119
- function mapScored(fp: MobileFingerprint): AutoAnchorResolution {
119
+ export function resolveScoredAutoAnchor(fp: MobileFingerprint): AutoAnchorResolution {
120
120
  const visible = matchFingerprint(fp, { instanceFilter: createVisibleInstanceFilter() });
121
121
  if (visible.instance) return mapMatch(visible);
122
122
 
@@ -178,5 +178,5 @@ export function resolveAutoAnchorHost(
178
178
  }
179
179
  }
180
180
 
181
- return mapScored(fp);
181
+ return resolveScoredAutoAnchor(fp);
182
182
  }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Resolve every host an ELEMENT condition should consider.
3
+ *
4
+ * Tooltips fail closed when an id matches more than one view. Conditions follow
5
+ * the web SDK instead: any match counts. AUTO exact ids (`userflowId`, then
6
+ * `testID`, then a real `nativeID`) use that same multi-host lookup, and fall
7
+ * through to scoring only when nothing matched.
8
+ */
9
+ import { decodeMobileAutoData, type FlowSelectorFragment } from '@userflow-tech/protocol';
10
+
11
+ import { type AnchorHost, anchorRegistry } from './AnchorRegistry';
12
+ import type { ElementTap } from './elementTapMonitor';
13
+ import { anchorIdsAboveTap } from './elementTargetMatch';
14
+ import { fiberScanRoot } from './fiber/FiberScanRoot';
15
+ import { type AnyFiber, publicInstanceFromHostFiber } from './fiber/fiberInternals';
16
+ import { findAnchorFibers } from './fiber/findAnchorFiber';
17
+ import {
18
+ createLazyHostFiberIndex,
19
+ lookupHostFiber,
20
+ type HostFiberIndexSource,
21
+ } from './fiber/indexHostFibers';
22
+ import { createFiberMeasureInWindow } from './fiber/measureHostFiber';
23
+ import { isFiberOnVisibleScreen } from './fiber/visibleScreens';
24
+ import {
25
+ exactIdFromFingerprint,
26
+ resolveAutoAnchorHost,
27
+ resolveScoredAutoAnchor,
28
+ type AutoAnchorResolution,
29
+ } from './resolveAutoAnchorHost';
30
+ import { resolveManualAnchorId } from './resolveManualAnchorId';
31
+
32
+ export interface ElementTarget {
33
+ host: AnchorHost;
34
+ fiber?: AnyFiber;
35
+ }
36
+
37
+ export type ElementTargetResolution =
38
+ | { status: 'found'; targets: ElementTarget[] }
39
+ | { status: 'absent' }
40
+ | {
41
+ status: 'unknown';
42
+ /**
43
+ * Ambiguous scored matches stay unknown without a warning. A missing
44
+ * selector or an AUTO payload that cannot be decoded still warns.
45
+ */
46
+ silent?: boolean;
47
+ };
48
+
49
+ export function resolveElementTargets(
50
+ selector: FlowSelectorFragment | null | undefined,
51
+ ): ElementTargetResolution {
52
+ if (selector == null) return { status: 'unknown' };
53
+
54
+ if ((selector.type as string | null) === 'AUTO') {
55
+ return resolveAutoTargets(selector.autoData);
56
+ }
57
+
58
+ const anchorId = resolveManualAnchorId(selector.css);
59
+ if (anchorId == null) return { status: 'unknown' };
60
+ return resolveManualTargets(anchorId);
61
+ }
62
+
63
+ /**
64
+ * False when no target of `selector` can contain `tap`, decided without a tree
65
+ * scan. A fiber-scan target carries its id itself or sits under a composite
66
+ * that does, so a MANUAL id that is neither registered nor on the tap's
67
+ * ancestor chain cannot match. AUTO selectors are not gated: only a scan tells
68
+ * whether the exact id or a scored match applies.
69
+ */
70
+ export function tapMayHitSelector(
71
+ selector: FlowSelectorFragment | null | undefined,
72
+ tap: ElementTap,
73
+ ): boolean {
74
+ if (selector == null) return false;
75
+ if ((selector.type as string | null) === 'AUTO') return true;
76
+
77
+ const anchorId = resolveManualAnchorId(selector.css);
78
+ if (anchorId == null) return false;
79
+ return anchorRegistry.get(anchorId) != null || anchorIdsAboveTap(tap).has(anchorId);
80
+ }
81
+
82
+ function resolveAutoTargets(autoData: string | null): ElementTargetResolution {
83
+ const decoded = decodeMobileAutoData(autoData);
84
+ const fingerprint = decoded?.fingerprint;
85
+ if (!decoded || !fingerprint) return { status: 'unknown' };
86
+
87
+ if (decoded.resolver === 'fiber' && decoded.fiberPath) {
88
+ return fromAutoResolution(resolveAutoAnchorHost(decoded));
89
+ }
90
+
91
+ const exactId = exactIdFromFingerprint(fingerprint);
92
+ if (exactId) {
93
+ const manual = resolveManualTargets(exactId);
94
+ if (manual.status === 'found') return manual;
95
+ }
96
+
97
+ // The exact id already missed above, so go straight to scoring rather than
98
+ // letting resolveAutoAnchorHost scan for it again.
99
+ return fromAutoResolution(resolveScoredAutoAnchor(fingerprint));
100
+ }
101
+
102
+ function fromAutoResolution(resolution: AutoAnchorResolution): ElementTargetResolution {
103
+ if (resolution.status === 'found') {
104
+ // Scored matches only try the direct ref pointer, which Paper release
105
+ // builds null out. Without the index fallback a tap could never match.
106
+ const fiber =
107
+ resolution.fiber ??
108
+ fiberForNode(createLazyHostFiberIndex(), resolution.match?.instance?.ref.current);
109
+ return {
110
+ status: 'found',
111
+ targets: [{ host: resolution.host, ...(fiber ? { fiber } : {}) }],
112
+ };
113
+ }
114
+ if (resolution.status === 'absent') return { status: 'absent' };
115
+ // Ambiguous and unknown both fail closed. Ambiguous is a real tie, not a
116
+ // broken selector, so it does not warn.
117
+ if (resolution.status === 'ambiguous') return { status: 'unknown', silent: true };
118
+ return { status: 'unknown' };
119
+ }
120
+
121
+ function resolveManualTargets(id: string): ElementTargetResolution {
122
+ const registered = anchorRegistry.getAll(id);
123
+ if (registered.length > 0) {
124
+ const index = createLazyHostFiberIndex();
125
+ // Registered views on a covered navigator screen still measure non-zero,
126
+ // so drop them here the way the fiber scan skips covered screens. A host
127
+ // whose fiber cannot be found is kept.
128
+ const targets = registered
129
+ .map((host): ElementTarget => {
130
+ const fiber = fiberForNode(index, host.node);
131
+ return fiber ? { host, fiber } : { host };
132
+ })
133
+ .filter((target) => !target.fiber || isFiberOnVisibleScreen(target.fiber));
134
+ return targets.length > 0 ? { status: 'found', targets } : { status: 'absent' };
135
+ }
136
+
137
+ const root = fiberScanRoot.getFiber();
138
+ if (!root) return { status: 'absent' };
139
+
140
+ const found = findAnchorFibers(root, id);
141
+ if (found.status === 'missing') return { status: 'absent' };
142
+
143
+ return {
144
+ status: 'found',
145
+ targets: found.fibers.map((fiber) => {
146
+ const node = publicInstanceFromHostFiber(fiber);
147
+ return {
148
+ host: {
149
+ measureInWindow: createFiberMeasureInWindow(fiber),
150
+ ...(node ? { node } : {}),
151
+ },
152
+ fiber,
153
+ };
154
+ }),
155
+ };
156
+ }
157
+
158
+ function fiberForNode(index: HostFiberIndexSource, node: unknown): AnyFiber | undefined {
159
+ if (!node) return undefined;
160
+ try {
161
+ return lookupHostFiber(index, node) ?? undefined;
162
+ } catch {
163
+ return undefined;
164
+ }
165
+ }
@@ -31,6 +31,17 @@ export interface TouchPoint {
31
31
  pageY: number;
32
32
  }
33
33
 
34
+ /** True when the finger moved no more than `slop` between start and end. */
35
+ export function withinTapSlop(
36
+ start: TouchPoint,
37
+ end: TouchPoint,
38
+ slop: number = TARGET_PRESS_TAP_SLOP,
39
+ ): boolean {
40
+ const dx = end.pageX - start.pageX;
41
+ const dy = end.pageY - start.pageY;
42
+ return dx * dx + dy * dy <= slop * slop;
43
+ }
44
+
34
45
  /**
35
46
  * True when start and end both land in `rect` and movement stays within tap
36
47
  * slop — mirrors a DOM click on the target without claiming the responder.
@@ -43,9 +54,7 @@ export function isTargetTap(
43
54
  ): boolean {
44
55
  if (!pointInRect(start.pageX, start.pageY, rect)) return false;
45
56
  if (!pointInRect(end.pageX, end.pageY, rect)) return false;
46
- const dx = end.pageX - start.pageX;
47
- const dy = end.pageY - start.pageY;
48
- return dx * dx + dy * dy <= slop * slop;
57
+ return withinTapSlop(start, end, slop);
49
58
  }
50
59
 
51
60
  /**
@@ -71,6 +71,7 @@ export function useUserflowAnchor(id: string): React.RefCallback<View> {
71
71
  measureInWindow: (callback) => {
72
72
  node.measureInWindow(callback);
73
73
  },
74
+ node,
74
75
  scrollIntoView: () => {
75
76
  const scrollable = node as View & {
76
77
  scrollIntoView?: (options?: { block?: string; inline?: string }) => void;
@@ -1,9 +1,17 @@
1
- import AsyncStorage from '@react-native-async-storage/async-storage';
2
1
  import { UserflowAuthError } from '@userflow-tech/core';
3
2
 
3
+ import { getAsyncStorage } from './storage/asyncStorage';
4
+
4
5
  /** Matches the web SDK's `userflow:anonymousId` localStorage key. */
5
6
  export const ANONYMOUS_ID_STORAGE_KEY = 'userflow:anonymousId';
6
7
 
8
+ export const ANONYMOUS_STORAGE_UNAVAILABLE =
9
+ 'Userflow.identifyAnonymous() is not supported when persistent storage is unavailable.';
10
+
11
+ export const ANONYMOUS_STORAGE_NOT_LINKED =
12
+ 'Userflow.identifyAnonymous() needs @react-native-async-storage/async-storage, which is not ' +
13
+ "linked in this native build. Add it to your app's package.json, run pod install, and rebuild.";
14
+
7
15
  export function uuid(): string {
8
16
  const webcrypto = globalThis.crypto;
9
17
  if (webcrypto?.randomUUID) return webcrypto.randomUUID();
@@ -15,24 +23,27 @@ export function uuid(): string {
15
23
  }
16
24
 
17
25
  export async function getOrCreateAnonymousId(): Promise<string> {
26
+ const storage = getAsyncStorage();
27
+ if (!storage) {
28
+ throw new UserflowAuthError(ANONYMOUS_STORAGE_NOT_LINKED, { isHostContract: true });
29
+ }
18
30
  try {
19
- let externalId = await AsyncStorage.getItem(ANONYMOUS_ID_STORAGE_KEY);
31
+ let externalId = await storage.getItem(ANONYMOUS_ID_STORAGE_KEY);
20
32
  if (!externalId) {
21
33
  externalId = `anon-${uuid()}`;
22
- await AsyncStorage.setItem(ANONYMOUS_ID_STORAGE_KEY, externalId);
34
+ await storage.setItem(ANONYMOUS_ID_STORAGE_KEY, externalId);
23
35
  }
24
36
  return externalId;
25
37
  } catch {
26
- throw new UserflowAuthError(
27
- 'Userflow.identifyAnonymous() is not supported when persistent storage is unavailable.',
28
- { isHostContract: true },
29
- );
38
+ throw new UserflowAuthError(ANONYMOUS_STORAGE_UNAVAILABLE, { isHostContract: true });
30
39
  }
31
40
  }
32
41
 
33
42
  export async function clearAnonymousId(): Promise<void> {
43
+ const storage = getAsyncStorage();
44
+ if (!storage) return;
34
45
  try {
35
- await AsyncStorage.removeItem(ANONYMOUS_ID_STORAGE_KEY);
46
+ await storage.removeItem(ANONYMOUS_ID_STORAGE_KEY);
36
47
  } catch {
37
48
  // Best-effort clear, mirroring the web SDK when storage access is denied.
38
49
  }
@@ -1,9 +1,12 @@
1
1
  import { Dimensions } from 'react-native';
2
2
 
3
+ import { isNativeModuleLinked } from '../nativeModules';
4
+
3
5
  import type { ElementPickerScreenshot } from './elementPickerController';
4
6
  import type { HitRect } from './hitTest';
5
7
 
6
8
  const MAX_CAPTURE_WIDTH = 480;
9
+ const VIEW_SHOT_MODULE = 'RNViewShot';
7
10
 
8
11
  const MISSING_VIEW_SHOT_WARNING =
9
12
  '[Userflow] Required peer react-native-view-shot is not installed; ' +
@@ -31,8 +34,14 @@ function warnMissingViewShot(): void {
31
34
  }
32
35
 
33
36
  function loadViewShot(): ViewShotModule | null {
37
+ // view-shot 4 calls TurboModuleRegistry.getEnforcing at import. Metro reports
38
+ // that throw as fatal before this try/catch can see it.
39
+ if (!isNativeModuleLinked(VIEW_SHOT_MODULE)) {
40
+ warnMissingViewShot();
41
+ return null;
42
+ }
34
43
  try {
35
- // Required peer; degrade if unlinked or the JS package is missing.
44
+ // Required peer; degrade if the JS package is missing.
36
45
  // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
37
46
  return require('react-native-view-shot') as ViewShotModule;
38
47
  } catch {
@@ -1,5 +1,6 @@
1
1
  import type { ComponentType, Ref } from 'react';
2
- import { NativeModules, TurboModuleRegistry } from 'react-native';
2
+
3
+ import { isNativeModuleLinked } from '../nativeModules';
3
4
 
4
5
  /**
5
6
  * Minimal surface of `react-native-webview` this spike uses.
@@ -45,20 +46,11 @@ let warnedMissing = false;
45
46
  /**
46
47
  * True when the native `react-native-webview` binary is linked. Do not
47
48
  * `require('react-native-webview')` before this passes — the JS entry calls
48
- * `TurboModuleRegistry.getEnforcing('RNCWebViewModule')` and can red-screen.
49
+ * `TurboModuleRegistry.getEnforcing('RNCWebViewModule')` and Metro reports that
50
+ * throw as fatal before try/catch.
49
51
  */
50
52
  export function isEvalJsWebViewNativeLinked(): boolean {
51
- try {
52
- if (typeof TurboModuleRegistry?.get === 'function') {
53
- if (TurboModuleRegistry.get('RNCWebViewModule') != null) {
54
- return true;
55
- }
56
- }
57
- const legacy = NativeModules.RNCWebViewModule as unknown;
58
- return legacy != null;
59
- } catch {
60
- return false;
61
- }
53
+ return isNativeModuleLinked('RNCWebViewModule');
62
54
  }
63
55
 
64
56
  /**
@@ -101,6 +93,7 @@ export function getEvalJsWebView(): EvalJsWebViewComponent | null {
101
93
  function warnMissing(reason: 'native' | 'js' | 'unavailable'): void {
102
94
  if (warnedMissing) return;
103
95
  warnedMissing = true;
96
+ if (!__DEV__) return;
104
97
  if (reason === 'native') {
105
98
  console.warn(
106
99
  'Userflow: react-native-webview is listed in package.json but RNCWebViewModule is not in ' +
@@ -2,7 +2,7 @@
2
2
  * Read the host app's native marketing version (store version), not the
3
3
  * SDK package version. React Native does not expose the store version on
4
4
  * `Platform`; Expo hosts may use optional expo modules; bare RN reads
5
- * `NativeModules.RNDeviceInfo` constants from the bundled device-info native
5
+ * `NativeModules.RNDeviceInfo` constants from the device-info native
6
6
  * module (after autolink + rebuild). The OS name is `Platform.OS`.
7
7
  *
8
8
  * Do not `require('react-native-device-info')` — that package throws at import
@@ -10,6 +10,8 @@
10
10
  * even inside try/catch.
11
11
  */
12
12
 
13
+ import { isNativeModuleLinked } from './nativeModules';
14
+
13
15
  export type HostAppVersionSource =
14
16
  | 'expo-application'
15
17
  | 'expo-constants'
@@ -42,6 +44,10 @@ function unwrapModule(mod: unknown): Record<string, unknown> | null {
42
44
  }
43
45
 
44
46
  function tryRequireExpoApplication(): unknown {
47
+ // expo-application calls requireNativeModule('ExpoApplication') at import.
48
+ // Skip the require when that module is not registered so a missing binary
49
+ // cannot surface as a fatal error.
50
+ if (!isNativeModuleLinked('ExpoApplication')) return undefined;
45
51
  try {
46
52
  // Optional on bare RN — Metro requires a string literal.
47
53
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires