@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.
- package/README.md +3 -0
- package/dist/esm/index.debug.js +1 -1
- package/dist/esm/index.debug.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/native.js +4 -0
- package/dist/intent.debug.umd.min.js +2 -2
- package/dist/intent.debug.umd.min.js.map +1 -1
- package/dist/intent.umd.min.js +2 -2
- package/dist/types/native.d.ts +784 -0
- package/package.json +30 -2
|
@@ -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 };
|