@wireai/activation 0.14.2 → 0.15.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 +1 -1
- package/CHANGELOG.md +144 -0
- package/README.md +23 -9
- package/dist/analytics/index.d.mts +5 -5
- package/dist/analytics/index.d.ts +5 -5
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.d.mts +13 -5
- package/dist/coachmarks/index.d.ts +13 -5
- package/dist/coachmarks/index.js +252 -58
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +204 -9
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-CW_5Mq4O.d.ts → currentSession-CFSRZ2wg.d.ts} +10 -4
- package/dist/{currentSession-CUvTOchb.d.mts → currentSession-orZy5p1e.d.mts} +10 -4
- package/dist/{decision-Bgo17oH7.d.mts → decision-3vWLuBlO.d.ts} +11 -2
- package/dist/{decision-Bkh_LigV.d.ts → decision-yBj2AyPW.d.mts} +11 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +3 -3
- package/dist/questionnaire/index.d.ts +3 -3
- package/dist/questionnaire/index.js +48 -3
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +48 -3
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +10 -6
- package/dist/reviews/index.d.ts +10 -6
- package/dist/reviews/index.js +167 -18
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +167 -18
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.d.mts +1 -1
- package/dist/showcase/index.d.ts +1 -1
- package/dist/showcase/index.js +282 -48
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +248 -12
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/{transport-j5gFfJhK.d.mts → transport-s5QxA-ci.d.mts} +18 -13
- package/dist/{transport-B_0SgCBe.d.ts → transport-xqqSFqxs.d.ts} +18 -13
- package/dist/{types-Cju-1_jT.d.mts → types-Byx306Kv.d.mts} +25 -10
- package/dist/{types-BcmagF6K.d.mts → types-D_0B0yay.d.mts} +7 -2
- package/dist/{types-BcmagF6K.d.ts → types-D_0B0yay.d.ts} +7 -2
- package/dist/{types-h2BZvl1t.d.ts → types-tdATL5z0.d.ts} +25 -10
- package/metro/index.js +16 -5
- package/package.json +1 -1
- package/src/WireOnboarding.tsx +4 -1
- package/src/coachmarks/GestureHint.tsx +16 -7
- package/src/coachmarks/SpotlightOverlay.tsx +19 -13
- package/src/coachmarks/index.ts +7 -2
- package/src/coachmarks/reanimated.ts +342 -0
- package/src/coachmarks/runtime.ts +103 -2
- package/src/coachmarks/types.ts +7 -2
- package/src/context/deviceId.ts +32 -0
- package/src/reviews/ReviewGate.tsx +86 -37
- package/src/reviews/decision.ts +11 -1
- package/src/reviews/idempotency.ts +71 -0
- package/src/reviews/index.ts +4 -2
- package/src/reviews/runtime.ts +5 -3
- package/src/reviews/transport.ts +17 -12
- package/src/reviews/types.ts +7 -0
- package/src/showcase/FeatureShowcase.tsx +25 -9
- package/src/showcase/blazejOnboarding.ts +136 -0
- package/src/showcase/index.ts +8 -4
- package/src/types.ts +10 -4
- package/src/utils/submitResult.ts +17 -9
package/AGENTS.md
CHANGED
|
@@ -81,7 +81,7 @@ subpaths are optional secondary feature modules; import one only if you use it.
|
|
|
81
81
|
| `persistKey` | `string` | no | Override the storage key (default `wireai:session:<config.appId>`). Scope it per-user if one device runs onboarding for multiple accounts. Only meaningful with `storage`. |
|
|
82
82
|
| `retainSessionOnComplete` | `boolean` | no | Opt IN: keep the persisted session seed alive ACROSS completion. A multi-stage or replay-after-complete re-entry within one signup (typically a `key=` remount) then resumes the SAME session instead of minting a fresh `metadata.sessionId`, which the backend adopts as a second `contextId` and double-counts as a `session_started`. Omitted or `false` clears the seed on completion, the legacy single-stage behavior, so leaving it unset changes nothing. When `true`, freshness comes from `sessionTtlMs` plus an explicit new-run signal (scope a new `persistKey`, e.g. a per-signup id). Only meaningful with `storage`. |
|
|
83
83
|
| `userContext` | `Record<string, string \| number \| boolean>` | no | Host-injected, non-PII context (signup method, referral, plan tier, a hashed user id). Forwarded to the backend + client events for funnel segmentation. No raw PII. |
|
|
84
|
-
| `autoJoinKey` | `boolean` | no | Opt OUT of the automatic join key. Default `true`. When `userContext` carries no `device_key` and `storage` is present and actually persists (a storage adapter that throws or rejects is treated exactly like no `storage`), the kit injects its own per-install key, the same id `createAnalytics` / `createWireActivation` mint and persist, so the `activated` funnel joins with no host wiring. Pass `autoJoinKey={false}` for a genuinely UNLINKED session: that restores the pre-0.12.2 behavior and the dev warning fires again. It never overrides a `device_key` you
|
|
84
|
+
| `autoJoinKey` | `boolean` | no | Opt OUT of the automatic join key. Default `true`. When `userContext` carries no `device_key` and `storage` is present and actually persists (a storage adapter that throws or rejects is treated exactly like no `storage`), the kit injects its own per-install key, the same id `createAnalytics` / `createWireActivation` mint and persist, so the `activated` funnel joins with no host wiring. Pass `autoJoinKey={false}` for a genuinely UNLINKED session: that restores the pre-0.12.2 behavior and the dev warning fires again. It never overrides a `device_key` you actually supplied, and it cannot inject without `storage`. The kit counts yours as supplied only when it is a STRING WITH AT LEAST ONE NON-WHITESPACE CHARACTER (`resolveIdentity`); the `userContext` type also allows a number or a boolean, and a non-string or an all-whitespace `device_key` is not a usable join key, so it is treated as absent and the auto key is merged over it. |
|
|
85
85
|
| `userId` | `string` | no | The host's OPAQUE PSEUDONYMOUS user id (not an email/name/phone), so onboarding sessions reconcile to real users. Late binding supported (mount, mid-session, or post-flow via `identifyOnboarding`). Capped at 128 chars. |
|
|
86
86
|
|
|
87
87
|
### `config` fields (`WireOnboardingConfig`)
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,150 @@
|
|
|
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.15.0] - 2026-08-19
|
|
7
|
+
|
|
8
|
+
**MINOR, not patch, and only because of new API. Two exported surfaces each GAINED an optional
|
|
9
|
+
member; nothing was lost or renamed.** `ReviewSubmission` gains an OPTIONAL `idempotency_key` and
|
|
10
|
+
`buildReviewSubmission` gains an OPTIONAL `idempotencyKey` input. Both are additive and optional, so
|
|
11
|
+
every existing call site compiles unchanged and sends a byte-identical body unless it opts in. A
|
|
12
|
+
gained public member is still a MINOR under this changelog's own precedent: 0.14.0 was bumped MINOR
|
|
13
|
+
for exactly this shape, an optional field added to an exported interface. Every behaviour change
|
|
14
|
+
below is a bug fix.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **The review gate can now reach the server's idempotent upsert path, so a recovery post merges
|
|
19
|
+
into the row it may already have written instead of adding a second one.** `POST /v1/reviews`
|
|
20
|
+
accepts an optional client-minted `idempotency_key` and derives the row id from it; the kit now
|
|
21
|
+
mints one per gate IMPRESSION and holds it across every post of that impression. This is a
|
|
22
|
+
CAPABILITY, not a guarantee, and the condition is load-bearing: the server honours the key only
|
|
23
|
+
when the post also carries a unit it can scope it to, and ignores it outright otherwise.
|
|
24
|
+
- **Every review post now carries that unit.** The server resolves it as `meta.device_key`, else
|
|
25
|
+
`session_id`. The gate lives on the home feed, where a user legitimately has no onboarding
|
|
26
|
+
session, and the kit never put a `device_key` into a review `meta` — so the key alone would have
|
|
27
|
+
been discarded on the default wiring. The gate now fills in the kit's own per-install auto id,
|
|
28
|
+
and only when the host supplied none: **any non-empty string you pass in `meta.device_key` wins
|
|
29
|
+
verbatim.** The kit counts a value as host-supplied only when it is a string with at least one
|
|
30
|
+
non-whitespace character, so a non-string or an all-whitespace one is not a usable unit and is
|
|
31
|
+
replaced. **Overriding `meta` cannot remove the unit**: the injection tests the MERGED bucket, so
|
|
32
|
+
stripping `device_key` there only gets the auto id back. Exactly one post carries no unit at all,
|
|
33
|
+
the one where the kit cannot name an id honestly: two tenants registered in one process, which
|
|
34
|
+
leaves the per-install id space ambiguous and the auto id `undefined`. That post reaches the
|
|
35
|
+
server with no unit, the server ignores the idempotency key, and that path keeps the previous
|
|
36
|
+
behaviour.
|
|
37
|
+
- The injected id is the SAME per-install id the analytics surfaces use, adopted from the process
|
|
38
|
+
registry rather than minted fresh, so the gate never opens a second id space for one install.
|
|
39
|
+
|
|
40
|
+
### Unchanged on purpose
|
|
41
|
+
|
|
42
|
+
- **The `rejected` verdict is still not retried.** Re-posting after a non-2xx is now safe wherever
|
|
43
|
+
the key is honoured, but it would also turn a genuine 4xx refusal into a retry needing its own
|
|
44
|
+
budget. That is a separate recovery decision and is deliberately not bundled here.
|
|
45
|
+
- Questionnaire submissions are untouched. The server has no idempotency path for them, so every
|
|
46
|
+
statement in `src/questionnaire/` about there being no key on the wire remains true.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **An ASYNC gate-storage adapter now fails CLOSED instead of misfiring the gates.** The sync gate
|
|
51
|
+
storage (`CoachmarkStorage`) silently accepted an async adapter — AsyncStorage handed straight to
|
|
52
|
+
`CoachmarkProvider` or a gate's `storage` prop. Its `getItem` returns a Promise, which is never
|
|
53
|
+
`=== "1"` and parses to `NaN`, so every seen-gate read "unseen" forever: coachmark tours and
|
|
54
|
+
showcases replayed on every open, and a review gate configured with `minSessions` 0/1 could
|
|
55
|
+
RE-FIRE its native prompt each open (the 2026-07-16 one-star incident class). A thenable probe on
|
|
56
|
+
first read (`validateGateStorage`, WeakSet-cached so each adapter pays for exactly one probe) now
|
|
57
|
+
substitutes a closed adapter: every gate reads "seen", nothing shows, nothing fires, and a
|
|
58
|
+
once-per-process dev warning names the fix (pass a sync adapter, e.g. a 3-line MMKV wrapper).
|
|
59
|
+
Never-fire is the only direction a once-per-user gate may err in. A rejecting probe promise is
|
|
60
|
+
detached, so a broken native store cannot surface as an unhandled rejection.
|
|
61
|
+
- Canary: `test/canary/asyncGateStorage.test.tsx` (6 tests, proven red with the probe neutered —
|
|
62
|
+
including the sync CONTROL that stays green) + the new
|
|
63
|
+
`@wireai/activation:asyncGateStorageWarned` latch registered in `globalSlotDiscipline`.
|
|
64
|
+
|
|
65
|
+
### Docs
|
|
66
|
+
|
|
67
|
+
- **The "a `device_key` you passed is never overridden" line is now qualified everywhere it ships**,
|
|
68
|
+
on the ONBOARDING join-key path as it already was on the reviews path. The bar is the same on both
|
|
69
|
+
and it is `resolveIdentity` / `hasDeviceKey`: a string with at least one non-whitespace character.
|
|
70
|
+
`userContext` also accepts numbers and booleans, so a non-string or an all-whitespace `device_key`
|
|
71
|
+
was never a usable join key — it reads as absent and the auto key is merged over it, which the
|
|
72
|
+
unqualified sentence denied. Corrected in `AGENTS.md`, `README.md` (both the props table and the
|
|
73
|
+
join-key rules), and the shipped JSDoc in `src/types.ts` + `src/WireOnboarding.tsx`. No behaviour
|
|
74
|
+
change: the predicate is unchanged, only the sentence describing it.
|
|
75
|
+
- `README.md`'s tarball install line named `@wireai/activation-0.1.3.tgz`, a file `npm pack` has
|
|
76
|
+
never produced under any version (it emits `wireai-activation-<version>.tgz`, scope flattened), so
|
|
77
|
+
an agent following it hit `ENOENT`. Fixed there and in the two Distribution mentions.
|
|
78
|
+
|
|
79
|
+
## [0.14.3] - 2026-08-18
|
|
80
|
+
|
|
81
|
+
**PATCH: no public surface gained, lost or renamed a member.** `SpotlightOverlay`, `GestureHint`
|
|
82
|
+
and `FeatureShowcase` keep their props and their exports; `react-native-reanimated` and
|
|
83
|
+
`@blazejkustra/react-native-onboarding` stay OPTIONAL peers. This finishes the class 0.14.2 started
|
|
84
|
+
with `expo-blur`: **no source under `src/` statically value-imports an optional peer any more.**
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- **The coachmarks subpath no longer force-imports its optional peer `react-native-reanimated`.**
|
|
89
|
+
`SpotlightOverlay` and `GestureHint` both did `import Animated, { … } from
|
|
90
|
+
"react-native-reanimated"` at module scope, and `coachmarks/index.ts` re-exports them with no
|
|
91
|
+
wildcard escape — so importing ANYTHING from `@wireai/activation/coachmarks`, even
|
|
92
|
+
`setCoachmarkStorage`, dragged the peer in, and `@wireai/activation/showcase` inherited the same
|
|
93
|
+
edge through `GestureHint`. A host that took "optional" at its word hit a Metro resolution
|
|
94
|
+
failure. The peer is now resolved through a guarded lazy require with a string-literal specifier
|
|
95
|
+
inside a try/catch (`src/coachmarks/reanimated.ts`, the same shape as `icons/expoIcons.ts` /
|
|
96
|
+
`coachmarks/expoBlur.ts`), so neither subpath carries a static top-level import of it.
|
|
97
|
+
- **Degradation, by design:** without the peer both surfaces render STATICALLY at their resting
|
|
98
|
+
frame. The spotlight keeps its ring, tooltip, copy, scrim/frost and gestures, with no fade-in
|
|
99
|
+
and no pulse; the gesture glyph keeps its capsules, ring, dot and motion track, at full opacity
|
|
100
|
+
and no travel. Nothing moves; everything renders. Reduce Motion is still honoured — the kit's
|
|
101
|
+
own `AccessibilityInfo`-backed hook stands in for the peer's `useReducedMotion`.
|
|
102
|
+
- The accessor re-exports the peer's own names (`useAnimatedStyle`, `useAnimatedProps`,
|
|
103
|
+
`withTiming`, …) and the call sites keep calling them unqualified, because reanimated's Babel
|
|
104
|
+
plugin decides what to workletize by the callee NAME. Hosts that HAVE the peer therefore get
|
|
105
|
+
byte-for-byte the same worklets, on the UI thread, as before.
|
|
106
|
+
|
|
107
|
+
- **The showcase subpath no longer force-imports its optional peer
|
|
108
|
+
`@blazejkustra/react-native-onboarding`.** `FeatureShowcase` imported the pager's default export
|
|
109
|
+
at module scope, so importing anything from `@wireai/activation/showcase` — even
|
|
110
|
+
`selectShowcaseSlides` — required a peer npm never installs. It is now resolved through a guarded
|
|
111
|
+
lazy require (`src/showcase/blazejOnboarding.ts`); only the TYPES are still imported statically,
|
|
112
|
+
which every toolchain erases.
|
|
113
|
+
- **Degradation, by design:** absent, the showcase renders null and calls `onDone` from an effect
|
|
114
|
+
— the SAME path the feature kill switch already takes — so the host's flow always advances
|
|
115
|
+
rather than stranding the user on a blank screen. The seen gate is deliberately NOT written, so
|
|
116
|
+
installing the peer later still plays the deck once.
|
|
117
|
+
|
|
118
|
+
- **A rule file prescribed the inverted optional-require contract, for the second time.**
|
|
119
|
+
`ai_rules/context_map.md` told the next author that the `expoIcons` callee "MUST be a local `req`,
|
|
120
|
+
never a literal `require`" — verbatim the `const req = require; req(name)` shape that
|
|
121
|
+
`icons/expoIcons.ts` autopsies as the 0.8.0 red-box bug (aliasing hides the call from Metro's
|
|
122
|
+
static collector, so the module never enters the bundle). 0.14.2 corrected this claim in
|
|
123
|
+
`frequent_rules.md`; this was its second copy, and it was still live. Rewritten to the one correct
|
|
124
|
+
contract, citing `src/icons/expoIcons.ts:98`.
|
|
125
|
+
|
|
126
|
+
### Changed
|
|
127
|
+
|
|
128
|
+
- **`metro/index.js` `OPTIONAL_MODULES` now lists both modules**, and `tsup.config.ts` drops them
|
|
129
|
+
from its static-peer list (`external` is derived from the union, so they stay external, and the
|
|
130
|
+
CJS keep-literal-require hook now covers them). The helper's "a statically-imported module must NOT be
|
|
131
|
+
stubbed" note named exactly these two; it is rewritten, because the kit now has NO
|
|
132
|
+
statically-imported optional peer — everything it imports statically is a REQUIRED peer whose
|
|
133
|
+
absence must stay a hard build error.
|
|
134
|
+
- **Docs that promised the old behaviour are corrected in the same wave**, both directions: the
|
|
135
|
+
coachmarks and showcase barrel docs and the README said importing a subpath "pulls in" these
|
|
136
|
+
peers (no longer true), while the README/AGENTS peer tables already claimed every optional peer
|
|
137
|
+
"sits behind a guarded lazy require" (not true until now).
|
|
138
|
+
|
|
139
|
+
### Added
|
|
140
|
+
|
|
141
|
+
- **A whole-class ratchet in `test/canary/metroResolution.test.tsx`:** every file under `src/` is
|
|
142
|
+
scanned for a static value-import of ANY module in `OPTIONAL_MODULES`, with a positive control
|
|
143
|
+
proving the scanner catches default, named, wrapped multi-line, bare and subpath imports while
|
|
144
|
+
ignoring `import type` and the guarded require itself. Each optional peer so far has arrived as
|
|
145
|
+
the same defect; a new one now inherits the check for free. Plus per-module collection tests
|
|
146
|
+
(Metro marks both new specifiers `isOptional`) and component-level canaries for both peers in
|
|
147
|
+
both directions, including the absent-reanimated/present-blur combination that pins the frost at
|
|
148
|
+
full intensity.
|
|
149
|
+
|
|
6
150
|
## [0.14.2] - 2026-08-18
|
|
7
151
|
|
|
8
152
|
**PATCH: no public surface gained, lost or renamed a member.** `SpotlightOverlay` keeps its props
|
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ Install the package and wire Metro with the one-call helper:
|
|
|
140
140
|
# from npm (public)
|
|
141
141
|
npm install @wireai/activation
|
|
142
142
|
# …or from a tarball (see Distribution below)
|
|
143
|
-
npm install
|
|
143
|
+
npm install ./wireai-activation-0.15.0.tgz
|
|
144
144
|
# …or over SSH
|
|
145
145
|
npm install git+ssh://git@github.com/<org>/@wireai/activation.git
|
|
146
146
|
```
|
|
@@ -204,9 +204,9 @@ returns the same config. `options.appRoot` defaults to `process.cwd()`.
|
|
|
204
204
|
## Distribution
|
|
205
205
|
|
|
206
206
|
`npm run pack:tarball` runs the build (via `prepack`) and produces
|
|
207
|
-
|
|
207
|
+
`wireai-activation-x.y.z.tgz` containing `dist` + `src` + `metro` + README +
|
|
208
208
|
CHANGELOG. The primary channel is **npm** (`npm install @wireai/activation`); a
|
|
209
|
-
tarball (`npm install
|
|
209
|
+
tarball (`npm install ./wireai-activation-x.y.z.tgz`) or a `git+ssh://…` install
|
|
210
210
|
work too for pre-publish or pinned builds.
|
|
211
211
|
|
|
212
212
|
## Quick start
|
|
@@ -252,7 +252,7 @@ import { WireOnboarding } from "@wireai/activation";
|
|
|
252
252
|
| `persistKey` | `string` | Override the storage key (default `wireai:session:<appId>`). Scope per-user if one device can onboard multiple accounts mid-flow. With `storage` only. |
|
|
253
253
|
| `retainSessionOnComplete` | `boolean` | Opt IN: keep the persisted session seed alive across completion. A multi-stage signup that re-enters onboarding (typically a `key=` remount) then resumes the SAME session instead of minting a fresh `metadata.sessionId`, which the backend adopts as a second `contextId` and double-counts as a `session_started`. Omitted or `false` clears the seed on completion, the legacy single-stage behavior, so leaving it unset changes nothing. With `true`, freshness comes from `sessionTtlMs` plus an explicit new-run signal (scope a new `persistKey`, e.g. a per-signup id). With `storage` only. |
|
|
254
254
|
| `userContext` | `Record<string, string \| number \| boolean>` | **This is where the join key goes.** `user_context.device_key` is the only thing that joins this onboarding session to the app's later events, so pass `userContext={activationJoinContext(deviceKey)}`. If your app owns no device id, pass a working `storage` and leave this prop alone: the kit injects its own key, and only after it has confirmed the key actually persists. ⛔ Do not hand-build that key with `activationJoinContext(resolveAutoDeviceKey({ appId, storage }))`, because `resolveAutoDeviceKey` is synchronous by contract (it hands back a fresh mint and adopts the persisted id a storage read later) and it cannot tell you whether the id survives the launch at all. Since 0.12.2, omitting it no longer empties the funnel silently: with `storage`, the kit injects its own key (see [The join key](#the-join-key-device_key-never-session_id) and `autoJoinKey` below). Second job: any other non-PII context the app already knows (signup method, referral, plan, a HASHED user id), forwarded on session metadata + client events for funnel segmentation. **No PII on THIS prop** (no raw emails/names); primitives only; the server caps size/keys. See [Device & user context](#device--user-context). ⚠️ Distinct from the analytics/activation **`WireUserContext`** object (`createAnalytics` / `useWireActivation`), which additionally accepts an opt-in raw `userEmail` — see [Rich user context & PII](#rich-user-context--pii). |
|
|
255
|
-
| `autoJoinKey` | `boolean` | Opt OUT of the automatic join key. Default `true`. When this prop is left alone, `storage` is present **and actually persists** (0.13.0: a `storage` adapter that rejects or throws is treated exactly like no `storage` — the kit declines and says so, because a key it cannot persist is a different key on every launch, which corrupts `min_sessions` rather than merely leaving the join empty), and `userContext` carries no `device_key`, the kit injects its own per-install key (the same id `createAnalytics` / `createWireActivation` mint and persist), so the `activated` funnel joins with no host wiring. Pass `autoJoinKey={false}` if you genuinely want an UNLINKED onboarding session: that restores the pre-0.12.2 behavior exactly, and the dev warning fires again. It never overrides a `device_key` you
|
|
255
|
+
| `autoJoinKey` | `boolean` | Opt OUT of the automatic join key. Default `true`. When this prop is left alone, `storage` is present **and actually persists** (0.13.0: a `storage` adapter that rejects or throws is treated exactly like no `storage` — the kit declines and says so, because a key it cannot persist is a different key on every launch, which corrupts `min_sessions` rather than merely leaving the join empty), and `userContext` carries no `device_key`, the kit injects its own per-install key (the same id `createAnalytics` / `createWireActivation` mint and persist), so the `activated` funnel joins with no host wiring. Pass `autoJoinKey={false}` if you genuinely want an UNLINKED onboarding session: that restores the pre-0.12.2 behavior exactly, and the dev warning fires again. It never overrides a `device_key` you actually supplied, and it cannot inject without `storage` (see [The join key](#the-join-key-device_key-never-session_id)). The kit counts yours as supplied only when it is a string with at least one non-whitespace character, so a non-string or an all-whitespace `device_key` is treated as absent and the auto key is merged over it. |
|
|
256
256
|
| `userId` | `string` | Your own OPAQUE user id, so onboarding sessions reconcile to real users later (console sessions to your user table / GA4 users). Optional and supports **late binding**: present at mount it rides the session-start metadata; if it changes mid-session (the user just registered) the kit emits an `identify` event; available only after the flow, use `identifyOnboarding(...)`. **No PII** (not an email/name/phone); trimmed and capped at 128 chars. See [User identity](#user-identity). |
|
|
257
257
|
|
|
258
258
|
## Helpers (the reusable substrate)
|
|
@@ -378,6 +378,8 @@ await clearUserContext({ storage, appId }); // the wire/activation path (then re
|
|
|
378
378
|
|
|
379
379
|
**3. The auto `device_key` is a per-install identifier.** When you supply no `deviceKey`, the analytics and activation surfaces auto-mint a stable per-install `device_key`, persist it via `storage` (`wireai:analytics:deviceKey:<appId>`), and stamp it on every event so the server's review/questionnaire gating and A/B stickiness work out of the box. It carries no hardware id, no IDFA/GAID, and cannot be joined across apps (the privacy category of a first-party cookie), but it IS a persistent per-install id. If you adopt these surfaces, declare it in your App Privacy / Data Safety accordingly. Supply your own `deviceKey` to override it.
|
|
380
380
|
|
|
381
|
+
**The review gate is on that list too, even if it is the only surface you mount.** Since 0.15.0 `<ReviewGate>` stamps `meta.device_key` onto its `POST /v1/reviews` body when you passed none. That id is the unit the server scopes the review's idempotency key to, and a post without it gets the key discarded. The gate adopts whatever per-install id the analytics and activation surfaces already registered in this process, so it never opens a second id space. On a reviews-only host, where nothing else registered one, it mints its own instead. `<ReviewGate>` takes no `storage` prop, so that minted id is process-scoped: a fresh value on each launch, never persisted, rather than a stable per-install one. Either way it leaves the device inside a review payload. Declare it. Pass your own non-blank `meta.device_key` and it is forwarded verbatim instead.
|
|
382
|
+
|
|
381
383
|
### Do not pass `sessionId` to `createAnalytics`
|
|
382
384
|
|
|
383
385
|
`CreateAnalyticsConfig.sessionId` is an opt-out knob, not a default. Set it and **every** event that
|
|
@@ -801,8 +803,12 @@ consuming app, since the kit has no local TS install).
|
|
|
801
803
|
|
|
802
804
|
The kit ships a performance-first guided-tour engine on a **subpath** so the main
|
|
803
805
|
barrel stays dependency-free — import it from `@wireai/activation/coachmarks`. It
|
|
804
|
-
|
|
805
|
-
(UI-thread gesture + ring animation) and `expo-blur` (the frosted spotlight).
|
|
806
|
+
can USE two **optional** peers, and needs neither: `react-native-reanimated`
|
|
807
|
+
(UI-thread gesture + ring animation) and `expo-blur` (the frosted spotlight). Both
|
|
808
|
+
sit behind a guarded lazy require, so importing the subpath never pulls them into
|
|
809
|
+
your bundle. Without reanimated the tour renders statically (the ring, tooltip,
|
|
810
|
+
copy and gestures are unchanged — nothing animates); without expo-blur the frost
|
|
811
|
+
becomes a plain dimmed scrim.
|
|
806
812
|
Source consumers get it through the existing `@wireai/activation/*` tsconfig path
|
|
807
813
|
mapping; installed apps resolve the subpath export.
|
|
808
814
|
|
|
@@ -814,8 +820,10 @@ below) — nothing to rewire.
|
|
|
814
820
|
**1. Mount the provider once, around your `NavigationContainer`.** It renders the
|
|
815
821
|
overlay host as a root sibling so a ring can paint _above_ the bottom tab bar
|
|
816
822
|
(which react-navigation draws over screen components). It takes a **synchronous**
|
|
817
|
-
storage adapter — gate reads
|
|
818
|
-
|
|
823
|
+
storage adapter — gate reads resolve during render. An async adapter (e.g.
|
|
824
|
+
AsyncStorage handed straight in) is caught by a probe and the gates FAIL CLOSED:
|
|
825
|
+
coachmarks, showcases and the review/questionnaire gates all stay silent, and a
|
|
826
|
+
dev-build warning names the fix. An MMKV wrapper is three lines:
|
|
819
827
|
|
|
820
828
|
```tsx
|
|
821
829
|
import { CoachmarkProvider } from "@wireai/activation/coachmarks";
|
|
@@ -922,6 +930,12 @@ theme colors, and gates once through the same storage as the tours
|
|
|
922
930
|
(`wire_showcase_<id>_seen`; `isTestingCoachmark` bypasses it). If already seen it
|
|
923
931
|
renders nothing and calls `onDone` from an effect.
|
|
924
932
|
|
|
933
|
+
The pager is a guarded lazy require, so importing `@wireai/activation/showcase`
|
|
934
|
+
does not pull it into your bundle. If you never install it the showcase renders
|
|
935
|
+
nothing and calls `onDone` — your flow advances exactly as if the deck had been
|
|
936
|
+
seen — and the seen gate is NOT written, so installing the peer later still plays
|
|
937
|
+
the deck once.
|
|
938
|
+
|
|
925
939
|
**Recommended placement (v0.3.0+): AFTER `onComplete`, as a personalized value
|
|
926
940
|
bridge.** The user just told you what they want in `result.answers`, so the
|
|
927
941
|
terminal recap is the highest-value slot to show the 2-3 slides that answer it,
|
|
@@ -1235,7 +1249,7 @@ Skip the onboarding half and the purchase events are still valid on their own, t
|
|
|
1235
1249
|
|
|
1236
1250
|
Three rules govern it:
|
|
1237
1251
|
|
|
1238
|
-
- **Your key always wins.**
|
|
1252
|
+
- **Your key always wins.** A `device_key` you pass is forwarded verbatim, untrimmed, and nothing is merged over it — as long as it is a string with at least one non-whitespace character, which is the kit's entire test for "the host supplied one" (the trim is used only to decide presence). A non-string (the `userContext` type also allows numbers and booleans) or an all-whitespace value is not a usable join key: it is treated as absent and the auto key is merged over it. Existing integrations that pass a real key see a byte-identical payload.
|
|
1239
1253
|
- **It needs `storage`.** Without persistence the kit's id is minted fresh on every launch, and a per-launch key is worse than no key: the server counts `min_sessions` by distinct app-opens grouped on `device_key`, so it corrupts the counter instead of leaving the join empty. With no `storage` the kit declines to inject and warns in dev.
|
|
1240
1254
|
- **You can opt out.** `autoJoinKey={false}` restores the pre-0.12.2 behavior for a host that genuinely wants an unlinked session. The dev warning fires again there, naming the opt-out as the reason nothing was injected.
|
|
1241
1255
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-
|
|
2
|
-
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
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-
|
|
1
|
+
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-s5QxA-ci.mjs';
|
|
2
|
+
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-orZy5p1e.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-orZy5p1e.mjs';
|
|
4
4
|
import { W as WireOnboardingStorage } from '../types-BpwiRpA8.mjs';
|
|
5
|
-
import '../types-
|
|
5
|
+
import '../types-Byx306Kv.mjs';
|
|
6
6
|
import '../types-BKfpdZzX.mjs';
|
|
7
|
-
import '../types-
|
|
7
|
+
import '../types-D_0B0yay.mjs';
|
|
8
8
|
import 'react';
|
|
9
9
|
import 'wireai-rn';
|
|
10
10
|
import 'react-native';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-
|
|
2
|
-
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
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-
|
|
1
|
+
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-xqqSFqxs.js';
|
|
2
|
+
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-CFSRZ2wg.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-CFSRZ2wg.js';
|
|
4
4
|
import { W as WireOnboardingStorage } from '../types-BpwiRpA8.js';
|
|
5
|
-
import '../types-
|
|
5
|
+
import '../types-tdATL5z0.js';
|
|
6
6
|
import '../types-BKfpdZzX.js';
|
|
7
|
-
import '../types-
|
|
7
|
+
import '../types-D_0B0yay.js';
|
|
8
8
|
import 'react';
|
|
9
9
|
import 'wireai-rn';
|
|
10
10
|
import 'react-native';
|