@wireai/activation 0.13.2 → 0.13.3
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 +4 -3
- package/CHANGELOG.md +228 -4
- package/README.md +16 -2
- package/dist/analytics/index.d.mts +4 -4
- package/dist/analytics/index.d.ts +4 -4
- package/dist/analytics/index.js +104 -470
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +102 -468
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.d.mts +14 -1
- package/dist/coachmarks/index.d.ts +14 -1
- package/dist/coachmarks/index.js +58 -14
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +58 -14
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-ClkLjcJ0.d.mts → currentSession-BoWtr3Jp.d.mts} +2 -2
- package/dist/{currentSession-DOVZEWJl.d.ts → currentSession-CCOMlaQ2.d.ts} +2 -2
- package/dist/{decision-Bl_M2y3r.d.mts → decision-Cjw0jbEj.d.mts} +1 -1
- package/dist/{decision-Cau5KmP6.d.ts → decision-GfpXAsk_.d.ts} +1 -1
- package/dist/index.d.mts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +220 -573
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +217 -570
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +42 -10
- package/dist/questionnaire/index.d.ts +42 -10
- package/dist/questionnaire/index.js +45 -13
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +45 -13
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +4 -4
- package/dist/reviews/index.d.ts +4 -4
- package/dist/reviews/index.js +27 -7
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +27 -7
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.js +7 -2
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +7 -2
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/{transport-DsRe4epC.d.ts → transport-CefdERPs.d.mts} +44 -5
- package/dist/{transport-CF_eHwzC.d.mts → transport-DFuPyCoJ.d.ts} +44 -5
- package/dist/{types-Buj9Lw9t.d.ts → types-UVYt9BJe.d.ts} +41 -1
- package/dist/{types-CNUqMK0D.d.mts → types-l2tfg23P.d.mts} +41 -1
- package/package.json +4 -1
- package/src/OnboardingFlow.tsx +44 -15
- package/src/WireOnboarding.tsx +7 -6
- package/src/activation/wireActivation.ts +3 -3
- package/src/analytics/analyticsFacade.ts +2 -2
- package/src/analytics/eventQueue.ts +83 -20
- package/src/cards/CardGridSelectCard.tsx +1 -1
- package/src/cards/ChipSelectCard.tsx +2 -2
- package/src/cards/SelectionCard.tsx +1 -1
- package/src/coachmarks/runtime.ts +62 -2
- package/src/coachmarks/useCoachmarkTour.ts +62 -14
- package/src/context/deviceId.ts +4 -4
- package/src/features/WireFeaturesProvider.tsx +4 -1
- package/src/identity/userIdentity.ts +1 -1
- package/src/questionnaire/QuestionnaireGate.tsx +90 -10
- package/src/questionnaire/index.ts +9 -1
- package/src/questionnaire/transport.ts +46 -11
- package/src/reviews/ReviewGate.tsx +61 -6
- package/src/reviews/index.ts +10 -1
- package/src/reviews/transport.ts +58 -12
- package/src/session-analytics/useLifecycleEvents.ts +1 -1
- package/src/theme/ThemeContext.tsx +42 -1
- package/src/types.ts +1 -1
- package/src/utils/submitResult.ts +39 -0
package/AGENTS.md
CHANGED
|
@@ -32,7 +32,7 @@ Everything an integration needs lives on the ROOT import (`@wireai/activation`).
|
|
|
32
32
|
subpaths are optional secondary feature modules; import one only if you use it.
|
|
33
33
|
|
|
34
34
|
- `@wireai/activation` (root): `WireOnboarding`, the helpers/types below, and the device-only event reporters (`reportClientEvent`, `makeSessionId`, `WIRE_ONBOARDING_EVENTS`, see "App events" below).
|
|
35
|
-
- `@wireai/activation/analytics`: the analytics FACADE and its queue: `createAnalytics`, `useAnalytics`, `createScreenTracker`, `reportAppEvent`, `createEventQueue`, `clearUserContext`.
|
|
35
|
+
- `@wireai/activation/analytics`: the analytics FACADE and its queue: `createAnalytics`, `useAnalytics`, `createScreenTracker`, `reportAppEvent`, `createEventQueue`, `clearUserContext`. Except for `clearUserContext`, these are NOT root exports; importing them from the root does not compile. `clearUserContext` IS also a root export (`import { clearUserContext } from "@wireai/activation"`), which is the form the README teaches for the sign-out purge.
|
|
36
36
|
- `@wireai/activation/coachmarks`: in-app coachmark tour (`CoachmarkProvider`, `useCoachmarkTour`).
|
|
37
37
|
- `@wireai/activation/showcase`: post-onboarding feature showcase (`FeatureShowcase`).
|
|
38
38
|
- `@wireai/activation/reviews`: store-review gating (`ReviewGate`, `ReviewModal`, `useReviewGate`, `fetchReviewDecision`).
|
|
@@ -100,7 +100,7 @@ FACADE (`createAnalytics` and friends) is a different surface and lives on `@wir
|
|
|
100
100
|
- `WIRE_ONBOARDING_EVENTS` + `toAnalyticsEvent(event)`: canonical `wire_onboarding_*` funnel names; log via the app's own analytics.
|
|
101
101
|
- `reportClientEvent(target, event)` / `reportClientEvents(target, events)`: report device-only funnel events. Contract: `POST {serverUrl}/v1/events`, header `Authorization: Bearer {apiKey}`, body `{ "events": [ ... ] }`. `target = { serverUrl, apiKey }` is derived from `WireOnboardingConfig`. Fire-and-forget: never throws into the UI.
|
|
102
102
|
- `makeSessionId()`: a unique-per-onboarding session id (also seeds the A2A `contextId` so client and server events correlate).
|
|
103
|
-
- `ClientEventType` is `"screen_skipped" | "dropped" | "client_fallback" | "identify"
|
|
103
|
+
- `ClientEventType` is `"screen_skipped" | "dropped" | "client_fallback" | "identify" | "app_event"` (`app_event` is the type the whole `wire.track` / `reportAppEvent` surface rides on). `<WireOnboarding>` auto-reports `dropped` (unmount without complete) and `client_fallback` (degrade to static). Hosts must NOT double-report fallback.
|
|
104
104
|
|
|
105
105
|
## Integration steps (ordered)
|
|
106
106
|
|
|
@@ -117,7 +117,8 @@ FACADE (`createAnalytics` and friends) is a different surface and lives on `@wir
|
|
|
117
117
|
## Review firing (do not hand-roll the decision fetch)
|
|
118
118
|
|
|
119
119
|
If you gate the review prompt on the server, call **`fetchReviewDecision(target, { deviceKey })`**
|
|
120
|
-
from `@wireai/activation/reviews` and pass the result to `useReviewGate({ decision })`.
|
|
120
|
+
from `@wireai/activation/reviews` and pass the result to `useReviewGate({ config, decision })`.
|
|
121
|
+
`config` is REQUIRED (the hook reads `config.id`); `decision` alone does not compile. Never
|
|
121
122
|
write your own fetch.
|
|
122
123
|
|
|
123
124
|
- It returns the FULL `{ fire, reason, arm }` on 2xx, and `null` ONLY when the server is
|
package/CHANGELOG.md
CHANGED
|
@@ -3,13 +3,237 @@
|
|
|
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.3] — 2026-08-02
|
|
7
|
+
|
|
8
|
+
The headline is a fix nobody could see from inside the kit: **on the DEFAULT configuration, a
|
|
9
|
+
backend that never answered left the user on the loading screen forever.** Everything else in this
|
|
10
|
+
release is the machinery that let a defect that severe stay invisible to a 638-test suite.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **A retry could be recorded as the user's FIRST ANSWER.** Caught pre-publish, and like the double
|
|
15
|
+
review row it was NEW in this release — the retry-budget fix below is what made it reachable. The
|
|
16
|
+
auto-start effect keyed the kickoff payload on `resumed`, a MOUNT-time prop, so a retry inside a
|
|
17
|
+
fresh run re-sent `startMessage`. The server records any non-empty message that arrives while a
|
|
18
|
+
question is pending as that question's answer (`if user_message and pending and not
|
|
19
|
+
_stop_requested(...)` → appends `{key, question, answer}` to `session["answers"]`). The trigger
|
|
20
|
+
is not an outage: the watchdog fires at `startTimeoutMs` (15s default) while a slow LLM is still
|
|
21
|
+
generating card 1, the server already has that card pending, and the retry lands as its answer.
|
|
22
|
+
The user's first onboarding answer silently becomes the literal kickoff string — and answer data
|
|
23
|
+
is the product. On 0.13.2 this was unreachable because no second kickoff could ever fire.
|
|
24
|
+
|
|
25
|
+
Only the first attempt of a fresh session now sends `startMessage`; a resumed mount and every
|
|
26
|
+
retry (auto, timeout-driven, and the "Try again" button) send the skip sentinel, which is safe in
|
|
27
|
+
both server states — with a pending question it skips one card recording nothing, with no pending
|
|
28
|
+
it is zeroed and behaves as a normal kickoff.
|
|
29
|
+
|
|
30
|
+
⚠️ **The cost, stated rather than hidden: when the server DID hold card 1 pending, the retry now
|
|
31
|
+
SKIPS that card — the user loses one question.** That is deliberate. A skipped card emits
|
|
32
|
+
`screen_skipped` and is visible in the funnel; a kickoff string stored as an answer is invisible
|
|
33
|
+
and corrupts the data every downstream decision reads. Losing a question beats inventing one.
|
|
34
|
+
|
|
35
|
+
- **A lost ack wrote TWO review rows.** Caught pre-publish, and it was NEW in this release: reading
|
|
36
|
+
the submit response is what made it possible, so 0.13.2 could not double-post. `submitReview`
|
|
37
|
+
returned `false` for two materially different outcomes — *nothing reached the server* and *the
|
|
38
|
+
server answered non-2xx* — and `ReviewGate.postOnce` un-latches on `false`, so the abandonment net
|
|
39
|
+
re-posted the identical body. A gateway 502 that arrives AFTER the row commits therefore wrote a
|
|
40
|
+
second review row, double-counting `count` and corrupting `avg`: the exact corruption the one-row
|
|
41
|
+
latch exists to prevent, on the code path added to protect the data. There is no idempotency key
|
|
42
|
+
on the wire (`create_review` mints the row id; `CreateReviewRequest` has no id field), so the kit
|
|
43
|
+
cannot tell the two apart after the fact — it has to stop guessing.
|
|
44
|
+
|
|
45
|
+
`submitReview` now resolves **`ReviewSubmitResult`** — `"accepted"` / `"rejected"` / `"unsent"` —
|
|
46
|
+
instead of a boolean, and `postOnce` un-latches on `"unsent"` alone. A response of ANY status
|
|
47
|
+
proves the server was reached, and a row the server answered for is the server's problem. The
|
|
48
|
+
shape change is free: 0.13.3 was never published. Still non-throwing, still fired synchronously —
|
|
49
|
+
the caller never awaits.
|
|
50
|
+
|
|
51
|
+
⚠️ **Honest residual: this is safer, not safe.** `"unsent"` is not proof the server never got the
|
|
52
|
+
row either — a connection dropped after the insert committed throws exactly like an offline
|
|
53
|
+
device, so the single in-process re-post can still duplicate in that narrow window. Closing it
|
|
54
|
+
needs a **client-minted idempotency key the server upserts on**, which is a server change and is
|
|
55
|
+
not something the kit can fake. Until then the kit prefers losing a row to inventing one: a lost
|
|
56
|
+
detractor is a gap in the data, a duplicated one is a lie in it.
|
|
57
|
+
|
|
58
|
+
- **The questionnaire had the same hole, and it was worse.** The review fix above landed and its
|
|
59
|
+
semantic twin got none of it. `submitQuestionnaireResponse` returned `void` over a body of
|
|
60
|
+
`void fetch(...).catch(() => {})`, so the response was never read at all, and
|
|
61
|
+
`QuestionnaireGate.finish` set `submittedRef`, fired `questionnaire_submitted` and showed the
|
|
62
|
+
thank-you unconditionally. `onResolved` → `markResolved` then writes the permanent
|
|
63
|
+
`wire_questionnaire_<id>_<ver>_seen` key. The questionnaire is the only surface that both burns a
|
|
64
|
+
permanent once-gate **and** had zero evidence of delivery: a user who answered offline lost their
|
|
65
|
+
answers and was never asked again. The CHANGELOG's own justification for the review fix applies
|
|
66
|
+
word for word.
|
|
67
|
+
|
|
68
|
+
`submitQuestionnaireResponse` now resolves **`QuestionnaireSubmitResult`** (the shared
|
|
69
|
+
`SubmitResult`: `"accepted"` / `"rejected"` / `"unsent"`), and the gate posts through a `postOnce`
|
|
70
|
+
latch that un-latches on `"unsent"` alone, with one in-process re-post when the gate unmounts.
|
|
71
|
+
Both surfaces now import the verdict from one place, so the retry rule cannot drift between them: a
|
|
72
|
+
server that ANSWERED, even non-2xx, is never retried, because the response id is server-minted and
|
|
73
|
+
the wire has no idempotency key. The signature change is free: 0.13.3 was never published. Still
|
|
74
|
+
non-throwing, still fired synchronously, the caller never awaits.
|
|
75
|
+
|
|
76
|
+
⚠️ **Same honest ceiling as reviews:** recovery is in-process only. A run answered offline and
|
|
77
|
+
never recovered before the app is killed is still lost, and the host still writes the once-gate on
|
|
78
|
+
`onResolved`. Durable cross-launch retry needs new public API and is deliberately not in this
|
|
79
|
+
release.
|
|
80
|
+
|
|
81
|
+
- **The theme context was not shared across the built subpath bundles.** `OnboardingThemeContext`
|
|
82
|
+
was a plain `createContext(...)` and tsup inlines it into five bundles, so `dist/index.js`,
|
|
83
|
+
`dist/reviews/index.js`, `dist/questionnaire/index.js`, `dist/coachmarks/index.js` and
|
|
84
|
+
`dist/showcase/index.js` each carried their own instance. `OnboardingThemeProvider` is exported
|
|
85
|
+
only from the root barrel, so on any `dist` resolution path (plain RN CLI,
|
|
86
|
+
`unstable_enablePackageExports: false`, node, jest, SSR) a host that wrapped its tree in
|
|
87
|
+
`<OnboardingThemeProvider>` and rendered `<ReviewGate>` from `@wireai/activation/reviews` wrote
|
|
88
|
+
one context and read another. Nothing crashed: `useOnboardingTheme`'s `ctx ?? mergeTheme()`
|
|
89
|
+
fallback served the neutral default theme, so the tenant's brand silently did not apply. Metro
|
|
90
|
+
masked it on-device by collapsing every subpath back to one source file, the same bundler accident
|
|
91
|
+
behind the coachmark-runtime split and the optional-peers bug fixed above.
|
|
92
|
+
|
|
93
|
+
The context now lives on a `globalThis` slot keyed by
|
|
94
|
+
`Symbol.for("@wireai/activation:themeContext")`, exactly as `coachmarks/runtime`,
|
|
95
|
+
`analytics/currentSession`, `context/deviceId` and `features/WireFeaturesProvider` already did.
|
|
96
|
+
Measured in the built artifact, before and after: the root provider reached **0** of the four
|
|
97
|
+
subpath bundles' theme contexts before, and exactly **1 shared instance** after.
|
|
98
|
+
`test/canary/themeContextIdentity.test.tsx` loads the real bundles and pins it.
|
|
99
|
+
|
|
100
|
+
- **`WireFeaturesProvider` read `global` where the other nine symbol slots read `globalThis`.**
|
|
101
|
+
`global` does not exist in a plain browser, so that line was a `ReferenceError` waiting for the
|
|
102
|
+
first RN-web or browser-SSR host to mount the provider.
|
|
103
|
+
|
|
104
|
+
- **`maxRetries` never re-armed, so the fallback was unreachable on every host running the default.**
|
|
105
|
+
`maxRetries` defaults to `1`, and `1` and `2` behaved *identically*: exactly one retry fired, ever,
|
|
106
|
+
and no terminal state was ever reached. Measured before the fix — `0` → `{kickoffs:1, fallback:1}`,
|
|
107
|
+
`1` → `{kickoffs:1, fallback:0}`, `2` → `{kickoffs:1, fallback:0}`. Only `maxRetries={0}` reached
|
|
108
|
+
the fallback, which is the one value that trades the retry away entirely. A backend hung at start
|
|
109
|
+
therefore produced a 15s loader and then a permanent "Getting started…": no error, no fallback, no
|
|
110
|
+
way out. The "Try again" button was dead from the same cause, sending `1 → 1`.
|
|
111
|
+
|
|
112
|
+
Two causes, both required. `restartThread()` reset a **ref**, and a ref cannot re-trigger an
|
|
113
|
+
effect — the auto-start effect's deps held only stable values. And the startup watchdog keyed on
|
|
114
|
+
`lastCard`, which is still `undefined` after a retry, so no new timer ever armed and `timedOut`
|
|
115
|
+
could not become true a second time. Both effects now key on an incrementing `runId` held in
|
|
116
|
+
**state**, so a retry genuinely re-arms both; `started`/`resumed` deliberately do **not** re-fire,
|
|
117
|
+
so a retry cannot double-count a session in the host's funnel. After the fix, `maxRetries` means
|
|
118
|
+
what it says: `1` → `{kickoffs:2, fallback:1}`, `2` → `{kickoffs:3, fallback:1}`.
|
|
119
|
+
|
|
120
|
+
It survived a 638-test suite because the canary's `wireai-rn` mock hardcoded `error: undefined`,
|
|
121
|
+
leaving the entire error/retry/fallback state machine with **zero render coverage**. The mock
|
|
122
|
+
gained an error mode and the budget is now asserted for 0, 1 and 2 along with the timeout path
|
|
123
|
+
and the "Try again" button.
|
|
124
|
+
|
|
125
|
+
- **A slow cold-start storage read silently destroyed the persisted analytics backlog.** The
|
|
126
|
+
hydration read races a 1500 ms deadline, and the loser resolved to `undefined` — indistinguishable
|
|
127
|
+
from "the adapter answered, there is no backlog". Hydration took its empty early return, the
|
|
128
|
+
surrounding `catch` never ran, and the next `persist()` wrote the in-memory list over the blob
|
|
129
|
+
nobody had read; with nothing pending it deleted the blob outright. Up to `maxSize` = **200**
|
|
130
|
+
real events lost on nothing worse than a cold start where storage took two seconds, with no error,
|
|
131
|
+
no log and nothing to grep for. A timeout is now a distinct sentinel, persistence is suppressed
|
|
132
|
+
until the read settles, and the original read is still awaited — the late backlog is merged and
|
|
133
|
+
flushed rather than abandoned. No test injected a slow adapter; one does now.
|
|
134
|
+
|
|
135
|
+
- **A review rating was latched as posted before the request settled.** The once-gate was written
|
|
136
|
+
regardless, and the transport was a fire-and-forget `fetch` whose response was never read — so a
|
|
137
|
+
1-4 star rating with mandatory free text travelled the kit's least durable path while
|
|
138
|
+
`analytics.track` got a persisted queue with backoff and six retries. A detractor who rated
|
|
139
|
+
offline was dropped *and* never asked again. `submitReview` now reports delivery, the latch is
|
|
140
|
+
released when the row did not land, and the undelivered body is re-posted by the existing
|
|
141
|
+
abandonment net. ⚠️ Recovery is in-process only: a rating taken offline and then killed with the
|
|
142
|
+
app is still lost. A persisted review outbox is deliberately left for a later release.
|
|
143
|
+
|
|
144
|
+
- **The coachmarks kill switch could leave a tour unable to finish.** The flag is a mutable global
|
|
145
|
+
read once with no subscription: the arm effect omitted it from its deps and the drive effect never
|
|
146
|
+
read it, so flipping it mid-tour made `show()` a silent no-op and left `finish()`/`onComplete`
|
|
147
|
+
unreachable. It is now subscribable and read through `useSyncExternalStore` in both effects, and a
|
|
148
|
+
mid-tour disable exits terminally **without** writing the once-gate, so the tour is not consumed.
|
|
149
|
+
|
|
150
|
+
- **Four of the six optional peers were missing from the published bundle entirely.** Measured with
|
|
151
|
+
Metro's own dependency collector, the 0.13.2 tarball's `dist/` collected **2 of 6**
|
|
152
|
+
`OPTIONAL_MODULES` while the same collector over `src/` collected **6 of 6** — so on every
|
|
153
|
+
resolution path that lands on `dist/` (plain RN CLI, `unstable_enablePackageExports: false`, node,
|
|
154
|
+
jest) `@expo/vector-icons`, `expo-constants`, `expo-application` and `expo-device` were not in the
|
|
155
|
+
bundle at all: `WireIcon` renders null and `by_model` / `by_app_version` stay empty, with the
|
|
156
|
+
guard's own `catch` hiding it. Source consumers (both monorepo apps, and anything else taking
|
|
157
|
+
the `react-native` condition) were never affected, which is why a green suite and two shipping
|
|
158
|
+
apps said nothing. Two independent build-config faults, both silent at exit 0 with zero warnings:
|
|
159
|
+
- `tsup.config.ts` `external` was a hand-maintained list covering 2 of the 6. The consequence
|
|
160
|
+
depended on the BUILD MACHINE: a module that happened to resolve in the kit's own
|
|
161
|
+
`node_modules` (`expo-constants`, pulled in transitively) was **inlined whole** into the bundle,
|
|
162
|
+
so the host's copy went unused; one that did not resolve was left as an orphan call. `external`
|
|
163
|
+
is now **derived from `metro/index.js`'s `OPTIONAL_MODULES`** — the same list the Metro helper
|
|
164
|
+
already treats as the source of truth — plus the statically-imported peers, with a build-time
|
|
165
|
+
throw if the two ever diverge. The output no longer depends on what is installed here.
|
|
166
|
+
- Marking them `external` was necessary and **not sufficient**. `treeshake: true` is not an
|
|
167
|
+
esbuild flag; tsup implements it as a Rollup pass whose ESM output is then re-derived into CJS,
|
|
168
|
+
and in that conversion esbuild renames `require("expo-device")` to `__require("expo-device")`,
|
|
169
|
+
external or not. Metro's collector matches only a callee literally named `require`, so a
|
|
170
|
+
renamed call is collected nowhere — the 0.8.0 bug, reintroduced by a build flag rather than by
|
|
171
|
+
source. The build now carries a small `renderChunk` hook that restores the literal `require`
|
|
172
|
+
for exactly the `OPTIONAL_MODULES` specifiers, and only in CJS output, where the rewrite is an
|
|
173
|
+
identity (esbuild defines `__require` as `typeof require !== "undefined" ? require : …`, so in
|
|
174
|
+
a CJS chunk it already *is* `require`). `treeshake` stays on and every other byte of the bundle
|
|
175
|
+
keeps its current shape — `dist/index.js` is 218.6 KB, down from 232.2 KB purely because
|
|
176
|
+
`expo-constants` is no longer inlined. `dist/*.mjs` keeps `__require`: ESM has no `require` to
|
|
177
|
+
preserve, and no React Native path reads it (`react-native` → `src/`, `main` / the `require`
|
|
178
|
+
condition → `dist/*.js`).
|
|
179
|
+
|
|
180
|
+
`test/canary/metroResolution.test.tsx` ran Metro's collector over `src/` only, which is how a
|
|
181
|
+
publish-surface defect stayed invisible to the test that owns exactly this property. It now runs
|
|
182
|
+
the same assertions over the BUILT `dist/` (build-if-stale, never skipped), with its own negative
|
|
183
|
+
control pinning that a `__require`-shaped call collects nothing. Verified against the pre-fix
|
|
184
|
+
config: the new assertion goes red and names all four missing modules.
|
|
185
|
+
|
|
186
|
+
- **The banned-name canary now actually gates a publish.** It was documented as a "build-time canary
|
|
187
|
+
that fails the pack" and it was neither: `prepack` was `npm run build` and ran no test at all, so a
|
|
188
|
+
planted banned name packed at **exit 0**, inside the tarball. The second half compounded it
|
|
189
|
+
— the canary derives its scan set from `package.json` `files`, which includes `dist`, but `dist` is
|
|
190
|
+
gitignored and sits in the scanner's `OPTIONAL_WHEN_ABSENT` list, so on an unbuilt tree the suite
|
|
191
|
+
was **6/6 green while covering none of the built output that ships**. Fixing either half alone
|
|
192
|
+
still ships a name under some ordering. The gate is now one script that closes both:
|
|
193
|
+
`prepublishOnly` = `npm run build && npm run test:canary`. It fires on `npm publish`, and it fires
|
|
194
|
+
*after* the build, so `dist/` is on disk and inside the scan. `test:canary` is a new script that
|
|
195
|
+
runs `test/canary/**` on its own, and `bannedNames.test.tsx` gained a test that asserts this wiring
|
|
196
|
+
from inside the suite, so deleting the gate goes red instead of silently reopening the hole.
|
|
197
|
+
⚠️ `npm pack` on its own still does not run the canary — the honest verb is *publish*, not *pack*.
|
|
198
|
+
|
|
199
|
+
### Changed
|
|
200
|
+
|
|
201
|
+
- **Test integrity.** `npm test` now builds first (`pretest`), because the new `dist/` assertions
|
|
202
|
+
shell out to a build with `clean: true` while other test files walk `dist/` in parallel — that
|
|
203
|
+
race could redden CI for a reason unrelated to any change. Separately, the pre-existing
|
|
204
|
+
`DE-DUP: re-enqueuing an identical pending event is collapsed` test was **flaky on `main`**: it
|
|
205
|
+
fires three synchronous enqueues against a live clock while the queue stamps `ts` into the de-dup
|
|
206
|
+
signature, so a millisecond boundary between them makes the third a genuine repeat by the queue's
|
|
207
|
+
own rules. Measured at 0.32% of runs on an untouched `main`, 0/6000 with the clock frozen; it is
|
|
208
|
+
now pinned exactly as its three sibling tests already were. And `mergePersisted`'s de-dup branch
|
|
209
|
+
had **zero coverage** — it could be deleted with the whole suite green — which matters precisely
|
|
210
|
+
because the backlog fix above newly merges a late read into a pending list that may already hold
|
|
211
|
+
the same event. It is covered now.
|
|
212
|
+
|
|
213
|
+
- **Four guards this release relies on had no test at all**, found by mutating the suite. Each one
|
|
214
|
+
was a promise made in writing and enforced by nothing, so each could be deleted with a fully green
|
|
215
|
+
run: (1) `started`/`resumed` not re-firing on a retry — stated right here as *"a retry cannot
|
|
216
|
+
double-count a session"*, and in tension with the `runId` re-arm by construction, so a host funnel
|
|
217
|
+
would have read one flaky cold start as three sessions; (2) `pretest` itself, whose removal
|
|
218
|
+
silently reopens the `clean: true` race described above; (3) both build-time throws in
|
|
219
|
+
`tsup.config.ts`, which are the only thing standing between a refactor of the `external`
|
|
220
|
+
derivation and a bundle that silently inlines or orphans every guarded optional peer at exit 0;
|
|
221
|
+
and (4) the dist half of `metroResolution`'s HARD-dependency check, which its own
|
|
222
|
+
`.filter((m) => !collected.has(m))` had rendered unfailable — a module collected optional in one
|
|
223
|
+
chunk excused a hard require of it in another, and Metro resolves per call site, not per package.
|
|
224
|
+
|
|
6
225
|
## [0.13.2] — 2026-07-31
|
|
7
226
|
|
|
8
227
|
The headline is a fix: every `track()` / `screen()` `app_event` was being silently discarded by the
|
|
9
228
|
server behind an HTTP 200, on every kit version since 0.11.0. This release also ships the injectable
|
|
10
229
|
mid-flow permission screens below, and it is the FIRST version whose published tarball is clean of
|
|
11
|
-
the banned client name — the 0.13.0 publish-surface scrub
|
|
12
|
-
|
|
230
|
+
the banned client name — the 0.13.0 publish-surface scrub rides a canary that fails the run if the
|
|
231
|
+
name ever reappears.
|
|
232
|
+
|
|
233
|
+
> ⚠️ **Corrected 2026-08-02.** This paragraph originally said the canary "fails the pack". It did
|
|
234
|
+
> not: at 0.13.2 nothing ran it on `npm pack` or `npm publish`, and on an unbuilt tree it did not
|
|
235
|
+
> scan `dist/` either. The 0.13.2 tarball is still clean — that was verified by hand — but it was
|
|
236
|
+
> clean without a gate holding it. See the 0.13.3 entry above for the fix.
|
|
13
237
|
|
|
14
238
|
### Fixed
|
|
15
239
|
|
|
@@ -146,7 +370,7 @@ direction: the kit now declines and says so, where it used to fabricate and retu
|
|
|
146
370
|
the question's prompt text (tenant flows slugify it and cut at 32 chars), so re-wording a question mints
|
|
147
371
|
a NEW key and a host reading `answers.interests` silently starts reading `undefined`.
|
|
148
372
|
⚠️ ~~**INERT UNTIL THE SERVER EMITS IT.** No deployed server sends `slot_id` today.~~
|
|
149
|
-
✅ **CORRECTED 2026-07-28: this is LIVE.** The deployed server
|
|
373
|
+
✅ **CORRECTED 2026-07-28: this is LIVE.** The deployed server sends `slot_id` on
|
|
150
374
|
`progress` for every AI-generated question, as `adaptive_<n>`, and for a configured question
|
|
151
375
|
whenever the tenant set one. Where no slot is configured the field is absent and
|
|
152
376
|
every path degrades to byte-identical 0.12.2 behaviour. Every fallback is decided
|
|
@@ -499,7 +723,7 @@ Architecture-audit fixes (#50/#51). One BEHAVIOR CHANGE, called out below.
|
|
|
499
723
|
store-declaration implications). The blanket "No PII / nothing identifies a device" claims are
|
|
500
724
|
scoped to the onboarding `userContext` prop + the `collectDeviceContext()` snapshot. `llms.txt`
|
|
501
725
|
now lists the `/analytics`, `/reviews`, `/questionnaire` subpath surfaces.
|
|
502
|
-
- Added the `session/sessionIdentity.integration.test.ts` dist-canary
|
|
726
|
+
- Added the `session/sessionIdentity.integration.test.ts` dist-canary.
|
|
503
727
|
|
|
504
728
|
## [0.10.0] - 2026-07-20
|
|
505
729
|
|
package/README.md
CHANGED
|
@@ -237,7 +237,9 @@ import { WireOnboarding } from "@wireai/activation";
|
|
|
237
237
|
| `validators` | `Record<string, StepValidator>` | Per-step, keyed by base-question key (e.g. `username`). Blocks advance + shows an inline error. When the backend sends a `progress.slot_id` for a screen, the kit looks up that slot FIRST and falls back to the question key, so a validator map written against today's keys keeps working. |
|
|
238
238
|
| `permissionScreens` | `PermissionScreenConfig[]` | Priming screens injected mid-flow, starting with notifications. The screen explains why, and the OS dialog opens **only** on the primary tap, never on mount. Zero new dependencies: you inject `request`. See [Permission screens](#permission-screens-the-priming-pattern). |
|
|
239
239
|
| `onSkip` | `() => void` | User skipped. |
|
|
240
|
-
| `onError` | `(err) => void` | Backend error
|
|
240
|
+
| `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. |
|
|
241
|
+
| `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. |
|
|
242
|
+
| `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. |
|
|
241
243
|
| `onEvent` | `(e: OnboardingEvent) => void` | `started` / `turn` / `error` — recover per-turn analytics since the kit owns the loop. |
|
|
242
244
|
| `copy` | `Partial<OnboardingCopy>` | Localize the kit's built-in English loader/completion strings. |
|
|
243
245
|
| `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. |
|
|
@@ -914,6 +916,18 @@ the "personalize from context" thesis. The once-gate
|
|
|
914
916
|
It reads the active `OnboardingThemeProvider` theme; pass `theme` to override just
|
|
915
917
|
the showcase, or `storage` / `isTesting` to gate independently of the provider.
|
|
916
918
|
|
|
919
|
+
Until 0.13.3 that sentence only held on a phone. `OnboardingThemeProvider` ships from
|
|
920
|
+
the root barrel, every subpath builds as its own bundle, and each of those bundles
|
|
921
|
+
carried a private copy of the theme context. Metro hid it: its `react-native` export
|
|
922
|
+
condition collapses all five subpaths back to one source file, so on-device the copies
|
|
923
|
+
were the same object by accident. Everywhere else (plain RN CLI,
|
|
924
|
+
`unstable_enablePackageExports: false`, node, jest, SSR) the provider wrote one context
|
|
925
|
+
while the gate read another, and `useOnboardingTheme` quietly falls back to the neutral
|
|
926
|
+
default theme instead of crashing. No error, no warning, wrong brand colors. The context
|
|
927
|
+
now lives in one `Symbol.for` slot on `globalThis`, next to the feature flags and the
|
|
928
|
+
coachmark runtime that already sat there, so a single provider themes every surface on
|
|
929
|
+
every resolution path.
|
|
930
|
+
|
|
917
931
|
**v2 server seam (future, not built).** The onboarding server's final turn will
|
|
918
932
|
later return an ordered `showcase: string[]` slide-key list chosen server-side from
|
|
919
933
|
the same answers; the host passes it straight in as the `selection` argument.
|
|
@@ -1293,7 +1307,7 @@ Least privilege: for the read-only loop, hand the agent just `WIREAI_SERVER_URL`
|
|
|
1293
1307
|
A pure-Markdown Claude skill for *using* Wire AI easily: install the kit + wire the
|
|
1294
1308
|
`WireOnboarding` provider, read your insights (via the MCP or `/v1/insights.md`), and run
|
|
1295
1309
|
the improve loop (read insights, then adjust the app description / first question /
|
|
1296
|
-
generation context).
|
|
1310
|
+
generation context). For the full
|
|
1297
1311
|
scaffold-the-screen integration job, the [`wire-rn-integration`](./INTEGRATION_PROMPT.md)
|
|
1298
1312
|
skill drives it end to end.
|
|
1299
1313
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-
|
|
2
|
-
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
3
|
-
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, 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-
|
|
4
|
-
import '../types-
|
|
1
|
+
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-CefdERPs.mjs';
|
|
2
|
+
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-BoWtr3Jp.mjs';
|
|
3
|
+
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, 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-BoWtr3Jp.mjs';
|
|
4
|
+
import '../types-l2tfg23P.mjs';
|
|
5
5
|
import '../types-BKfpdZzX.mjs';
|
|
6
6
|
import '../types-BcmagF6K.mjs';
|
|
7
7
|
import '../types-CKFhyrMu.mjs';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-
|
|
2
|
-
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
3
|
-
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, 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-
|
|
4
|
-
import '../types-
|
|
1
|
+
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-DFuPyCoJ.js';
|
|
2
|
+
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-CCOMlaQ2.js';
|
|
3
|
+
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, 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-CCOMlaQ2.js';
|
|
4
|
+
import '../types-UVYt9BJe.js';
|
|
5
5
|
import '../types-BKfpdZzX.js';
|
|
6
6
|
import '../types-BcmagF6K.js';
|
|
7
7
|
import '../types-CKFhyrMu.js';
|