import * as react from 'react'; import { ReactNode, ComponentType, Ref } from 'react'; type Rect = { x: number; y: number; w: number; h: number; }; /** One actionable affordance inside a segment — a choice on offer. */ type DigestAction = { label: string; role: string; /** * Where the action leads. A URL on web; a route/screen name on native. * Absent for actions that don't navigate — `javascript:` and its kin are * affordances with no destination, and `navigatingHref` in `digest-shape` * is what holds this field to that promise. */ href?: string; }; type DigestImage = { alt?: string; src?: string; }; type SegmentDigest = { /** Content-derived id; identical blocks share it across sessions. */ ref: string; /** * Why this became a segment. On web the segmenter's reason ('repeated', * 'heading', …); on native the role the app declared ('listItem', 'card', …). */ kind: string; heading?: string; text: string; actions: DigestAction[]; images: DigestImage[]; }; /** A segment visible right now, with how long it has been on screen. */ type OnScreenEntry = { ref: string; ratio: number; dwellMs: number; rect: Rect; }; /** What the user acted on. */ type ChosenTarget = { tag: string; label: string; role?: string; /** The enclosing segment, when the target sits inside one. */ ref?: string; value?: string; }; type Viewport = { w: number; h: number; scrollY: number; docH: number; /** * Device pixel ratio / RN `PixelRatio`. Web omits it (the server assumes 1); * native reports it because layout units are density-independent there and * the number is needed to reason about what the user could actually read. */ scale?: number; /** Safe-area insets. Native only — a notch genuinely occludes content. */ insets?: { top: number; right: number; bottom: number; left: number; }; }; type Coverage = { segments: number; onScreen: number; registered: number; }; /** * Which producer built the payload. The ingest endpoint needs this: `url` means * a location on web and a route path on native, and the two are not comparable * without knowing which is which. */ type Platform = 'web' | 'native'; /** * Synchronous key/value storage for a handful of short strings. * * Synchronous on purpose. Session identity has to be readable inside the * teardown path — `pagehide` on web, background transition on native — and an * await there loses the batch. Web uses `localStorage`; native should use MMKV. * AsyncStorage users can wrap it with `createHydratedStorage` (see * `src/native/storage.ts`), which hydrates once at init and writes back * asynchronously. */ type IntentStorage = { get(key: string): string | undefined; set(key: string, value: string): void; remove(key: string): void; }; type SendOptions = { /** * The page or app is going away. On web this selects `sendBeacon`; on native * it means the batch must also be persisted, because the OS may kill the * process before the request completes. */ beacon: boolean; byteLength: number; }; type IntentHost = { platform: Platform; /** * Monotonic milliseconds. Used only for durations (dwell), never for * timestamps, so it is safe for this to have an arbitrary origin. */ now(): number; /** Wall-clock milliseconds for event timestamps. */ wallClock(): number; storage: IntentStorage; /** Resolves true when the batch was accepted for delivery. */ send(url: string, body: string, options: SendOptions): Promise; /** Yield to the host's render loop before doing expensive work. */ scheduleIdle(): Promise; log(...args: unknown[]): void; }; /** Matches `NativeMethods.measureInWindow`. Window-relative, in DIPs. */ type MeasureInWindow = (callback: (x: number, y: number, width: number, height: number) => void) => void; /** Matches the `layout` payload of RN's `onLayout` event. */ type LayoutRectangle = { x: number; y: number; width: number; height: number; }; type LayoutEvent = { nativeEvent: { layout: LayoutRectangle; }; }; /** Matches `AppStateStatus`. Widened because platforms add their own values. */ type AppStateStatus = 'active' | 'background' | 'inactive' | (string & {}); type WindowMetrics = { width: number; height: number; /** `PixelRatio.get()`. Layout units are density-independent on native. */ scale: number; }; type EdgeInsets = { top: number; right: number; bottom: number; left: number; }; /** * Structural subset of `fetch`. Declared rather than referencing the DOM lib so * this module says what it actually needs — and so a consumer can pass a * instrumented or retrying client of their own. */ type FetchLike = (url: string, init: { method: string; headers: Record; body: string; }) => Promise<{ ok: boolean; status: number; }>; /** * What the host app supplies once, at init. Build it from the `react-native` * module with `createNativeAdapters(require('react-native'))`, or assemble it * by hand in tests and on other runtimes. */ type NativeAdapters = { /** * Synchronous key/value storage. MMKV is the intended backing store; see * `createHydratedStorage` for AsyncStorage, which cannot be read * synchronously and so must be hydrated at startup. */ storage: IntentStorage; /** Returns an unsubscribe function, like `AppState.addEventListener`. */ onAppStateChange(listener: (status: AppStateStatus) => void): () => void; getWindow(): WindowMetrics; /** * Safe-area insets. Optional because not every app installs * react-native-safe-area-context, but worth supplying: a notch or a home * indicator genuinely occludes content, and without this the SDK reports a * segment as on-screen when the user cannot see it. */ getInsets?(): EdgeInsets; fetch?: FetchLike; /** * Yield before expensive work. `InteractionManager.runAfterInteractions` is * the direct analogue of the web's `requestIdleCallback` ladder, and is what * `createNativeAdapters` wires up. */ scheduleIdle?(): Promise; /** * Session id generator. Defaults to a `Math.random`-based v4 shape, because * Hermes has no `crypto.randomUUID` unless the app installs * react-native-get-random-values. Supply a real one when available. */ randomId?(): string; }; type SegmentId = number; /** What a component declares about itself. */ type SegmentContent = { /** * What sort of unit this is: 'listItem', 'card', 'section', 'header'… * Free-form, and it lands in the digest verbatim as `kind`. Mirrors the * web segmenter's `reason` field so the server sees one vocabulary. */ kind: string; heading?: string; text?: string; actions?: DigestAction[]; images?: DigestImage[]; }; type Registration = { id: SegmentId; /** Enclosing segment, for the "which segment was pressed" walk. */ parent?: SegmentId; content: SegmentContent; fingerprint: number; /** * Window-relative measurement, supplied by the view's ref. Absent for * segments whose visibility arrives from a list's viewability callback, * which is both cheaper and more accurate than measuring. */ measure?: MeasureInWindow; /** * Re-measure on every pass rather than only when something reported a layout * change. * * For inferred segments this is not an optimisation to skip but a * correctness requirement: they have no `onLayout` to mark the tracker * dirty, and scrolling would not fire one anyway — a scroll changes the * offset, not the layout. Without it their geometry is measured once and * frozen, so a scrolled list keeps reporting the rows that were on screen * when the screen first settled. * * Affordable because these measure through `getBoundingClientRect`, which is * synchronous, rather than `measureInWindow`, which is a bridge round trip. */ alwaysMeasure?: boolean; /** * Last reported opacity. Animated fade-ins park views at 0 exactly as * scroll-reveal animations do on the web, and geometry alone cannot see it. */ opacity: number; /** * Set by the viewability path. When present, the measurement pass skips * this segment entirely — `onViewableItemsChanged` already answered. */ viewability?: { visible: boolean; ratio: number; }; }; declare class SegmentRegistry { private nextId; private readonly registrations; /** * Stack of segments that cover what is beneath them. A `` or a * bottom sheet occludes the entire screen behind it, and nothing in the * geometry says so — the views underneath still measure exactly where they * were. Without this the SDK would keep accruing dwell for a product grid * the user cannot see behind an open checkout sheet. */ private readonly overlays; /** * Notified whenever the set of registered segments changes. * * Callers that resolve a segment from a view ref cannot know when that view * became resolvable. With inference the id does not exist until a visibility * pass has walked the tree, which happens after mount — so a hook that * resolves once on mount, finds nothing and returns would never install its * effect, however long the app then runs. */ private readonly listeners; subscribe(listener: () => void): () => void; private notify; register(content: SegmentContent, options?: { parent?: SegmentId; measure?: MeasureInWindow; alwaysMeasure?: boolean; }): SegmentId; /** * Replace a segment's content. The fingerprint change is what tells the * ledger to re-extract while keeping the accumulated dwell — the React * re-render equivalent of the web's content-hash check. */ update(id: SegmentId, content: SegmentContent): void; unregister(id: SegmentId): void; setMeasure(id: SegmentId, measure: MeasureInWindow | undefined): void; setOpacity(id: SegmentId, opacity: number): void; setViewability(id: SegmentId, visible: boolean, ratio: number): void; get(id: SegmentId): Registration | undefined; has(id: SegmentId): boolean; values(): Registration[]; pushOverlay(id: SegmentId): void; popOverlay(id: SegmentId): void; /** * True when something is covering this segment. Only the topmost overlay * matters: anything not inside it is behind it. */ isOccluded(id: SegmentId): boolean; /** Walk from a segment up through its declared parents. */ isWithin(id: SegmentId, ancestor: SegmentId): boolean; clear(): void; } /** * Everything about a text field that bears on whether its value may leave the * device. Declared structurally so it accepts a `TextInput`'s own props object. */ type NativeFieldProps = { value?: string; secureTextEntry?: boolean; autoComplete?: string; textContentType?: string; accessibilityRole?: string; /** RN's `keyboardType`: `email-address`, `phone-pad`, `visible-password`, … */ keyboardType?: string; /** RN's `inputMode`: `email`, `tel`, `numeric`, … */ inputMode?: string; /** * The app's own name for what the field collects — `FormFieldTarget.type`, * and `describeField`'s output. Free-form, so it is resolved through the same * two tables as the props above: `email` is a web token, `creditCardNumber` * is an iOS one, and an app writes whichever it thinks in. */ type?: string; /** * Explicit override, the native analogue of `data-intent-redact`. * `'mask'` replaces the value with a labelled placeholder; `'ignore'` * removes the field from the digest entirely. */ redact?: 'mask' | 'ignore'; redactLabel?: string; }; declare function isSensitiveField(props: NativeFieldProps): boolean; declare function fieldPlaceholderLabel(props: NativeFieldProps): string; declare function readFieldValue(props: NativeFieldProps): string; type Geometry = { intersecting: boolean; ratio: number; rect: Rect; }; type VisibilityOptions = { registry: SegmentRegistry; getWindow(): WindowMetrics; getInsets?(): EdgeInsets; /** Below this share of its own height on screen, a segment is not "seen". */ minRatio: number; /** Below this opacity, a segment is not "seen" however well it measures. */ minOpacity: number; /** Give up on a measurement pass after this long; unmounted views never call back. */ measureTimeoutMs: number; }; /** * Fraction of a rect that falls inside the visible window area. * * Both axes count. A horizontal carousel scrolls its off-screen cards fully out * sideways while their vertical extent stays perfectly in view, and a * height-only calculation would report every one of them as seen. */ declare function visibleRatio(rect: Rect, viewport: Rect): number; declare class VisibilityTracker { private readonly options; private readonly rects; private dirty; constructor(options: VisibilityOptions); /** * Something moved. Called from `onLayout` and from scroll handlers, both of * which are cheap and push-based; the expensive measurement only follows if * this was set. */ markDirty(): void; /** The window area a user can actually see, insets removed. */ viewportRect(): Rect; /** Last known window-relative rect for a segment. */ rectFor(id: SegmentId): Rect; /** * Measure everything that does not get its visibility from a list, then * report geometry for every registered segment. * * Returns an empty array when nothing has moved since the last pass, so a * still screen costs one map lookup rather than one native call per view. */ pass({ force }?: { force?: boolean | undefined; }): Promise>; /** * Visibility that geometry cannot answer: a view faded to transparent, or * one sitting behind an open modal. The direct analogue of the web's * `checkVisibility()` correction, and consulted on the same terms — only for * segments already believed to be intersecting. */ probe(id: SegmentId): boolean; forget(id: SegmentId): void; clear(): void; /** * Fire every measurement at once and wait for the callbacks. * * Bounded by a timeout because a view unmounted between registration and * measurement never calls back at all, and one such view would otherwise * stall the pass — and with it every subsequent one — forever. */ private measure; } type NativeInitOptions = { clientId: string; adapters: NativeAdapters; baseUrl?: string; consent?: boolean; debug?: boolean; /** How often the visibility pass runs. */ visibilityIntervalMs?: number; /** * How long the stored `sid` stays valid, in seconds. Defaults to 30 days, * the same default and the same name as `initIntent` on the web. */ sidMaxAgeSeconds?: number; batchSize?: number; flushIntervalMs?: number; maxBuffered?: number; minRatio?: number; /** * Infer segments from the view tree instead of requiring every screen to * declare them, the way the browser SDK reads the DOM. * * Needs a root: pass {@link setIntentRoot} as the `ref` of the view wrapping * the app. Inferred segments register alongside declared ones, so a codebase * can migrate a screen at a time. * * Off by default. It reads React's private prop bridge for the things the * public node tree cannot express, and that is an opt-in dependency. */ inferSegments?: boolean; }; /** * Wait for the first visibility + registration cycle. * * The first screen view otherwise beats the ledger and ships an empty digest — * the single most important event of the session, describing nothing. */ declare function whenReady(timeoutMs?: number): Promise; /** * Nominate the view to infer segments from. Use it as a `ref`: * * {children} * * Only meaningful with `inferSegments: true`. */ declare function setIntentRoot(node: unknown): void; declare function initIntentNative(options: NativeInitOptions): void; declare function shutdownIntentNative(): void; declare function setConsent(granted: boolean): void; declare function getIntentSessionId(): string; /** * A screen view — the native pageview. * * Call it from React Navigation's `onStateChange` or an Expo Router effect. * Comparing on the resolved path means a param-only change (a tab index, a * filter) does not emit a duplicate, mirroring the web's pathname+search * comparison that stops scroll-spy hash rewrites emitting a pageview each. */ declare function trackScreen(name: string, params?: Record): void; type PressTarget = { label: string; role?: string; /** The enclosing segment, from `useIntentSegment`'s context. */ segmentId?: SegmentId; }; declare function trackPress(target: PressTarget): void; type InputTarget = PressTarget & { field: NativeFieldProps; }; declare function trackInput(target: InputTarget): void; type FormFieldTarget = { /** Stable key for the answer. Falls back to `label` when absent. */ name?: string; /** The question as the visitor read it. */ label?: string; /** `text`, `email`, `creditCardNumber`, … Free-form; describes the control. */ type?: string; /** Redacted by the same policy as `trackInput`. */ field: NativeFieldProps; }; type FormSubmitTarget = { name?: string; /** The segment the form lives in, so the submission is placed on screen. */ segmentId?: SegmentId; fields: readonly FormFieldTarget[]; }; /** * Report a submitted form. * * The web SDK discovers this from a `
` element's submit event. Native has * no such thing — there is no form element and no submit event — so the screen * declares what it collected, exactly as it declares its segments. * * Values ride on the event and never on the segment: `ref` is a content hash, * so folding answers into the digest would mint a new segment per submission. */ declare function trackFormSubmit(target: FormSubmitTarget): void; declare function trackEvent(event: string, props?: Record): void; /** Force a flush. Useful before a deliberate teardown, e.g. logout. */ declare function flushIntent(): Promise; declare function getTransportStats(): { buffered: number; sent: number; failed: number; dropped: number; }; /** * How much of the screen was actually accounted for. * * With screenshots gone there is no fallback behind poor segmentation, so an * app whose structure the SDK cannot see must show up as a low number rather * than as quietly thin data. Tolerant of not being started, because this is * exactly what a diagnostics view calls first. */ declare function getCoverage(): Coverage; /** Every segment visible right now, with how long it has been on screen. */ declare function getOnScreen(): OnScreenEntry[]; /** Any view whose ref exposes `measureInWindow` — every RN host component. */ type MeasurableView = { measureInWindow?: MeasureInWindow; }; /** * The `ref` a segment hook hands back. * * The parameter is `unknown` on purpose. React Native's own ref types differ by * component and by version (`Ref`, `Ref>`, the * Animated wrappers, FlashList's cells), and a callback typed to anything * narrower fails to be assignable to half of them. A parameter of `unknown` * accepts every one, and the implementation checks for `measureInWindow` * before using it — which is the honest contract anyway, since a component * that does not expose it simply falls back to being untracked geometrically. */ type IntentRefCallback = (node: unknown) => void; /** * Enclosing segment. Lets a deeply nested Pressable attribute itself to the * card it sits in without the card threading an id down through props. */ declare const IntentSegmentContext: react.Context; declare function IntentSegmentProvider({ id, children }: { id: SegmentId | undefined; children: ReactNode; }): react.FunctionComponentElement>; declare function useIntentSegmentId(): SegmentId | undefined; type UseIntentSegmentOptions = { /** * `keyExtractor` value for this row, when it lives in a FlatList/FlashList * wired up with `useIntentList`. Supplying it means visibility comes from * the list's own viewability pass — cheaper and more accurate than * measuring, and it picks up `minimumViewTime` for free. */ viewabilityKey?: string; /** Override the enclosing segment instead of taking it from context. */ parent?: SegmentId; }; type IntentSegmentBinding = { segmentId: SegmentId | undefined; ref: IntentRefCallback; onLayout: (event?: LayoutEvent) => void; }; /** * Declare a segment. * * This is the native replacement for the web's structural segmentation: rather * than infer a content unit from markup, the component states what it is. The * content is extracted once, on idle, when the segment first becomes visible — * never during a render and never during a gesture. */ declare function useIntentSegment(content: SegmentContent, options?: UseIntentSegmentOptions): IntentSegmentBinding; /** * A press handler that attributes the press to its enclosing segment. * * Costs a map lookup. The segment's content was extracted on idle long before * the finger landed. */ declare function useIntentPress(target: Omit & { segmentId?: SegmentId; }): () => void; /** Emit a screen view. Deduped on the resolved name by `trackScreen`. */ declare function useIntentScreen(name: string, params?: Record): void; type ViewToken = { key?: string; isViewable: boolean; item?: unknown; index?: number | null; }; type IntentListBinding = { viewabilityConfig: { itemVisiblePercentThreshold: number; minimumViewTime: number; }; onViewableItemsChanged: (info: { viewableItems: ViewToken[]; changed: ViewToken[]; }) => void; }; /** * Wire a FlatList / SectionList / FlashList's viewability into the ledger. * * This is the tier the web has no equivalent of. RN already computes * percent-visible per row and can gate on a minimum view time, which is exactly * the "was this actually seen" question the web needs IntersectionObserver plus * a checkVisibility correction to answer. Rows bound this way are never * measured. * * Spread the result onto the list and give each row's `useIntentSegment` the * same `viewabilityKey` your `keyExtractor` returns. */ declare function useIntentList({ itemVisiblePercentThreshold, minimumViewTime, }?: { itemVisiblePercentThreshold?: number | undefined; minimumViewTime?: number | undefined; }): IntentListBinding; /** * Mark a segment as covering everything behind it while `active`. * * A `` or a bottom sheet occludes the whole screen, and nothing in the * geometry says so — the views underneath still measure exactly where they * were. Without this the SDK keeps accruing dwell for a product grid the user * cannot see behind an open checkout sheet. */ /** * Either a segment id, or a ref to the view that is the segment. * * The ref form is what lets a screen stop declaring segments purely to obtain an * id: the enclosing segment is found by walking the view's ancestors, so an * inferred segment works exactly as a declared one does. */ type SegmentTarget = SegmentId | { current: unknown; } | undefined; declare function useIntentOverlay(target: SegmentTarget, active: boolean): void; /** * Report an animated opacity so faded-out content stops counting as seen. * * The native equivalent of the web's `checkVisibility()` opacity correction, * which exists because scroll-reveal animations park elements at opacity 0 * while they still intersect perfectly. Drive it from an `Animated.Value` * listener when a segment fades. */ declare function useIntentOpacity(target: SegmentTarget, opacity: number): void; type FormFieldReading = { name?: string; label?: string; type?: string; value: string; /** Opted out via `redact="ignore"`; excluded from submissions entirely. */ ignored?: boolean; }; type HostFieldProps = { value?: string; placeholder?: string; accessibilityLabel?: string; accessibilityRole?: string; secureTextEntry?: boolean; autoComplete?: string; textContentType?: string; keyboardType?: string; inputMode?: string; }; /** The only props these components add. All are optional. */ type IntentFieldExtras = { /** Overrides the question. Defaults to accessibilityLabel, then placeholder. */ label?: string; /** Stable key for the answer. Defaults to the resolved label. */ intentName?: string; /** Native analogue of `data-intent-redact`. */ redact?: 'mask' | 'ignore'; redactLabel?: string; }; type IntentPressExtras = { /** * Report the fields in this button's segment along with the press. * * Opt-in rather than automatic: a segment's fields are reported on every * press otherwise, so tabbing through a checkout emits the form repeatedly, * each time half-filled. One word is a fair price for not shipping partial * answers over and over. */ submits?: boolean; /** Overrides the label. Defaults to the button's own visible text. */ label?: string; }; /** * Turn a field's props into what a submission reports. * * Exported because it holds every decision worth testing — the label fallback * chain, type derivation, and the point at which redaction is applied — while * the components around it are thin React plumbing. */ declare function describeField(props: IntentFieldExtras & HostFieldProps): FormFieldReading; declare function createIntentComponents

(rn: { TextInput: ComponentType

; Pressable: ComponentType; }): { TextInput: ComponentType

; }>; Pressable: ComponentType; }>; }; /** The pieces of `react-native` this needs, declared by shape. */ type ReactNativeModule = { AppState: { addEventListener(type: 'change', handler: (status: AppStateStatus) => void): { remove(): void; } | undefined; }; Dimensions: { get(dimension: 'window'): { width: number; height: number; scale?: number; }; }; PixelRatio?: { get(): number; }; InteractionManager?: { runAfterInteractions(task: () => void): unknown; }; }; type AdapterExtras = { storage: IntentStorage; getInsets?(): EdgeInsets; randomId?(): string; /** * Replace the HTTP client. Defaults to the global `fetch`. * * `NativeAdapters` has always accepted this; leaving it off `AdapterExtras` * meant `createNativeAdapters` silently produced adapters that ignored it, * so a caller passing an instrumented client got the global one and no * indication why their logging never fired. */ fetch?: FetchLike; /** Override the idle scheduler. Defaults to the ladder below. */ scheduleIdle?(): Promise; }; /** Minimal `IntentStorage` over react-native-mmkv's synchronous API. */ type MmkvLike = { getString(key: string): string | undefined; set(key: string, value: string): void; delete(key: string): void; }; declare function mmkvStorage(mmkv: MmkvLike): IntentStorage; declare function createNativeAdapters(rn: ReactNativeModule, extras: AdapterExtras): NativeAdapters; type AsyncKeyValueStore = { getAllKeys(): Promise; multiGet(keys: readonly string[]): Promise>; setItem(key: string, value: string): Promise; removeItem(key: string): Promise; }; declare function createMemoryStorage(initial?: Record): IntentStorage; /** * Wrap AsyncStorage as synchronous storage. * * `hydrate()` must be awaited before the SDK starts, otherwise the first events * of a session carry no `sid` and the server treats a returning user as new. * Only keys under the `intent:` prefix are loaded — the host app's storage is * none of the SDK's business and could be large. * * Writes are fire-and-forget. A write lost to a hard kill costs at most the * current visit id, which the next launch regenerates. */ declare function createHydratedStorage(store: AsyncKeyValueStore, prefix?: string): IntentStorage & { hydrate(): Promise; }; 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 }; 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 };