@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.
- package/AGENTS.md +82 -40
- package/CHANGELOG.md +17 -2
- package/INTEGRATION_PROMPT.md +8 -8
- package/README.md +38 -38
- package/dist/analytics/index.d.mts +92 -0
- package/dist/analytics/index.d.ts +92 -0
- package/dist/analytics/index.js +439 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/index.mjs +426 -0
- package/dist/analytics/index.mjs.map +1 -0
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/decision-BzbiKwk3.d.mts +79 -0
- package/dist/decision-plDEOCkt.d.ts +79 -0
- package/dist/eventQueue-CxKi7Qd5.d.mts +546 -0
- package/dist/eventQueue-rV1dtJJR.d.ts +546 -0
- package/dist/index.d.mts +162 -418
- package/dist/index.d.ts +162 -418
- package/dist/index.js +407 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +402 -5
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +2 -1
- package/dist/questionnaire/index.d.ts +2 -1
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +5 -40
- package/dist/reviews/index.d.ts +5 -40
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/transport-BeO_Brcu.d.mts +40 -0
- package/dist/transport-DLpd1v5_.d.ts +40 -0
- package/dist/{decision-Cl8OFYzu.d.mts → types-A6pTxIZV.d.mts} +1 -77
- package/dist/{decision-CFvGY6nP.d.ts → types-BhpXJGlg.d.ts} +1 -77
- package/llms.txt +7 -7
- package/metro/index.d.ts +3 -3
- package/metro/index.js +3 -3
- package/package.json +15 -1
- package/src/analytics/contextEnvelope.ts +72 -0
- package/src/analytics/eventQueue.ts +331 -0
- package/src/analytics/index.ts +47 -0
- package/src/analytics/screenTracking.ts +122 -0
- package/src/analytics/useScreenTracking.ts +48 -0
- package/src/coachmarks/index.ts +2 -2
- package/src/features/WireFeaturesProvider.tsx +1 -1
- package/src/features/index.ts +2 -2
- package/src/index.ts +18 -1
- package/src/questionnaire/index.ts +2 -2
- package/src/reviews/index.ts +2 -2
- package/src/session-analytics/index.ts +20 -0
- package/src/session-analytics/lifecycle.ts +236 -0
- package/src/session-analytics/reportSessionStart.ts +27 -7
- package/src/session-analytics/useLifecycleEvents.ts +184 -0
- package/src/showcase/index.ts +2 -2
- package/src/types.ts +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { c as ReviewDecision, i as ReviewSubmission, e as RatingRoute } from './types-BhpXJGlg.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* decision.ts — the pure decision logic for the review gate.
|
|
5
|
+
*
|
|
6
|
+
* `decideReview` is the AI seam, mirroring `selectTourSteps` exactly: ONE function that
|
|
7
|
+
* turns an optional injected decision into the live verdict. No injected decision → the
|
|
8
|
+
* local rules stand; a server decision present → it OVERRIDES them. Building the seam now
|
|
9
|
+
* is the whole cost of being AI-ready: today the host passes local rules only; later the
|
|
10
|
+
* Wire server computes `{fire, reason}` from the onboarding learnings and the host passes
|
|
11
|
+
* it straight in — zero app changes.
|
|
12
|
+
*
|
|
13
|
+
* // v1 (local rules only)
|
|
14
|
+
* const verdict = decideReview(evaluateGate(rules, signals));
|
|
15
|
+
*
|
|
16
|
+
* // AI on (server-provided decision wins)
|
|
17
|
+
* const verdict = decideReview(evaluateGate(rules, signals), serverDecision);
|
|
18
|
+
*
|
|
19
|
+
* `evaluateGate`, `routeRating`, and `buildReviewSubmission` are pure so the gate logic
|
|
20
|
+
* is verifiable in isolation (the kit has no test runner; correctness lives in these).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The seam. `decision` absent → the local verdict stands; present → it overrides.
|
|
25
|
+
* Same contract philosophy as `selectTourSteps(catalog, selection?)`.
|
|
26
|
+
*/
|
|
27
|
+
declare const decideReview: (local: ReviewDecision, decision?: ReviewDecision) => ReviewDecision;
|
|
28
|
+
/** The signals the local rules evaluate against (read from injected sync storage + host). */
|
|
29
|
+
interface GateSignals {
|
|
30
|
+
/** Sessions observed on this device (incl. the current one). */
|
|
31
|
+
sessions: number;
|
|
32
|
+
/** Tracked app events observed (host-owned). */
|
|
33
|
+
events: number;
|
|
34
|
+
/** Epoch ms of the last time the gate was shown, or null if never. */
|
|
35
|
+
lastShownAt: number | null;
|
|
36
|
+
/** Whether the once-per-version gate is already satisfied. */
|
|
37
|
+
seen: boolean;
|
|
38
|
+
/** Current epoch ms (injected so the function stays pure). */
|
|
39
|
+
now: number;
|
|
40
|
+
}
|
|
41
|
+
/** The resolved local rules (defaults applied). */
|
|
42
|
+
interface GateRules {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
minSessions: number;
|
|
45
|
+
minEvents: number;
|
|
46
|
+
cooldownDays: number;
|
|
47
|
+
oncePerVersion: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** Fill a partial `ReviewConfig` with the rule defaults. */
|
|
50
|
+
declare const resolveRules: (config: {
|
|
51
|
+
enabled?: boolean;
|
|
52
|
+
minSessions?: number;
|
|
53
|
+
minEvents?: number;
|
|
54
|
+
cooldownDays?: number;
|
|
55
|
+
oncePerVersion?: boolean;
|
|
56
|
+
}) => GateRules;
|
|
57
|
+
/**
|
|
58
|
+
* Deterministic v1 local decision. Order: disabled → once-gate → min-sessions →
|
|
59
|
+
* min-events → cooldown → fire. Reasons mirror the server evaluator's vocabulary.
|
|
60
|
+
*/
|
|
61
|
+
declare const evaluateGate: (rules: GateRules, s: GateSignals) => ReviewDecision;
|
|
62
|
+
/**
|
|
63
|
+
* Route a rating. 5 stars → the native store review; 1-4 → the feedback form. The
|
|
64
|
+
* sentiment question itself is neutral — this is the ONLY place 5 is treated specially,
|
|
65
|
+
* never a button labeled "rate us 5 stars" (see the store-policy notes in the README).
|
|
66
|
+
*/
|
|
67
|
+
declare const routeRating: (stars: number) => RatingRoute;
|
|
68
|
+
/** Build the `POST /v1/reviews` body. Feedback text/contact belong ONLY here, never in events. */
|
|
69
|
+
declare const buildReviewSubmission: (input: {
|
|
70
|
+
stars: number;
|
|
71
|
+
feedbackText?: string;
|
|
72
|
+
suggestion?: string;
|
|
73
|
+
contact?: string;
|
|
74
|
+
sessionId?: string;
|
|
75
|
+
source?: string;
|
|
76
|
+
meta?: Record<string, unknown>;
|
|
77
|
+
}) => ReviewSubmission;
|
|
78
|
+
|
|
79
|
+
export { type GateRules as G, type GateSignals as a, buildReviewSubmission as b, routeRating as c, decideReview as d, evaluateGate as e, resolveRules as r };
|
|
@@ -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.mjs';
|
|
5
|
+
import { b as WireOnboardingStorage } from './types-CMuOexw0.mjs';
|
|
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 };
|