@userflow-tech/react-native 0.3.0 → 0.3.1

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 (197) hide show
  1. package/README.md +325 -63
  2. package/docs/anchoring.md +58 -42
  3. package/docs/troubleshooting.md +63 -27
  4. package/expo-plugin/hostDependencyWarnings.d.ts +18 -0
  5. package/expo-plugin/hostDependencyWarnings.d.ts.map +1 -0
  6. package/expo-plugin/hostDependencyWarnings.js +91 -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/index.js +5 -0
  36. package/lib/commonjs/index.js.map +1 -1
  37. package/lib/commonjs/installCheck.js +151 -0
  38. package/lib/commonjs/installCheck.js.map +1 -0
  39. package/lib/commonjs/pairing/deviceId.js +14 -11
  40. package/lib/commonjs/pairing/deviceId.js.map +1 -1
  41. package/lib/commonjs/runtime/TargetPressBridge.js +12 -1
  42. package/lib/commonjs/runtime/TargetPressBridge.js.map +1 -1
  43. package/lib/commonjs/runtime/UserflowProvider.js +6 -3
  44. package/lib/commonjs/runtime/UserflowProvider.js.map +1 -1
  45. package/lib/commonjs/storage/asyncStorage.js +67 -0
  46. package/lib/commonjs/storage/asyncStorage.js.map +1 -0
  47. package/lib/commonjs/surfaces/safeArea.js +68 -28
  48. package/lib/commonjs/surfaces/safeArea.js.map +1 -1
  49. package/lib/commonjs/version.js +1 -1
  50. package/lib/module/anchors/AnchorRegistry.js +7 -0
  51. package/lib/module/anchors/AnchorRegistry.js.map +1 -1
  52. package/lib/module/anchors/elementConditionEvaluator.js +189 -55
  53. package/lib/module/anchors/elementConditionEvaluator.js.map +1 -1
  54. package/lib/module/anchors/elementTapMonitor.js +87 -0
  55. package/lib/module/anchors/elementTapMonitor.js.map +1 -0
  56. package/lib/module/anchors/elementTargetMatch.js +78 -0
  57. package/lib/module/anchors/elementTargetMatch.js.map +1 -0
  58. package/lib/module/anchors/fiber/findAnchorFiber.js +62 -39
  59. package/lib/module/anchors/fiber/findAnchorFiber.js.map +1 -1
  60. package/lib/module/anchors/fiber/touchTargetFiber.js +100 -0
  61. package/lib/module/anchors/fiber/touchTargetFiber.js.map +1 -0
  62. package/lib/module/anchors/resolveAnchorHost.js +2 -1
  63. package/lib/module/anchors/resolveAnchorHost.js.map +1 -1
  64. package/lib/module/anchors/resolveAutoAnchorHost.js +3 -3
  65. package/lib/module/anchors/resolveAutoAnchorHost.js.map +1 -1
  66. package/lib/module/anchors/resolveElementTargets.js +152 -0
  67. package/lib/module/anchors/resolveElementTargets.js.map +1 -0
  68. package/lib/module/anchors/targetPressMonitor.js +8 -3
  69. package/lib/module/anchors/targetPressMonitor.js.map +1 -1
  70. package/lib/module/anchors/useUserflowAnchor.js +1 -0
  71. package/lib/module/anchors/useUserflowAnchor.js.map +1 -1
  72. package/lib/module/anonymousId.js +15 -5
  73. package/lib/module/anonymousId.js.map +1 -1
  74. package/lib/module/index.js +5 -0
  75. package/lib/module/index.js.map +1 -1
  76. package/lib/module/installCheck.js +139 -0
  77. package/lib/module/installCheck.js.map +1 -0
  78. package/lib/module/pairing/deviceId.js +14 -10
  79. package/lib/module/pairing/deviceId.js.map +1 -1
  80. package/lib/module/runtime/TargetPressBridge.js +12 -1
  81. package/lib/module/runtime/TargetPressBridge.js.map +1 -1
  82. package/lib/module/runtime/UserflowProvider.js +7 -4
  83. package/lib/module/runtime/UserflowProvider.js.map +1 -1
  84. package/lib/module/storage/asyncStorage.js +61 -0
  85. package/lib/module/storage/asyncStorage.js.map +1 -0
  86. package/lib/module/surfaces/safeArea.js +65 -28
  87. package/lib/module/surfaces/safeArea.js.map +1 -1
  88. package/lib/module/version.js +1 -1
  89. package/lib/typescript/commonjs/src/anchors/AnchorRegistry.d.ts +7 -0
  90. package/lib/typescript/commonjs/src/anchors/AnchorRegistry.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/src/anchors/elementConditionEvaluator.d.ts +12 -16
  92. package/lib/typescript/commonjs/src/anchors/elementConditionEvaluator.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/src/anchors/elementTapMonitor.d.ts +36 -0
  94. package/lib/typescript/commonjs/src/anchors/elementTapMonitor.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/src/anchors/elementTargetMatch.d.ts +20 -0
  96. package/lib/typescript/commonjs/src/anchors/elementTargetMatch.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/src/anchors/fiber/findAnchorFiber.d.ts +17 -5
  98. package/lib/typescript/commonjs/src/anchors/fiber/findAnchorFiber.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/src/anchors/fiber/touchTargetFiber.d.ts +24 -0
  100. package/lib/typescript/commonjs/src/anchors/fiber/touchTargetFiber.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/src/anchors/resolveAnchorHost.d.ts +2 -1
  102. package/lib/typescript/commonjs/src/anchors/resolveAnchorHost.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/src/anchors/resolveAutoAnchorHost.d.ts +10 -1
  104. package/lib/typescript/commonjs/src/anchors/resolveAutoAnchorHost.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/src/anchors/resolveElementTargets.d.ts +39 -0
  106. package/lib/typescript/commonjs/src/anchors/resolveElementTargets.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/src/anchors/targetPressMonitor.d.ts +2 -0
  108. package/lib/typescript/commonjs/src/anchors/targetPressMonitor.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/src/anchors/useUserflowAnchor.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/src/anonymousId.d.ts +2 -0
  111. package/lib/typescript/commonjs/src/anonymousId.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/src/installCheck.d.ts +33 -0
  114. package/lib/typescript/commonjs/src/installCheck.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/src/pairing/deviceId.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/src/runtime/TargetPressBridge.d.ts +2 -1
  117. package/lib/typescript/commonjs/src/runtime/TargetPressBridge.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/src/runtime/UserflowProvider.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/src/storage/asyncStorage.d.ts +15 -0
  120. package/lib/typescript/commonjs/src/storage/asyncStorage.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/src/surfaces/safeArea.d.ts +4 -2
  122. package/lib/typescript/commonjs/src/surfaces/safeArea.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/src/version.d.ts +1 -1
  124. package/lib/typescript/module/src/anchors/AnchorRegistry.d.ts +7 -0
  125. package/lib/typescript/module/src/anchors/AnchorRegistry.d.ts.map +1 -1
  126. package/lib/typescript/module/src/anchors/elementConditionEvaluator.d.ts +12 -16
  127. package/lib/typescript/module/src/anchors/elementConditionEvaluator.d.ts.map +1 -1
  128. package/lib/typescript/module/src/anchors/elementTapMonitor.d.ts +36 -0
  129. package/lib/typescript/module/src/anchors/elementTapMonitor.d.ts.map +1 -0
  130. package/lib/typescript/module/src/anchors/elementTargetMatch.d.ts +20 -0
  131. package/lib/typescript/module/src/anchors/elementTargetMatch.d.ts.map +1 -0
  132. package/lib/typescript/module/src/anchors/fiber/findAnchorFiber.d.ts +17 -5
  133. package/lib/typescript/module/src/anchors/fiber/findAnchorFiber.d.ts.map +1 -1
  134. package/lib/typescript/module/src/anchors/fiber/touchTargetFiber.d.ts +24 -0
  135. package/lib/typescript/module/src/anchors/fiber/touchTargetFiber.d.ts.map +1 -0
  136. package/lib/typescript/module/src/anchors/resolveAnchorHost.d.ts +2 -1
  137. package/lib/typescript/module/src/anchors/resolveAnchorHost.d.ts.map +1 -1
  138. package/lib/typescript/module/src/anchors/resolveAutoAnchorHost.d.ts +10 -1
  139. package/lib/typescript/module/src/anchors/resolveAutoAnchorHost.d.ts.map +1 -1
  140. package/lib/typescript/module/src/anchors/resolveElementTargets.d.ts +39 -0
  141. package/lib/typescript/module/src/anchors/resolveElementTargets.d.ts.map +1 -0
  142. package/lib/typescript/module/src/anchors/targetPressMonitor.d.ts +2 -0
  143. package/lib/typescript/module/src/anchors/targetPressMonitor.d.ts.map +1 -1
  144. package/lib/typescript/module/src/anchors/useUserflowAnchor.d.ts.map +1 -1
  145. package/lib/typescript/module/src/anonymousId.d.ts +2 -0
  146. package/lib/typescript/module/src/anonymousId.d.ts.map +1 -1
  147. package/lib/typescript/module/src/index.d.ts.map +1 -1
  148. package/lib/typescript/module/src/installCheck.d.ts +33 -0
  149. package/lib/typescript/module/src/installCheck.d.ts.map +1 -0
  150. package/lib/typescript/module/src/pairing/deviceId.d.ts.map +1 -1
  151. package/lib/typescript/module/src/runtime/TargetPressBridge.d.ts +2 -1
  152. package/lib/typescript/module/src/runtime/TargetPressBridge.d.ts.map +1 -1
  153. package/lib/typescript/module/src/runtime/UserflowProvider.d.ts.map +1 -1
  154. package/lib/typescript/module/src/storage/asyncStorage.d.ts +15 -0
  155. package/lib/typescript/module/src/storage/asyncStorage.d.ts.map +1 -0
  156. package/lib/typescript/module/src/surfaces/safeArea.d.ts +4 -2
  157. package/lib/typescript/module/src/surfaces/safeArea.d.ts.map +1 -1
  158. package/lib/typescript/module/src/version.d.ts +1 -1
  159. package/package.json +13 -1
  160. package/src/anchors/AnchorRegistry.ts +12 -0
  161. package/src/anchors/elementConditionEvaluator.ts +235 -70
  162. package/src/anchors/elementTapMonitor.ts +95 -0
  163. package/src/anchors/elementTargetMatch.ts +90 -0
  164. package/src/anchors/fiber/findAnchorFiber.ts +66 -35
  165. package/src/anchors/fiber/touchTargetFiber.ts +125 -0
  166. package/src/anchors/resolveAnchorHost.ts +2 -1
  167. package/src/anchors/resolveAutoAnchorHost.ts +3 -3
  168. package/src/anchors/resolveElementTargets.ts +165 -0
  169. package/src/anchors/targetPressMonitor.ts +12 -3
  170. package/src/anchors/useUserflowAnchor.ts +1 -0
  171. package/src/anonymousId.ts +19 -8
  172. package/src/index.ts +10 -0
  173. package/src/installCheck.ts +191 -0
  174. package/src/pairing/deviceId.ts +14 -11
  175. package/src/runtime/TargetPressBridge.tsx +17 -1
  176. package/src/runtime/UserflowProvider.tsx +11 -4
  177. package/src/storage/asyncStorage.ts +88 -0
  178. package/src/surfaces/safeArea.ts +80 -40
  179. package/src/version.ts +1 -1
  180. package/vendor/core/dist/constants.d.ts +2 -0
  181. package/vendor/core/dist/constants.d.ts.map +1 -1
  182. package/vendor/core/dist/constants.js +2 -0
  183. package/vendor/core/dist/constants.js.map +1 -1
  184. package/vendor/core/dist/engine/ClientConditionEngine.d.ts +20 -1
  185. package/vendor/core/dist/engine/ClientConditionEngine.d.ts.map +1 -1
  186. package/vendor/core/dist/engine/ClientConditionEngine.js +13 -10
  187. package/vendor/core/dist/engine/ClientConditionEngine.js.map +1 -1
  188. package/vendor/core/dist/engine/UserflowCore.d.ts.map +1 -1
  189. package/vendor/core/dist/engine/UserflowCore.js +21 -2
  190. package/vendor/core/dist/engine/UserflowCore.js.map +1 -1
  191. package/vendor/core/dist/index.d.ts +1 -1
  192. package/vendor/core/dist/index.d.ts.map +1 -1
  193. package/vendor/core/dist/index.js.map +1 -1
  194. package/vendor/core/dist/version.d.ts +1 -1
  195. package/vendor/core/dist/version.js +1 -1
  196. package/vendor/core/package.json +1 -1
  197. package/vendor/protocol/package.json +1 -1
package/README.md CHANGED
@@ -9,18 +9,28 @@ release for every content change.
9
9
 
10
10
  ## Requirements
11
11
 
12
- | Peer / dependency | Required? | Why |
13
- | ------------------------------------------- | ---------- | ------------------------------------------------------------------------ |
14
- | `react`, `react-native` | Yes | Host runtime. |
15
- | `@react-native-async-storage/async-storage` | Yes (dep) | Persists the anonymous user id. Installed with this package. |
16
- | `react-native-device-info` | Yes (dep) | Reads the host app store version (bundled; native rebuild required). |
17
- | `react-native-view-shot` | Yes (peer) | Builder in-app picker screenshots. Native module; rebuild after install. |
18
- | `react-native-webview` | Yes (dep) | Evaluate JavaScript / WebView banners. Native rebuild required. |
19
- | `react-native-safe-area-context` | Optional | Correct insets for modal/tooltip surfaces. |
20
- | `@react-navigation/native` | Optional | Screen tracking + NAVIGATE routing (React Navigation). |
21
- | `expo-router` | Optional | Screen tracking + NAVIGATE routing (Expo Router). |
22
- | `@react-native-community/netinfo` | Optional | Instant offline→online reconnect (enhancement, not required). |
23
- | `expo-secure-store` | Optional | Persist Builder pairing JWT in Keychain/Keystore. |
12
+ Declared minimums: React `>=18.2.0`, React Native `>=0.74.0`. This package's
13
+ test suite runs on React Native 0.81 / React 19.1. The TinyTime sample app in
14
+ this repo runs on Expo SDK 57 / React Native 0.86. Expo Go is not supported.
15
+
16
+ Install AsyncStorage, WebView, and (on bare React Native) device-info in the
17
+ **app**, even though this package also depends on them. Expo SDK 54 and later
18
+ autolink transitive native modules. Bare React Native, and Expo SDK 53 and
19
+ older, only autolink packages listed in the app's own `package.json`. A direct
20
+ install also picks a version that matches the app's React Native.
21
+
22
+ | Package | Required? | Why | If missing |
23
+ | ------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `react`, `react-native` | Yes | Host runtime. | The app cannot load the SDK. |
25
+ | `@react-native-async-storage/async-storage` | Yes, install in the app | Anonymous user id and the pairing device id. | `identifyAnonymous()` throws. Pairing keeps a device id in memory for the session. A dev-only warning is logged from `Userflow.init`. |
26
+ | `react-native-device-info` | Yes on bare React Native | Host app `app_version` / `app_build`. Expo reads `expo-application` / `expo-constants` instead. | Those attributes are omitted. A dev-only warning is logged on bare React Native. |
27
+ | `react-native-view-shot` `>=5.0.0` | Yes (peer) | Builder element-selection screenshots. | The picker has no screenshot. A dev-only warning is logged from `Userflow.init`. |
28
+ | `react-native-webview` `>=13.8.0` | Yes, install in the app | Evaluate JavaScript and WebView banners. | Those surfaces do not run. A dev-only warning is logged from `Userflow.init`. |
29
+ | `react-native-safe-area-context` | Optional | Insets for modal and tooltip surfaces. | One dev warning. Flow cards use the device's initial safe-area insets, or zero when those are unavailable. |
30
+ | `@react-navigation/native` | Optional | Screen tracking and NAVIGATE routing (React Navigation). | Report screens yourself with `updateScreen` / `useUserflowScreen`. |
31
+ | `expo-router` | Optional | Screen tracking and NAVIGATE routing (Expo Router). | Report screens yourself with `updateScreen` / `useUserflowScreen`. |
32
+ | `@react-native-community/netinfo` | Optional | Instant offline→online reconnect. | Recovery still runs on the heartbeat and when the app returns to the foreground. |
33
+ | `expo-secure-store` | Optional | Persist the Builder pairing JWT in Keychain/Keystore. | The pairing token stays in memory and is lost when the process ends. |
24
34
 
25
35
  Optional peers are declared `optional: true` in `peerDependenciesMeta`, so
26
36
  package managers will not warn if you skip them. Required peers (not marked
@@ -28,46 +38,75 @@ optional) will warn — or fail the install — if they are missing.
28
38
 
29
39
  ### Expo Go is not supported
30
40
 
31
- Use an **Expo development build** (or bare React Native). The SDK is not
32
- compatible with Expo Go.
41
+ Use an **Expo development build** (or bare React Native). In development,
42
+ `Userflow.init` logs:
43
+
44
+ ```text
45
+ [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).
46
+ ```
33
47
 
34
48
  ## Install
35
49
 
36
- Packages publish to the private `@userflow-tech` npm org. Create a **granular
37
- access token** on npm (Read-only, limited to `@userflow-tech`) and put it in
38
- the host app’s `.npmrc` via an env var — do not commit the token:
50
+ ### Access
51
+
52
+ Packages publish to the private `@userflow-tech` npm org. Userflow adds the
53
+ client's npm user or organization to a **read-only team** on that org. The
54
+ client then creates a **granular access token** on npm (Read-only, limited to
55
+ `@userflow-tech`). A `404` from `npm install` or `npm view @userflow-tech/react-native`
56
+ means the token is missing, or that user or org is not on the team. npm uses
57
+ 404 for a private package the caller cannot see.
58
+
59
+ ### npm, Yarn, and CI
60
+
61
+ npm and Yarn 1 read `.npmrc`. Do not commit the token:
39
62
 
40
63
  ```ini
41
64
  @userflow-tech:registry=https://registry.npmjs.org/
42
65
  //registry.npmjs.org/:_authToken=${NPM_TOKEN}
43
66
  ```
44
67
 
68
+ Yarn 2 and later ignore `.npmrc`. Add this to `.yarnrc.yml` instead:
69
+
70
+ ```yaml
71
+ npmScopes:
72
+ userflow-tech:
73
+ npmRegistryServer: 'https://registry.npmjs.org'
74
+ npmAuthToken: '${NPM_TOKEN}'
75
+ ```
76
+
77
+ Set `NPM_TOKEN` in the environment that installs dependencies: a local shell,
78
+ CI, and EAS (an EAS secret named `NPM_TOKEN`, available to the install step).
79
+
80
+ ### Packages
81
+
45
82
  ```bash
46
83
  # Expo (dev build)
47
- npx expo install @userflow-tech/react-native react-native-view-shot@^5.0.0 react-native-webview
84
+ npx expo install @userflow-tech/react-native @react-native-async-storage/async-storage react-native-webview react-native-view-shot@^5.0.0
48
85
 
49
86
  # Bare React Native
50
- npm install @userflow-tech/react-native react-native-view-shot react-native-webview
87
+ npm install @userflow-tech/react-native @react-native-async-storage/async-storage react-native-device-info react-native-webview react-native-view-shot@^5.0.0
51
88
  cd ios && pod install
52
89
  ```
53
90
 
54
91
  `react-native-view-shot` is a **required peer** (`>=5.0.0`). It is a native
55
- module — rebuild after install (Metro reload is not enough). On Expo SDK 54,
92
+ module — rebuild after install (a Metro reload is not enough). On Expo SDK 54,
56
93
  unpinned `npx expo install react-native-view-shot` resolves **4.0.3**, which
57
- does not satisfy the peer and breaks New Architecture / bridgeless. Pin
58
- `^5.0.0` (or `@latest`) as in the command above.
59
-
60
- `@react-native-async-storage/async-storage` and `react-native-device-info` are
61
- dependencies of this package and are installed automatically with a normal npm
62
- install. After adding or upgrading the SDK, rebuild the native app (`cd ios &&
63
- pod install`, then run again). If autolinking misses a bundled native module
64
- (common with local `file:` links), install it in the host app explicitly:
94
+ does not satisfy the peer. Pin `^5.0.0` as in the command above. If `expo doctor`
95
+ reports that pin, exclude it from Expo's version check:
65
96
 
66
- ```bash
67
- npm install @react-native-async-storage/async-storage react-native-device-info
68
- cd ios && pod install # iOS
97
+ ```json
98
+ {
99
+ "expo": {
100
+ "install": {
101
+ "exclude": ["react-native-view-shot"]
102
+ }
103
+ }
104
+ }
69
105
  ```
70
106
 
107
+ On a bare React Native app, `expo-secure-store` needs Expo modules installed
108
+ first (`npx install-expo-modules@latest`), then `npx expo install expo-secure-store`.
109
+
71
110
  Optional peers as needed:
72
111
 
73
112
  ```bash
@@ -79,42 +118,116 @@ For local tarball installs from this monorepo, pack with `pnpm pack:all` at the
79
118
  repo root and install the single `@userflow-tech/react-native` tarball. Steps:
80
119
  `docs/local-testing.md` in the monorepo.
81
120
 
121
+ ### Install with an AI agent
122
+
123
+ Cursor, Claude Code, and other agents that read Agent Skills can do this
124
+ install. The skill is `install-react-native-with-ai` in
125
+ [userflow/ai-skills-library](https://github.com/userflow/ai-skills-library).
126
+ Web apps use `install-with-ai` from that library instead.
127
+
128
+ ```bash
129
+ npx skills add userflow/ai-skills-library@install-react-native-with-ai
130
+ ```
131
+
132
+ Claude Code loads it from the plugin:
133
+
134
+ ```text
135
+ /plugin marketplace add userflow/ai-skills-library
136
+ /plugin install userflow@ai-skills-library
137
+ /userflow:install-react-native-with-ai
138
+ ```
139
+
82
140
  ## Quick start
83
141
 
84
142
  ```tsx
85
143
  import { Userflow, UserflowProvider } from '@userflow-tech/react-native';
86
144
 
87
145
  await Userflow.init({
88
- clientToken: 'ct_...', // from the Userflow dashboard → Environments
89
- // serverEndpoint: 'https://e.userflow.com', // default; set for staging/local
90
- // pairingEndpoint: 'https://e.userflow.com', // default; set for local Builder pairing
146
+ clientToken: 'ct_...', // from the Userflow dashboard → Environments. Never an esk_ secret key.
147
+ // serverEndpoint: 'https://e.userflow.com', // default (US). EU: 'https://e.eu.userflow.com'
148
+ // pairingEndpoint: 'https://e.userflow.com', // default; pairing stays on the US host
91
149
  });
92
150
 
93
151
  // Identified user — signature is computed by YOUR backend, never in the app.
94
152
  // `locale_code` is the same user attribute as web (see Localization below).
95
153
  await Userflow.identify('user-123', { name: 'Ada', locale_code: 'da-DK' }, { signature });
96
154
 
97
- // …or anonymous (id stored in AsyncStorage):
155
+ // Anonymous users count toward MAU. Call this only when you intend to identify
156
+ // signed-out users. The id is stored in AsyncStorage.
98
157
  await Userflow.identifyAnonymous();
99
158
  ```
100
159
 
101
- Wrap the app tree so flows can render:
160
+ Call `Userflow.teardown()` when tearing the SDK down (removes AppState and
161
+ NetInfo subscriptions). Call `Userflow.reset()` on logout to clear identity and
162
+ disconnect. `reset()` does not apply a new `clientToken` or endpoint; restart
163
+ the app to change those.
164
+
165
+ ### Where to put `init`, `identify`, and `UserflowProvider`
166
+
167
+ Call `Userflow.init` once, as early as you can, from the root component
168
+ (`app/_layout.tsx` with Expo Router, `App.tsx` with bare React Native or React
169
+ Navigation). `UserflowProvider` can mount before that promise resolves. Put it
170
+ once, near the top of the tree, inside `SafeAreaProvider`. Call `identify` after
171
+ sign-in. `identifyAnonymous` identifies signed-out users and counts them toward
172
+ MAU, so use it only when you mean to. Call `Userflow.reset()` on logout.
173
+
174
+ Expo Router:
102
175
 
103
176
  ```tsx
104
- import { UserflowProvider } from '@userflow-tech/react-native';
177
+ import { useEffect } from 'react';
178
+ import { Userflow, UserflowProvider } from '@userflow-tech/react-native';
179
+ import { UserflowExpoRouterScreenTracker } from '@userflow-tech/react-native/expo-router';
180
+ import { Stack } from 'expo-router';
181
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
105
182
 
106
183
  export default function RootLayout() {
184
+ useEffect(() => {
185
+ void Userflow.init({ clientToken: 'ct_...' });
186
+ }, []);
187
+
107
188
  return (
108
- <UserflowProvider>
109
- <App />
110
- </UserflowProvider>
189
+ <SafeAreaProvider>
190
+ <UserflowProvider>
191
+ <UserflowExpoRouterScreenTracker />
192
+ <Stack />
193
+ </UserflowProvider>
194
+ </SafeAreaProvider>
111
195
  );
112
196
  }
113
197
  ```
114
198
 
115
- Call `Userflow.teardown()` when tearing the SDK down (removes AppState and
116
- NetInfo subscriptions). Call `Userflow.reset()` on logout to clear identity and
117
- disconnect.
199
+ React Navigation:
200
+
201
+ ```tsx
202
+ import { useEffect } from 'react';
203
+ import { Userflow, UserflowProvider } from '@userflow-tech/react-native';
204
+ import { WithUserflowReactNavigation } from '@userflow-tech/react-native/navigation';
205
+ import { NavigationContainer } from '@react-navigation/native';
206
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
207
+
208
+ export default function App() {
209
+ useEffect(() => {
210
+ void Userflow.init({ clientToken: 'ct_...' });
211
+ }, []);
212
+
213
+ return (
214
+ <SafeAreaProvider>
215
+ <UserflowProvider>
216
+ <WithUserflowReactNavigation>
217
+ <NavigationContainer>{/* navigators */}</NavigationContainer>
218
+ </WithUserflowReactNavigation>
219
+ </UserflowProvider>
220
+ </SafeAreaProvider>
221
+ );
222
+ }
223
+ ```
224
+
225
+ Without `SafeAreaProvider`, development logs this once and flow cards fall back
226
+ to the device's initial insets:
227
+
228
+ ```text
229
+ [Userflow] UserflowProvider is not inside a <SafeAreaProvider>, so flow cards use the device's initial safe-area insets. Wrap your app in <SafeAreaProvider> from react-native-safe-area-context.
230
+ ```
118
231
 
119
232
  ### Call order
120
233
 
@@ -129,9 +242,15 @@ disconnect.
129
242
 
130
243
  Other rules:
131
244
 
132
- - `init()` is **idempotent**. A second call returns the cached promise and does
133
- **not** apply new options. Restart the process (or call `reset()` then
134
- re-init carefully) if you need different credentials.
245
+ - `init()` is **idempotent** once it has succeeded. A later call returns the
246
+ cached promise and does **not** apply new options. In development, a call
247
+ with a different `clientToken`, `serverEndpoint`, or `pairingEndpoint` while
248
+ that init is in flight or has succeeded logs
249
+ `[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.`
250
+ Restart the app to change them after a successful init. If `init` rejects
251
+ (wrong token, or the EU endpoint hint), call it again with the corrected
252
+ options; that retry runs. `Userflow.reset()` clears the signed-in user;
253
+ it does not apply new credentials.
135
254
  - `identify()` with a **different** user id resets the previous identity first.
136
255
  Same-id re-identify merges attributes.
137
256
  - Identity verification `signature` is optional in the type, but **required by
@@ -191,13 +310,16 @@ subpath under `UserflowProvider` (the tracker is a separate entry so apps withou
191
310
  import { UserflowProvider } from '@userflow-tech/react-native';
192
311
  import { UserflowExpoRouterScreenTracker } from '@userflow-tech/react-native/expo-router';
193
312
  import { Stack } from 'expo-router';
313
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
194
314
 
195
315
  export default function RootLayout() {
196
316
  return (
197
- <UserflowProvider>
198
- <UserflowExpoRouterScreenTracker />
199
- <Stack />
200
- </UserflowProvider>
317
+ <SafeAreaProvider>
318
+ <UserflowProvider>
319
+ <UserflowExpoRouterScreenTracker />
320
+ <Stack />
321
+ </UserflowProvider>
322
+ </SafeAreaProvider>
201
323
  );
202
324
  }
203
325
  ```
@@ -211,14 +333,17 @@ Wrap `NavigationContainer` with `WithUserflowReactNavigation` from the
211
333
  import { UserflowProvider } from '@userflow-tech/react-native';
212
334
  import { WithUserflowReactNavigation } from '@userflow-tech/react-native/navigation';
213
335
  import { NavigationContainer } from '@react-navigation/native';
336
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
214
337
 
215
338
  export default function App() {
216
339
  return (
217
- <UserflowProvider>
218
- <WithUserflowReactNavigation>
219
- <NavigationContainer>{/* navigators */}</NavigationContainer>
220
- </WithUserflowReactNavigation>
221
- </UserflowProvider>
340
+ <SafeAreaProvider>
341
+ <UserflowProvider>
342
+ <WithUserflowReactNavigation>
343
+ <NavigationContainer>{/* navigators */}</NavigationContainer>
344
+ </WithUserflowReactNavigation>
345
+ </UserflowProvider>
346
+ </SafeAreaProvider>
222
347
  );
223
348
  }
224
349
  ```
@@ -236,9 +361,11 @@ useUserflowScreen(route.name);
236
361
  ## Element targeting (anchors)
237
362
 
238
363
  Tooltip steps and ELEMENT client conditions resolve a dashboard selector like
239
- `#invite-teammates` to a host view. Prefer a plain `testID` (or `nativeID` /
240
- `accessibilityLabel`) — no SDK hook required. A misspelled or missing anchor can
241
- mean the tooltip misses **or** a gated flow never starts at all.
364
+ `#invite-teammates` to a host view. Tag it with `userflowId` (strongest), then
365
+ `testID`, then `nativeID`, then `accessibilityLabel` — no SDK hook required. A
366
+ misspelled or missing anchor can mean the tooltip misses **or** a gated flow
367
+ never starts at all. `PRESENT`, `NOT_PRESENT`, `CLICK`, and `NOT_CLICK`
368
+ auto-start conditions are evaluated on device.
242
369
 
243
370
  ```tsx
244
371
  import { Pressable, Text } from 'react-native';
@@ -352,8 +479,8 @@ Screen tracking ≠ action routing. React Navigation apps need
352
479
  | `setOnNavigate(handler)` | Override or clear the host NAVIGATE handler. |
353
480
  | `reportActivity()` | Reset the idle timer; reconnect an idle-suspended socket. |
354
481
  | `didHandleURL(url)` | Handle a Userflow preview deep link (`…://?token=…`). |
355
- | `handlePairingUrl(url)` | Handle a Builder pairing deep link (`…://?pairing_token=…`; dev builds only). |
356
- | `exitPreview()` | Leave Builder preview mode; disconnect the pairing websocket (dev only). |
482
+ | `handlePairingUrl(url)` | Handle a Builder pairing deep link (`…://?pairing_token=…`). Works in every build; the signed JWT gates the session. |
483
+ | `exitPreview()` | Leave Builder preview mode; disconnect the pairing websocket. Works in every build. |
357
484
  | `inspectPairingDeepLink(url)` | Classify a URL as pairing without consuming it; reports rejection reason. |
358
485
  | `isPairingAttempt(reason)` | True when a rejection was a recognisable pairing attempt worth reporting. |
359
486
  | `parsePairingDeepLink(url)` / `isUserflowPairingUrl(url)` | Parse / detect a pairing deep link. |
@@ -374,7 +501,7 @@ Screen tracking ≠ action routing. React Navigation apps need
374
501
  | `useUserflow()` | Provider context (`{ engine }`); throws outside the provider. |
375
502
  | `useFlowSessionState()` | Reactive view of the active mid-flow session (advanced / custom UI). |
376
503
  | `useUserflowScreen(name)` | Report the active screen from a React tree. |
377
- | `useUserflowAnchor(id)` | Optional override: register a view as a MANUAL target (beats `testID`). |
504
+ | `useUserflowAnchor(id)` | Optional override: register a view as a MANUAL target (beats `userflowId` / `testID`). |
378
505
  | `useUserflowScrollContainer(onScroll?)` | Optional override: register a `ScrollView` for tooltip scroll-into-view. |
379
506
  | `Userflow.setScrollPadding(padding)` | Sticky chrome insets when scrolling targets into view. |
380
507
  | `Userflow.setCustomScrollIntoView(fn)` | Replace scroll-into-view (e.g. unsupported scroll libraries). |
@@ -411,11 +538,146 @@ Screen tracking ≠ action routing. React Navigation apps need
411
538
  - AUTO / codeless element targeting (exact-id fast path + scored matcher;
412
539
  scored path needs Babel full mode to populate the auto-anchor registry).
413
540
  See [docs/anchoring.md](docs/anchoring.md).
541
+ - Build-time stamping with `@userflow-tech/react-native/babel-plugin`. Jest
542
+ runs the plugin too, which changes snapshots. Read
543
+ [Babel plugin and snapshot tests](#babel-plugin-and-snapshot-tests) before
544
+ enabling it.
545
+
546
+ ## Install diagnostics
547
+
548
+ In development, the first `Userflow.init` checks native module registration and
549
+ logs one warning when something the SDK needs is not linked. It does not
550
+ `require` an unlinked package. Rebuild the native app after fixing the list; a
551
+ Metro reload is not enough. The warning looks like this (only the missing lines
552
+ are included):
553
+
554
+ ```text
555
+ [Userflow] Native setup is incomplete. Flows may not run until you fix the following, then rebuild the app (a Metro reload is not enough):
556
+ - @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.
557
+ - 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.
558
+ - react-native-webview is not linked. Evaluate JavaScript and WebView banners will not run. Install it (npx expo install react-native-webview) and rebuild.
559
+ - react-native-device-info is not linked, so app_version and app_build are not sent. npm install react-native-device-info, then rebuild.
560
+ See docs/troubleshooting.md.
561
+ ```
562
+
563
+ Passing an environment secret key as `clientToken` throws:
564
+
565
+ ```text
566
+ clientToken is an environment secret key (esk_...). That key must never ship in the app. Pass the client token (ct_...) from Settings → Environments instead.
567
+ ```
568
+
569
+ When the default US endpoint rejects the token with `invalid_client_token`, the
570
+ error also says: `For EU-hosted environments pass serverEndpoint: 'https://e.eu.userflow.com'.`
571
+ Pairing stays on `https://e.userflow.com`.
572
+
573
+ `expo prebuild` warns when the config plugin has no `schemeId`, when
574
+ `react-native-view-shot` is missing or older than 5, and (on Expo SDK 53 and
575
+ older) when AsyncStorage or WebView is not a direct dependency. Passing an
576
+ environment UUID as `schemeId` fails the prebuild:
577
+
578
+ ```text
579
+ [@userflow-tech/react-native] schemeId "550e8400-e29b-41d4-a716-446655440000" looks like an environment id. Pass the scheme_id value that starts with userflow- (an opaque id from the dashboard), not the environment UUID.
580
+ ```
581
+
582
+ ## Jest
583
+
584
+ Mock the package in the host app's Jest setup so tests do not load native
585
+ modules. Mock the router entry you import as well:
586
+
587
+ ```ts
588
+ // jest.setup.ts
589
+ import type { ReactNode } from 'react';
590
+
591
+ jest.mock('@userflow-tech/react-native', () => ({
592
+ Userflow: {
593
+ init: jest.fn(() => Promise.resolve()),
594
+ identify: jest.fn(() => Promise.resolve()),
595
+ identifyAnonymous: jest.fn(() => Promise.resolve()),
596
+ reset: jest.fn(() => Promise.resolve()),
597
+ track: jest.fn(() => Promise.resolve()),
598
+ updateScreen: jest.fn(),
599
+ },
600
+ UserflowProvider: ({ children }: { children: ReactNode }) => children,
601
+ }));
602
+
603
+ // Expo Router
604
+ jest.mock('@userflow-tech/react-native/expo-router', () => ({
605
+ UserflowExpoRouterScreenTracker: () => null,
606
+ }));
607
+
608
+ // React Navigation
609
+ jest.mock('@userflow-tech/react-native/navigation', () => ({
610
+ WithUserflowReactNavigation: ({ children }: { children: ReactNode }) => children,
611
+ }));
612
+ ```
613
+
614
+ In a JavaScript setup file, drop the `ReactNode` import and the type
615
+ annotations.
616
+
617
+ A custom Metro config that does not extend `getDefaultConfig` from `expo/metro-config`
618
+ or `@react-native/metro-config` leaves `allowOptionalDependencies` off. Optional
619
+ peers that this package loads inside `try/catch` then fail the bundle with
620
+ `Unable to resolve module`. Extend the default config so that flag stays on.
621
+
622
+ ### Babel plugin and snapshot tests
623
+
624
+ If you add the optional `@userflow-tech/react-native/babel-plugin` to
625
+ `babel.config.js`, Jest runs it too, because `babel-jest` reads the same config.
626
+ The plugin adds props to `View`, `Text`, `Pressable`, the Touchables,
627
+ `TextInput`, and `Image`, and React Native's Jest mocks render them, so they
628
+ appear in snapshots:
629
+
630
+ ```diff
631
+ <Text
632
+ + __ufid="src/SettingsScreen.tsx::SettingsScreen > View#0 > Text{text=Settings}"
633
+ + collapsable={false}
634
+ + nativeID="uf_1mcelg9"
635
+ >
636
+ Settings
637
+ </Text>
638
+ ```
639
+
640
+ - `nativeID="uf_…"` is added to elements without a `testID`, `nativeID`, or
641
+ `userflowId`.
642
+ - `collapsable={false}` is added to `View`, `Text`, `Pressable`, and the
643
+ Touchables, including elements that have a `testID`.
644
+ - `__ufid` (the element's path in the file) is added in every non-production
645
+ build, which includes Jest.
646
+ - `__ufManual="true"` is added to elements that have a `testID`, `nativeID`, or
647
+ `userflowId`.
648
+
649
+ Updating the snapshots once does not settle it. The ids come from the file path,
650
+ the component name, each element's position among siblings of the same type, and
651
+ static labels. Changing a label, adding an element above others of the same type,
652
+ or moving or renaming a file changes them again. The plugin also imports this
653
+ package into every stamped file, so any test that renders a stamped component
654
+ loads the SDK. Keep the mock above in the Jest setup file.
655
+
656
+ Skip the plugin under Jest:
657
+
658
+ ```js
659
+ // babel.config.js
660
+ module.exports = function (api) {
661
+ const isTest = api.env('test');
662
+ return {
663
+ presets: ['babel-preset-expo'],
664
+ plugins: isTest ? [] : ['@userflow-tech/react-native/babel-plugin'],
665
+ };
666
+ };
667
+ ```
668
+
669
+ Remove `api.cache(true)` if your config has it. Called before `api.env`, it makes
670
+ Babel throw `Caching has already been configured with .never or .forever()`.
671
+ `api.env` already caches the config per environment. Development and release
672
+ builds still run the plugin.
673
+
674
+ To keep some stamping under test, pass `{ stampOnly: isTest }` to the plugin
675
+ instead. Snapshots then gain only `nativeID` and `collapsable`, and stamped files
676
+ skip the SDK import. The plugin's `exclude` option does not help here: it skips
677
+ the files it lists, not the components a test renders.
414
678
 
415
679
  ## What is not available yet
416
680
 
417
- - Banners and embed (inline) surfaces
418
- - `Userflow.track()` / custom event tracking
419
681
  - `INPUT_VALUE` / `FILLED_IN_INPUT` client conditions
420
682
 
421
683
  Flows gated on unsupported client conditions stay unknown and **never start**.