@wireai/activation 0.14.0 → 0.14.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,87 @@
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.14.2] - 2026-08-18
7
+
8
+ **PATCH: no public surface gained, lost or renamed a member.** `SpotlightOverlay` keeps its props
9
+ and its export; `expo-blur` stays an OPTIONAL peer.
10
+
11
+ ### Fixed
12
+
13
+ - **The coachmarks subpath no longer force-imports its optional peer `expo-blur`.**
14
+ `SpotlightOverlay` did `import { BlurView } from "expo-blur"` at module scope and built its
15
+ animated blur component at module load, and `coachmarks/index.ts` re-exports the overlay with no
16
+ wildcard escape — so importing ANYTHING from `@wireai/activation/coachmarks`, even
17
+ `setCoachmarkStorage`, dragged `expo-blur` in. A host that took the peer's "optional" at its word
18
+ and skipped installing it hit a Metro resolution failure the moment it touched coachmarks, which
19
+ is why the AI Mobile Launcher tiers (and any Expo Go build) could not adopt the capability. The
20
+ peer is now resolved through a guarded lazy require with a string-literal specifier inside a
21
+ try/catch (the same shape as `icons/expoIcons.ts` / `device/appVersion.ts`), and the animated
22
+ component is built once on first render, so the subpath carries no static top-level import of the
23
+ peer.
24
+ - **Degradation, by design:** when `expo-blur` is absent the spotlight drops the frost to an
25
+ equivalent dimmed scrim. The ring, tooltip and gestures behave identically — a missing
26
+ decorative frost never blanks a tour, never crashes, never renders empty.
27
+ - `metro/index.js` `OPTIONAL_MODULES` and `tsup.config.ts` now treat `expo-blur` as a guarded
28
+ optional peer (moved off the static-peer list), so the built `dist/` collects it as optional and
29
+ the metro helper's empty-module safety net covers it for hosts that disable Metro's
30
+ `allowOptionalDependencies`.
31
+
32
+ ## [0.14.1] - 2026-08-18
33
+
34
+ Two rulings from the 0.14.0 audit, applied. Both were reproduced there and left for a decision
35
+ rather than guessed at; both decisions came back on 2026-08-18.
36
+
37
+ **PATCH: no public surface gained, lost or renamed a member.** But read the first entry before you
38
+ upgrade — it is a behaviour change you will see in your funnel, not a silent internal fix.
39
+
40
+ ### Fixed
41
+
42
+ - **The review/questionnaire gates counted JS PROCESSES, not app-opens, so `minSessions` stalled at
43
+ 1 on iOS.** `useLifecycleEvents` already defines an app-open correctly — a foreground after 30+
44
+ minutes in the background — and fires a fresh `app.session_started` for it. The device-side
45
+ counter behind `minSessions` did not follow: it pinned one id for the life of the JS process, and
46
+ iOS suspends apps rather than killing them. So a user who opened the app every day for a week
47
+ produced seven server-side opens while `wire_review_<id>_sessions` stayed at 1, and any gate with
48
+ the fail-closed `minSessions: 2` default could never fire on a phone that is never force-quit —
49
+ which is most phones. **⚠️ UPGRADE NOTE: gates that have been silently dormant will start firing.**
50
+ That is the fix, but if you have been reading a flat prompt rate as "users decline", the number
51
+ will move. The counter still refuses the case it was pinned for in the first place: on a cold
52
+ start the gate renders (and counts) before the root lifecycle effect registers the session id, and
53
+ that first registration NAMES the open already counted rather than starting a second one — so the
54
+ 2026-07-16 incident, where `minSessions: 2` became satisfiable inside a user's very first launch,
55
+ stays closed. One accepted under-count remains, deliberately: if the mount open never registers (a
56
+ refused non-durable device key over a broken store) and a later foreground is the first
57
+ registration the launch sees, it is adopted rather than counted. A fail-closed gate may only err
58
+ toward prompting less.
59
+
60
+ - **A permission record that could not be READ is no longer read as "nothing settled".** A timed-out
61
+ or throwing storage read arrived as `[]`, which is indistinguishable from a genuine empty record,
62
+ and the flow acted on that guess twice: it re-showed a permission screen the user may already have
63
+ answered — spending the ONE native prompt iOS ever grants — and the answer then persisted a record
64
+ containing just that one id, OVER whatever the store actually held. A slow adapter could therefore
65
+ permanently shrink the memory that exists to prevent the re-ask. The read now reports whether the
66
+ store answered at all; on "it did not", permission screens are suppressed for that session and
67
+ nothing is written back. A successful read of a corrupt, absent or other-session entry is still a
68
+ genuine empty and still shows the screen. The trade is stated rather than hidden: on a host whose
69
+ adapter never answers, configured permission screens silently never show, so it warns in dev
70
+ naming the adapter.
71
+
72
+ ### Changed
73
+
74
+ - **`@wireai/activation:processOpenId` is re-declared LIVE (was LATCH).** The governance canary in
75
+ `test/canary/globalSlotDiscipline.test.tsx` asserted that this slot must NOT follow the session
76
+ registry, which is what froze the counter above; it now drives the slot through two real opens and
77
+ requires it to move. The cold-start property it used to guard — the first registration of a launch
78
+ must not re-pin — is asserted in `src/reviews/sessionCount.test.ts`, alongside the incident it
79
+ exists for. The slot holds the pinned open id plus the registered id observed when it was pinned,
80
+ because "is this a new open?" is only answerable against what was last seen.
81
+
82
+ Tests 808 → 814, all green, and the deferred `todo` reproduction is now a passing assertion.
83
+ Every fix was falsified first: reverting the counter fix reddens exactly the app-open test plus the
84
+ slot canary and leaves the cold-start test green; collapsing the unknown read back to empty reddens
85
+ both unit tests and the end-to-end canary while its positive control stays green.
86
+
6
87
  ## [0.14.0] - 2026-08-17
7
88
 
8
89
  A durability release, in two rounds. Round 1 stopped the two counted lifecycle events from riding a
package/README.md CHANGED
@@ -1162,6 +1162,11 @@ const gate = useReviewGate({ config: { id: "home", minSessions: 2 }, decision, s
1162
1162
  await track("journal_done"); // POST stored → revalidation bumps → effect re-fetches → gate can fire
1163
1163
  ```
1164
1164
 
1165
+ `minSessions` counts **app-opens**, not mounts and not launches: a foreground after 30+ minutes in
1166
+ the background is a new one. That matters on iOS, where an app is suspended rather than killed — a
1167
+ user who never force-quits still advances the counter. (Before 0.14.1 it counted JS processes, so
1168
+ on those devices it stayed at 1 forever and a `minSessions: 2` gate could never fire.)
1169
+
1165
1170
  The questionnaire gate uses the identical shape: swap `fetchReviewDecision` / `useReviewGate`
1166
1171
  for `fetchQuestionnaireDecision` / `useQuestionnaireGate` from `@wireai/activation/questionnaire`
1167
1172
  (both take a `decision` prop). One `wire.track` call feeds every subscribed gate.