@wireai/activation 0.13.6-next.0 → 0.13.6

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 CHANGED
@@ -60,7 +60,7 @@ subpaths are optional secondary feature modules; import one only if you use it.
60
60
  | Prop | Type | Required | Notes |
61
61
  |---|---|---|---|
62
62
  | `config` | `WireOnboardingConfig` | yes | A2A transport + tenant. See the `config` fields table below. |
63
- | `onComplete` | `(result: { answers, raw }) => void` | yes | Terminal recap CTA tapped. Persist `answers` via your profile-update path, then navigate on. |
63
+ | `onComplete` | `(result: OnboardingResult) => void` | yes | Terminal recap CTA tapped. Persist `answers` via your profile-update path, then navigate on. `result` always has `answers` + `raw`; since 0.13.6 it also carries `plan` (the backend's onboarding plan, AI path only) and `variant` (the assigned experiment arm) when the backend sent them. Both keys are ABSENT otherwise, so an integration written before 0.13.6 sees no change. The kit does not interpret either one: validate `plan` before applying it. |
64
64
  | `theme` | `Partial<OnboardingTheme>` | no | Colors + font family names + radius/spacing, deep-merged over a neutral default. For dark/light, pass a different theme per app theme-state. |
65
65
  | `components` | `WireAIComponent[]` | no | Override registered cards (default `onboardingComponents`). |
66
66
  | `illustrations` | `Record<string, ReactNode>` | no | Host artwork for `InterstitialCard`, keyed by name. `{ ...defaultIllustrations, ...myArt }`. |
package/CHANGELOG.md CHANGED
@@ -3,6 +3,81 @@
3
3
  All notable changes to `@wireai/activation` (formerly `wireai-onboarding`).
4
4
  Historical entries below the rename keep the old package name on purpose.
5
5
 
6
+ ## [0.13.6] - 2026-08-09
7
+
8
+ Two things the backend already decides, handed to the host instead of guessed at. Both ride the
9
+ same boundary: the kit reads a payload the server sent, carries it out through `onComplete`, and
10
+ stops. It does not interpret either one, does not validate their contents, does not log them and
11
+ never attaches them to an analytics event.
12
+
13
+ The `plan` half of this release was published as `0.13.6-next.0` on 2026-08-08 with no changelog
14
+ entry. This entry is that debt paid, plus the `variant` half, released together as 0.13.6.
15
+
16
+ ### Added
17
+
18
+ - **`OnboardingResult.plan`, the backend's onboarding plan reaching the host uninterpreted.**
19
+ On the AI path the server appends a SECOND A2A DataPart to the turn it finishes on,
20
+ `{ kind: "onboarding_plan", plan: {...} }`, alongside the component envelope the renderer already
21
+ consumes. A new pure reader, `src/utils/readPlan.ts`, lifts it off the thread and `handleFinish`
22
+ puts it on the result. The static (non-AI) flow carries no plan and is unchanged.
23
+
24
+ The one thing the reader is allowed to reject is a structural fact: a plan is an object. A scalar,
25
+ an array, a misspelled marker or a missing payload yields `undefined` and the host falls back to
26
+ exactly its pre-plan behaviour. No field is required and no unknown field is stripped, because a
27
+ kit-side schema would make the kit the thing that rejects a plan the host could have used, and the
28
+ server's lenient path adds markers the kit must neither read nor strip. **The host must validate
29
+ the payload before applying it.**
30
+
31
+ **It finds the plan by MARKER, never by position**, and that is the load-bearing part of the read.
32
+ The kit walks the thread newest turn first and, inside a turn, returns the first part whose `kind`
33
+ is `onboarding_plan` and whose payload is an object. Anything else is skipped rather than treated
34
+ as a terminator, so a thread carrying two plans resolves to the last one, the turn the flow
35
+ completes on, and a malformed part can never shadow a good one. Position would be the wrong tool
36
+ here: the SDK collects a task's parts across agent messages LATEST-FIRST, so on a task that
37
+ carries history a previous turn's envelope can sit in the same array as this turn's plan. The
38
+ marker makes that harmless, and it is also why nothing in this pipe promises wire order.
39
+
40
+ It needs an SDK that surfaces the DataParts PAST the first one. The first DataPart is the
41
+ component envelope the renderer already consumes, and it is the one the SDK turns into the string
42
+ it returns for the turn, so the plan is one of the parts left behind after it.
43
+ `wireai-rn@0.2.5-next.1` hands those over on `Message.dataParts`, and a turn that carried nothing
44
+ but the envelope leaves the member ABSENT rather than an empty array (checked against the
45
+ published tarball, not against a paraphrase of it). The peer range here is unchanged at `>=0.2.3`,
46
+ so this degrades rather than breaks: an older SDK, `0.2.4` included, sets no `dataParts`, the kit
47
+ reads no plan, and the host lands on exactly the path a tenant that sends none already takes.
48
+
49
+ - **`OnboardingResult.variant`, the experiment arm the backend assigned, surfaced to the host.**
50
+ The server rides the arm key on the render envelope at `props.progress.variant`, a sibling of
51
+ `step` / `total` / `key` / `slot_id`. The kit had no way to hand it over, so a host that wanted to
52
+ branch on the arm had to INFER it from the shape of the flow (counting answers against a
53
+ hard-coded maximum), an inference that goes silently wrong the day the server changes a flow
54
+ length.
55
+
56
+ `readProgress` whitelists `variant` exactly the way it already whitelists `slot_id`: a non-string
57
+ or absent key reads as "no experiment", never as an error. `OnboardingFlow` latches the arm at
58
+ first sight into a ref that is never cleared, because the assignment is sticky per session but
59
+ only rides the render envelope, and a later card that omits it must not un-assign it.
60
+
61
+ It is also available DURING the flow: the `turn` event carries `variant` from the first card that
62
+ declares one onwards, reusing an event hosts already subscribe to instead of adding a prop for a
63
+ scalar. What an arm key MEANS is the host's decision; the kit does not map it to a name, brand it,
64
+ default it or log it. **It is deliberately never attached to an analytics event either**, on
65
+ either surface, so a host that wants arm-segmented funnels has to put the arm on its own events.
66
+
67
+ ### Unchanged
68
+
69
+ - **Both keys are byte-identical no-ops when the backend sends nothing.** `plan` and `variant` are
70
+ each set on the result ONLY when one was actually seen, so a run without them leaves
71
+ `Object.keys(result)` as `["answers", "raw"]` and `"plan" in result` / `"variant" in result`
72
+ false. Setting a key to `undefined` would pass an `=== undefined` check while breaking every host
73
+ deep-equality assertion, so the canaries assert the KEY SET, not the value. The same rule governs
74
+ the `turn` event.
75
+ - **Neither read can throw.** Both sit on the single code path to `onComplete`, where a throw would
76
+ cost the user the completion of an onboarding they already finished. Every read is runtime-guarded
77
+ and `readPlan` declares its own structural type rather than casting: an `as` cast silences the
78
+ compiler in both directions, including the day the published SDK type disagrees.
79
+ - No new runtime dependency, no export renamed or removed, no change to the exports map.
80
+
6
81
  ## [0.13.5] - 2026-08-03
7
82
 
8
83
  Two halves of one idea: stop finding out about a broken events integration from a funnel report.
@@ -866,7 +941,11 @@ true, and pins the wire with tests.
866
941
  - Version reset to `0.1.0` to mark the start of the new package line. The GitHub repository
867
942
  stays `chohra-med/wireai-onboarding`.
868
943
 
869
- ## [Unreleased]
944
+ ## [0.9.0] — 2026-07-17
945
+
946
+ *This section carried the title `[Unreleased]` until 2026-08-09, long after it shipped. Everything
947
+ below went out in 0.9.0; the three analytics/device items it also held belong to 0.8.0 and have been
948
+ moved to their own section under this one.*
870
949
 
871
950
  ### Added: `fetchReviewDecision` — the kit now owns the review firing decision (live user harm)
872
951
 
@@ -1025,6 +1104,11 @@ All four from Malik's on-device test, 2026-07-16. The **review** gate's chrome i
1025
1104
  button. The 5-star → native store review route (`routeRating`) is untouched.
1026
1105
  - `theme/mergeThemeOver` extracts the helper both gates had copy-pasted.
1027
1106
 
1107
+ ## [0.8.0] — 2026-07-15
1108
+
1109
+ *Also mis-filed under `[Unreleased]` until 2026-08-09. These three items shipped in 0.8.0, one
1110
+ release before everything in the section above.*
1111
+
1028
1112
  ### Added: "device" is now fully automatic — auto-minted, persisted per-install `device_key`
1029
1113
 
1030
1114
  - The analytics façade (`createAnalytics`) now auto-mints a stable, NON-PII per-install `device_key`
@@ -1058,7 +1142,7 @@ All four from Malik's on-device test, 2026-07-16. The **review** gate's chrome i
1058
1142
  - Backward compatible: no existing export renamed or removed; zero new dependencies. The one
1059
1143
  behavior change is intentional — events now carry an auto `device_key` when the host omits one.
1060
1144
 
1061
- ## [0.7.0] — Unreleased
1145
+ ## [0.7.0] — 2026-07-15
1062
1146
 
1063
1147
  ### Added: `WireUserContext` — one rich user-context object → every event's `user_context`
1064
1148
 
@@ -1439,7 +1523,7 @@ All four from Malik's on-device test, 2026-07-16. The **review** gate's chrome i
1439
1523
  `showcaseColorsFromTheme(theme, accent)` (new `src/showcase/showcaseColors.ts`) with a
1440
1524
  regression test asserting the primary button is the accent and never the background.
1441
1525
 
1442
- ## [0.3.0] — Unreleased
1526
+ ## [0.3.0] — 2026-07-12
1443
1527
 
1444
1528
  > Pending release. `0.2.1` is already published on npm (coachmarks + showcase, no
1445
1529
  > identity API), so this identity surface — alongside the unpublished `/reviews`
package/README.md CHANGED
@@ -231,7 +231,7 @@ import { WireOnboarding } from "@wireai/activation";
231
231
  | Prop | Type | Notes |
232
232
  |---|---|---|
233
233
  | `config` | `{ apiKey, serverUrl, appId, metadata?, appVersion? }` | A2A transport; the key resolves the tenant server-side. `appVersion` is host-injected (e.g. `Constants.expoConfig?.version`) and forwarded for analytics segmentation. **Required.** |
234
- | `onComplete` | `(result: { answers, raw }) => void` | Fires when the user taps the terminal recap's CTA. **Required.** |
234
+ | `onComplete` | `(result: OnboardingResult) => void` | Fires when the user taps the terminal recap's CTA. `result` always carries `answers` and `raw`. Since 0.13.6 it may also carry `plan` (the backend's onboarding plan, AI path only) and `variant` (the experiment arm the backend assigned, when the tenant runs one). Each of those two keys is set ONLY when the backend actually sent one, so a run without them returns exactly the object earlier versions returned: `"plan" in result` and `"variant" in result` both stay false. The kit does not interpret either: **validate `plan` before you apply it.** **Required.** |
235
235
  | `theme` | `Partial<OnboardingTheme>` | Brand colors/fonts/radius/spacing, deep-merged over a neutral default. |
236
236
  | `illustrations` | `Record<string, ReactNode>` | App artwork for `InterstitialCard`, keyed by name. |
237
237
  | `icons` | `Record<string, ReactNode>` | Your own icon nodes, keyed by the semantic name the AI emits (`{ instagram: <BrandIg/> }`). Checked first, so use it to put your brand mark on a choice card, to add names the vocabulary does not carry, or to get icons at all without installing `@expo/vector-icons`. Anything you leave out falls back to that optional peer, then to no icon. Never a crash. See [Icons](#icons). |
@@ -241,7 +241,7 @@ import { WireOnboarding } from "@wireai/activation";
241
241
  | `onError` | `(err) => void` | Backend error or first-card timeout, fired **after retries are exhausted**, not on the first failure. When `fallbackFlow` is not supplied, the host owns recovery (e.g. route to a static flow). With neither, the kit shows an inline retry. `fallbackFlow` takes precedence over this. |
242
242
  | `fallbackFlow` | `ReactNode` | Your predefined STATIC onboarding, rendered in place once the AI flow fails and retries are exhausted. **Takes precedence over `onError`.** This is the "it can never break your onboarding" guarantee: pass the same flow you shipped before adding Wire AI and the user always keeps moving. |
243
243
  | `maxRetries` | `number` | Consecutive failures to auto-retry before degrading to `fallbackFlow`/`onError`. Default `1` (one silent retry, then degrade). `0` degrades on the first failure. Budget it deliberately: each retry restarts the flow, so with `startTimeoutMs` at its `15000` default the default `maxRetries` puts 30s of loader in front of a user before they see the fallback. A retry also sends a skip sentinel, so the user loses one question and a retry is never recorded as their first answer. |
244
- | `onEvent` | `(e: OnboardingEvent) => void` | `started` / `turn` / `error` recover per-turn analytics since the kit owns the loop. |
244
+ | `onEvent` | `(e: OnboardingEvent) => void` | `started` / `resumed` / `turn` / `error` / `retry` / `fallback` / `permission`, the full union in `OnboardingEvent`. Recover per-turn analytics since the kit owns the loop. See the note under the helpers table. |
245
245
  | `copy` | `Partial<OnboardingCopy>` | Localize the kit's built-in English loader/completion strings. |
246
246
  | `approxScreens` | `number` | Approx total screens — paces the bar as `step/total` (capped, never shown). A backend `progress.total` (the screen budget, sent every turn incl. init) wins, so usually unnecessary. |
247
247
  | `components` | `WireAIComponent[]` | Override the registered cards (defaults to `onboardingComponents`). |
@@ -271,7 +271,7 @@ You rarely hand-roll config, gating, analytics, or attribution — the kit ships
271
271
  | `themeFromBrand({ primary })` · `defaultIllustrations` | One-color theme; dependency-free fallback artwork. |
272
272
  | `motionSpec` | The motion constants behind every kit animation (durations, springs, the design ease). Read-only for hosts. `setMotionDurationScale` / `getMotionDurationScale` are QA tools for the playground's animation-speed knob: never call them in production code. |
273
273
 
274
- > **`onEvent`** fires `started` / `resumed` / `turn` / `error` / `retry` / `fallback` (the kit owns the loop, so this is how you recover per-turn analytics). `resumed` fires INSTEAD of `started` when a persisted session was restored (see the `storage` prop) — don't count both as flow starts. Completion is signalled via `onComplete`, not `onEvent` — log `WIRE_ONBOARDING_EVENTS.completed` there. `started` and `resumed` also carry a `contextId` (the A2A session id): capture it if you might bind a `userId` after the flow finishes (see [User identity](#user-identity)).
274
+ > **`onEvent`** fires `started` / `resumed` / `turn` / `error` / `retry` / `fallback` / `permission` (the kit owns the loop, so this is how you recover per-turn analytics). `resumed` fires INSTEAD of `started` when a persisted session was restored (see the `storage` prop) — don't count both as flow starts. Since 0.13.6 the `turn` event also carries `variant`, the experiment arm the backend assigned, from the first card that declares one onwards; that is how you read the arm DURING the flow rather than waiting for `onComplete`. The key is absent when no experiment is running, and the kit never attaches the arm to an analytics event, so arm-segmented funnels are the host's job. Completion is signalled via `onComplete`, not `onEvent` — log `WIRE_ONBOARDING_EVENTS.completed` there. `started` and `resumed` also carry a `contextId` (the A2A session id): capture it if you might bind a `userId` after the flow finishes (see [User identity](#user-identity)).
275
275
 
276
276
  **Integrating with an AI agent?** See [`INTEGRATION_PROMPT.md`](./INTEGRATION_PROMPT.md) (copy-paste prompt for Claude Code) and [`llms.txt`](./llms.txt). Fastest of all: the `wire-rn-integration` Claude skill.
277
277
 
@@ -1310,49 +1310,47 @@ then *improve* Wire AI, not just eyeball it.
1310
1310
 
1311
1311
  ### MCP server (`mcp/`)
1312
1312
 
1313
- A local Model Context Protocol server (`@wireai/mcp`, stdio, TypeScript). It is a
1314
- repo-local dev tool **not** published to npm (excluded from the package `files`
1315
- allowlist). Build it in place:
1313
+ A Model Context Protocol server (`@wireai/mcp`, TypeScript) that exposes the whole
1314
+ onboarding control plane as agent tools. It runs two ways, same tools either way:
1316
1315
 
1317
- ```bash
1318
- cd mcp && npm install && npm run build
1319
- ```
1320
-
1321
- Then add it to your agent client's MCP config:
1316
+ 1. **Hosted, and this is the default.** `https://wireai-mcp.fly.dev/mcp` over Streamable
1317
+ HTTP. You authenticate per request with an `Authorization: Bearer` header, so there is
1318
+ no build step, no login step, and nothing on disk.
1319
+ 2. **Local stdio.** `cd mcp && npm install && npm run build`, then point your client at
1320
+ `mcp/dist/index.js`. Auth is a one-time `node dist/index.js login`, or raw keys via
1321
+ `WIREAI_SERVER_URL` / `WIREAI_API_KEY` / `WIREAI_ADMIN_KEY`. This package is repo-local
1322
+ and **not** published to npm (it is excluded from the kit's `files` allowlist).
1322
1323
 
1323
1324
  ```json
1324
1325
  {
1325
1326
  "mcpServers": {
1326
1327
  "wireai": {
1327
- "command": "node",
1328
- "args": ["<abs-path>/@wireai/activation/mcp/dist/index.js"],
1329
- "env": {
1330
- "WIREAI_SERVER_URL": "https://<your-backend>.fly.dev",
1331
- "WIREAI_API_KEY": "wai_<tenant-key>",
1332
- "WIREAI_ADMIN_KEY": "wai_admin_<operator-key>"
1333
- }
1328
+ "type": "http",
1329
+ "url": "https://wireai-mcp.fly.dev/mcp",
1330
+ "headers": { "Authorization": "Bearer <your-token>" }
1334
1331
  }
1335
1332
  }
1336
1333
  }
1337
1334
  ```
1338
1335
 
1339
- Tools (P1 is read-biased; the only writes are the existing register/update):
1336
+ **Three credential classes, and you probably want the third.** A tenant `wai_` key reads
1337
+ your own app's funnel and files learning reports. The operator admin key reaches the
1338
+ cross-tenant management surface. A **client console token** (minted from a Wire console
1339
+ session) drives your own apps over the `/v1/me/*` console routes, which means a customer
1340
+ runs the full management surface from an agent *without* holding the platform admin key.
1340
1341
 
1341
- | Tool | Auth key | Does |
1342
- |---|---|---|
1343
- | `wireai_get_integration_guide` | none | the current integration recipe |
1344
- | `wireai_list_components` | none | the card vocabulary |
1345
- | `wireai_get_insights` | tenant `WIREAI_API_KEY` | **read** the funnel brief (`/v1/insights.md`), app resolved from the key; a valid "no data yet" brief when empty |
1346
- | `wireai_get_onboarding_config` | admin `WIREAI_ADMIN_KEY` | **read** the live generation config (context, first question, allowed components) |
1347
- | `wireai_register_onboarding_app` | admin `WIREAI_ADMIN_KEY` | create an app + get its key |
1348
- | `wireai_list_onboarding_apps` | admin `WIREAI_ADMIN_KEY` | list apps |
1349
- | `wireai_get_onboarding_app` | admin `WIREAI_ADMIN_KEY` | one app's raw record |
1350
- | `wireai_update_onboarding_app` | admin `WIREAI_ADMIN_KEY` | patch config, no release |
1351
- | `wireai_get_onboarding_analytics` | admin `WIREAI_ADMIN_KEY` | funnel JSON |
1352
- | `wireai_list_reviews` | admin `WIREAI_ADMIN_KEY` | **read** in-app reviews across tenants (stars, feedback, contact) |
1353
-
1354
- Least privilege: for the read-only loop, hand the agent just `WIREAI_SERVER_URL` +
1355
- `WIREAI_API_KEY` (tenant) and skip the admin key. Full detail: [`mcp/README.md`](./mcp/README.md).
1342
+ **Where the tool list lives.** Ask the server: a connected client's `tools/list` is the
1343
+ only copy that is current by construction, and it is already scoped to the credential you
1344
+ presented. `wireai_get_integration_guide` needs no auth at all and returns the recipe.
1345
+ Inside the repo the written table is `mcp/README.md`, and `mcp/src/toolAccess.test.ts`
1346
+ re-derives it from the handlers and goes red when the two disagree.
1347
+
1348
+ This section deliberately does not restate that table. It used to, and the copy drifted to
1349
+ 10 tools of 21 with a "the only writes are register/update" line the code had stopped
1350
+ honouring, because nothing gated it. A duplicated list with no test on it goes stale.
1351
+
1352
+ Least privilege still applies: for the read-only improve loop, hand the agent a tenant
1353
+ credential and skip the admin key entirely.
1356
1354
 
1357
1355
  ### `wire-ai` skill (`.claude/skills/wire-ai`)
1358
1356
 
@@ -1,6 +1,6 @@
1
1
  export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-j5gFfJhK.mjs';
2
- import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-Cs3lweFZ.mjs';
3
- export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-Cs3lweFZ.mjs';
2
+ import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-DngW-QoD.mjs';
3
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-DngW-QoD.mjs';
4
4
  import { W as WireOnboardingStorage } from '../types-BpwiRpA8.mjs';
5
5
  import '../types-Cju-1_jT.mjs';
6
6
  import '../types-BKfpdZzX.mjs';
@@ -1,6 +1,6 @@
1
1
  export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-B_0SgCBe.js';
2
- import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-DD6dKB0i.js';
3
- export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-DD6dKB0i.js';
2
+ import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-C5976akx.js';
3
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-C5976akx.js';
4
4
  import { W as WireOnboardingStorage } from '../types-BpwiRpA8.js';
5
5
  import '../types-h2BZvl1t.js';
6
6
  import '../types-BKfpdZzX.js';
@@ -484,6 +484,16 @@ type OnboardingResult = {
484
484
  * the host's decision, not the kit's.
485
485
  */
486
486
  plan?: unknown;
487
+ /**
488
+ * The experiment ARM the backend assigned this session, when it is running one. Absent when
489
+ * the tenant runs no experiment, which is the common case, and absent is NOT an error.
490
+ * The kit does NOT interpret it, does not log it, does not brand it and never attaches it
491
+ * to an analytics event. What an arm key MEANS is the host's decision.
492
+ *
493
+ * Like {@link plan}, the KEY is set only when an arm was actually seen, so a run with no
494
+ * experiment leaves the result byte-identical to what every host already reads.
495
+ */
496
+ variant?: string;
487
497
  };
488
498
  /**
489
499
  * Lifecycle events emitted as the flow runs, for host-side analytics. The kit owns
@@ -495,7 +505,11 @@ type OnboardingResult = {
495
505
  * - `resumed`: a persisted session was restored after an app kill (fires INSTEAD of
496
506
  * `started`, so host funnels don't double-count the same session). Also carries
497
507
  * `contextId`. Requires the `storage` prop.
498
- * - `turn`: a new assistant card arrived (`step` = 1-based index of cards seen).
508
+ * - `turn`: a new assistant card arrived (`step` = 1-based index of cards seen). Carries
509
+ * `variant`, the backend's experiment arm for this session, from the first card that
510
+ * declared one onwards — this is how the arm is available DURING the flow rather than
511
+ * only at completion. The key is absent whenever no arm has been seen, which is every
512
+ * turn of every tenant running no experiment.
499
513
  * - `error`: the backend errored or the first-card watchdog timed out.
500
514
  * - `retry`: a transient failure is being auto-retried (`attempt` = 1-based).
501
515
  * - `fallback`: retries are exhausted; the kit degraded to the static `fallbackFlow`
@@ -516,6 +530,7 @@ type OnboardingEvent = {
516
530
  type: "turn";
517
531
  step: number;
518
532
  component?: string;
533
+ variant?: string;
519
534
  } | {
520
535
  type: "error";
521
536
  reason: "backend" | "timeout";
@@ -790,6 +805,17 @@ type OnboardingProgress = {
790
805
  slot_id?: string;
791
806
  /** Whether the CURRENT screen may be skipped (backend-marked; default false → no Skip shown). */
792
807
  skippable?: boolean;
808
+ /**
809
+ * The EXPERIMENT ARM the backend assigned this session, when the tenant is running one. Rides the
810
+ * render envelope as a sibling of {@link step}/{@link total}/{@link key}/{@link slot_id}, and is
811
+ * OMITTED entirely for a tenant running no experiment — which is the common case, and is not an
812
+ * error. The assignment is sticky for the session, so a later card that omits it does not
813
+ * un-assign it.
814
+ *
815
+ * The kit does NOT interpret this. It is whitelisted, latched and handed to the host exactly as
816
+ * received; what an arm key MEANS is the host's decision. See {@link OnboardingResult.variant}.
817
+ */
818
+ variant?: string;
793
819
  };
794
820
 
795
821
  /**
@@ -484,6 +484,16 @@ type OnboardingResult = {
484
484
  * the host's decision, not the kit's.
485
485
  */
486
486
  plan?: unknown;
487
+ /**
488
+ * The experiment ARM the backend assigned this session, when it is running one. Absent when
489
+ * the tenant runs no experiment, which is the common case, and absent is NOT an error.
490
+ * The kit does NOT interpret it, does not log it, does not brand it and never attaches it
491
+ * to an analytics event. What an arm key MEANS is the host's decision.
492
+ *
493
+ * Like {@link plan}, the KEY is set only when an arm was actually seen, so a run with no
494
+ * experiment leaves the result byte-identical to what every host already reads.
495
+ */
496
+ variant?: string;
487
497
  };
488
498
  /**
489
499
  * Lifecycle events emitted as the flow runs, for host-side analytics. The kit owns
@@ -495,7 +505,11 @@ type OnboardingResult = {
495
505
  * - `resumed`: a persisted session was restored after an app kill (fires INSTEAD of
496
506
  * `started`, so host funnels don't double-count the same session). Also carries
497
507
  * `contextId`. Requires the `storage` prop.
498
- * - `turn`: a new assistant card arrived (`step` = 1-based index of cards seen).
508
+ * - `turn`: a new assistant card arrived (`step` = 1-based index of cards seen). Carries
509
+ * `variant`, the backend's experiment arm for this session, from the first card that
510
+ * declared one onwards — this is how the arm is available DURING the flow rather than
511
+ * only at completion. The key is absent whenever no arm has been seen, which is every
512
+ * turn of every tenant running no experiment.
499
513
  * - `error`: the backend errored or the first-card watchdog timed out.
500
514
  * - `retry`: a transient failure is being auto-retried (`attempt` = 1-based).
501
515
  * - `fallback`: retries are exhausted; the kit degraded to the static `fallbackFlow`
@@ -516,6 +530,7 @@ type OnboardingEvent = {
516
530
  type: "turn";
517
531
  step: number;
518
532
  component?: string;
533
+ variant?: string;
519
534
  } | {
520
535
  type: "error";
521
536
  reason: "backend" | "timeout";
@@ -790,6 +805,17 @@ type OnboardingProgress = {
790
805
  slot_id?: string;
791
806
  /** Whether the CURRENT screen may be skipped (backend-marked; default false → no Skip shown). */
792
807
  skippable?: boolean;
808
+ /**
809
+ * The EXPERIMENT ARM the backend assigned this session, when the tenant is running one. Rides the
810
+ * render envelope as a sibling of {@link step}/{@link total}/{@link key}/{@link slot_id}, and is
811
+ * OMITTED entirely for a tenant running no experiment — which is the common case, and is not an
812
+ * error. The assignment is sticky for the session, so a later card that omits it does not
813
+ * un-assign it.
814
+ *
815
+ * The kit does NOT interpret this. It is whitelisted, latched and handed to the host exactly as
816
+ * received; what an arm key MEANS is the host's decision. See {@link OnboardingResult.variant}.
817
+ */
818
+ variant?: string;
793
819
  };
794
820
 
795
821
  /**
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
- import { I as WireOnboardingProps, J as WireOnboardingConfig, O as OnboardingResult, K as WirePermissionStatus, P as PermissionStage, L as WirePermissionOutcome, S as StepValidator, M as OnboardingEvent, N as OnboardingCopy, C as ClientEventTarget, Q as DeviceContext, T as PermissionScreenConfig, W as WireUserContext, U as PermissionPlacement, c as ClientEvent, g as EnvelopeSource } from './currentSession-Cs3lweFZ.mjs';
4
- export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, d as ClientEventType, V as DEFAULT_PERMISSION_COPY, X as DeviceFormFactor, D as DeviceKeyStorage, Y as EXTRA_KEY_PREFIX, Z as GENERIC_PERMISSION_COPY, _ as IdentifyOnboardingBinding, $ as IdentifyOnboardingOptions, a0 as IdentityRecord, a1 as IdentitySource, a2 as IdentitySpace, a3 as NOTIFICATIONS_PERMISSION_COPY, a4 as OnboardingProgress, a5 as PermissionScreenCopy, R as ResolveAutoDeviceKeyOptions, a6 as ResolveUserContextOptions, a7 as ResolvedUserContext, a8 as USER_ID_MAX_LENGTH, i as WIRE_ONBOARDING_EVENTS, a9 as WIRE_PERMISSION_EVENTS, j as WireOnboardingEventName, aa as WirePermissionEventName, ab as WirePermissionKind, ac as activationJoinContext, k as analyticsUserIdStorageKey, m as clearPiiFromContext, n as clearUserContext, ad as collectDeviceContext, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, ae as hashEmailFnv1a, af as hostIdentity, ag as hydrateAutoDeviceKey, ah as hydrateDeviceIdentity, ai as identifyOnboarding, aj as isWireScalar, s as looksLikeEmail, t as makeSessionId, ak as mintDeviceId, al as namespaceExtra, am as normalizePermissionStatus, an as permissionEventName, ao as permissionEventProps, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, ap as resetIdentityProvenance, F as resolveAutoDeviceKey, aq as resolveIdentity, ar as resolvePermissionCopy, as as resolveUserContext, at as sanitizeUserId, G as setCurrentSessionId, H as toAnalyticsEvent } from './currentSession-Cs3lweFZ.mjs';
3
+ import { I as WireOnboardingProps, J as WireOnboardingConfig, O as OnboardingResult, K as WirePermissionStatus, P as PermissionStage, L as WirePermissionOutcome, S as StepValidator, M as OnboardingEvent, N as OnboardingCopy, C as ClientEventTarget, Q as DeviceContext, T as PermissionScreenConfig, W as WireUserContext, U as PermissionPlacement, c as ClientEvent, g as EnvelopeSource } from './currentSession-DngW-QoD.mjs';
4
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, d as ClientEventType, V as DEFAULT_PERMISSION_COPY, X as DeviceFormFactor, D as DeviceKeyStorage, Y as EXTRA_KEY_PREFIX, Z as GENERIC_PERMISSION_COPY, _ as IdentifyOnboardingBinding, $ as IdentifyOnboardingOptions, a0 as IdentityRecord, a1 as IdentitySource, a2 as IdentitySpace, a3 as NOTIFICATIONS_PERMISSION_COPY, a4 as OnboardingProgress, a5 as PermissionScreenCopy, R as ResolveAutoDeviceKeyOptions, a6 as ResolveUserContextOptions, a7 as ResolvedUserContext, a8 as USER_ID_MAX_LENGTH, i as WIRE_ONBOARDING_EVENTS, a9 as WIRE_PERMISSION_EVENTS, j as WireOnboardingEventName, aa as WirePermissionEventName, ab as WirePermissionKind, ac as activationJoinContext, k as analyticsUserIdStorageKey, m as clearPiiFromContext, n as clearUserContext, ad as collectDeviceContext, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, ae as hashEmailFnv1a, af as hostIdentity, ag as hydrateAutoDeviceKey, ah as hydrateDeviceIdentity, ai as identifyOnboarding, aj as isWireScalar, s as looksLikeEmail, t as makeSessionId, ak as mintDeviceId, al as namespaceExtra, am as normalizePermissionStatus, an as permissionEventName, ao as permissionEventProps, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, ap as resetIdentityProvenance, F as resolveAutoDeviceKey, aq as resolveIdentity, ar as resolvePermissionCopy, as as resolveUserContext, at as sanitizeUserId, G as setCurrentSessionId, H as toAnalyticsEvent } from './currentSession-DngW-QoD.mjs';
5
5
  import { O as OnboardingTheme } from './types-BKfpdZzX.mjs';
6
6
  export { a as OnboardingButtonStyle, b as OnboardingColors, c as OnboardingFonts, d as OnboardingRadius, e as OnboardingSpacing } from './types-BKfpdZzX.mjs';
7
7
  export { C as CenteredModal, a as CenteredModalHandle, b as CenteredModalProps } from './CenteredModal-C3qQBHsA.mjs';
@@ -1286,6 +1286,8 @@ type PartialProgress = {
1286
1286
  /** The stable per-slot identity, when the backend sends one. See `OnboardingProgress.slot_id`. */
1287
1287
  slot_id?: string;
1288
1288
  skippable?: boolean;
1289
+ /** The experiment arm assigned to this session, when the tenant runs one. See `OnboardingProgress.variant`. */
1290
+ variant?: string;
1289
1291
  };
1290
1292
  declare const readProgress: (response?: WireAIResponse) => PartialProgress;
1291
1293
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
- import { I as WireOnboardingProps, J as WireOnboardingConfig, O as OnboardingResult, K as WirePermissionStatus, P as PermissionStage, L as WirePermissionOutcome, S as StepValidator, M as OnboardingEvent, N as OnboardingCopy, C as ClientEventTarget, Q as DeviceContext, T as PermissionScreenConfig, W as WireUserContext, U as PermissionPlacement, c as ClientEvent, g as EnvelopeSource } from './currentSession-DD6dKB0i.js';
4
- export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, d as ClientEventType, V as DEFAULT_PERMISSION_COPY, X as DeviceFormFactor, D as DeviceKeyStorage, Y as EXTRA_KEY_PREFIX, Z as GENERIC_PERMISSION_COPY, _ as IdentifyOnboardingBinding, $ as IdentifyOnboardingOptions, a0 as IdentityRecord, a1 as IdentitySource, a2 as IdentitySpace, a3 as NOTIFICATIONS_PERMISSION_COPY, a4 as OnboardingProgress, a5 as PermissionScreenCopy, R as ResolveAutoDeviceKeyOptions, a6 as ResolveUserContextOptions, a7 as ResolvedUserContext, a8 as USER_ID_MAX_LENGTH, i as WIRE_ONBOARDING_EVENTS, a9 as WIRE_PERMISSION_EVENTS, j as WireOnboardingEventName, aa as WirePermissionEventName, ab as WirePermissionKind, ac as activationJoinContext, k as analyticsUserIdStorageKey, m as clearPiiFromContext, n as clearUserContext, ad as collectDeviceContext, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, ae as hashEmailFnv1a, af as hostIdentity, ag as hydrateAutoDeviceKey, ah as hydrateDeviceIdentity, ai as identifyOnboarding, aj as isWireScalar, s as looksLikeEmail, t as makeSessionId, ak as mintDeviceId, al as namespaceExtra, am as normalizePermissionStatus, an as permissionEventName, ao as permissionEventProps, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, ap as resetIdentityProvenance, F as resolveAutoDeviceKey, aq as resolveIdentity, ar as resolvePermissionCopy, as as resolveUserContext, at as sanitizeUserId, G as setCurrentSessionId, H as toAnalyticsEvent } from './currentSession-DD6dKB0i.js';
3
+ import { I as WireOnboardingProps, J as WireOnboardingConfig, O as OnboardingResult, K as WirePermissionStatus, P as PermissionStage, L as WirePermissionOutcome, S as StepValidator, M as OnboardingEvent, N as OnboardingCopy, C as ClientEventTarget, Q as DeviceContext, T as PermissionScreenConfig, W as WireUserContext, U as PermissionPlacement, c as ClientEvent, g as EnvelopeSource } from './currentSession-C5976akx.js';
4
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, d as ClientEventType, V as DEFAULT_PERMISSION_COPY, X as DeviceFormFactor, D as DeviceKeyStorage, Y as EXTRA_KEY_PREFIX, Z as GENERIC_PERMISSION_COPY, _ as IdentifyOnboardingBinding, $ as IdentifyOnboardingOptions, a0 as IdentityRecord, a1 as IdentitySource, a2 as IdentitySpace, a3 as NOTIFICATIONS_PERMISSION_COPY, a4 as OnboardingProgress, a5 as PermissionScreenCopy, R as ResolveAutoDeviceKeyOptions, a6 as ResolveUserContextOptions, a7 as ResolvedUserContext, a8 as USER_ID_MAX_LENGTH, i as WIRE_ONBOARDING_EVENTS, a9 as WIRE_PERMISSION_EVENTS, j as WireOnboardingEventName, aa as WirePermissionEventName, ab as WirePermissionKind, ac as activationJoinContext, k as analyticsUserIdStorageKey, m as clearPiiFromContext, n as clearUserContext, ad as collectDeviceContext, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, ae as hashEmailFnv1a, af as hostIdentity, ag as hydrateAutoDeviceKey, ah as hydrateDeviceIdentity, ai as identifyOnboarding, aj as isWireScalar, s as looksLikeEmail, t as makeSessionId, ak as mintDeviceId, al as namespaceExtra, am as normalizePermissionStatus, an as permissionEventName, ao as permissionEventProps, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, ap as resetIdentityProvenance, F as resolveAutoDeviceKey, aq as resolveIdentity, ar as resolvePermissionCopy, as as resolveUserContext, at as sanitizeUserId, G as setCurrentSessionId, H as toAnalyticsEvent } from './currentSession-C5976akx.js';
5
5
  import { O as OnboardingTheme } from './types-BKfpdZzX.js';
6
6
  export { a as OnboardingButtonStyle, b as OnboardingColors, c as OnboardingFonts, d as OnboardingRadius, e as OnboardingSpacing } from './types-BKfpdZzX.js';
7
7
  export { C as CenteredModal, a as CenteredModalHandle, b as CenteredModalProps } from './CenteredModal-Cdgns6--.js';
@@ -1286,6 +1286,8 @@ type PartialProgress = {
1286
1286
  /** The stable per-slot identity, when the backend sends one. See `OnboardingProgress.slot_id`. */
1287
1287
  slot_id?: string;
1288
1288
  skippable?: boolean;
1289
+ /** The experiment arm assigned to this session, when the tenant runs one. See `OnboardingProgress.variant`. */
1290
+ variant?: string;
1289
1291
  };
1290
1292
  declare const readProgress: (response?: WireAIResponse) => PartialProgress;
1291
1293
 
package/dist/index.js CHANGED
@@ -1161,7 +1161,10 @@ var readProgress = (response) => {
1161
1161
  key: typeof p.key === "string" ? p.key : void 0,
1162
1162
  // Whitelisted the same way as every other field: an old backend simply omits it.
1163
1163
  slot_id: typeof p.slot_id === "string" ? p.slot_id : void 0,
1164
- skippable: typeof p.skippable === "boolean" ? p.skippable : void 0
1164
+ skippable: typeof p.skippable === "boolean" ? p.skippable : void 0,
1165
+ // Whitelisted like the rest: a non-string (or absent) arm key reads as "no experiment", never
1166
+ // as an error. The kit does not interpret the value — see `OnboardingProgress.variant`.
1167
+ variant: typeof p.variant === "string" ? p.variant : void 0
1165
1168
  };
1166
1169
  };
1167
1170
 
@@ -1703,6 +1706,7 @@ var OnboardingFlow = ({
1703
1706
  const clientContextRef = React19.useRef(clientContext);
1704
1707
  clientContextRef.current = clientContext;
1705
1708
  const lastScreenIndexRef = React19.useRef(-1);
1709
+ const variantRef = React19.useRef(void 0);
1706
1710
  const previewTimer = React19.useRef(null);
1707
1711
  const [runId, setRunId] = React19.useState(0);
1708
1712
  const startedRunRef = React19.useRef(-1);
@@ -1752,14 +1756,17 @@ var OnboardingFlow = ({
1752
1756
  React19.useEffect(() => {
1753
1757
  var _a2, _b2, _c2, _d2;
1754
1758
  setValidationError(void 0);
1759
+ if (variantRef.current === void 0 && progress.variant) variantRef.current = progress.variant;
1755
1760
  if (!(lastCard == null ? void 0 : lastCard.id)) return;
1756
1761
  const step2 = (_a2 = progress.step) != null ? _a2 : renderedCount;
1757
1762
  lastScreenIndexRef.current = Math.max(0, step2 - 1);
1758
- (_c2 = onEventRef.current) == null ? void 0 : _c2.call(onEventRef, {
1763
+ const turn = {
1759
1764
  type: "turn",
1760
1765
  step: step2,
1761
1766
  component: (_b2 = lastCard.response) == null ? void 0 : _b2.component
1762
- });
1767
+ };
1768
+ if (variantRef.current !== void 0) turn.variant = variantRef.current;
1769
+ (_c2 = onEventRef.current) == null ? void 0 : _c2.call(onEventRef, turn);
1763
1770
  if (((_d2 = lastCard.response) == null ? void 0 : _d2.component) === "InterstitialCard") {
1764
1771
  sendPreview(reportTargetRef.current, {
1765
1772
  sessionId: sessionIdRef.current,
@@ -1820,6 +1827,7 @@ var OnboardingFlow = ({
1820
1827
  const result = { answers: deriveAnswers(messages), raw: messages };
1821
1828
  const plan = readPlan(messages);
1822
1829
  if (plan !== void 0) result.plan = plan;
1830
+ if (variantRef.current !== void 0) result.variant = variantRef.current;
1823
1831
  onCompleteRef.current(result);
1824
1832
  }, [messages]);
1825
1833
  const handleRetry = React19.useCallback(() => {