@wireai/activation 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/AGENTS.md +82 -40
  2. package/CHANGELOG.md +17 -2
  3. package/INTEGRATION_PROMPT.md +8 -8
  4. package/README.md +38 -38
  5. package/dist/analytics/index.d.mts +92 -0
  6. package/dist/analytics/index.d.ts +92 -0
  7. package/dist/analytics/index.js +439 -0
  8. package/dist/analytics/index.js.map +1 -0
  9. package/dist/analytics/index.mjs +426 -0
  10. package/dist/analytics/index.mjs.map +1 -0
  11. package/dist/coachmarks/index.js.map +1 -1
  12. package/dist/coachmarks/index.mjs.map +1 -1
  13. package/dist/decision-BzbiKwk3.d.mts +79 -0
  14. package/dist/decision-plDEOCkt.d.ts +79 -0
  15. package/dist/eventQueue-CxKi7Qd5.d.mts +546 -0
  16. package/dist/eventQueue-rV1dtJJR.d.ts +546 -0
  17. package/dist/index.d.mts +162 -418
  18. package/dist/index.d.ts +162 -418
  19. package/dist/index.js +407 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +402 -5
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/questionnaire/index.d.mts +2 -1
  24. package/dist/questionnaire/index.d.ts +2 -1
  25. package/dist/questionnaire/index.js.map +1 -1
  26. package/dist/questionnaire/index.mjs.map +1 -1
  27. package/dist/reviews/index.d.mts +5 -40
  28. package/dist/reviews/index.d.ts +5 -40
  29. package/dist/reviews/index.js.map +1 -1
  30. package/dist/reviews/index.mjs.map +1 -1
  31. package/dist/showcase/index.js.map +1 -1
  32. package/dist/showcase/index.mjs.map +1 -1
  33. package/dist/transport-BeO_Brcu.d.mts +40 -0
  34. package/dist/transport-DLpd1v5_.d.ts +40 -0
  35. package/dist/{decision-Cl8OFYzu.d.mts → types-A6pTxIZV.d.mts} +1 -77
  36. package/dist/{decision-CFvGY6nP.d.ts → types-BhpXJGlg.d.ts} +1 -77
  37. package/llms.txt +7 -7
  38. package/metro/index.d.ts +3 -3
  39. package/metro/index.js +3 -3
  40. package/package.json +15 -1
  41. package/src/analytics/contextEnvelope.ts +72 -0
  42. package/src/analytics/eventQueue.ts +331 -0
  43. package/src/analytics/index.ts +47 -0
  44. package/src/analytics/screenTracking.ts +122 -0
  45. package/src/analytics/useScreenTracking.ts +48 -0
  46. package/src/coachmarks/index.ts +2 -2
  47. package/src/features/WireFeaturesProvider.tsx +1 -1
  48. package/src/features/index.ts +2 -2
  49. package/src/index.ts +18 -1
  50. package/src/questionnaire/index.ts +2 -2
  51. package/src/reviews/index.ts +2 -2
  52. package/src/session-analytics/index.ts +20 -0
  53. package/src/session-analytics/lifecycle.ts +236 -0
  54. package/src/session-analytics/reportSessionStart.ts +27 -7
  55. package/src/session-analytics/useLifecycleEvents.ts +184 -0
  56. package/src/showcase/index.ts +2 -2
  57. package/src/types.ts +1 -1
@@ -0,0 +1,546 @@
1
+ import * as React from 'react';
2
+ import * as wireai_rn from 'wireai-rn';
3
+ import { Message } from 'wireai-rn';
4
+ import { O as OnboardingTheme } from './types-BKfpdZzX.js';
5
+ import { b as WireOnboardingStorage } from './types-CMuOexw0.js';
6
+ import { Platform } from 'react-native';
7
+
8
+ /**
9
+ * deviceContext — collect a small, privacy-label-neutral snapshot of the device so
10
+ * onboarding analytics can segment the funnel (platform / form factor / locale) WITHOUT
11
+ * adding a single dependency to the kit or changing a host app's App Privacy / Data Safety
12
+ * declarations.
13
+ *
14
+ * HARD RULE (why this file has no imports beyond React Native built-ins):
15
+ * The kit stays dependency-free. Everything here comes from `Platform`, `Dimensions`,
16
+ * `I18nManager`, and the standard `Intl` global. There are NO advertising IDs, NO
17
+ * `getUniqueId`/IDFA/GAID/fingerprinting APIs, and nothing that would require a new
18
+ * privacy-label entry. A host can adopt this without touching its store declarations.
19
+ *
20
+ * DEFENSIVE BY DESIGN: `collectDeviceContext()` never throws. Every read is guarded and
21
+ * a missing/unavailable field is simply omitted (Hermes may ship without full `Intl`,
22
+ * `Platform.constants` differs per OS and RN version, etc.). Analytics must never be able
23
+ * to break onboarding.
24
+ */
25
+
26
+ /** Coarse device class. iOS uses the reported interface idiom; else a screen-size heuristic. */
27
+ type DeviceFormFactor = "phone" | "tablet";
28
+ /**
29
+ * A privacy-label-neutral device snapshot. ALL fields except `platform` are optional and are
30
+ * omitted when unavailable. Nothing here identifies a user or device uniquely.
31
+ */
32
+ type DeviceContext = {
33
+ /** `Platform.OS` — "ios" | "android" | "windows" | "macos" | "web". Always present. */
34
+ platform: typeof Platform.OS;
35
+ /** OS version string (iOS `osVersion`/`Platform.Version`, Android `Release`). */
36
+ osVersion?: string;
37
+ /** Android device brand (e.g. "samsung"). Android only. */
38
+ brand?: string;
39
+ /** Android device model (e.g. "SM-G991B"). Android only. */
40
+ model?: string;
41
+ /** iOS interface idiom ("phone" | "pad" | …), when reported. iOS only. */
42
+ interfaceIdiom?: string;
43
+ /** Derived device class. */
44
+ formFactor?: DeviceFormFactor;
45
+ /** `Dimensions.get('screen')` width in dp. */
46
+ screenWidth?: number;
47
+ /** `Dimensions.get('screen')` height in dp. */
48
+ screenHeight?: number;
49
+ /** Screen pixel density (`scale`). */
50
+ screenScale?: number;
51
+ /** Right-to-left layout (`I18nManager.isRTL`). */
52
+ isRTL?: boolean;
53
+ /** Resolved locale (e.g. "en-US"), from `Intl` when available. */
54
+ locale?: string;
55
+ /** IANA time zone (e.g. "Europe/Berlin"), from `Intl` when available. */
56
+ timeZone?: string;
57
+ /**
58
+ * Host app version (e.g. "1.4.2). HOST-INJECTED — NOT collected here. `WireOnboarding`
59
+ * merges `config.appVersion` into the snapshot; `collectDeviceContext()` never sets it.
60
+ * Hosts typically pass it from `expo-constants` (the kit itself adds no dependency).
61
+ */
62
+ appVersion?: string;
63
+ };
64
+ /**
65
+ * Collect the device snapshot. Pure, synchronous, and never throws — call it once per
66
+ * onboarding session. Missing fields are omitted rather than sent as null/undefined so the
67
+ * payload (and the server's stored dict) stays compact.
68
+ */
69
+ declare const collectDeviceContext: () => DeviceContext;
70
+
71
+ /**
72
+ * reportClientEvent — forward DEVICE-ONLY onboarding events to the Wire AI analytics
73
+ * backend (`POST {serverUrl}/v1/events`), completing the funnel for events the server
74
+ * can't observe on its own.
75
+ *
76
+ * The backend already records the server-observable funnel during the A2A flow
77
+ * (`session_started`, `screen_shown`, `answer_submitted`, `completed`, `llm_fallback`,
78
+ * and even `screen_skipped` — it derives that from the kit's skip sentinel). The one
79
+ * event no server request can capture is `dropped`: the user closing the app / unmounting
80
+ * the flow without finishing. That's what this reporter is for.
81
+ *
82
+ * Contract (server: routers/onboarding.py → analytics/events.py):
83
+ * POST {serverUrl}/v1/events
84
+ * Authorization: Bearer {apiKey}
85
+ * { "events": [ { event_type, session_id, screen_index?, component?, question_key?,
86
+ * latency_ms?, meta?, device?, user_context? } ] }
87
+ * The server fills `app_id` + `environment` from the resolving key (never send app_id),
88
+ * and silently skips malformed events — one bad payload never fails the batch.
89
+ *
90
+ * ⚠️ Correlation: `session_id` MUST equal the A2A `contextId` the server uses to key the
91
+ * server-side events, or the funnel report (which groups by `session_id`) treats this as a
92
+ * phantom session. See `makeSessionId` + WireOnboarding for how the kit seeds it.
93
+ *
94
+ * Fire-and-forget: this never throws into the UI and never awaits — analytics must never
95
+ * be able to break onboarding.
96
+ */
97
+
98
+ /** Event types a CLIENT may report. The rest of the funnel is server-side; sending those
99
+ * here would double-count. `screen_skipped` is included for completeness, but the kit does
100
+ * NOT emit it — the backend already derives it from the skip sentinel (see OnboardingFlow).
101
+ * `client_fallback` is emitted by the kit when the AI flow degrades to the static fallback,
102
+ * so the dashboard's fallback-rate counts the whole-flow case (distinct from the server's
103
+ * per-turn `llm_fallback`). The server back-fills a `session_started` for it if unseen.
104
+ * This is the SINGLE fallback signal — hosts must NOT also report their own.
105
+ * `identify` binds the host's opaque `user_id` to this `session_id` (late binding — the user
106
+ * registered during/after onboarding). It carries no funnel weight; the server maps the
107
+ * session to the user and back-fills a `session_started` if it never saw the session. */
108
+ type ClientEventType = "screen_skipped" | "dropped" | "client_fallback" | "identify";
109
+ /** One client-reported event. Mirrors the server's `OnboardingEvent` (client-settable fields). */
110
+ type ClientEvent = {
111
+ event_type: ClientEventType;
112
+ /** Must match the server-side A2A contextId for this onboarding (see makeSessionId). */
113
+ session_id: string;
114
+ /** 0-based index of the screen the event refers to (matches server `screen_shown`). */
115
+ screen_index?: number;
116
+ component?: string;
117
+ question_key?: string;
118
+ latency_ms?: number;
119
+ /** JSON-stringified extras; the server stores it verbatim. */
120
+ meta?: string;
121
+ /**
122
+ * Privacy-label-neutral device snapshot (platform / form factor / locale / host appVersion).
123
+ * Sent as an object; the server sanitizes + persists it and derives a coarse country. Old
124
+ * servers ignore this unknown field — fully backward compatible. See device/deviceContext.ts.
125
+ */
126
+ device?: DeviceContext;
127
+ /**
128
+ * Host-injected, non-PII context (signup method, referral, plan, hashed user id). Old servers
129
+ * ignore it. MUST NOT contain PII like raw emails — see the README `userContext` section.
130
+ */
131
+ user_context?: Record<string, string | number | boolean>;
132
+ /**
133
+ * The host's OPAQUE PSEUDONYMOUS user id (their internal id, NOT an email/name). Required on
134
+ * `identify`, optional (rides along) on other events. Trimmed + capped at 128 chars host-side.
135
+ * Lets the backend reconcile onboarding sessions to real users. Old servers ignore it.
136
+ */
137
+ user_id?: string;
138
+ };
139
+ /** Where to POST. Derived from `WireOnboardingConfig` (`serverUrl` + `apiKey`). */
140
+ type ClientEventTarget = {
141
+ /** Base server URL (same as `WireOnboardingConfig.serverUrl`); `/v1/events` is appended. */
142
+ serverUrl: string;
143
+ /** Tenant API key; sent as `Authorization: Bearer`. */
144
+ apiKey: string;
145
+ };
146
+ /**
147
+ * A unique-per-onboarding session id. Used both as the client event `session_id` AND as the
148
+ * seed the kit forwards to the backend so the SERVER adopts it as the A2A `contextId` — making
149
+ * client and server agree (see WireOnboarding + the SDK-correlation note in the kit docs).
150
+ * No crypto dependency: timestamp + random is collision-safe for a single device's onboarding.
151
+ */
152
+ declare const makeSessionId: () => string;
153
+ /**
154
+ * POST one or more client events, fire-and-forget. A missing/invalid target, a build error,
155
+ * a missing `fetch`, or a network failure is swallowed — the call returns immediately and the
156
+ * request (if any) runs in the background.
157
+ */
158
+ declare const reportClientEvents: (target: ClientEventTarget | undefined, events: ClientEvent[]) => void;
159
+ /** Convenience single-event wrapper around {@link reportClientEvents}. */
160
+ declare const reportClientEvent: (target: ClientEventTarget | undefined, event: ClientEvent) => void;
161
+
162
+ /** Transport + tenant config for the managed Wire AI onboarding backend (A2A). */
163
+ type WireOnboardingConfig = {
164
+ /** Tenant API key (resolves the app server-side). */
165
+ apiKey: string;
166
+ /** Base server URL; the kit appends `/a2a`. */
167
+ serverUrl: string;
168
+ /** App id — passed as the A2A `model` (informational; the key resolves the app). */
169
+ appId: string;
170
+ /**
171
+ * Extra metadata merged into every A2A request (e.g. install attribution).
172
+ * NOTE: the kit reserves `sessionId` (correlation seed) and `supportedComponents`
173
+ * (the renderable card names this device advertises to the backend) — it sets both
174
+ * automatically, so don't override them here.
175
+ */
176
+ metadata?: Record<string, unknown>;
177
+ /**
178
+ * Host app version string (e.g. "1.4.2"). HOST-INJECTED — the kit adds no dependency to
179
+ * read it; hosts typically pass it from `expo-constants`
180
+ * (`Constants.expoConfig?.version`). Forwarded to the backend on the session metadata and
181
+ * on client events (merged into the `device` snapshot as `device.appVersion`) so analytics
182
+ * can segment the funnel by app version. Optional; omit if unknown.
183
+ */
184
+ appVersion?: string;
185
+ };
186
+ /**
187
+ * A per-step validator, keyed by a base-question `key` (e.g. `username`). Runs
188
+ * before the answer is sent to the backend. Return `{ ok: false, error }` to
189
+ * block advancing and show the error inline on the card.
190
+ */
191
+ type StepValidator = (value: string) => Promise<{
192
+ ok: boolean;
193
+ error?: string;
194
+ }>;
195
+ /** Result handed to `onComplete` when the flow reaches its terminal StatusCard. */
196
+ type OnboardingResult = {
197
+ /** Question-key → captured value, derived from the thread. */
198
+ answers: Record<string, unknown>;
199
+ /** The raw message thread, for custom downstream parsing. */
200
+ raw: Message[];
201
+ };
202
+ /**
203
+ * Lifecycle events emitted as the flow runs, for host-side analytics. The kit owns
204
+ * the thread loop, so this is how a host recovers per-turn telemetry it used to get
205
+ * by driving the loop itself.
206
+ * - `started`: the very first message was sent to the backend. Carries `contextId` (the
207
+ * A2A session id). Capture it if you may need to bind a user AFTER the flow
208
+ * finishes (see `identifyOnboarding` and the `userId` prop).
209
+ * - `resumed`: a persisted session was restored after an app kill (fires INSTEAD of
210
+ * `started`, so host funnels don't double-count the same session). Also carries
211
+ * `contextId`. Requires the `storage` prop.
212
+ * - `turn`: a new assistant card arrived (`step` = 1-based index of cards seen).
213
+ * - `error`: the backend errored or the first-card watchdog timed out.
214
+ * - `retry`: a transient failure is being auto-retried (`attempt` = 1-based).
215
+ * - `fallback`: retries are exhausted; the kit degraded to the static `fallbackFlow`
216
+ * (or handed off to `onError`). This is the client-side mirror of the
217
+ * backend's `llm_fallback` reliability event.
218
+ */
219
+ type OnboardingEvent = {
220
+ type: "started";
221
+ contextId: string;
222
+ } | {
223
+ type: "resumed";
224
+ contextId: string;
225
+ } | {
226
+ type: "turn";
227
+ step: number;
228
+ component?: string;
229
+ } | {
230
+ type: "error";
231
+ reason: "backend" | "timeout";
232
+ } | {
233
+ type: "retry";
234
+ reason: "backend" | "timeout";
235
+ attempt: number;
236
+ } | {
237
+ type: "fallback";
238
+ reason: "backend" | "timeout";
239
+ };
240
+ /**
241
+ * Copy overrides for the kit's built-in (English) strings, so a host can localize
242
+ * the loaders / completion fallback via its own i18n. Any field left unset keeps
243
+ * the kit default.
244
+ */
245
+ type OnboardingCopy = {
246
+ /** First "thinking" screen, before any card arrives. */
247
+ startingTitle: string;
248
+ startingHint: string;
249
+ /** While the persisted session id is being restored from `storage` (pre-mount gate). */
250
+ restoringTitle: string;
251
+ restoringHint: string;
252
+ /** Between-turns loader, while the next card is generated. */
253
+ thinkingTitle: string;
254
+ thinkingHint: string;
255
+ /** While a per-step validator is running. */
256
+ checkingTitle: string;
257
+ checkingHint: string;
258
+ /** Completion fallback when the backend omits a title/CTA. */
259
+ completeTitle: string;
260
+ completeCta: string;
261
+ };
262
+ type WireOnboardingProps = {
263
+ config: WireOnboardingConfig;
264
+ /** Partial theme merged over the neutral default. */
265
+ theme?: Partial<OnboardingTheme>;
266
+ /** Override the registered cards (defaults to the kit's `onboardingComponents`). */
267
+ components?: wireai_rn.WireAIComponent[];
268
+ /**
269
+ * App-supplied artwork for InterstitialCard, keyed by name. The backend names one
270
+ * via `illustration`; the kit slots the matching node (keeping itself dependency-free).
271
+ */
272
+ illustrations?: Record<string, React.ReactNode>;
273
+ /** Per-step validators keyed by base-question key, e.g. `{ username: checkUsername }`. */
274
+ validators?: Record<string, StepValidator>;
275
+ /** Fired once the flow reaches its terminal StatusCard. */
276
+ onComplete: (result: OnboardingResult) => void;
277
+ /**
278
+ * Retained for back-compat. NOTE: per-question Skip is now INTERNAL — the kit shows
279
+ * a Skip control only on screens the backend marks `skippable`, and it advances ONE
280
+ * question (it does not exit the flow). This callback is no longer wired to that control.
281
+ */
282
+ onSkip?: () => void;
283
+ /**
284
+ * Fired on a backend error or first-card timeout, AFTER retries are exhausted.
285
+ * When `fallbackFlow` is NOT supplied and this is provided, the host owns recovery
286
+ * (e.g. navigate to its own static onboarding) instead of the kit's inline retry.
287
+ */
288
+ onError?: (err: unknown) => void;
289
+ /**
290
+ * Your predefined STATIC onboarding, rendered in place when the AI flow fails and
291
+ * retries are exhausted — so a generation/backend/timeout error degrades to your
292
+ * static flow instead of breaking the app. Takes precedence over `onError`. This
293
+ * is the "it can never break your onboarding" guarantee: supply the same flow you
294
+ * shipped before adding Wire AI and the user always keeps moving.
295
+ */
296
+ fallbackFlow?: React.ReactNode;
297
+ /**
298
+ * Consecutive failures to auto-retry before degrading to `fallbackFlow`/`onError`.
299
+ * Default 1 (one silent retry, then degrade). 0 = degrade on the first failure.
300
+ */
301
+ maxRetries?: number;
302
+ /** Lifecycle hook for host-side analytics (started / per-turn / error). */
303
+ onEvent?: (event: OnboardingEvent) => void;
304
+ /**
305
+ * Host-injected, non-PII context the app already knows about the user — signup method,
306
+ * referral source, plan tier, a HASHED user id, etc. Same host-injection philosophy as
307
+ * `storage`: the kit collects nothing here; the host passes what it wants. Forwarded to the
308
+ * backend on the session metadata AND on client events so analytics can segment the funnel.
309
+ *
310
+ * MUST NOT contain PII such as raw emails, names, or phone numbers — pass a hash if you need
311
+ * a user key. Values are limited to primitives (`string | number | boolean`); the server caps
312
+ * key count / size and drops deep nesting. Old servers ignore it (backward compatible).
313
+ */
314
+ userContext?: Record<string, string | number | boolean>;
315
+ /**
316
+ * The host's own user id, so onboarding sessions can be reconciled to real users later
317
+ * (console sessions ↔ your user table / GA4 users). First-class alongside `userContext`.
318
+ *
319
+ * An OPAQUE PSEUDONYMOUS string YOU own — your internal user id, NOT an email/name/phone.
320
+ * Trimmed and capped at 128 chars (longer is truncated). **No PII.**
321
+ *
322
+ * LATE BINDING: users often register DURING or AFTER onboarding, so this is fully optional
323
+ * and can arrive late:
324
+ * - present at MOUNT → rides the A2A session-start metadata (server binds it at session start);
325
+ * - CHANGES mid-session (e.g. the user just registered) → the kit emits an `identify` event
326
+ * that attaches the id to the LIVE session;
327
+ * - available only AFTER completion → capture the `contextId` from the `started`/`resumed`
328
+ * `onEvent` while the flow runs, then call `identifyOnboarding({ contextId, userId })` once
329
+ * the user registers. Completion clears the persisted session, so the captured `contextId`
330
+ * is the reliable post-flow handle.
331
+ *
332
+ * Backward compatible: omit it and nothing changes; old servers ignore the extra field.
333
+ */
334
+ userId?: string;
335
+ /** Localized overrides for the kit's built-in English strings. */
336
+ copy?: Partial<OnboardingCopy>;
337
+ /**
338
+ * Approximate total number of screens, if known (e.g. the backend screen budget).
339
+ * Paces the progress bar as `step / approxScreens` (capped, never shown as a
340
+ * number) instead of the asymptotic default. A backend-supplied `progress.total`
341
+ * takes precedence when present.
342
+ */
343
+ approxScreens?: number;
344
+ /** First message that kicks off the backend flow. Default `"start"`. */
345
+ startMessage?: string;
346
+ /** Ms to wait for the first card before showing the error/retry state. Default 15000. */
347
+ startTimeoutMs?: number;
348
+ /**
349
+ * Host-injected storage for session-id persistence (AsyncStorage-compatible subset:
350
+ * pass `@react-native-async-storage/async-storage` as-is, or a small MMKV wrapper).
351
+ * When set, the kit caches its session id so an app KILL mid-onboarding resumes the
352
+ * SAME backend session instead of minting a new one — keeping the analytics funnel's
353
+ * `started` count honest (no phantom drops). Omit for the previous per-mount behavior.
354
+ * This persists the kit's own correlation seed only — never answers.
355
+ */
356
+ storage?: WireOnboardingStorage;
357
+ /**
358
+ * How long a persisted session id stays resumable, in ms. Default 3 600 000 (1h),
359
+ * matching the backend's session TTL. Only meaningful with `storage`.
360
+ */
361
+ sessionTtlMs?: number;
362
+ /**
363
+ * Override the storage key (default `wireai:session:<config.appId>`). Scope it
364
+ * per-user (e.g. append a user id) if one device can run onboarding for multiple
365
+ * accounts mid-flow. Only meaningful with `storage`.
366
+ */
367
+ persistKey?: string;
368
+ };
369
+ /** Backend-supplied progress, read off `response.props.progress` when present. */
370
+ type OnboardingProgress = {
371
+ step: number;
372
+ total: number;
373
+ /** Base-question key for the CURRENT screen, when known (used to pick a validator). */
374
+ key?: string;
375
+ /** Whether the CURRENT screen may be skipped (backend-marked; default false → no Skip shown). */
376
+ skippable?: boolean;
377
+ };
378
+
379
+ /**
380
+ * Canonical analytics names for the onboarding funnel. The kit already emits a typed
381
+ * `OnboardingEvent` (`started | turn | error | retry | fallback`) — but one app logged them as
382
+ * `onboarding_*` and another as `AI_ONBOARDING_*`, so the same funnel reads differently per app.
383
+ * This maps the kit event to ONE canonical `wire_onboarding_*` name + params, and the app logs
384
+ * it through whatever transport it already has (Firebase, Amplitude, console). The app still
385
+ * owns the logger; only the NAMES are standardized.
386
+ *
387
+ * <WireOnboarding
388
+ * onEvent={(e) => { const a = toAnalyticsEvent(e); logEvent(a.name, a.params); }}
389
+ * onComplete={(r) => { logEvent(WIRE_ONBOARDING_EVENTS.completed, { answers: Object.keys(r.answers).length }); persist(r); }}
390
+ * />
391
+ *
392
+ * `completed` has no kit `OnboardingEvent` (the kit signals completion via `onComplete`, not
393
+ * `onEvent`) — the app logs it explicitly on `onComplete` using the constant below, so the
394
+ * funnel name stays canonical.
395
+ */
396
+
397
+ declare const WIRE_ONBOARDING_EVENTS: {
398
+ readonly started: "wire_onboarding_started";
399
+ /** A persisted session was restored after an app kill (fires instead of `started`). */
400
+ readonly resumed: "wire_onboarding_resumed";
401
+ readonly turn: "wire_onboarding_turn";
402
+ readonly error: "wire_onboarding_error";
403
+ readonly retry: "wire_onboarding_retry";
404
+ readonly fallback: "wire_onboarding_fallback";
405
+ /** Logged by the host on `onComplete` (no matching kit `OnboardingEvent`). */
406
+ readonly completed: "wire_onboarding_completed";
407
+ };
408
+ type WireOnboardingEventName = (typeof WIRE_ONBOARDING_EVENTS)[keyof typeof WIRE_ONBOARDING_EVENTS];
409
+ type AnalyticsEvent = {
410
+ name: WireOnboardingEventName;
411
+ params?: Record<string, unknown>;
412
+ };
413
+ /**
414
+ * Map a kit `OnboardingEvent` to its canonical `{ name, params }`. Exhaustive over the union
415
+ * (the `never` default makes a new event type a compile error here — intentional).
416
+ */
417
+ declare const toAnalyticsEvent: (event: OnboardingEvent) => AnalyticsEvent;
418
+
419
+ /**
420
+ * contextEnvelope — a small, PRIVACY-NEUTRAL context bundle stamped onto every outgoing
421
+ * analytics event, giving the Wire dashboard the Sentry/Firebase-parity segmentation fields
422
+ * (device model, OS + version, screen, locale, timezone, form factor) plus a few host-injected
423
+ * scalars (session correlation id, app version + native build number, connectivity type).
424
+ *
425
+ * WHY a separate builder (not just `collectDeviceContext`): the envelope COMPOSES the existing
426
+ * device snapshot with the handful of extras a host can cheaply supply but the kit can't collect
427
+ * dependency-free (native build number, connectivity type). It never re-implements device
428
+ * collection — it reuses `collectDeviceContext()` verbatim (see device/deviceContext.ts).
429
+ *
430
+ * HARD PRIVACY RULE (why this file, like deviceContext.ts, adds nothing new):
431
+ * NEVER GPS / location, NEVER an advertising id (IDFA / GAID), NEVER a device fingerprint.
432
+ * Location is derived SERVER-SIDE from IP-geo only — nothing here carries a coordinate or an
433
+ * ad id, so a host adopting this changes no App Privacy / Data Safety declaration. There is a
434
+ * test (contextEnvelope.test.ts) that asserts the ABSENCE of any such field.
435
+ *
436
+ * DEPENDENCY-FREE: the only import is the kit's own `collectDeviceContext`. `networkType` and
437
+ * `appBuild` are HOST-INJECTED — there is no dependency-free RN core signal for either, so the
438
+ * envelope simply omits them when the host does not pass them (no forced peer dependency).
439
+ */
440
+
441
+ /**
442
+ * The context stamped onto every event. `device` is always present (from
443
+ * `collectDeviceContext`); every scalar is optional and OMITTED when the host does not supply it.
444
+ */
445
+ type ContextEnvelope = {
446
+ /** The privacy-neutral device snapshot (reused from `collectDeviceContext`). */
447
+ device: DeviceContext;
448
+ /** Correlation id for this app-open / flow (caller-supplied). */
449
+ sessionId?: string;
450
+ /** Host app version, e.g. "1.4.2" (mirrors `device.appVersion`; host-injected). */
451
+ appVersion?: string;
452
+ /** Host native build number, e.g. "412" (from `expo-constants` `nativeBuildVersion`). */
453
+ appBuild?: string;
454
+ /** Host connectivity signal, e.g. "wifi" | "cellular" (from `@react-native-community/netinfo`). */
455
+ networkType?: string;
456
+ };
457
+ /** Host-injected inputs for {@link buildContextEnvelope}. All optional; each is omitted when absent. */
458
+ type ContextEnvelopeInput = {
459
+ sessionId?: string;
460
+ appVersion?: string;
461
+ appBuild?: string;
462
+ networkType?: string;
463
+ };
464
+ /**
465
+ * Build a fresh context envelope. Reuses `collectDeviceContext()` for the device block and layers
466
+ * the host-injected scalars on top. `appVersion` is additionally merged onto `device.appVersion`
467
+ * when the device block lacks it (mirroring how `useSessionStart` back-fills the host version).
468
+ *
469
+ * Returns a NEW object on every call (no shared mutable reference), so a caller can hold or mutate
470
+ * the result without leaking into the next envelope. Never throws — `collectDeviceContext` is
471
+ * itself guarded, and the rest is plain assignment.
472
+ */
473
+ declare const buildContextEnvelope: (input?: ContextEnvelopeInput) => ContextEnvelope;
474
+
475
+ /**
476
+ * eventQueue — the OFFLINE-FIRST, persistent transport buffer for client analytics events.
477
+ *
478
+ * The existing `reportClientEvents` is a blind fire-and-forget POST: it returns `void`, has no
479
+ * success signal, and drops events when the network is down. The analytics data story depends on
480
+ * NEVER losing an event offline, so this queue adds the missing durability layer on top of the
481
+ * same `POST {serverUrl}/v1/events` contract:
482
+ *
483
+ * • Persists pending events to the host-injected `WireOnboardingStorage` (survives app kills).
484
+ * • Batches them into one request body `{ events: [...] }`.
485
+ * • Owns its OWN awaited `fetch` that reads `res.ok` — the only way to drive retry + dequeue,
486
+ * since `reportClientEvents` cannot ack. A 2xx dequeues the batch; a non-ok / rejected / thrown
487
+ * response keeps it and schedules an exponential backoff retry.
488
+ * • Flushes on `enqueue`, on an explicit `flush()`, and on `notifyOnline()` (host reconnect).
489
+ * • Caps the buffer (drop-OLDEST under pressure) and de-dups identical pending events.
490
+ * • Stamps the current context envelope (device + host scalars) onto every event before send.
491
+ *
492
+ * FIRE-AND-FORGET (load-bearing): `enqueue` returns immediately and NEVER throws into the UI. A
493
+ * missing `fetch`, a hung/broken storage, a rejecting network, or a JSON error is swallowed and
494
+ * degrades gracefully — analytics must never be able to break the app. In-memory fallback covers
495
+ * the no-storage case (survives re-renders, not app kills).
496
+ *
497
+ * DEPENDENCY-FREE: no network-detection or persistence library. Connectivity is host-driven via
498
+ * `notifyOnline()`; persistence is the host-injected AsyncStorage-compatible subset.
499
+ */
500
+
501
+ /** Envelope source: a fixed envelope or a provider evaluated at enqueue time (fresh network type). */
502
+ type EnvelopeSource = ContextEnvelope | (() => ContextEnvelope | undefined);
503
+ /** Options for {@link createEventQueue}. Only `target` is conceptually required to actually send. */
504
+ type EventQueueOptions = {
505
+ /** Where to POST — the tenant transport (`serverUrl` + `apiKey`), same as `WireOnboardingConfig`. */
506
+ target: ClientEventTarget | undefined;
507
+ /**
508
+ * Host persistence (AsyncStorage-compatible subset). When omitted, the queue runs in the
509
+ * documented DEGRADED in-memory mode — it survives re-renders but not an app kill.
510
+ */
511
+ storage?: WireOnboardingStorage;
512
+ /** Tenant/app id used to namespace the default storage key (`wireai:evtq:<appId>`). */
513
+ appId?: string;
514
+ /** Explicit storage key override (wins over the `appId`-derived default). */
515
+ storageKey?: string;
516
+ /** The context envelope stamped onto every event before send (device + host scalars). */
517
+ envelope?: EnvelopeSource;
518
+ /** Max pending events; enqueuing past this DROPS THE OLDEST first (default 200). */
519
+ maxSize?: number;
520
+ /** Events per POST batch (default 20). */
521
+ batchSize?: number;
522
+ /** First retry delay in ms; doubles each failed attempt (default 1000). */
523
+ baseBackoffMs?: number;
524
+ /** Backoff ceiling in ms (default 30000). */
525
+ maxBackoffMs?: number;
526
+ /** Max AUTOMATIC backoff retries before pausing (default 6); `notifyOnline()`/`flush()` re-arm it. */
527
+ maxRetries?: number;
528
+ };
529
+ /** The queue's public surface. `enqueue` is fire-and-forget (returns immediately, never throws). */
530
+ type EventQueue = {
531
+ /** Buffer one event (envelope-stamped), persist, and schedule a flush. Never throws. */
532
+ enqueue(event: ClientEvent): void;
533
+ /** Attempt an immediate drain of the pending buffer. Fire-and-forget. */
534
+ flush(): void;
535
+ /** Host reconnect signal: reset backoff and drain immediately. Fire-and-forget. */
536
+ notifyOnline(): void;
537
+ /** Current pending (in-memory) count. */
538
+ size(): number;
539
+ };
540
+ /**
541
+ * Create an offline-first event queue. Loads any persisted backlog on creation so a
542
+ * killed-and-relaunched app resumes where it left off. Returns the {@link EventQueue} surface.
543
+ */
544
+ declare const createEventQueue: (options: EventQueueOptions) => EventQueue;
545
+
546
+ export { type AnalyticsEvent as A, type ClientEvent as C, type DeviceContext as D, type EnvelopeSource as E, type OnboardingResult as O, type StepValidator as S, WIRE_ONBOARDING_EVENTS as W, type ClientEventTarget as a, type ClientEventType as b, type ContextEnvelope as c, type ContextEnvelopeInput as d, type EventQueue as e, type EventQueueOptions as f, type WireOnboardingEventName as g, buildContextEnvelope as h, createEventQueue as i, reportClientEvents as j, type WireOnboardingProps as k, type WireOnboardingConfig as l, makeSessionId as m, type OnboardingEvent as n, type OnboardingCopy as o, type DeviceFormFactor as p, type OnboardingProgress as q, reportClientEvent as r, collectDeviceContext as s, toAnalyticsEvent as t };