@userflow-tech/react-native 0.6.0 → 0.7.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 (204) hide show
  1. package/README.md +41 -33
  2. package/docs/troubleshooting.md +38 -38
  3. package/expo-plugin/hostDependencyWarnings.d.ts +5 -5
  4. package/expo-plugin/hostDependencyWarnings.js +7 -7
  5. package/expo-plugin/hostDependencyWarnings.js.map +1 -1
  6. package/lib/commonjs/anchors/AnchorRegistry.js +5 -0
  7. package/lib/commonjs/anchors/AnchorRegistry.js.map +1 -1
  8. package/lib/commonjs/anchors/ScrollContainerRegistry.js +8 -0
  9. package/lib/commonjs/anchors/ScrollContainerRegistry.js.map +1 -1
  10. package/lib/commonjs/anchors/fiber/presentedModal.js +215 -0
  11. package/lib/commonjs/anchors/fiber/presentedModal.js.map +1 -0
  12. package/lib/commonjs/anchors/fiber/touchTargetFiber.js +1 -0
  13. package/lib/commonjs/anchors/fiber/touchTargetFiber.js.map +1 -1
  14. package/lib/commonjs/anchors/fiber/visibleRect.js +12 -3
  15. package/lib/commonjs/anchors/fiber/visibleRect.js.map +1 -1
  16. package/lib/commonjs/anchors/useUserflowAnchor.js.map +1 -1
  17. package/lib/commonjs/anchors/useUserflowScrollContainer.js.map +1 -1
  18. package/lib/commonjs/authoring/ElementPickerOverlay.js +197 -138
  19. package/lib/commonjs/authoring/ElementPickerOverlay.js.map +1 -1
  20. package/lib/commonjs/authoring/hitTest.js +67 -10
  21. package/lib/commonjs/authoring/hitTest.js.map +1 -1
  22. package/lib/commonjs/authoring/hostAtPoint.js +45 -21
  23. package/lib/commonjs/authoring/hostAtPoint.js.map +1 -1
  24. package/lib/commonjs/content/RichContentRenderer.js +19 -7
  25. package/lib/commonjs/content/RichContentRenderer.js.map +1 -1
  26. package/lib/commonjs/deepLinking.js.map +1 -1
  27. package/lib/commonjs/deviceContext.js +39 -19
  28. package/lib/commonjs/deviceContext.js.map +1 -1
  29. package/lib/commonjs/index.js +11 -5
  30. package/lib/commonjs/index.js.map +1 -1
  31. package/lib/commonjs/installCheck.js +3 -3
  32. package/lib/commonjs/installCheck.js.map +1 -1
  33. package/lib/commonjs/lifecycle.js +2 -0
  34. package/lib/commonjs/lifecycle.js.map +1 -1
  35. package/lib/commonjs/pairing/PairingController.js.map +1 -1
  36. package/lib/commonjs/runtime/UserflowOverlay.js +17 -13
  37. package/lib/commonjs/runtime/UserflowOverlay.js.map +1 -1
  38. package/lib/commonjs/runtime/WindowOverlayHost.js +83 -0
  39. package/lib/commonjs/runtime/WindowOverlayHost.js.map +1 -0
  40. package/lib/commonjs/runtime/userflowCore.js +9 -2
  41. package/lib/commonjs/runtime/userflowCore.js.map +1 -1
  42. package/lib/commonjs/surfaces/TooltipSurface.js +109 -51
  43. package/lib/commonjs/surfaces/TooltipSurface.js.map +1 -1
  44. package/lib/commonjs/surfaces/tooltip/TooltipHighlight.js +6 -6
  45. package/lib/commonjs/surfaces/tooltip/TooltipHighlight.js.map +1 -1
  46. package/lib/commonjs/surfaces/tooltip/targetOrigin.js +74 -0
  47. package/lib/commonjs/surfaces/tooltip/targetOrigin.js.map +1 -0
  48. package/lib/commonjs/version.js +1 -1
  49. package/lib/module/anchors/AnchorRegistry.js +5 -0
  50. package/lib/module/anchors/AnchorRegistry.js.map +1 -1
  51. package/lib/module/anchors/ScrollContainerRegistry.js +8 -0
  52. package/lib/module/anchors/ScrollContainerRegistry.js.map +1 -1
  53. package/lib/module/anchors/fiber/presentedModal.js +206 -0
  54. package/lib/module/anchors/fiber/presentedModal.js.map +1 -0
  55. package/lib/module/anchors/fiber/touchTargetFiber.js +1 -0
  56. package/lib/module/anchors/fiber/touchTargetFiber.js.map +1 -1
  57. package/lib/module/anchors/fiber/visibleRect.js +12 -3
  58. package/lib/module/anchors/fiber/visibleRect.js.map +1 -1
  59. package/lib/module/anchors/useUserflowAnchor.js.map +1 -1
  60. package/lib/module/anchors/useUserflowScrollContainer.js.map +1 -1
  61. package/lib/module/authoring/ElementPickerOverlay.js +199 -140
  62. package/lib/module/authoring/ElementPickerOverlay.js.map +1 -1
  63. package/lib/module/authoring/hitTest.js +67 -11
  64. package/lib/module/authoring/hitTest.js.map +1 -1
  65. package/lib/module/authoring/hostAtPoint.js +45 -21
  66. package/lib/module/authoring/hostAtPoint.js.map +1 -1
  67. package/lib/module/content/RichContentRenderer.js +19 -7
  68. package/lib/module/content/RichContentRenderer.js.map +1 -1
  69. package/lib/module/deepLinking.js.map +1 -1
  70. package/lib/module/deviceContext.js +38 -17
  71. package/lib/module/deviceContext.js.map +1 -1
  72. package/lib/module/index.js +11 -5
  73. package/lib/module/index.js.map +1 -1
  74. package/lib/module/installCheck.js +3 -3
  75. package/lib/module/installCheck.js.map +1 -1
  76. package/lib/module/lifecycle.js +2 -0
  77. package/lib/module/lifecycle.js.map +1 -1
  78. package/lib/module/pairing/PairingController.js.map +1 -1
  79. package/lib/module/runtime/UserflowOverlay.js +18 -14
  80. package/lib/module/runtime/UserflowOverlay.js.map +1 -1
  81. package/lib/module/runtime/WindowOverlayHost.js +76 -0
  82. package/lib/module/runtime/WindowOverlayHost.js.map +1 -0
  83. package/lib/module/runtime/userflowCore.js +9 -2
  84. package/lib/module/runtime/userflowCore.js.map +1 -1
  85. package/lib/module/surfaces/TooltipSurface.js +109 -51
  86. package/lib/module/surfaces/TooltipSurface.js.map +1 -1
  87. package/lib/module/surfaces/tooltip/TooltipHighlight.js +6 -6
  88. package/lib/module/surfaces/tooltip/TooltipHighlight.js.map +1 -1
  89. package/lib/module/surfaces/tooltip/targetOrigin.js +67 -0
  90. package/lib/module/surfaces/tooltip/targetOrigin.js.map +1 -0
  91. package/lib/module/version.js +1 -1
  92. package/lib/typescript/commonjs/src/anchors/AnchorRegistry.d.ts +7 -0
  93. package/lib/typescript/commonjs/src/anchors/AnchorRegistry.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/src/anchors/ScrollContainerRegistry.d.ts +11 -2
  95. package/lib/typescript/commonjs/src/anchors/ScrollContainerRegistry.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/src/anchors/fiber/presentedModal.d.ts +45 -0
  97. package/lib/typescript/commonjs/src/anchors/fiber/presentedModal.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/src/anchors/fiber/visibleRect.d.ts +5 -2
  99. package/lib/typescript/commonjs/src/anchors/fiber/visibleRect.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/src/anchors/useUserflowAnchor.d.ts +2 -2
  101. package/lib/typescript/commonjs/src/anchors/useUserflowAnchor.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/src/anchors/useUserflowScrollContainer.d.ts +3 -2
  103. package/lib/typescript/commonjs/src/anchors/useUserflowScrollContainer.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/src/authoring/ElementPickerOverlay.d.ts +4 -0
  105. package/lib/typescript/commonjs/src/authoring/ElementPickerOverlay.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/src/authoring/hitTest.d.ts +25 -2
  107. package/lib/typescript/commonjs/src/authoring/hitTest.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/src/authoring/hostAtPoint.d.ts +6 -1
  109. package/lib/typescript/commonjs/src/authoring/hostAtPoint.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/src/deepLinking.d.ts +1 -1
  111. package/lib/typescript/commonjs/src/deepLinking.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/src/deviceContext.d.ts +14 -5
  113. package/lib/typescript/commonjs/src/deviceContext.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/src/index.d.ts +11 -5
  115. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/src/installCheck.d.ts +2 -2
  117. package/lib/typescript/commonjs/src/installCheck.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/src/lifecycle.d.ts +1 -0
  119. package/lib/typescript/commonjs/src/lifecycle.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/src/pairing/PairingController.d.ts +1 -1
  121. package/lib/typescript/commonjs/src/pairing/PairingController.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/src/runtime/UserflowOverlay.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/src/runtime/WindowOverlayHost.d.ts +34 -0
  124. package/lib/typescript/commonjs/src/runtime/WindowOverlayHost.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/src/runtime/userflowCore.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/src/surfaces/TooltipSurface.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/src/surfaces/tooltip/targetOrigin.d.ts +26 -0
  128. package/lib/typescript/commonjs/src/surfaces/tooltip/targetOrigin.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/src/version.d.ts +1 -1
  130. package/lib/typescript/module/src/anchors/AnchorRegistry.d.ts +7 -0
  131. package/lib/typescript/module/src/anchors/AnchorRegistry.d.ts.map +1 -1
  132. package/lib/typescript/module/src/anchors/ScrollContainerRegistry.d.ts +11 -2
  133. package/lib/typescript/module/src/anchors/ScrollContainerRegistry.d.ts.map +1 -1
  134. package/lib/typescript/module/src/anchors/fiber/presentedModal.d.ts +45 -0
  135. package/lib/typescript/module/src/anchors/fiber/presentedModal.d.ts.map +1 -0
  136. package/lib/typescript/module/src/anchors/fiber/visibleRect.d.ts +5 -2
  137. package/lib/typescript/module/src/anchors/fiber/visibleRect.d.ts.map +1 -1
  138. package/lib/typescript/module/src/anchors/useUserflowAnchor.d.ts +2 -2
  139. package/lib/typescript/module/src/anchors/useUserflowAnchor.d.ts.map +1 -1
  140. package/lib/typescript/module/src/anchors/useUserflowScrollContainer.d.ts +3 -2
  141. package/lib/typescript/module/src/anchors/useUserflowScrollContainer.d.ts.map +1 -1
  142. package/lib/typescript/module/src/authoring/ElementPickerOverlay.d.ts +4 -0
  143. package/lib/typescript/module/src/authoring/ElementPickerOverlay.d.ts.map +1 -1
  144. package/lib/typescript/module/src/authoring/hitTest.d.ts +25 -2
  145. package/lib/typescript/module/src/authoring/hitTest.d.ts.map +1 -1
  146. package/lib/typescript/module/src/authoring/hostAtPoint.d.ts +6 -1
  147. package/lib/typescript/module/src/authoring/hostAtPoint.d.ts.map +1 -1
  148. package/lib/typescript/module/src/deepLinking.d.ts +1 -1
  149. package/lib/typescript/module/src/deepLinking.d.ts.map +1 -1
  150. package/lib/typescript/module/src/deviceContext.d.ts +14 -5
  151. package/lib/typescript/module/src/deviceContext.d.ts.map +1 -1
  152. package/lib/typescript/module/src/index.d.ts +11 -5
  153. package/lib/typescript/module/src/index.d.ts.map +1 -1
  154. package/lib/typescript/module/src/installCheck.d.ts +2 -2
  155. package/lib/typescript/module/src/installCheck.d.ts.map +1 -1
  156. package/lib/typescript/module/src/lifecycle.d.ts +1 -0
  157. package/lib/typescript/module/src/lifecycle.d.ts.map +1 -1
  158. package/lib/typescript/module/src/pairing/PairingController.d.ts +1 -1
  159. package/lib/typescript/module/src/pairing/PairingController.d.ts.map +1 -1
  160. package/lib/typescript/module/src/runtime/UserflowOverlay.d.ts.map +1 -1
  161. package/lib/typescript/module/src/runtime/WindowOverlayHost.d.ts +34 -0
  162. package/lib/typescript/module/src/runtime/WindowOverlayHost.d.ts.map +1 -0
  163. package/lib/typescript/module/src/runtime/userflowCore.d.ts.map +1 -1
  164. package/lib/typescript/module/src/surfaces/TooltipSurface.d.ts.map +1 -1
  165. package/lib/typescript/module/src/surfaces/tooltip/targetOrigin.d.ts +26 -0
  166. package/lib/typescript/module/src/surfaces/tooltip/targetOrigin.d.ts.map +1 -0
  167. package/lib/typescript/module/src/version.d.ts +1 -1
  168. package/package.json +5 -4
  169. package/src/anchors/AnchorRegistry.ts +8 -0
  170. package/src/anchors/ScrollContainerRegistry.ts +15 -3
  171. package/src/anchors/fiber/presentedModal.ts +203 -0
  172. package/src/anchors/fiber/touchTargetFiber.ts +2 -1
  173. package/src/anchors/fiber/visibleRect.ts +12 -3
  174. package/src/anchors/useUserflowAnchor.ts +4 -5
  175. package/src/anchors/useUserflowScrollContainer.ts +5 -4
  176. package/src/authoring/ElementPickerOverlay.tsx +242 -166
  177. package/src/authoring/hitTest.ts +88 -13
  178. package/src/authoring/hostAtPoint.ts +67 -21
  179. package/src/content/RichContentRenderer.tsx +11 -9
  180. package/src/deepLinking.ts +2 -1
  181. package/src/deviceContext.ts +50 -14
  182. package/src/index.ts +11 -5
  183. package/src/installCheck.ts +3 -3
  184. package/src/lifecycle.ts +2 -1
  185. package/src/pairing/PairingController.ts +2 -1
  186. package/src/runtime/UserflowOverlay.tsx +29 -21
  187. package/src/runtime/WindowOverlayHost.tsx +95 -0
  188. package/src/runtime/userflowCore.ts +8 -2
  189. package/src/surfaces/TooltipSurface.tsx +103 -45
  190. package/src/surfaces/tooltip/TooltipHighlight.tsx +3 -5
  191. package/src/surfaces/tooltip/targetOrigin.ts +74 -0
  192. package/src/version.ts +1 -1
  193. package/vendor/core/dist/engine/UserflowCore.d.ts +7 -0
  194. package/vendor/core/dist/engine/UserflowCore.d.ts.map +1 -1
  195. package/vendor/core/dist/engine/UserflowCore.js +3 -2
  196. package/vendor/core/dist/engine/UserflowCore.js.map +1 -1
  197. package/vendor/core/dist/transport/phoenix.d.ts +12 -2
  198. package/vendor/core/dist/transport/phoenix.d.ts.map +1 -1
  199. package/vendor/core/dist/transport/phoenix.js +7 -6
  200. package/vendor/core/dist/transport/phoenix.js.map +1 -1
  201. package/vendor/core/dist/version.d.ts +1 -1
  202. package/vendor/core/dist/version.js +1 -1
  203. package/vendor/core/package.json +1 -1
  204. package/vendor/protocol/package.json +1 -1
@@ -17,11 +17,16 @@ import {
17
17
  } from '../anchors/fiber/fiberInternals';
18
18
  import { createLazyHostFiberIndex, lookupHostFiber } from '../anchors/fiber/indexHostFibers';
19
19
  import { measureHostFiber } from '../anchors/fiber/measureHostFiber';
20
+ import {
21
+ isOutsideModal,
22
+ locatePresentedModal,
23
+ rectInWindow,
24
+ type PresentedModal,
25
+ } from '../anchors/fiber/presentedModal';
20
26
  import { readHostText } from '../anchors/fiber/readHostContent';
21
27
  import {
22
28
  clipToScrollViewports,
23
29
  fiberIsUnder,
24
- topmostVisibleModalHost,
25
30
  type VisibleRect,
26
31
  } from '../anchors/fiber/visibleRect';
27
32
  import { isFiberOnVisibleScreen } from '../anchors/fiber/visibleScreens';
@@ -61,7 +66,17 @@ export interface HitCandidate {
61
66
 
62
67
  export type MeasureInstance = (inst: AutoAnchorInstance) => Promise<HitRect | null>;
63
68
  export type InstanceFilter = (inst: AutoAnchorInstance) => boolean;
64
- export type HostAtPoint = (x: number, y: number) => Promise<AnyFiber | null>;
69
+ /**
70
+ * Why a tap on a presented modal found nothing: the modal cannot be placed in
71
+ * the window, or the tap was outside it.
72
+ */
73
+ export type ModalMiss = 'unplaceable' | 'outside';
74
+
75
+ export type HostAtPoint = (
76
+ x: number,
77
+ y: number,
78
+ presented?: PresentedModal | null,
79
+ ) => Promise<AnyFiber | null>;
65
80
 
66
81
  const MEASURE_TIMEOUT_MS = 400;
67
82
 
@@ -259,10 +274,14 @@ function targetForHost(
259
274
  * {@link FiberScanRoot}. Hosts above it (the provider's own wrappers) are never
260
275
  * resolvable at play time. Empty when `host` is on a covered screen: its
261
276
  * ancestors above the navigator are not what was tapped.
277
+ *
278
+ * The walk also stops at `boundary`, a presented modal: hosts above it are
279
+ * drawn beneath the modal, not around what was tapped.
262
280
  */
263
281
  export function candidateChain(
264
282
  host: AnyFiber,
265
283
  byFiber: Map<AnyFiber, AutoAnchorInstance>,
284
+ boundary?: AnyFiber | null,
266
285
  ): PickTarget[] {
267
286
  const chain: PickTarget[] = [];
268
287
  if (!isFiberOnVisibleScreen(host)) return chain;
@@ -281,6 +300,7 @@ export function candidateChain(
281
300
  seenFibers.add(target.fiber);
282
301
  chain.push(target);
283
302
  }
303
+ if (boundary && (current === boundary || current === getAlternate(boundary))) break;
284
304
  current = getReturn(current);
285
305
  }
286
306
  return chain;
@@ -335,16 +355,30 @@ export function defaultCandidateIndex(candidates: HitCandidate[]): number {
335
355
  return index >= 0 ? index : 0;
336
356
  }
337
357
 
358
+ function targetFiber(target: PickTarget): AnyFiber | null {
359
+ return target.source === 'fiber'
360
+ ? target.fiber
361
+ : fiberFromPublicInstanceSafe(target.instance.ref.current);
362
+ }
363
+
338
364
  async function clipCandidate(candidate: HitCandidate): Promise<HitCandidate | null> {
339
- const fiber =
340
- candidate.target.source === 'fiber'
341
- ? candidate.target.fiber
342
- : fiberFromPublicInstanceSafe(candidate.target.instance.ref.current);
365
+ const fiber = targetFiber(candidate.target);
343
366
  const clipped = await clipToScrollViewports(fiber, candidate.rect as VisibleRect);
344
367
  if (!clipped) return null;
345
368
  return { ...candidate, rect: clipped, area: areaOf(clipped) };
346
369
  }
347
370
 
371
+ /** `candidate` in window coordinates. A presented modal's content is measured in the modal's space. */
372
+ function candidateInWindow(
373
+ candidate: HitCandidate,
374
+ presented: PresentedModal | null,
375
+ ): HitCandidate {
376
+ if (!presented) return candidate;
377
+ const fiber = targetFiber(candidate.target);
378
+ if (fiber && !fiberIsUnder(fiber, presented.host)) return candidate;
379
+ return { ...candidate, rect: rectInWindow(candidate.rect, presented) };
380
+ }
381
+
348
382
  /**
349
383
  * Hit-test window coordinates.
350
384
  *
@@ -353,6 +387,12 @@ async function clipCandidate(candidate: HitCandidate): Promise<HitCandidate | nu
353
387
  * lands on a covered screen. A visible native hit with no identifiable
354
388
  * ancestor returns no candidates: geometry ignores stacking order and would
355
389
  * offer whatever lies underneath the tap.
390
+ *
391
+ * While a modal is presented, only its content is a candidate, and candidate
392
+ * rects come back in window coordinates. A modal that cannot be placed in the
393
+ * window yields no candidates rather than a guess, and so does a tap outside
394
+ * the modal, on the screen it covers. Both call `onModalMiss` so the caller
395
+ * can say why.
356
396
  */
357
397
  export async function hitTestAt(
358
398
  x: number,
@@ -362,6 +402,7 @@ export async function hitTestAt(
362
402
  measure?: MeasureInstance;
363
403
  instances?: AutoAnchorInstance[];
364
404
  hostAtPoint?: HostAtPoint;
405
+ onModalMiss?: (miss: ModalMiss) => void;
365
406
  },
366
407
  ): Promise<HitCandidate[]> {
367
408
  const filter = opts?.instanceFilter ?? createVisibleInstanceFilter();
@@ -369,21 +410,33 @@ export async function hitTestAt(
369
410
  const pool = (opts?.instances ?? autoAnchorRegistry.allInstances()).filter(filter);
370
411
  autoAnchorRegistry.refreshLiveContent(pool);
371
412
 
413
+ const presented = await locatePresentedModal(fiberScanRoot.getFiber());
414
+ if (presented && !presented.origin) {
415
+ opts?.onModalMiss?.('unplaceable');
416
+ return [];
417
+ }
418
+ if (presented && isOutsideModal(x, y, presented)) {
419
+ opts?.onModalMiss?.('outside');
420
+ return [];
421
+ }
422
+
372
423
  const injected = opts?.hostAtPoint;
373
424
  const locate = injected ?? (opts?.measure === undefined ? hostAtPoint : null);
374
425
  if (locate) {
375
426
  try {
376
- const host = await locate(x, y);
427
+ const host = await locate(x, y, presented);
377
428
  if (host && isFiberOnVisibleScreen(host)) {
378
- const chain = candidateChain(host, registryMap(pool));
379
- return chain.length > 0 ? candidatesFromChain(chain, measure) : [];
429
+ const chain = candidateChain(host, registryMap(pool), presented?.host);
430
+ if (chain.length === 0) return [];
431
+ const candidates = await candidatesFromChain(chain, measure);
432
+ return candidates.map((candidate) => candidateInWindow(candidate, presented));
380
433
  }
381
434
  } catch {
382
435
  // Native hit-testing is optional; geometry still runs.
383
436
  }
384
437
  }
385
438
 
386
- return geometryHits(x, y, pool, measure);
439
+ return geometryHits(x, y, pool, measure, presented);
387
440
  }
388
441
 
389
442
  async function geometryHits(
@@ -391,9 +444,10 @@ async function geometryHits(
391
444
  y: number,
392
445
  pool: AutoAnchorInstance[],
393
446
  measure: MeasureInstance,
447
+ presented: PresentedModal | null,
394
448
  ): Promise<HitCandidate[]> {
395
449
  const root = fiberScanRoot.getFiber();
396
- const modal = root ? topmostVisibleModalHost(root) : null;
450
+ const modal = presented?.host ?? null;
397
451
  const byFiber = registryMap(pool);
398
452
 
399
453
  const scoped = modal
@@ -426,14 +480,35 @@ async function geometryHits(
426
480
  if (!raw) return null;
427
481
  const clipped = await clipCandidate({ target, rect: raw, area: areaOf(raw) });
428
482
  if (!clipped) return null;
429
- if (!pointInRect(x, y, clipped.rect)) return null;
430
- return clipped;
483
+ const placed = candidateInWindow(clipped, presented);
484
+ if (!pointInRect(x, y, placed.rect)) return null;
485
+ return placed;
431
486
  }),
432
487
  );
433
488
 
434
489
  return measured.filter((c): c is HitCandidate => c != null).sort((a, b) => a.area - b.area);
435
490
  }
436
491
 
492
+ /**
493
+ * `candidates` measured again, for when the screen may have moved under a
494
+ * selection: the keyboard opening or closing resizes or moves a sheet. Null
495
+ * when one can no longer be measured, or its modal can no longer be placed,
496
+ * so the caller drops the selection rather than keep a stale rect.
497
+ */
498
+ export async function remeasureCandidates(
499
+ candidates: HitCandidate[],
500
+ measure: MeasureInstance = measureInstanceRect,
501
+ ): Promise<HitCandidate[] | null> {
502
+ const presented = await locatePresentedModal(fiberScanRoot.getFiber());
503
+ if (presented && !presented.origin) return null;
504
+ const measured = await candidatesFromChain(
505
+ candidates.map((candidate) => candidate.target),
506
+ measure,
507
+ );
508
+ if (measured.length !== candidates.length) return null;
509
+ return measured.map((candidate) => candidateInWindow(candidate, presented));
510
+ }
511
+
437
512
  /** Cycle to the next-larger containing candidate (wraps). */
438
513
  export function nextContainingIndex(candidates: HitCandidate[], currentIndex: number): number {
439
514
  if (candidates.length === 0) return -1;
@@ -11,9 +11,10 @@
11
11
  * (see `FindNodeAtPointTest.overlappingViewsWithParentPointerEventsNone`).
12
12
  * - Scroll views subtract `contentOffset` in `getContentOriginOffset`, so
13
13
  * content scrolled out of the viewport is not a hit.
14
- * - Modal content lives under `ModalHostView` in the same shadow tree, but
15
- * its frame is relative to the modal window. If the root hit misses the
16
- * visible modal's subtree, retry rooted at that modal.
14
+ * - Content of a presented modal (`RNSModalScreen`, `ModalHostView`) lives in
15
+ * the same shadow tree, but Fabric lays it out relative to the modal. While
16
+ * one is presented, everything else is drawn beneath it, so only the modal is
17
+ * hit, with the point moved into its space (see `presentedModal.ts`).
17
18
  * - Screens a navigator covers stay laid out in the shadow tree, on top of the
18
19
  * visible one, and react-native-screens clears their zIndex, so the
19
20
  * last-mounted sibling wins. After visiting a later tab, a tap on an earlier
@@ -33,7 +34,11 @@ import {
33
34
  import { nearestHostDescendant } from '../anchors/fiber/findAnchorFiber';
34
35
  import { createHostFiberIndex } from '../anchors/fiber/indexHostFibers';
35
36
  import { measureHostFiber } from '../anchors/fiber/measureHostFiber';
36
- import { fiberIsUnder, topmostVisibleModalHost } from '../anchors/fiber/visibleRect';
37
+ import {
38
+ locatePresentedModal,
39
+ type Point,
40
+ type PresentedModal,
41
+ } from '../anchors/fiber/presentedModal';
37
42
  import { isFiberOnVisibleScreen, screensCoveringFiber } from '../anchors/fiber/visibleScreens';
38
43
 
39
44
  const HIT_TIMEOUT_MS = 400;
@@ -167,11 +172,20 @@ function originInParent(
167
172
  }, none);
168
173
  }
169
174
 
170
- async function hitFabric(rootHost: AnyFiber, x: number, y: number): Promise<AnyFiber | null> {
175
+ /**
176
+ * `rootOrigin` is where `rootHost` sits in the point's space, for a root that
177
+ * `measureInWindow` cannot place: a presented modal, whose space is its own.
178
+ */
179
+ async function hitFabric(
180
+ rootHost: AnyFiber,
181
+ x: number,
182
+ y: number,
183
+ rootOrigin?: Point,
184
+ ): Promise<AnyFiber | null> {
171
185
  const manager = fabricManager();
172
186
  const node = shadowNodeFromHost(rootHost);
173
187
  if (!manager || node == null) return null;
174
- const origin = await measureOrigin(rootHost);
188
+ const origin = rootOrigin ?? (await measureOrigin(rootHost));
175
189
  if (!origin) return null;
176
190
  const inParent = await originInParent(manager, node);
177
191
  const handle = await withTimeout<unknown>((settle) => {
@@ -199,25 +213,43 @@ async function hitPaper(rootHost: AnyFiber, x: number, y: number): Promise<AnyFi
199
213
  return index.byNativeTag.get(nativeTag) ?? null;
200
214
  }
201
215
 
202
- async function hitUnder(rootHost: AnyFiber, x: number, y: number): Promise<AnyFiber | null> {
203
- const fabric = await hitFabric(rootHost, x, y);
216
+ /**
217
+ * `inModalSpace`: the point is in a presented modal's own space. Paper's
218
+ * lookup takes window points, so it would land elsewhere there (and
219
+ * bridgeless logs an error when it finds nothing).
220
+ */
221
+ async function hitUnder(
222
+ rootHost: AnyFiber,
223
+ x: number,
224
+ y: number,
225
+ rootOrigin: Point | undefined,
226
+ inModalSpace: boolean,
227
+ ): Promise<AnyFiber | null> {
228
+ const fabric = await hitFabric(rootHost, x, y, rootOrigin);
204
229
  if (fabric) return fabric;
230
+ if (inModalSpace) return null;
205
231
  return hitPaper(rootHost, x, y);
206
232
  }
207
233
 
208
- /** {@link hitUnder}, re-hit from the visible screen while the hit lands on a covered one. */
234
+ /**
235
+ * {@link hitUnder}, re-hit from the visible screen while the hit lands on a
236
+ * covered one. A `rootOrigin` means the point is in a presented modal's space;
237
+ * screens inside the modal measure in that space too.
238
+ */
209
239
  async function hitOnVisibleScreen(
210
240
  rootHost: AnyFiber,
211
241
  x: number,
212
242
  y: number,
243
+ rootOrigin?: Point,
213
244
  ): Promise<AnyFiber | null> {
214
- let hit = await hitUnder(rootHost, x, y);
245
+ const inModalSpace = rootOrigin !== undefined;
246
+ let hit = await hitUnder(rootHost, x, y, rootOrigin, inModalSpace);
215
247
  for (let attempt = 0; hit && attempt < MAX_COVERED_SCREEN_RETRIES; attempt += 1) {
216
248
  const screens = screensCoveringFiber(hit);
217
249
  if (screens.length === 0) return hit;
218
250
  let next: AnyFiber | null = null;
219
251
  for (const screen of screens) {
220
- next = await hitUnder(screen, x, y);
252
+ next = await hitUnder(screen, x, y, undefined, inModalSpace);
221
253
  if (next) break;
222
254
  }
223
255
  hit = next;
@@ -226,22 +258,36 @@ async function hitOnVisibleScreen(
226
258
  return hit && isFiberOnVisibleScreen(hit) ? hit : null;
227
259
  }
228
260
 
261
+ const MODAL_SPACE_ORIGIN: Point = { x: 0, y: 0 };
262
+
229
263
  /**
230
264
  * Host fiber drawn on top at window coordinates, or null when neither Fabric
231
265
  * nor Paper hit-testing is available.
266
+ *
267
+ * While a modal is presented, only its content can be hit, and nothing is
268
+ * when the modal cannot be placed in the window. Pass `presented` when the
269
+ * caller has already looked it up.
232
270
  */
233
- export async function hostAtPoint(x: number, y: number): Promise<AnyFiber | null> {
271
+ export async function hostAtPoint(
272
+ x: number,
273
+ y: number,
274
+ presented?: PresentedModal | null,
275
+ ): Promise<AnyFiber | null> {
234
276
  const root = fiberScanRoot.getFiber();
235
277
  if (!root) return null;
236
- const rootHost = nearestHostDescendant(root);
237
- if (!rootHost) return null;
238
278
 
239
- let hit = await hitOnVisibleScreen(rootHost, x, y);
240
- const modal = topmostVisibleModalHost(root);
241
- const modalHost = modal ? nearestHostDescendant(modal) : null;
242
- if (modalHost && (!hit || !fiberIsUnder(hit, modal))) {
243
- const modalHit = await hitOnVisibleScreen(modalHost, x, y);
244
- if (modalHit) hit = modalHit;
279
+ const modal = presented === undefined ? await locatePresentedModal(root) : presented;
280
+ if (modal) {
281
+ if (!modal.origin) return null;
282
+ return hitOnVisibleScreen(
283
+ modal.host,
284
+ x - modal.origin.x,
285
+ y - modal.origin.y,
286
+ modal.modalSpace ? MODAL_SPACE_ORIGIN : undefined,
287
+ );
245
288
  }
246
- return hit;
289
+
290
+ const rootHost = nearestHostDescendant(root);
291
+ if (!rootHost) return null;
292
+ return hitOnVisibleScreen(rootHost, x, y);
247
293
  }
@@ -293,7 +293,8 @@ function TextBlock({
293
293
  | Heading1Element
294
294
  | Heading2Element
295
295
  | { align?: TextAlign | null; children: RichContentInlineNode[] };
296
- style: TextStyle;
296
+ // React Native 0.88's StyleSheet.flatten can return null or undefined.
297
+ style: TextStyle | null | undefined;
297
298
  accessibilityRole?: 'header' | 'text';
298
299
  }): React.ReactElement {
299
300
  return (
@@ -576,15 +577,16 @@ function baseText(theme: ThemeFragment): TextStyle {
576
577
  }
577
578
 
578
579
  function markStyle(node: AbstractRichContentTextFormatting): TextStyle {
579
- const style: TextStyle = {};
580
- if (node.bold) style.fontWeight = '700';
581
- if (node.italic) style.fontStyle = 'italic';
582
580
  const decoration = textDecoration(node);
583
- if (decoration) style.textDecorationLine = decoration;
584
- if (node.small) style.fontSize = 12;
585
- else if (node.sup) style.fontSize = 10;
586
- if (node.color) style.color = node.color;
587
- return style;
581
+ const fontSize = node.small ? 12 : node.sup ? 10 : undefined;
582
+ // Built in one literal: React Native 0.88 types TextStyle as readonly.
583
+ return {
584
+ ...(node.bold ? { fontWeight: '700' as const } : null),
585
+ ...(node.italic ? { fontStyle: 'italic' as const } : null),
586
+ ...(decoration ? { textDecorationLine: decoration } : null),
587
+ ...(fontSize != null ? { fontSize } : null),
588
+ ...(node.color ? { color: node.color } : null),
589
+ };
588
590
  }
589
591
 
590
592
  /** View wrapper so inline code padding/radius work reliably on Android. */
@@ -27,7 +27,8 @@ export interface DeepLinkingDeps {
27
27
  startFlowWithToken: DeepLinkStartFlowWithToken;
28
28
  /** Optional Linking overrides for tests. */
29
29
  linking?: {
30
- getInitialURL: () => Promise<string | null>;
30
+ // React Native 0.88 adds undefined to the result.
31
+ getInitialURL: () => Promise<string | null | undefined>;
31
32
  addEventListener: (
32
33
  type: 'url',
33
34
  handler: (event: { url: string }) => void,
@@ -1,16 +1,24 @@
1
1
  /**
2
- * Resolve mobile platform metadata for Userflow user attributes.
2
+ * Resolve mobile platform metadata and the running SDK version for Userflow
3
+ * user attributes.
3
4
  *
4
5
  * Cached once during {@link Userflow.init} and merged into identify attributes
5
6
  * (same lifecycle as {@link hostAppVersion.ts}).
6
7
  */
7
8
 
9
+ import { SDK_VERSION } from './version';
10
+
8
11
  export type SdkPlatform = 'react_native';
9
12
  export type DeviceOs = 'ios' | 'android';
10
13
  export type DeviceType = 'phone' | 'tablet';
11
14
 
12
15
  export interface DeviceContext {
13
16
  sdk: SdkPlatform;
17
+ /**
18
+ * Published `@userflow-tech/react-native` version, e.g. `0.4.6`. Always set by
19
+ * the SDK; optional so context objects built by host code (mocks) stay valid.
20
+ */
21
+ sdkVersion?: string;
14
22
  deviceOs?: DeviceOs;
15
23
  deviceType?: DeviceType;
16
24
  }
@@ -19,6 +27,7 @@ export interface DeviceContext {
19
27
  export const ANDROID_TABLET_SMALLEST_WIDTH_DP = 600;
20
28
 
21
29
  export const SDK_USER_ATTRIBUTE = 'sdk';
30
+ export const SDK_VERSION_USER_ATTRIBUTE = 'sdk_version';
22
31
  export const DEVICE_OS_USER_ATTRIBUTE = 'device_os';
23
32
  export const DEVICE_TYPE_USER_ATTRIBUTE = 'device_type';
24
33
 
@@ -70,6 +79,7 @@ export function resolveDeviceContextFromSources(sources: {
70
79
  const deviceType = resolveDeviceTypeFromSources(sources);
71
80
  return {
72
81
  sdk: 'react_native',
82
+ sdkVersion: SDK_VERSION,
73
83
  ...(deviceOs ? { deviceOs } : {}),
74
84
  ...(deviceType ? { deviceType } : {}),
75
85
  };
@@ -103,6 +113,7 @@ export function resolveDeviceContext(): DeviceContext {
103
113
 
104
114
  /** `undefined` until the first {@link cacheDeviceContextAtInit}; then resolved. */
105
115
  let cachedDeviceContext: DeviceContext | undefined;
116
+ let didWarnSdkVersionOverride = false;
106
117
 
107
118
  /**
108
119
  * Resolve and cache device metadata during {@link Userflow.init}.
@@ -118,33 +129,57 @@ export function getCachedDeviceContext(): DeviceContext | null {
118
129
  return cachedDeviceContext ?? null;
119
130
  }
120
131
 
121
- /** Returns cached device fields for UpsertUser, or `{}` before init. */
122
- export function deviceContextUserAttributes(): Record<string, string> {
123
- const cached = getCachedDeviceContext();
124
- if (!cached) return {};
132
+ /** Device fields for UpsertUser that identify attributes may override. */
133
+ function overridableDeviceAttributes(context: DeviceContext): Record<string, string> {
125
134
  return {
126
- [SDK_USER_ATTRIBUTE]: cached.sdk,
127
- ...(cached.deviceOs ? { [DEVICE_OS_USER_ATTRIBUTE]: cached.deviceOs } : {}),
128
- ...(cached.deviceType ? { [DEVICE_TYPE_USER_ATTRIBUTE]: cached.deviceType } : {}),
135
+ [SDK_USER_ATTRIBUTE]: context.sdk,
136
+ ...(context.deviceOs ? { [DEVICE_OS_USER_ATTRIBUTE]: context.deviceOs } : {}),
137
+ ...(context.deviceType ? { [DEVICE_TYPE_USER_ATTRIBUTE]: context.deviceType } : {}),
129
138
  };
130
139
  }
131
140
 
132
141
  /** Optional user-attribute keys added by {@link mergeDeviceContextUserAttributes}. */
133
142
  export type DeviceContextUserAttributeMap = Partial<{
134
143
  [SDK_USER_ATTRIBUTE]: SdkPlatform;
144
+ [SDK_VERSION_USER_ATTRIBUTE]: string;
135
145
  [DEVICE_OS_USER_ATTRIBUTE]: DeviceOs;
136
146
  [DEVICE_TYPE_USER_ATTRIBUTE]: DeviceType;
137
147
  }>;
138
148
 
139
- /** Merges cached device attrs into identify attributes; caller values win on key conflicts. */
149
+ /** One development warning when identify attributes try to set `sdk_version`. */
150
+ function warnIfSdkVersionOverridden(attributes: Record<string, unknown> | undefined): void {
151
+ if (!__DEV__ || didWarnSdkVersionOverride) return;
152
+ const passed = attributes?.[SDK_VERSION_USER_ATTRIBUTE];
153
+ // `{ set: value }` is the long form of a bare literal.
154
+ const value =
155
+ typeof passed === 'object' && passed !== null && 'set' in passed
156
+ ? (passed as { set: unknown }).set
157
+ : passed;
158
+ if (value === undefined || value === SDK_VERSION) return;
159
+ didWarnSdkVersionOverride = true;
160
+ // eslint-disable-next-line no-console -- intentional development diagnostic
161
+ console.warn(
162
+ `[Userflow] "${SDK_VERSION_USER_ATTRIBUTE}" is set by the SDK (${SDK_VERSION}); the value passed to identify / identifyAnonymous was ignored.`,
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Merges cached device attrs into identify attributes. Caller values win on key
168
+ * conflicts, except `sdk_version`, which always reports the running SDK.
169
+ */
140
170
  export function mergeDeviceContextUserAttributes<T extends Record<string, unknown>>(
141
171
  attributes?: T,
142
172
  ): (T & DeviceContextUserAttributeMap) | undefined {
143
- const system = deviceContextUserAttributes();
144
- if (Object.keys(system).length === 0) {
173
+ const cached = getCachedDeviceContext();
174
+ if (!cached) {
145
175
  return attributes as (T & DeviceContextUserAttributeMap) | undefined;
146
176
  }
147
- return { ...system, ...(attributes ?? {}) } as T & DeviceContextUserAttributeMap;
177
+ warnIfSdkVersionOverridden(attributes);
178
+ return {
179
+ ...overridableDeviceAttributes(cached),
180
+ ...(attributes ?? {}),
181
+ [SDK_VERSION_USER_ATTRIBUTE]: SDK_VERSION,
182
+ } as T & DeviceContextUserAttributeMap;
148
183
  }
149
184
 
150
185
  export function formatDeviceContextLog(resolved: DeviceContext | null): string {
@@ -153,7 +188,7 @@ export function formatDeviceContextLog(resolved: DeviceContext | null): string {
153
188
  }
154
189
  const os = resolved.deviceOs ?? 'unknown';
155
190
  const type = resolved.deviceType ?? 'unknown';
156
- return `[Userflow] device context sdk=${resolved.sdk} device_os=${os} device_type=${type}`;
191
+ return `[Userflow] device context sdk=${resolved.sdk} sdk_version=${resolved.sdkVersion ?? 'unknown'} device_os=${os} device_type=${type}`;
157
192
  }
158
193
 
159
194
  /** Log cached user-attribute values. Requires {@link cacheDeviceContextAtInit}. */
@@ -162,7 +197,8 @@ export function logDeviceContext(): void {
162
197
  console.log(formatDeviceContextLog(getCachedDeviceContext()));
163
198
  }
164
199
 
165
- /** @internal Resets init cache between tests. */
200
+ /** @internal Resets init cache and the one-time override warning between tests. */
166
201
  export function resetCachedDeviceContextForTests(): void {
167
202
  cachedDeviceContext = undefined;
203
+ didWarnSdkVersionOverride = false;
168
204
  }
package/src/index.ts CHANGED
@@ -368,7 +368,10 @@ export const Userflow = {
368
368
  setOnNavigate(handler: FlowNavigateHandler | null | undefined): void {
369
369
  setHostOnNavigate(handler);
370
370
  },
371
- /** Identify the current user and upsert their attributes. */
371
+ /**
372
+ * Identify the current user and upsert their attributes. The SDK adds `sdk_version`
373
+ * itself; a value passed for it here is ignored.
374
+ */
372
375
  async identify(
373
376
  userId: string,
374
377
  attributes?: UserflowAttributes,
@@ -391,7 +394,10 @@ export const Userflow = {
391
394
  await core.identify(userId, mergeSystemUserAttributes(attributes), options);
392
395
  await afterIdentify();
393
396
  },
394
- /** Identify an anonymous user with a stable auto-generated id stored on device. */
397
+ /**
398
+ * Identify an anonymous user with a stable auto-generated id stored on device. Like
399
+ * {@link identify}, the SDK sets `sdk_version` itself.
400
+ */
395
401
  async identifyAnonymous(
396
402
  attributes?: UserflowAttributes,
397
403
  options?: UserflowIdentifyOptions,
@@ -478,8 +484,8 @@ export const Userflow = {
478
484
  return getCachedHostAppVersion();
479
485
  },
480
486
  /**
481
- * Platform metadata cached during {@link init} (`sdk`, `deviceOs`, `deviceType`),
482
- * or `null` before init.
487
+ * Platform metadata cached during {@link init} (`sdk`, `sdkVersion`, `deviceOs`,
488
+ * `deviceType`), or `null` before init.
483
489
  */
484
490
  getDeviceContext(): DeviceContext | null {
485
491
  return getCachedDeviceContext();
@@ -576,7 +582,7 @@ export const Userflow = {
576
582
  logHostAppVersionToConsole();
577
583
  },
578
584
  /**
579
- * Log cached user-attribute values (`sdk`, `device_os`, `device_type`).
585
+ * Log cached user-attribute values (`sdk`, `sdk_version`, `device_os`, `device_type`).
580
586
  * Requires {@link init}; before init, logs that init is required.
581
587
  */
582
588
  logDeviceContext(): void {
@@ -10,7 +10,7 @@ export const INSTALL_ASYNC_STORAGE_LINE =
10
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
11
 
12
12
  export const INSTALL_VIEW_SHOT_LINE =
13
- '- react-native-view-shot is not linked, so Builder element selection gets no screenshot. Install it and rebuild. On Expo SDK 56 and 57, run npx expo install react-native-view-shot unpinned, with no expo.install.exclude entry for it. Otherwise install react-native-view-shot@^5.0.0; on Expo SDK 55 and older, an unpinned npx expo install resolves 4.0.3.';
13
+ '- react-native-view-shot is not linked, so Builder element selection gets no screenshot. Install it and rebuild. On Expo SDK 56 to 58, run npx expo install react-native-view-shot unpinned, with no expo.install.exclude entry for it. Otherwise install react-native-view-shot@^5.0.0; on Expo SDK 55 and older, an unpinned npx expo install resolves 4.0.3.';
14
14
 
15
15
  export const INSTALL_WEBVIEW_LINE =
16
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.';
@@ -36,7 +36,7 @@ export const INSTALL_SAFE_AREA_WARNING =
36
36
  export function formatViewShotVersionWarning(version: string): string {
37
37
  return (
38
38
  `[Userflow] react-native-view-shot@${version} is older than 5.0.0, so Builder element selection may not get a screenshot. ` +
39
- 'On Expo SDK 56 and 57, run npx expo install react-native-view-shot unpinned, with no expo.install.exclude entry for it, and rebuild. ' +
39
+ 'On Expo SDK 56 to 58, run npx expo install react-native-view-shot unpinned, with no expo.install.exclude entry for it, and rebuild. ' +
40
40
  'Otherwise install react-native-view-shot@^5.0.0 in the same command as the SDK and rebuild; on Expo SDK 55 and older, also add "react-native-view-shot" to expo.install.exclude. ' +
41
41
  'Do not retry the install with --force or --legacy-peer-deps; that leaves this version in place.\n' +
42
42
  INSTALL_CHECK_FOOTER
@@ -89,7 +89,7 @@ function readViewShotVersion(): string | null {
89
89
  }
90
90
  }
91
91
 
92
- /** Dev warning when view-shot 4.x is installed. Null on 5.x or when it is absent. */
92
+ /** Dev warning when view-shot 4.x is installed. Null on 5.x and later, or when it is absent. */
93
93
  export function viewShotVersionWarning(): string | null {
94
94
  const version = readViewShotVersion();
95
95
  if (!version) return null;
package/src/lifecycle.ts CHANGED
@@ -94,7 +94,8 @@ export class LifecycleController {
94
94
  private readonly scheduler: LifecycleScheduler;
95
95
  private readonly now: () => number;
96
96
  private readonly suspendGraceMs: number;
97
- private readonly getCurrentAppState: () => AppStateStatus;
97
+ /** Wider than AppStateStatus: React Native 0.88 types currentState as `string | null | undefined`. */
98
+ private readonly getCurrentAppState: () => string | null | undefined;
98
99
  private readonly subscribeAppState: (handler: (status: AppStateStatus) => void) => () => void;
99
100
 
100
101
  private unsubscribeAppState: (() => void) | null = null;
@@ -59,7 +59,8 @@ export interface PairingControllerOptions {
59
59
  */
60
60
  cancelPendingDraftEnd?: () => void;
61
61
  LinkingModule?: {
62
- getInitialURL(): Promise<string | null>;
62
+ // React Native 0.88 adds undefined to the result.
63
+ getInitialURL(): Promise<string | null | undefined>;
63
64
  addEventListener(type: 'url', handler: (event: { url: string }) => void): { remove(): void };
64
65
  };
65
66
  AppStateModule?: {