@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
package/src/index.ts CHANGED
@@ -52,6 +52,12 @@ import {
52
52
  mergeHostAppVersionUserAttributes,
53
53
  type HostAppVersion,
54
54
  } from './hostAppVersion';
55
+ import {
56
+ commitInitOptions,
57
+ releaseFailedInitOptions,
58
+ warnIfInitOptionsIgnored,
59
+ warnIfInstallIncomplete,
60
+ } from './installCheck';
55
61
  import { LifecycleController } from './lifecycle';
56
62
  import {
57
63
  openFlowNavigateUrl,
@@ -285,10 +291,12 @@ async function afterIdentify(): Promise<void> {
285
291
  export const Userflow = {
286
292
  /** Validate credentials, store configuration, and wire app-lifecycle handling. */
287
293
  init(config: UserflowInitConfig): Promise<UserflowConfig> {
294
+ warnIfInitOptionsIgnored(config);
288
295
  cacheHostAppVersionAtInit({
289
296
  ...(config.appVersion !== undefined ? { appVersion: config.appVersion } : {}),
290
297
  ...(config.appBuild !== undefined ? { appBuild: config.appBuild } : {}),
291
298
  });
299
+ warnIfInstallIncomplete();
292
300
  cacheDeviceContextAtInit();
293
301
  setHostOnNavigate(config.onNavigate);
294
302
  lifecycle.start();
@@ -318,6 +326,7 @@ export const Userflow = {
318
326
 
319
327
  return core.init(config).then(
320
328
  (resolved) => {
329
+ commitInitOptions();
321
330
  enableScreenTracking();
322
331
  // Pairing connect can take the full socket timeout. Identify, and the
323
332
  // end-user socket that delivers the preview, must not wait on it.
@@ -326,6 +335,7 @@ export const Userflow = {
326
335
  return resolved;
327
336
  },
328
337
  (error: unknown) => {
338
+ releaseFailedInitOptions();
329
339
  pairing.notifyInitFailed(error);
330
340
  throw error;
331
341
  },
@@ -0,0 +1,225 @@
1
+ import { isEvalJsWebViewNativeLinked } from './evalJs/getWebView';
2
+ import { isNativeModuleLinked } from './nativeModules';
3
+ import { isAsyncStorageLinked } from './storage/asyncStorage';
4
+ import { isSafeAreaContextInstalled } from './surfaces/safeArea';
5
+
6
+ export const INSTALL_CHECK_HEADER =
7
+ '[Userflow] Native setup is incomplete. Flows may not run until you fix the following, then rebuild the app (a Metro reload is not enough):';
8
+
9
+ export const INSTALL_ASYNC_STORAGE_LINE =
10
+ '- @react-native-async-storage/async-storage is not linked. Anonymous users and Builder pairing will not persist. Add it to the app package.json (npx expo install @react-native-async-storage/async-storage), run pod install on iOS, and rebuild.';
11
+
12
+ export const INSTALL_VIEW_SHOT_LINE =
13
+ '- react-native-view-shot is not linked, so Builder element selection gets no screenshot. Install react-native-view-shot@^5.0.0 and rebuild. On Expo SDK 54, pin ^5.0.0; an unpinned npx expo install resolves 4.0.3.';
14
+
15
+ export const INSTALL_WEBVIEW_LINE =
16
+ '- react-native-webview is not linked. Evaluate JavaScript and WebView banners will not run. Install it (npx expo install react-native-webview) and rebuild.';
17
+
18
+ export const INSTALL_DEVICE_INFO_LINE =
19
+ '- react-native-device-info is not linked, so app_version and app_build are not sent. npm install react-native-device-info, then rebuild.';
20
+
21
+ export const INSTALL_CHECK_FOOTER =
22
+ 'See node_modules/@userflow-tech/react-native/docs/troubleshooting.md.';
23
+
24
+ /**
25
+ * Separate from {@link INSTALL_CHECK_HEADER}. Flows still run without
26
+ * safe-area-context; they draw under the status bar.
27
+ */
28
+ export const INSTALL_SAFE_AREA_WARNING =
29
+ '[Userflow] react-native-safe-area-context is not installed, so banners, flow cards, and Builder bars draw under the status bar and home indicator. Install it (npx expo install react-native-safe-area-context) and wrap the app in <SafeAreaProvider>.\n' +
30
+ INSTALL_CHECK_FOOTER;
31
+
32
+ /**
33
+ * Separate from {@link INSTALL_CHECK_HEADER}. Flows still run on view-shot 4;
34
+ * Builder screenshots may not. The installed version is interpolated.
35
+ */
36
+ export function formatViewShotVersionWarning(version: string): string {
37
+ return (
38
+ `[Userflow] react-native-view-shot@${version} is older than 5.0.0, so Builder element selection may not get a screenshot. ` +
39
+ 'Install react-native-view-shot@^5.0.0 in the same command as the SDK and rebuild. ' +
40
+ 'Do not retry the install with --force or --legacy-peer-deps; that leaves this version in place. ' +
41
+ 'On Expo SDK 54, pin ^5.0.0 and add "react-native-view-shot" to expo.install.exclude.\n' +
42
+ INSTALL_CHECK_FOOTER
43
+ );
44
+ }
45
+
46
+ export const EXPO_GO_WARNING =
47
+ '[Userflow] This app is running in Expo Go, which cannot load the SDK native modules. Builder preview links will not open the app. Use a development build (npx expo run:ios / npx expo run:android).';
48
+
49
+ export const INIT_OPTIONS_IGNORED_WARNING =
50
+ '[Userflow] Userflow.init() already ran with different options. The new clientToken, serverEndpoint, and pairingEndpoint are ignored because init runs once per process. Userflow.reset() does not apply new credentials. Restart the app to change them.';
51
+
52
+ const VIEW_SHOT_MODULE = 'RNViewShot';
53
+ const DEVICE_INFO_MODULE = 'RNDeviceInfo';
54
+
55
+ interface ExpoConstantsModule {
56
+ executionEnvironment?: string;
57
+ default?: { executionEnvironment?: string };
58
+ }
59
+
60
+ let didWarn = false;
61
+
62
+ function readExpoConstants(): ExpoConstantsModule | null {
63
+ try {
64
+ // Optional. Bare React Native does not have this package.
65
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
66
+ const mod = require('expo-constants') as ExpoConstantsModule;
67
+ const record = mod?.default ?? mod;
68
+ if (record == null || typeof record !== 'object') return null;
69
+ // Touch the field so a throwing getter (missing package stand-in) is caught.
70
+ void record.executionEnvironment;
71
+ return record;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Installed view-shot version, read from package.json only. Requiring the
79
+ * package entry loads the native module, and view-shot 4 calls
80
+ * `TurboModuleRegistry.getEnforcing` at import.
81
+ */
82
+ function readViewShotVersion(): string | null {
83
+ try {
84
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
85
+ const pkg = require('react-native-view-shot/package.json') as { version?: unknown };
86
+ return typeof pkg.version === 'string' && pkg.version.length > 0 ? pkg.version : null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /** Dev warning when view-shot 4.x is installed. Null on 5.x or when it is absent. */
93
+ export function viewShotVersionWarning(): string | null {
94
+ const version = readViewShotVersion();
95
+ if (!version) return null;
96
+ const match = version.match(/\d+/);
97
+ const major = match ? Number(match[0]) : null;
98
+ if (major == null || major >= 5) return null;
99
+ return formatViewShotVersionWarning(version);
100
+ }
101
+
102
+ /** Lines for missing native modules. Empty when the build looks complete. */
103
+ export function collectInstallFindings(): string[] {
104
+ const findings: string[] = [];
105
+ if (!isAsyncStorageLinked()) findings.push(INSTALL_ASYNC_STORAGE_LINE);
106
+ if (!isNativeModuleLinked(VIEW_SHOT_MODULE)) findings.push(INSTALL_VIEW_SHOT_LINE);
107
+ if (!isEvalJsWebViewNativeLinked()) findings.push(INSTALL_WEBVIEW_LINE);
108
+ // Expo reads the store version from expo-constants / expo-application.
109
+ // device-info is the bare React Native path.
110
+ if (readExpoConstants() == null && !isNativeModuleLinked(DEVICE_INFO_MODULE)) {
111
+ findings.push(INSTALL_DEVICE_INFO_LINE);
112
+ }
113
+ return findings;
114
+ }
115
+
116
+ export function expoGoWarning(): string | null {
117
+ const constants = readExpoConstants();
118
+ const environment = constants?.executionEnvironment;
119
+ return environment === 'storeClient' ? EXPO_GO_WARNING : null;
120
+ }
121
+
122
+ export function formatInstallWarning(findings: readonly string[]): string {
123
+ return [INSTALL_CHECK_HEADER, ...findings, INSTALL_CHECK_FOOTER].join('\n');
124
+ }
125
+
126
+ /**
127
+ * One development warning for missing native modules, separate warnings when
128
+ * safe-area-context is not installed or view-shot is older than 5, and a
129
+ * separate warning when the host is Expo Go. Release builds stay quiet. The
130
+ * safe-area and old view-shot warnings are not part of the native-module list:
131
+ * flows still run, and that list says they may not.
132
+ */
133
+ export function warnIfInstallIncomplete(): void {
134
+ if (!__DEV__ || didWarn) return;
135
+ didWarn = true;
136
+
137
+ const findings = collectInstallFindings();
138
+ if (findings.length > 0) {
139
+ // eslint-disable-next-line no-console -- intentional install diagnostic
140
+ console.warn(formatInstallWarning(findings));
141
+ }
142
+
143
+ if (!isSafeAreaContextInstalled()) {
144
+ // eslint-disable-next-line no-console -- intentional install diagnostic
145
+ console.warn(INSTALL_SAFE_AREA_WARNING);
146
+ }
147
+
148
+ const oldViewShot = viewShotVersionWarning();
149
+ if (oldViewShot) {
150
+ // eslint-disable-next-line no-console -- intentional install diagnostic
151
+ console.warn(oldViewShot);
152
+ }
153
+
154
+ const expoGo = expoGoWarning();
155
+ if (expoGo) {
156
+ // eslint-disable-next-line no-console -- intentional install diagnostic
157
+ console.warn(expoGo);
158
+ }
159
+ }
160
+
161
+ /** @internal */
162
+ export function resetInstallCheckForTests(): void {
163
+ didWarn = false;
164
+ recordedInitOptions = null;
165
+ initOptionsCommitted = false;
166
+ }
167
+
168
+ interface RecordedInitOptions {
169
+ clientToken: string;
170
+ serverEndpoint: string;
171
+ pairingEndpoint: string;
172
+ }
173
+
174
+ let recordedInitOptions: RecordedInitOptions | null = null;
175
+ /** True once an `init` promise has resolved. A rejection before that clears the record. */
176
+ let initOptionsCommitted = false;
177
+
178
+ function recordInitOptions(config: {
179
+ clientToken: string;
180
+ serverEndpoint?: string;
181
+ pairingEndpoint?: string;
182
+ }): RecordedInitOptions {
183
+ return {
184
+ clientToken: config.clientToken,
185
+ serverEndpoint: config.serverEndpoint ?? '',
186
+ pairingEndpoint: config.pairingEndpoint ?? '',
187
+ };
188
+ }
189
+
190
+ export function warnIfInitOptionsIgnored(config: {
191
+ clientToken: string;
192
+ serverEndpoint?: string;
193
+ pairingEndpoint?: string;
194
+ }): void {
195
+ const next = recordInitOptions(config);
196
+ if (!recordedInitOptions) {
197
+ recordedInitOptions = next;
198
+ return;
199
+ }
200
+ if (
201
+ recordedInitOptions.clientToken === next.clientToken &&
202
+ recordedInitOptions.serverEndpoint === next.serverEndpoint &&
203
+ recordedInitOptions.pairingEndpoint === next.pairingEndpoint
204
+ ) {
205
+ return;
206
+ }
207
+ if (!__DEV__) return;
208
+ // eslint-disable-next-line no-console -- intentional install diagnostic
209
+ console.warn(INIT_OPTIONS_IGNORED_WARNING);
210
+ }
211
+
212
+ /** The in-flight `init` resolved, so later calls really are ignored. */
213
+ export function commitInitOptions(): void {
214
+ initOptionsCommitted = true;
215
+ }
216
+
217
+ /**
218
+ * The in-flight `init` rejected. Core drops its cached promise and will run
219
+ * the next call, so a corrected token or endpoint must not warn.
220
+ * A rejection after a successful init does not forget that success.
221
+ */
222
+ export function releaseFailedInitOptions(): void {
223
+ if (initOptionsCommitted) return;
224
+ recordedInitOptions = null;
225
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Whether a native module is registered in this binary.
3
+ *
4
+ * Check this before `require`ing a package that throws while it loads when the
5
+ * native half is missing. Metro reports that throw as a fatal error before the
6
+ * caller's try/catch runs, which is a red screen in debug and can crash a
7
+ * release build.
8
+ *
9
+ * React Native modules show up on `TurboModuleRegistry` or `NativeModules`.
10
+ * Expo modules are on `globalThis.expo.modules` (see `requireOptionalNativeModule`).
11
+ */
12
+
13
+ interface NativeLookup {
14
+ NativeModules?: Record<string, unknown>;
15
+ TurboModuleRegistry?: { get?: (name: string) => unknown };
16
+ }
17
+
18
+ interface ExpoRuntime {
19
+ modules?: Record<string, unknown>;
20
+ }
21
+
22
+ function loadReactNative(): NativeLookup | null {
23
+ try {
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
25
+ return require('react-native') as NativeLookup;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ export function isNativeModuleLinked(name: string): boolean {
32
+ const expoModules = (globalThis as { expo?: ExpoRuntime }).expo?.modules;
33
+ if (expoModules?.[name] != null) return true;
34
+
35
+ const reactNative = loadReactNative();
36
+ if (!reactNative) return false;
37
+ try {
38
+ const turbo = reactNative.TurboModuleRegistry;
39
+ if (typeof turbo?.get === 'function' && turbo.get(name) != null) return true;
40
+ } catch {
41
+ // Fall through to the legacy registry.
42
+ }
43
+ try {
44
+ return reactNative.NativeModules?.[name] != null;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
package/src/network.ts CHANGED
@@ -14,6 +14,8 @@
14
14
  * recovers within a few seconds of the next beat / foreground return.
15
15
  */
16
16
 
17
+ import { isNativeModuleLinked } from './nativeModules';
18
+
17
19
  export interface NetworkStatus {
18
20
  isConnected: boolean | null;
19
21
  /** `null` means unknown — treat as reachable when connected. */
@@ -30,10 +32,31 @@ function isOnline(status: NetworkStatus): boolean {
30
32
  return status.isConnected === true && status.isInternetReachable !== false;
31
33
  }
32
34
 
35
+ const NETINFO_MODULE = 'RNCNetInfo';
36
+
37
+ const NETINFO_UNAVAILABLE_WARNING =
38
+ '[Userflow] Optional peer @react-native-community/netinfo is not installed or not linked; ' +
39
+ 'skipping instant network-aware reconnect. Connection recovery still works ' +
40
+ 'via the adaptive heartbeat and foreground liveness probe.';
41
+
42
+ function warnNetInfoUnavailable(): void {
43
+ if (!__DEV__) return;
44
+ // eslint-disable-next-line no-console -- intentional missing-optional-peer diagnostic
45
+ console.warn(NETINFO_UNAVAILABLE_WARNING);
46
+ }
47
+
33
48
  function defaultSubscribe(handler: (status: NetworkStatus) => void): () => void {
34
- // Lazy require so the SDK works without the optional peer. When installed,
35
- // NetInfo makes offline→online reconnect fast; when absent the transport's
36
- // adaptive heartbeat + foreground probe still recover the connection.
49
+ // Check the native module before require. NetInfo throws while its module
50
+ // loads when RNCNetInfo is missing, and Metro reports that as fatal before
51
+ // this try/catch can see it.
52
+ if (!isNativeModuleLinked(NETINFO_MODULE)) {
53
+ warnNetInfoUnavailable();
54
+ return () => {};
55
+ }
56
+
57
+ // Lazy require so the SDK works without the optional peer. When installed
58
+ // and linked, NetInfo makes offline→online reconnect fast; when absent the
59
+ // transport's adaptive heartbeat + foreground probe still recover.
37
60
  try {
38
61
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- optional peer at import time
39
62
  const NetInfo = require('@react-native-community/netinfo') as {
@@ -51,14 +74,7 @@ function defaultSubscribe(handler: (status: NetworkStatus) => void): () => void
51
74
  });
52
75
  });
53
76
  } catch {
54
- if (__DEV__) {
55
- // eslint-disable-next-line no-console -- intentional missing-optional-peer diagnostic
56
- console.warn(
57
- '[Userflow] Optional peer @react-native-community/netinfo is not installed; ' +
58
- 'skipping instant network-aware reconnect. Connection recovery still works ' +
59
- 'via the adaptive heartbeat and foreground liveness probe.',
60
- );
61
- }
77
+ warnNetInfoUnavailable();
62
78
  return () => {};
63
79
  }
64
80
  }
@@ -1,6 +1,5 @@
1
- import AsyncStorage from '@react-native-async-storage/async-storage';
2
-
3
1
  import { uuid } from '../anonymousId';
2
+ import { getAsyncStorage } from '../storage/asyncStorage';
4
3
 
5
4
  /**
6
5
  * Stable id for this install on the pairing channel.
@@ -16,21 +15,25 @@ let memoized: string | null = null;
16
15
  export async function getOrCreatePairingDeviceId(): Promise<string> {
17
16
  if (memoized !== null) return memoized;
18
17
 
19
- try {
20
- const stored = await AsyncStorage.getItem(PAIRING_DEVICE_ID_STORAGE_KEY);
21
- if (typeof stored === 'string' && stored.length > 0) {
22
- memoized = stored;
23
- return stored;
18
+ const storage = getAsyncStorage();
19
+ if (storage) {
20
+ try {
21
+ const stored = await storage.getItem(PAIRING_DEVICE_ID_STORAGE_KEY);
22
+ if (typeof stored === 'string' && stored.length > 0) {
23
+ memoized = stored;
24
+ return stored;
25
+ }
26
+ } catch {
27
+ memoized = uuid();
28
+ return memoized;
24
29
  }
25
- } catch {
26
- memoized = uuid();
27
- return memoized;
28
30
  }
29
31
 
30
32
  const created = uuid();
31
33
  memoized = created;
34
+ if (!storage) return created;
32
35
  try {
33
- await AsyncStorage.setItem(PAIRING_DEVICE_ID_STORAGE_KEY, created);
36
+ await storage.setItem(PAIRING_DEVICE_ID_STORAGE_KEY, created);
34
37
  } catch {
35
38
  // Keep the in-memory id for this process. A later launch may mint another.
36
39
  }
@@ -6,6 +6,8 @@
6
6
 
7
7
  import type { PairingPurpose } from '@userflow-tech/core';
8
8
 
9
+ import { isNativeModuleLinked } from '../nativeModules';
10
+
9
11
  import { parsePairingPurpose } from './deepLink';
10
12
 
11
13
  // SecureStore rejects keys outside [A-Za-z0-9._-], so no `userflow:` prefix here.
@@ -34,12 +36,24 @@ type SecureStoreModule = {
34
36
 
35
37
  let cachedSecureStore: SecureStoreModule | null | undefined;
36
38
 
39
+ const SECURE_STORE_MODULE = 'ExpoSecureStore';
40
+
41
+ const SECURE_STORE_UNAVAILABLE_WARNING =
42
+ '[Userflow] Optional peer expo-secure-store is unavailable; ' +
43
+ 'pairing JWT persistence falls back to in-memory storage.';
44
+
37
45
  /** Expo modules must be installed natively — not just the JS package. */
38
46
  function isExpoModulesRuntimeAvailable(): boolean {
39
47
  const expo = (globalThis as { expo?: { EventEmitter?: unknown } }).expo;
40
48
  return typeof expo?.EventEmitter === 'function';
41
49
  }
42
50
 
51
+ function warnSecureStoreUnavailable(): void {
52
+ if (!__DEV__) return;
53
+ // eslint-disable-next-line no-console -- intentional missing-optional-peer diagnostic
54
+ console.warn(SECURE_STORE_UNAVAILABLE_WARNING);
55
+ }
56
+
43
57
  function tryLoadExpoSecureStore(): SecureStoreModule | null {
44
58
  if (cachedSecureStore !== undefined) return cachedSecureStore;
45
59
 
@@ -48,19 +62,21 @@ function tryLoadExpoSecureStore(): SecureStoreModule | null {
48
62
  return cachedSecureStore;
49
63
  }
50
64
 
65
+ // expo-secure-store calls requireNativeModule at import and throws when the
66
+ // native module is missing. Metro reports that as fatal before try/catch.
67
+ if (!isNativeModuleLinked(SECURE_STORE_MODULE)) {
68
+ cachedSecureStore = null;
69
+ warnSecureStoreUnavailable();
70
+ return cachedSecureStore;
71
+ }
72
+
51
73
  try {
52
74
  // Optional peer — absent in bare RN without expo-secure-store.
53
75
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- optional peer at import time
54
76
  cachedSecureStore = require('expo-secure-store') as SecureStoreModule;
55
77
  } catch {
56
78
  cachedSecureStore = null;
57
- if (__DEV__) {
58
- // eslint-disable-next-line no-console -- intentional missing-optional-peer diagnostic
59
- console.warn(
60
- '[Userflow] Optional peer expo-secure-store is unavailable; ' +
61
- 'pairing JWT persistence falls back to in-memory storage.',
62
- );
63
- }
79
+ warnSecureStoreUnavailable();
64
80
  }
65
81
 
66
82
  return cachedSecureStore;
@@ -1,6 +1,8 @@
1
1
  import * as React from 'react';
2
2
  import { StyleSheet, View, type GestureResponderEvent } from 'react-native';
3
3
 
4
+ import { elementTapMonitor } from '../anchors/elementTapMonitor';
5
+ import { touchBelongsToEvent, touchTargetFiber } from '../anchors/fiber/touchTargetFiber';
4
6
  import { targetPressMonitor } from '../anchors/targetPressMonitor';
5
7
 
6
8
  import { changedTouchPoints, forEachChangedTouch } from './changedTouchPoints';
@@ -17,7 +19,8 @@ export interface TargetPressBridgeProps {
17
19
  *
18
20
  * The Userflow overlay is a sibling above the app, so hole Pressables would
19
21
  * intercept presses. Instead we hit-test bubbled `onTouchStart` / `onTouchEnd`
20
- * against the rect registered by {@link TooltipSurface}.
22
+ * against the rect registered by {@link TooltipSurface}, and — while an
23
+ * element click condition is tracked — against the fiber under the finger.
21
24
  *
22
25
  * Preview-chrome double taps are observed by `DoubleTapBridge` around the
23
26
  * whole provider tree — this bridge never sees overlay touches.
@@ -27,16 +30,27 @@ export interface TargetPressBridgeProps {
27
30
  export function TargetPressBridge({ children }: TargetPressBridgeProps): React.ReactElement {
28
31
  const onTouchStart = React.useCallback((event: GestureResponderEvent) => {
29
32
  isolateSync('target-press', () => {
33
+ const observingTaps = elementTapMonitor.subscriberCount > 0;
30
34
  forEachChangedTouch(event, (identifier, pageX, pageY) => {
31
35
  targetPressMonitor.handleTouchStart(identifier, pageX, pageY);
36
+ if (!observingTaps || !touchBelongsToEvent(event, identifier)) return;
37
+ elementTapMonitor.handleTouchStart(
38
+ identifier,
39
+ pageX,
40
+ pageY,
41
+ touchTargetFiber(event, identifier),
42
+ );
32
43
  });
33
44
  });
34
45
  }, []);
35
46
 
36
47
  const onTouchEnd = React.useCallback((event: GestureResponderEvent) => {
37
48
  isolateSync('target-press', () => {
49
+ const observingTaps = elementTapMonitor.subscriberCount > 0;
38
50
  forEachChangedTouch(event, (identifier, pageX, pageY) => {
39
51
  targetPressMonitor.handleTouchEnd(identifier, pageX, pageY);
52
+ if (!observingTaps) return;
53
+ elementTapMonitor.handleTouchEnd(identifier, pageX, pageY);
40
54
  });
41
55
  });
42
56
  }, []);
@@ -46,10 +60,12 @@ export function TargetPressBridge({ children }: TargetPressBridgeProps): React.R
46
60
  const points = changedTouchPoints(event);
47
61
  if (points.length === 0) {
48
62
  targetPressMonitor.cancelTouch();
63
+ elementTapMonitor.cancelTouch();
49
64
  return;
50
65
  }
51
66
  for (const touch of points) {
52
67
  targetPressMonitor.cancelTouch(touch.identifier);
68
+ elementTapMonitor.cancelTouch(touch.identifier);
53
69
  }
54
70
  });
55
71
  }, []);
@@ -6,7 +6,11 @@ import { FiberScanRoot } from '../anchors/fiber/FiberScanRoot';
6
6
  import { DoubleTapBridge } from '../authoring/chrome/DoubleTapBridge';
7
7
  import { elementPickerController } from '../authoring/elementPickerController';
8
8
  import { ABSOLUTE_FILL } from '../surfaces/absoluteFill';
9
- import { getSafeAreaInsetsContext, useUserflowSafeAreaInsets } from '../surfaces/safeArea';
9
+ import {
10
+ getSafeAreaInsetsContext,
11
+ useParentSafeAreaInsets,
12
+ useUserflowSafeAreaInsets,
13
+ } from '../surfaces/safeArea';
10
14
  import type { EdgeInsets } from '../surfaces/safeArea';
11
15
  import { SurfaceLayoutInsetsProvider } from '../surfaces/surfaceLayoutInsets';
12
16
 
@@ -198,11 +202,14 @@ function HostSafeArea({
198
202
  children: React.ReactNode;
199
203
  }): React.ReactElement {
200
204
  const Context = getSafeAreaInsetsContext();
201
- const defaultInsets = useUserflowSafeAreaInsets();
202
- if (!Context) {
205
+ // Re-provide only when a parent already supplied insets, or when a banner
206
+ // override applies. Supplying a value with no parent would hide the host's
207
+ // own missing SafeAreaProvider from its useSafeAreaInsets() calls.
208
+ const parent = useParentSafeAreaInsets();
209
+ if (!Context || (insets == null && parent == null)) {
203
210
  return <>{children}</>;
204
211
  }
205
- return <Context.Provider value={insets ?? defaultInsets}>{children}</Context.Provider>;
212
+ return <Context.Provider value={insets ?? parent}>{children}</Context.Provider>;
206
213
  }
207
214
 
208
215
  /** Access the flow session engine from inside {@link UserflowProvider}. */
@@ -0,0 +1,88 @@
1
+ interface ReactNativeNativeLookup {
2
+ NativeModules?: Record<string, unknown>;
3
+ TurboModuleRegistry?: { get?: (name: string) => unknown };
4
+ }
5
+ const ASYNC_STORAGE_NATIVE_MODULES = [
6
+ 'RNAsyncStorage',
7
+ 'RNCAsyncStorage',
8
+ 'RNC_AsyncSQLiteDBStorage',
9
+ 'PlatformLocalStorage',
10
+ 'AsyncSQLiteDBStorage',
11
+ 'AsyncLocalStorage',
12
+ ] as const;
13
+
14
+ export interface AsyncStorageLike {
15
+ getItem(key: string): Promise<string | null>;
16
+ setItem(key: string, value: string): Promise<void>;
17
+ removeItem(key: string): Promise<void>;
18
+ }
19
+
20
+ let cached: AsyncStorageLike | null | undefined;
21
+
22
+ function loadReactNative(): ReactNativeNativeLookup | null {
23
+ try {
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
25
+ return require('react-native') as ReactNativeNativeLookup;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function nativeModuleRegistered(name: string, reactNative: ReactNativeNativeLookup): boolean {
32
+ try {
33
+ const turbo = reactNative.TurboModuleRegistry;
34
+ if (typeof turbo?.get === 'function' && turbo.get(name) != null) {
35
+ return true;
36
+ }
37
+ } catch {
38
+ // A missing turbo registry is not a failed link check.
39
+ }
40
+ try {
41
+ return reactNative.NativeModules?.[name] != null;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /** True when some AsyncStorage native binary is in this build. */
48
+ export function isAsyncStorageLinked(): boolean {
49
+ const reactNative = loadReactNative();
50
+ if (!reactNative) return false;
51
+ return ASYNC_STORAGE_NATIVE_MODULES.some((name) => nativeModuleRegistered(name, reactNative));
52
+ }
53
+
54
+ /**
55
+ * The AsyncStorage JS API, or `null` when the package or its native module is
56
+ * missing. Never requires the package until the native module is registered.
57
+ */
58
+ export function getAsyncStorage(): AsyncStorageLike | null {
59
+ if (cached !== undefined) return cached;
60
+
61
+ if (typeof require !== 'function' || !isAsyncStorageLinked()) {
62
+ cached = null;
63
+ return cached;
64
+ }
65
+
66
+ try {
67
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
68
+ const mod = require('@react-native-async-storage/async-storage') as {
69
+ default?: Partial<AsyncStorageLike>;
70
+ } & Partial<AsyncStorageLike>;
71
+ const storage = mod?.default ?? mod;
72
+ cached =
73
+ typeof storage?.getItem === 'function' &&
74
+ typeof storage.setItem === 'function' &&
75
+ typeof storage.removeItem === 'function'
76
+ ? (storage as AsyncStorageLike)
77
+ : null;
78
+ } catch {
79
+ cached = null;
80
+ }
81
+
82
+ return cached;
83
+ }
84
+
85
+ /** @internal */
86
+ export function resetAsyncStorageCacheForTests(): void {
87
+ cached = undefined;
88
+ }
@@ -61,11 +61,13 @@ export function BannerSurface({
61
61
  if (!webView) {
62
62
  if (!warnedMissingWebView) {
63
63
  warnedMissingWebView = true;
64
- console.warn(
65
- 'Userflow: WebView banner requires react-native-webview in the native binary. ' +
66
- 'Add it to your app (e.g. npx expo install react-native-webview), run pod install under ios/, ' +
67
- 'then rebuild with expo run:ios (not Expo Go). Falling back to the native banner bar.',
68
- );
64
+ if (__DEV__) {
65
+ console.warn(
66
+ 'Userflow: WebView banner requires react-native-webview in the native binary. ' +
67
+ 'Add it to your app (e.g. npx expo install react-native-webview), run pod install under ios/, ' +
68
+ 'then rebuild with expo run:ios (not Expo Go). Falling back to the native banner bar.',
69
+ );
70
+ }
69
71
  }
70
72
  return <BannerBar session={session} onActions={onActions} onDismiss={onDismiss} />;
71
73
  }