@solomei-ai/intent 2.0.6 → 2.1.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.
@@ -0,0 +1,784 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, ComponentType, Ref } from 'react';
3
+
4
+ type Rect = {
5
+ x: number;
6
+ y: number;
7
+ w: number;
8
+ h: number;
9
+ };
10
+ /** One actionable affordance inside a segment — a choice on offer. */
11
+ type DigestAction = {
12
+ label: string;
13
+ role: string;
14
+ /**
15
+ * Where the action leads. A URL on web; a route/screen name on native.
16
+ * Absent for actions that don't navigate — `javascript:` and its kin are
17
+ * affordances with no destination, and `navigatingHref` in `digest-shape`
18
+ * is what holds this field to that promise.
19
+ */
20
+ href?: string;
21
+ };
22
+ type DigestImage = {
23
+ alt?: string;
24
+ src?: string;
25
+ };
26
+ type SegmentDigest = {
27
+ /** Content-derived id; identical blocks share it across sessions. */
28
+ ref: string;
29
+ /**
30
+ * Why this became a segment. On web the segmenter's reason ('repeated',
31
+ * 'heading', …); on native the role the app declared ('listItem', 'card', …).
32
+ */
33
+ kind: string;
34
+ heading?: string;
35
+ text: string;
36
+ actions: DigestAction[];
37
+ images: DigestImage[];
38
+ };
39
+ /** A segment visible right now, with how long it has been on screen. */
40
+ type OnScreenEntry = {
41
+ ref: string;
42
+ ratio: number;
43
+ dwellMs: number;
44
+ rect: Rect;
45
+ };
46
+ /** What the user acted on. */
47
+ type ChosenTarget = {
48
+ tag: string;
49
+ label: string;
50
+ role?: string;
51
+ /** The enclosing segment, when the target sits inside one. */
52
+ ref?: string;
53
+ value?: string;
54
+ };
55
+ type Viewport = {
56
+ w: number;
57
+ h: number;
58
+ scrollY: number;
59
+ docH: number;
60
+ /**
61
+ * Device pixel ratio / RN `PixelRatio`. Web omits it (the server assumes 1);
62
+ * native reports it because layout units are density-independent there and
63
+ * the number is needed to reason about what the user could actually read.
64
+ */
65
+ scale?: number;
66
+ /** Safe-area insets. Native only — a notch genuinely occludes content. */
67
+ insets?: {
68
+ top: number;
69
+ right: number;
70
+ bottom: number;
71
+ left: number;
72
+ };
73
+ };
74
+ type Coverage = {
75
+ segments: number;
76
+ onScreen: number;
77
+ registered: number;
78
+ };
79
+ /**
80
+ * Which producer built the payload. The ingest endpoint needs this: `url` means
81
+ * a location on web and a route path on native, and the two are not comparable
82
+ * without knowing which is which.
83
+ */
84
+ type Platform = 'web' | 'native';
85
+
86
+ /**
87
+ * Synchronous key/value storage for a handful of short strings.
88
+ *
89
+ * Synchronous on purpose. Session identity has to be readable inside the
90
+ * teardown path — `pagehide` on web, background transition on native — and an
91
+ * await there loses the batch. Web uses `localStorage`; native should use MMKV.
92
+ * AsyncStorage users can wrap it with `createHydratedStorage` (see
93
+ * `src/native/storage.ts`), which hydrates once at init and writes back
94
+ * asynchronously.
95
+ */
96
+ type IntentStorage = {
97
+ get(key: string): string | undefined;
98
+ set(key: string, value: string): void;
99
+ remove(key: string): void;
100
+ };
101
+ type SendOptions = {
102
+ /**
103
+ * The page or app is going away. On web this selects `sendBeacon`; on native
104
+ * it means the batch must also be persisted, because the OS may kill the
105
+ * process before the request completes.
106
+ */
107
+ beacon: boolean;
108
+ byteLength: number;
109
+ };
110
+ type IntentHost = {
111
+ platform: Platform;
112
+ /**
113
+ * Monotonic milliseconds. Used only for durations (dwell), never for
114
+ * timestamps, so it is safe for this to have an arbitrary origin.
115
+ */
116
+ now(): number;
117
+ /** Wall-clock milliseconds for event timestamps. */
118
+ wallClock(): number;
119
+ storage: IntentStorage;
120
+ /** Resolves true when the batch was accepted for delivery. */
121
+ send(url: string, body: string, options: SendOptions): Promise<boolean>;
122
+ /** Yield to the host's render loop before doing expensive work. */
123
+ scheduleIdle(): Promise<void>;
124
+ log(...args: unknown[]): void;
125
+ };
126
+
127
+ /** Matches `NativeMethods.measureInWindow`. Window-relative, in DIPs. */
128
+ type MeasureInWindow = (callback: (x: number, y: number, width: number, height: number) => void) => void;
129
+ /** Matches the `layout` payload of RN's `onLayout` event. */
130
+ type LayoutRectangle = {
131
+ x: number;
132
+ y: number;
133
+ width: number;
134
+ height: number;
135
+ };
136
+ type LayoutEvent = {
137
+ nativeEvent: {
138
+ layout: LayoutRectangle;
139
+ };
140
+ };
141
+ /** Matches `AppStateStatus`. Widened because platforms add their own values. */
142
+ type AppStateStatus = 'active' | 'background' | 'inactive' | (string & {});
143
+ type WindowMetrics = {
144
+ width: number;
145
+ height: number;
146
+ /** `PixelRatio.get()`. Layout units are density-independent on native. */
147
+ scale: number;
148
+ };
149
+ type EdgeInsets = {
150
+ top: number;
151
+ right: number;
152
+ bottom: number;
153
+ left: number;
154
+ };
155
+ /**
156
+ * Structural subset of `fetch`. Declared rather than referencing the DOM lib so
157
+ * this module says what it actually needs — and so a consumer can pass a
158
+ * instrumented or retrying client of their own.
159
+ */
160
+ type FetchLike = (url: string, init: {
161
+ method: string;
162
+ headers: Record<string, string>;
163
+ body: string;
164
+ }) => Promise<{
165
+ ok: boolean;
166
+ status: number;
167
+ }>;
168
+ /**
169
+ * What the host app supplies once, at init. Build it from the `react-native`
170
+ * module with `createNativeAdapters(require('react-native'))`, or assemble it
171
+ * by hand in tests and on other runtimes.
172
+ */
173
+ type NativeAdapters = {
174
+ /**
175
+ * Synchronous key/value storage. MMKV is the intended backing store; see
176
+ * `createHydratedStorage` for AsyncStorage, which cannot be read
177
+ * synchronously and so must be hydrated at startup.
178
+ */
179
+ storage: IntentStorage;
180
+ /** Returns an unsubscribe function, like `AppState.addEventListener`. */
181
+ onAppStateChange(listener: (status: AppStateStatus) => void): () => void;
182
+ getWindow(): WindowMetrics;
183
+ /**
184
+ * Safe-area insets. Optional because not every app installs
185
+ * react-native-safe-area-context, but worth supplying: a notch or a home
186
+ * indicator genuinely occludes content, and without this the SDK reports a
187
+ * segment as on-screen when the user cannot see it.
188
+ */
189
+ getInsets?(): EdgeInsets;
190
+ fetch?: FetchLike;
191
+ /**
192
+ * Yield before expensive work. `InteractionManager.runAfterInteractions` is
193
+ * the direct analogue of the web's `requestIdleCallback` ladder, and is what
194
+ * `createNativeAdapters` wires up.
195
+ */
196
+ scheduleIdle?(): Promise<void>;
197
+ /**
198
+ * Session id generator. Defaults to a `Math.random`-based v4 shape, because
199
+ * Hermes has no `crypto.randomUUID` unless the app installs
200
+ * react-native-get-random-values. Supply a real one when available.
201
+ */
202
+ randomId?(): string;
203
+ };
204
+
205
+ type SegmentId = number;
206
+ /** What a component declares about itself. */
207
+ type SegmentContent = {
208
+ /**
209
+ * What sort of unit this is: 'listItem', 'card', 'section', 'header'…
210
+ * Free-form, and it lands in the digest verbatim as `kind`. Mirrors the
211
+ * web segmenter's `reason` field so the server sees one vocabulary.
212
+ */
213
+ kind: string;
214
+ heading?: string;
215
+ text?: string;
216
+ actions?: DigestAction[];
217
+ images?: DigestImage[];
218
+ };
219
+ type Registration = {
220
+ id: SegmentId;
221
+ /** Enclosing segment, for the "which segment was pressed" walk. */
222
+ parent?: SegmentId;
223
+ content: SegmentContent;
224
+ fingerprint: number;
225
+ /**
226
+ * Window-relative measurement, supplied by the view's ref. Absent for
227
+ * segments whose visibility arrives from a list's viewability callback,
228
+ * which is both cheaper and more accurate than measuring.
229
+ */
230
+ measure?: MeasureInWindow;
231
+ /**
232
+ * Re-measure on every pass rather than only when something reported a layout
233
+ * change.
234
+ *
235
+ * For inferred segments this is not an optimisation to skip but a
236
+ * correctness requirement: they have no `onLayout` to mark the tracker
237
+ * dirty, and scrolling would not fire one anyway — a scroll changes the
238
+ * offset, not the layout. Without it their geometry is measured once and
239
+ * frozen, so a scrolled list keeps reporting the rows that were on screen
240
+ * when the screen first settled.
241
+ *
242
+ * Affordable because these measure through `getBoundingClientRect`, which is
243
+ * synchronous, rather than `measureInWindow`, which is a bridge round trip.
244
+ */
245
+ alwaysMeasure?: boolean;
246
+ /**
247
+ * Last reported opacity. Animated fade-ins park views at 0 exactly as
248
+ * scroll-reveal animations do on the web, and geometry alone cannot see it.
249
+ */
250
+ opacity: number;
251
+ /**
252
+ * Set by the viewability path. When present, the measurement pass skips
253
+ * this segment entirely — `onViewableItemsChanged` already answered.
254
+ */
255
+ viewability?: {
256
+ visible: boolean;
257
+ ratio: number;
258
+ };
259
+ };
260
+ declare class SegmentRegistry {
261
+ private nextId;
262
+ private readonly registrations;
263
+ /**
264
+ * Stack of segments that cover what is beneath them. A `<Modal>` or a
265
+ * bottom sheet occludes the entire screen behind it, and nothing in the
266
+ * geometry says so — the views underneath still measure exactly where they
267
+ * were. Without this the SDK would keep accruing dwell for a product grid
268
+ * the user cannot see behind an open checkout sheet.
269
+ */
270
+ private readonly overlays;
271
+ /**
272
+ * Notified whenever the set of registered segments changes.
273
+ *
274
+ * Callers that resolve a segment from a view ref cannot know when that view
275
+ * became resolvable. With inference the id does not exist until a visibility
276
+ * pass has walked the tree, which happens after mount — so a hook that
277
+ * resolves once on mount, finds nothing and returns would never install its
278
+ * effect, however long the app then runs.
279
+ */
280
+ private readonly listeners;
281
+ subscribe(listener: () => void): () => void;
282
+ private notify;
283
+ register(content: SegmentContent, options?: {
284
+ parent?: SegmentId;
285
+ measure?: MeasureInWindow;
286
+ alwaysMeasure?: boolean;
287
+ }): SegmentId;
288
+ /**
289
+ * Replace a segment's content. The fingerprint change is what tells the
290
+ * ledger to re-extract while keeping the accumulated dwell — the React
291
+ * re-render equivalent of the web's content-hash check.
292
+ */
293
+ update(id: SegmentId, content: SegmentContent): void;
294
+ unregister(id: SegmentId): void;
295
+ setMeasure(id: SegmentId, measure: MeasureInWindow | undefined): void;
296
+ setOpacity(id: SegmentId, opacity: number): void;
297
+ setViewability(id: SegmentId, visible: boolean, ratio: number): void;
298
+ get(id: SegmentId): Registration | undefined;
299
+ has(id: SegmentId): boolean;
300
+ values(): Registration[];
301
+ pushOverlay(id: SegmentId): void;
302
+ popOverlay(id: SegmentId): void;
303
+ /**
304
+ * True when something is covering this segment. Only the topmost overlay
305
+ * matters: anything not inside it is behind it.
306
+ */
307
+ isOccluded(id: SegmentId): boolean;
308
+ /** Walk from a segment up through its declared parents. */
309
+ isWithin(id: SegmentId, ancestor: SegmentId): boolean;
310
+ clear(): void;
311
+ }
312
+
313
+ /**
314
+ * Everything about a text field that bears on whether its value may leave the
315
+ * device. Declared structurally so it accepts a `TextInput`'s own props object.
316
+ */
317
+ type NativeFieldProps = {
318
+ value?: string;
319
+ secureTextEntry?: boolean;
320
+ autoComplete?: string;
321
+ textContentType?: string;
322
+ accessibilityRole?: string;
323
+ /** RN's `keyboardType`: `email-address`, `phone-pad`, `visible-password`, … */
324
+ keyboardType?: string;
325
+ /** RN's `inputMode`: `email`, `tel`, `numeric`, … */
326
+ inputMode?: string;
327
+ /**
328
+ * The app's own name for what the field collects — `FormFieldTarget.type`,
329
+ * and `describeField`'s output. Free-form, so it is resolved through the same
330
+ * two tables as the props above: `email` is a web token, `creditCardNumber`
331
+ * is an iOS one, and an app writes whichever it thinks in.
332
+ */
333
+ type?: string;
334
+ /**
335
+ * Explicit override, the native analogue of `data-intent-redact`.
336
+ * `'mask'` replaces the value with a labelled placeholder; `'ignore'`
337
+ * removes the field from the digest entirely.
338
+ */
339
+ redact?: 'mask' | 'ignore';
340
+ redactLabel?: string;
341
+ };
342
+ declare function isSensitiveField(props: NativeFieldProps): boolean;
343
+ declare function fieldPlaceholderLabel(props: NativeFieldProps): string;
344
+ declare function readFieldValue(props: NativeFieldProps): string;
345
+
346
+ type Geometry = {
347
+ intersecting: boolean;
348
+ ratio: number;
349
+ rect: Rect;
350
+ };
351
+
352
+ type VisibilityOptions = {
353
+ registry: SegmentRegistry;
354
+ getWindow(): WindowMetrics;
355
+ getInsets?(): EdgeInsets;
356
+ /** Below this share of its own height on screen, a segment is not "seen". */
357
+ minRatio: number;
358
+ /** Below this opacity, a segment is not "seen" however well it measures. */
359
+ minOpacity: number;
360
+ /** Give up on a measurement pass after this long; unmounted views never call back. */
361
+ measureTimeoutMs: number;
362
+ };
363
+ /**
364
+ * Fraction of a rect that falls inside the visible window area.
365
+ *
366
+ * Both axes count. A horizontal carousel scrolls its off-screen cards fully out
367
+ * sideways while their vertical extent stays perfectly in view, and a
368
+ * height-only calculation would report every one of them as seen.
369
+ */
370
+ declare function visibleRatio(rect: Rect, viewport: Rect): number;
371
+ declare class VisibilityTracker {
372
+ private readonly options;
373
+ private readonly rects;
374
+ private dirty;
375
+ constructor(options: VisibilityOptions);
376
+ /**
377
+ * Something moved. Called from `onLayout` and from scroll handlers, both of
378
+ * which are cheap and push-based; the expensive measurement only follows if
379
+ * this was set.
380
+ */
381
+ markDirty(): void;
382
+ /** The window area a user can actually see, insets removed. */
383
+ viewportRect(): Rect;
384
+ /** Last known window-relative rect for a segment. */
385
+ rectFor(id: SegmentId): Rect;
386
+ /**
387
+ * Measure everything that does not get its visibility from a list, then
388
+ * report geometry for every registered segment.
389
+ *
390
+ * Returns an empty array when nothing has moved since the last pass, so a
391
+ * still screen costs one map lookup rather than one native call per view.
392
+ */
393
+ pass({ force }?: {
394
+ force?: boolean | undefined;
395
+ }): Promise<Array<{
396
+ id: SegmentId;
397
+ geometry: Geometry;
398
+ }>>;
399
+ /**
400
+ * Visibility that geometry cannot answer: a view faded to transparent, or
401
+ * one sitting behind an open modal. The direct analogue of the web's
402
+ * `checkVisibility()` correction, and consulted on the same terms — only for
403
+ * segments already believed to be intersecting.
404
+ */
405
+ probe(id: SegmentId): boolean;
406
+ forget(id: SegmentId): void;
407
+ clear(): void;
408
+ /**
409
+ * Fire every measurement at once and wait for the callbacks.
410
+ *
411
+ * Bounded by a timeout because a view unmounted between registration and
412
+ * measurement never calls back at all, and one such view would otherwise
413
+ * stall the pass — and with it every subsequent one — forever.
414
+ */
415
+ private measure;
416
+ }
417
+
418
+ type NativeInitOptions = {
419
+ clientId: string;
420
+ adapters: NativeAdapters;
421
+ baseUrl?: string;
422
+ consent?: boolean;
423
+ debug?: boolean;
424
+ /** How often the visibility pass runs. */
425
+ visibilityIntervalMs?: number;
426
+ batchSize?: number;
427
+ flushIntervalMs?: number;
428
+ maxBuffered?: number;
429
+ minRatio?: number;
430
+ /**
431
+ * Infer segments from the view tree instead of requiring every screen to
432
+ * declare them, the way the browser SDK reads the DOM.
433
+ *
434
+ * Needs a root: pass {@link setIntentRoot} as the `ref` of the view wrapping
435
+ * the app. Inferred segments register alongside declared ones, so a codebase
436
+ * can migrate a screen at a time.
437
+ *
438
+ * Off by default. It reads React's private prop bridge for the things the
439
+ * public node tree cannot express, and that is an opt-in dependency.
440
+ */
441
+ inferSegments?: boolean;
442
+ };
443
+ /**
444
+ * Wait for the first visibility + registration cycle.
445
+ *
446
+ * The first screen view otherwise beats the ledger and ships an empty digest —
447
+ * the single most important event of the session, describing nothing.
448
+ */
449
+ declare function whenReady(timeoutMs?: number): Promise<void>;
450
+ /**
451
+ * Nominate the view to infer segments from. Use it as a `ref`:
452
+ *
453
+ * <View ref={setIntentRoot} style={{flex: 1}}>{children}</View>
454
+ *
455
+ * Only meaningful with `inferSegments: true`.
456
+ */
457
+ declare function setIntentRoot(node: unknown): void;
458
+ declare function initIntentNative(options: NativeInitOptions): void;
459
+ declare function shutdownIntentNative(): void;
460
+ declare function setConsent(granted: boolean): void;
461
+ declare function getIntentSessionId(): string;
462
+ /**
463
+ * A screen view — the native pageview.
464
+ *
465
+ * Call it from React Navigation's `onStateChange` or an Expo Router effect.
466
+ * Comparing on the resolved path means a param-only change (a tab index, a
467
+ * filter) does not emit a duplicate, mirroring the web's pathname+search
468
+ * comparison that stops scroll-spy hash rewrites emitting a pageview each.
469
+ */
470
+ declare function trackScreen(name: string, params?: Record<string, unknown>): void;
471
+ type PressTarget = {
472
+ label: string;
473
+ role?: string;
474
+ /** The enclosing segment, from `useIntentSegment`'s context. */
475
+ segmentId?: SegmentId;
476
+ };
477
+ declare function trackPress(target: PressTarget): void;
478
+ type InputTarget = PressTarget & {
479
+ field: NativeFieldProps;
480
+ };
481
+ declare function trackInput(target: InputTarget): void;
482
+ type FormFieldTarget = {
483
+ /** Stable key for the answer. Falls back to `label` when absent. */
484
+ name?: string;
485
+ /** The question as the visitor read it. */
486
+ label?: string;
487
+ /** `text`, `email`, `creditCardNumber`, … Free-form; describes the control. */
488
+ type?: string;
489
+ /** Redacted by the same policy as `trackInput`. */
490
+ field: NativeFieldProps;
491
+ };
492
+ type FormSubmitTarget = {
493
+ name?: string;
494
+ /** The segment the form lives in, so the submission is placed on screen. */
495
+ segmentId?: SegmentId;
496
+ fields: readonly FormFieldTarget[];
497
+ };
498
+ /**
499
+ * Report a submitted form.
500
+ *
501
+ * The web SDK discovers this from a `<form>` element's submit event. Native has
502
+ * no such thing — there is no form element and no submit event — so the screen
503
+ * declares what it collected, exactly as it declares its segments.
504
+ *
505
+ * Values ride on the event and never on the segment: `ref` is a content hash,
506
+ * so folding answers into the digest would mint a new segment per submission.
507
+ */
508
+ declare function trackFormSubmit(target: FormSubmitTarget): void;
509
+ declare function trackEvent(event: string, props?: Record<string, unknown>): void;
510
+ /** Force a flush. Useful before a deliberate teardown, e.g. logout. */
511
+ declare function flushIntent(): Promise<void>;
512
+ declare function getTransportStats(): {
513
+ buffered: number;
514
+ sent: number;
515
+ failed: number;
516
+ dropped: number;
517
+ };
518
+ /**
519
+ * How much of the screen was actually accounted for.
520
+ *
521
+ * With screenshots gone there is no fallback behind poor segmentation, so an
522
+ * app whose structure the SDK cannot see must show up as a low number rather
523
+ * than as quietly thin data. Tolerant of not being started, because this is
524
+ * exactly what a diagnostics view calls first.
525
+ */
526
+ declare function getCoverage(): Coverage;
527
+ /** Every segment visible right now, with how long it has been on screen. */
528
+ declare function getOnScreen(): OnScreenEntry[];
529
+
530
+ /** Any view whose ref exposes `measureInWindow` — every RN host component. */
531
+ type MeasurableView = {
532
+ measureInWindow?: MeasureInWindow;
533
+ };
534
+ /**
535
+ * The `ref` a segment hook hands back.
536
+ *
537
+ * The parameter is `unknown` on purpose. React Native's own ref types differ by
538
+ * component and by version (`Ref<View>`, `Ref<View | LegacyRef<View>>`, the
539
+ * Animated wrappers, FlashList's cells), and a callback typed to anything
540
+ * narrower fails to be assignable to half of them. A parameter of `unknown`
541
+ * accepts every one, and the implementation checks for `measureInWindow`
542
+ * before using it — which is the honest contract anyway, since a component
543
+ * that does not expose it simply falls back to being untracked geometrically.
544
+ */
545
+ type IntentRefCallback = (node: unknown) => void;
546
+ /**
547
+ * Enclosing segment. Lets a deeply nested Pressable attribute itself to the
548
+ * card it sits in without the card threading an id down through props.
549
+ */
550
+ declare const IntentSegmentContext: react.Context<number | undefined>;
551
+ declare function IntentSegmentProvider({ id, children }: {
552
+ id: SegmentId | undefined;
553
+ children: ReactNode;
554
+ }): react.FunctionComponentElement<react.ProviderProps<number | undefined>>;
555
+ declare function useIntentSegmentId(): SegmentId | undefined;
556
+ type UseIntentSegmentOptions = {
557
+ /**
558
+ * `keyExtractor` value for this row, when it lives in a FlatList/FlashList
559
+ * wired up with `useIntentList`. Supplying it means visibility comes from
560
+ * the list's own viewability pass — cheaper and more accurate than
561
+ * measuring, and it picks up `minimumViewTime` for free.
562
+ */
563
+ viewabilityKey?: string;
564
+ /** Override the enclosing segment instead of taking it from context. */
565
+ parent?: SegmentId;
566
+ };
567
+ type IntentSegmentBinding = {
568
+ segmentId: SegmentId | undefined;
569
+ ref: IntentRefCallback;
570
+ onLayout: (event?: LayoutEvent) => void;
571
+ };
572
+ /**
573
+ * Declare a segment.
574
+ *
575
+ * This is the native replacement for the web's structural segmentation: rather
576
+ * than infer a content unit from markup, the component states what it is. The
577
+ * content is extracted once, on idle, when the segment first becomes visible —
578
+ * never during a render and never during a gesture.
579
+ */
580
+ declare function useIntentSegment(content: SegmentContent, options?: UseIntentSegmentOptions): IntentSegmentBinding;
581
+ /**
582
+ * A press handler that attributes the press to its enclosing segment.
583
+ *
584
+ * Costs a map lookup. The segment's content was extracted on idle long before
585
+ * the finger landed.
586
+ */
587
+ declare function useIntentPress(target: Omit<PressTarget, 'segmentId'> & {
588
+ segmentId?: SegmentId;
589
+ }): () => void;
590
+ /** Emit a screen view. Deduped on the resolved name by `trackScreen`. */
591
+ declare function useIntentScreen(name: string, params?: Record<string, unknown>): void;
592
+ type ViewToken = {
593
+ key?: string;
594
+ isViewable: boolean;
595
+ item?: unknown;
596
+ index?: number | null;
597
+ };
598
+ type IntentListBinding = {
599
+ viewabilityConfig: {
600
+ itemVisiblePercentThreshold: number;
601
+ minimumViewTime: number;
602
+ };
603
+ onViewableItemsChanged: (info: {
604
+ viewableItems: ViewToken[];
605
+ changed: ViewToken[];
606
+ }) => void;
607
+ };
608
+ /**
609
+ * Wire a FlatList / SectionList / FlashList's viewability into the ledger.
610
+ *
611
+ * This is the tier the web has no equivalent of. RN already computes
612
+ * percent-visible per row and can gate on a minimum view time, which is exactly
613
+ * the "was this actually seen" question the web needs IntersectionObserver plus
614
+ * a checkVisibility correction to answer. Rows bound this way are never
615
+ * measured.
616
+ *
617
+ * Spread the result onto the list and give each row's `useIntentSegment` the
618
+ * same `viewabilityKey` your `keyExtractor` returns.
619
+ */
620
+ declare function useIntentList({ itemVisiblePercentThreshold, minimumViewTime, }?: {
621
+ itemVisiblePercentThreshold?: number | undefined;
622
+ minimumViewTime?: number | undefined;
623
+ }): IntentListBinding;
624
+ /**
625
+ * Mark a segment as covering everything behind it while `active`.
626
+ *
627
+ * A `<Modal>` or a bottom sheet occludes the whole screen, and nothing in the
628
+ * geometry says so — the views underneath still measure exactly where they
629
+ * were. Without this the SDK keeps accruing dwell for a product grid the user
630
+ * cannot see behind an open checkout sheet.
631
+ */
632
+ /**
633
+ * Either a segment id, or a ref to the view that is the segment.
634
+ *
635
+ * The ref form is what lets a screen stop declaring segments purely to obtain an
636
+ * id: the enclosing segment is found by walking the view's ancestors, so an
637
+ * inferred segment works exactly as a declared one does.
638
+ */
639
+ type SegmentTarget = SegmentId | {
640
+ current: unknown;
641
+ } | undefined;
642
+ declare function useIntentOverlay(target: SegmentTarget, active: boolean): void;
643
+ /**
644
+ * Report an animated opacity so faded-out content stops counting as seen.
645
+ *
646
+ * The native equivalent of the web's `checkVisibility()` opacity correction,
647
+ * which exists because scroll-reveal animations park elements at opacity 0
648
+ * while they still intersect perfectly. Drive it from an `Animated.Value`
649
+ * listener when a segment fades.
650
+ */
651
+ declare function useIntentOpacity(target: SegmentTarget, opacity: number): void;
652
+
653
+ type FormFieldReading = {
654
+ name?: string;
655
+ label?: string;
656
+ type?: string;
657
+ value: string;
658
+ /** Opted out via `redact="ignore"`; excluded from submissions entirely. */
659
+ ignored?: boolean;
660
+ };
661
+ type HostFieldProps = {
662
+ value?: string;
663
+ placeholder?: string;
664
+ accessibilityLabel?: string;
665
+ accessibilityRole?: string;
666
+ secureTextEntry?: boolean;
667
+ autoComplete?: string;
668
+ textContentType?: string;
669
+ keyboardType?: string;
670
+ inputMode?: string;
671
+ };
672
+ /** The only props these components add. All are optional. */
673
+ type IntentFieldExtras = {
674
+ /** Overrides the question. Defaults to accessibilityLabel, then placeholder. */
675
+ label?: string;
676
+ /** Stable key for the answer. Defaults to the resolved label. */
677
+ intentName?: string;
678
+ /** Native analogue of `data-intent-redact`. */
679
+ redact?: 'mask' | 'ignore';
680
+ redactLabel?: string;
681
+ };
682
+ type IntentPressExtras = {
683
+ /**
684
+ * Report the fields in this button's segment along with the press.
685
+ *
686
+ * Opt-in rather than automatic: a segment's fields are reported on every
687
+ * press otherwise, so tabbing through a checkout emits the form repeatedly,
688
+ * each time half-filled. One word is a fair price for not shipping partial
689
+ * answers over and over.
690
+ */
691
+ submits?: boolean;
692
+ /** Overrides the label. Defaults to the button's own visible text. */
693
+ label?: string;
694
+ };
695
+ /**
696
+ * Turn a field's props into what a submission reports.
697
+ *
698
+ * Exported because it holds every decision worth testing — the label fallback
699
+ * chain, type derivation, and the point at which redaction is applied — while
700
+ * the components around it are thin React plumbing.
701
+ */
702
+ declare function describeField(props: IntentFieldExtras & HostFieldProps): FormFieldReading;
703
+ declare function createIntentComponents<P extends HostFieldProps, Q extends object>(rn: {
704
+ TextInput: ComponentType<P>;
705
+ Pressable: ComponentType<Q>;
706
+ }): {
707
+ TextInput: ComponentType<P & IntentFieldExtras & {
708
+ ref?: Ref<unknown>;
709
+ }>;
710
+ Pressable: ComponentType<Q & IntentPressExtras & {
711
+ ref?: Ref<unknown>;
712
+ }>;
713
+ };
714
+
715
+ /** The pieces of `react-native` this needs, declared by shape. */
716
+ type ReactNativeModule = {
717
+ AppState: {
718
+ addEventListener(type: 'change', handler: (status: AppStateStatus) => void): {
719
+ remove(): void;
720
+ } | undefined;
721
+ };
722
+ Dimensions: {
723
+ get(dimension: 'window'): {
724
+ width: number;
725
+ height: number;
726
+ scale?: number;
727
+ };
728
+ };
729
+ PixelRatio?: {
730
+ get(): number;
731
+ };
732
+ InteractionManager?: {
733
+ runAfterInteractions(task: () => void): unknown;
734
+ };
735
+ };
736
+ type AdapterExtras = {
737
+ storage: IntentStorage;
738
+ getInsets?(): EdgeInsets;
739
+ randomId?(): string;
740
+ /**
741
+ * Replace the HTTP client. Defaults to the global `fetch`.
742
+ *
743
+ * `NativeAdapters` has always accepted this; leaving it off `AdapterExtras`
744
+ * meant `createNativeAdapters` silently produced adapters that ignored it,
745
+ * so a caller passing an instrumented client got the global one and no
746
+ * indication why their logging never fired.
747
+ */
748
+ fetch?: FetchLike;
749
+ /** Override the idle scheduler. Defaults to the ladder below. */
750
+ scheduleIdle?(): Promise<void>;
751
+ };
752
+ /** Minimal `IntentStorage` over react-native-mmkv's synchronous API. */
753
+ type MmkvLike = {
754
+ getString(key: string): string | undefined;
755
+ set(key: string, value: string): void;
756
+ delete(key: string): void;
757
+ };
758
+ declare function mmkvStorage(mmkv: MmkvLike): IntentStorage;
759
+ declare function createNativeAdapters(rn: ReactNativeModule, extras: AdapterExtras): NativeAdapters;
760
+
761
+ type AsyncKeyValueStore = {
762
+ getAllKeys(): Promise<readonly string[]>;
763
+ multiGet(keys: readonly string[]): Promise<ReadonlyArray<readonly [string, string | null]>>;
764
+ setItem(key: string, value: string): Promise<void>;
765
+ removeItem(key: string): Promise<void>;
766
+ };
767
+ declare function createMemoryStorage(initial?: Record<string, string>): IntentStorage;
768
+ /**
769
+ * Wrap AsyncStorage as synchronous storage.
770
+ *
771
+ * `hydrate()` must be awaited before the SDK starts, otherwise the first events
772
+ * of a session carry no `sid` and the server treats a returning user as new.
773
+ * Only keys under the `intent:` prefix are loaded — the host app's storage is
774
+ * none of the SDK's business and could be large.
775
+ *
776
+ * Writes are fire-and-forget. A write lost to a hard kill costs at most the
777
+ * current visit id, which the next launch regenerates.
778
+ */
779
+ declare function createHydratedStorage(store: AsyncKeyValueStore, prefix?: string): IntentStorage & {
780
+ hydrate(): Promise<void>;
781
+ };
782
+
783
+ export { IntentSegmentContext, IntentSegmentProvider, SegmentRegistry, VisibilityTracker, createHydratedStorage, createIntentComponents, createMemoryStorage, createNativeAdapters, describeField, fieldPlaceholderLabel, flushIntent, getCoverage, getIntentSessionId, getOnScreen, getTransportStats, initIntentNative, isSensitiveField, mmkvStorage, readFieldValue, setConsent, setIntentRoot, shutdownIntentNative, trackEvent, trackFormSubmit, trackInput, trackPress, trackScreen, useIntentList, useIntentOpacity, useIntentOverlay, useIntentPress, useIntentScreen, useIntentSegment, useIntentSegmentId, visibleRatio, whenReady };
784
+ export type { AdapterExtras, AppStateStatus, AsyncKeyValueStore, ChosenTarget, Coverage, DigestAction, DigestImage, EdgeInsets, FetchLike, FormFieldTarget, FormSubmitTarget, InputTarget, IntentFieldExtras, IntentHost, IntentListBinding, IntentPressExtras, IntentRefCallback, IntentSegmentBinding, IntentStorage, LayoutEvent, LayoutRectangle, MeasurableView, MeasureInWindow, MmkvLike, NativeAdapters, NativeFieldProps, NativeInitOptions, OnScreenEntry, PressTarget, ReactNativeModule, Registration, SegmentContent, SegmentDigest, SegmentId, SegmentTarget, UseIntentSegmentOptions, ViewToken, Viewport, WindowMetrics };