@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.
Files changed (67) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +23 -9
  4. package/dist/analytics/index.d.mts +5 -5
  5. package/dist/analytics/index.d.ts +5 -5
  6. package/dist/analytics/index.js.map +1 -1
  7. package/dist/analytics/index.mjs.map +1 -1
  8. package/dist/coachmarks/index.d.mts +13 -5
  9. package/dist/coachmarks/index.d.ts +13 -5
  10. package/dist/coachmarks/index.js +252 -58
  11. package/dist/coachmarks/index.js.map +1 -1
  12. package/dist/coachmarks/index.mjs +204 -9
  13. package/dist/coachmarks/index.mjs.map +1 -1
  14. package/dist/{currentSession-CW_5Mq4O.d.ts → currentSession-CFSRZ2wg.d.ts} +10 -4
  15. package/dist/{currentSession-CUvTOchb.d.mts → currentSession-orZy5p1e.d.mts} +10 -4
  16. package/dist/{decision-Bgo17oH7.d.mts → decision-3vWLuBlO.d.ts} +11 -2
  17. package/dist/{decision-Bkh_LigV.d.ts → decision-yBj2AyPW.d.mts} +11 -2
  18. package/dist/index.d.mts +2 -2
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/questionnaire/index.d.mts +3 -3
  23. package/dist/questionnaire/index.d.ts +3 -3
  24. package/dist/questionnaire/index.js +48 -3
  25. package/dist/questionnaire/index.js.map +1 -1
  26. package/dist/questionnaire/index.mjs +48 -3
  27. package/dist/questionnaire/index.mjs.map +1 -1
  28. package/dist/reviews/index.d.mts +10 -6
  29. package/dist/reviews/index.d.ts +10 -6
  30. package/dist/reviews/index.js +167 -18
  31. package/dist/reviews/index.js.map +1 -1
  32. package/dist/reviews/index.mjs +167 -18
  33. package/dist/reviews/index.mjs.map +1 -1
  34. package/dist/showcase/index.d.mts +1 -1
  35. package/dist/showcase/index.d.ts +1 -1
  36. package/dist/showcase/index.js +282 -48
  37. package/dist/showcase/index.js.map +1 -1
  38. package/dist/showcase/index.mjs +248 -12
  39. package/dist/showcase/index.mjs.map +1 -1
  40. package/dist/{transport-j5gFfJhK.d.mts → transport-s5QxA-ci.d.mts} +18 -13
  41. package/dist/{transport-B_0SgCBe.d.ts → transport-xqqSFqxs.d.ts} +18 -13
  42. package/dist/{types-Cju-1_jT.d.mts → types-Byx306Kv.d.mts} +25 -10
  43. package/dist/{types-BcmagF6K.d.mts → types-D_0B0yay.d.mts} +7 -2
  44. package/dist/{types-BcmagF6K.d.ts → types-D_0B0yay.d.ts} +7 -2
  45. package/dist/{types-h2BZvl1t.d.ts → types-tdATL5z0.d.ts} +25 -10
  46. package/metro/index.js +16 -5
  47. package/package.json +1 -1
  48. package/src/WireOnboarding.tsx +4 -1
  49. package/src/coachmarks/GestureHint.tsx +16 -7
  50. package/src/coachmarks/SpotlightOverlay.tsx +19 -13
  51. package/src/coachmarks/index.ts +7 -2
  52. package/src/coachmarks/reanimated.ts +342 -0
  53. package/src/coachmarks/runtime.ts +103 -2
  54. package/src/coachmarks/types.ts +7 -2
  55. package/src/context/deviceId.ts +32 -0
  56. package/src/reviews/ReviewGate.tsx +86 -37
  57. package/src/reviews/decision.ts +11 -1
  58. package/src/reviews/idempotency.ts +71 -0
  59. package/src/reviews/index.ts +4 -2
  60. package/src/reviews/runtime.ts +5 -3
  61. package/src/reviews/transport.ts +17 -12
  62. package/src/reviews/types.ts +7 -0
  63. package/src/showcase/FeatureShowcase.tsx +25 -9
  64. package/src/showcase/blazejOnboarding.ts +136 -0
  65. package/src/showcase/index.ts +8 -4
  66. package/src/types.ts +10 -4
  67. package/src/utils/submitResult.ts +17 -9
@@ -1,4 +1,4 @@
1
- import { k as ReviewTarget, S as SubmitResult, g as ReviewDecisionResponse, j as ReviewSubmission } from './types-h2BZvl1t.js';
1
+ import { k as ReviewTarget, S as SubmitResult, g as ReviewDecisionResponse, j as ReviewSubmission } from './types-tdATL5z0.js';
2
2
 
3
3
  /**
4
4
  * What became of a review POST — the SHARED three-outcome verdict, re-exported under the name the
@@ -25,23 +25,28 @@ type ReviewSubmitResult = SubmitResult;
25
25
  *
26
26
  * It was, for exactly one unpublished release, and the boolean was the bug. `false` meant both
27
27
  * "nothing reached the server" and "the server answered non-2xx", and the one caller that reads
28
- * this (`ReviewGate.postOnce`) treats `false` as "still owed" and re-posts. But the server mints
29
- * its own row id (`create_review` / `_new_id()`) and `CreateReviewRequest` carries no id, so there
30
- * is NO idempotency key on the wire: a 502 returned AFTER the insert commits means the re-post
31
- * writes a SECOND row, double-counting `count` and corrupting `avg` the precise corruption the
32
- * one-row latch exists to prevent. A response of any status proves the server was reached, and
33
- * that is a different question from whether it liked the row. So the two are different values.
28
+ * this (`ReviewGate.postOnce`) treats `false` as "still owed" and re-posts. At the time there was
29
+ * no idempotency key on the wire at all, so a 502 returned AFTER the insert commits meant the
30
+ * re-post wrote a SECOND row, double-counting `count` and corrupting `avg` the precise corruption
31
+ * the one-row latch exists to prevent. A response of any status proves the server was reached, and
32
+ * that is a different question from whether it liked the row. So the two are different values, and
33
+ * they stay different: the split is what lets the caller decide, and the retry-on-`rejected`
34
+ * question is a recovery-budget decision the key does not answer on its own.
34
35
  *
35
36
  * ── THE RESIDUAL, STATED HONESTLY ────────────────────────────────────────────────────────────
36
37
  *
37
38
  * `unsent` is not proof the server never got the row. A connection dropped after the request was
38
39
  * written — or after the row committed — surfaces as a thrown/rejected `fetch` here, exactly like
39
- * an offline device. Retrying only on `unsent` is therefore SAFER, not SAFE: it removes the
40
- * double-post the server itself told us about, and leaves the narrow window where the answer never
41
- * made it back onto the wire. Closing that window needs a CLIENT-MINTED IDEMPOTENCY KEY the server
42
- * upserts on, which is a server change (`CreateReviewRequest` + `create_review`) and is not
43
- * something the kit can fake. Until it exists, prefer losing a row over inventing one: a lost
44
- * detractor is a gap in the data, a duplicated one is a lie in the data.
40
+ * an offline device. That used to make the `unsent` re-post SAFER rather than SAFE, and it is what
41
+ * the client-minted idempotency key now covers: `ReviewSubmission.idempotency_key` reaches
42
+ * `CreateReviewRequest`, and the server upserts the derived row rather than inserting a second one.
43
+ *
44
+ * IT IS A CAPABILITY, NOT A GUARANTEE, and the condition is not ours to enforce from in here. The
45
+ * server honours the key only when the post also carries a unit it can scope it to
46
+ * `meta.device_key`, else `session_id` — and ignores it outright otherwise. `ReviewGate` fills the
47
+ * device key in for exactly this reason, but this function posts whatever body it is handed, so a
48
+ * caller that assembles its own body without a unit is back in the old world where a lost detractor
49
+ * is a gap in the data and a duplicated one is a lie in the data.
45
50
  */
46
51
  declare const submitReview: (target: ReviewTarget | undefined, review: ReviewSubmission) => Promise<ReviewSubmitResult>;
47
52
  /**
@@ -1,5 +1,5 @@
1
1
  import { O as OnboardingTheme } from './types-BKfpdZzX.mjs';
2
- import { C as CoachmarkStorage } from './types-BcmagF6K.mjs';
2
+ import { C as CoachmarkStorage } from './types-D_0B0yay.mjs';
3
3
  import { a as WireFeatures, b as WireFeaturesConfig } from './types-BpwiRpA8.mjs';
4
4
 
5
5
  /**
@@ -25,20 +25,28 @@ import { a as WireFeatures, b as WireFeaturesConfig } from './types-BpwiRpA8.mjs
25
25
  *
26
26
  * `submitReview` was a boolean for exactly one unpublished release, and the boolean WAS the bug.
27
27
  * `false` meant both "nothing reached the server" and "the server answered non-2xx", and the caller
28
- * treats `false` as "still owed" and re-posts. But the server mints its own row id
29
- * (`create_review` / `_new_id()`), and neither `CreateReviewRequest` nor the questionnaire response
30
- * body carries a client id, so there is NO idempotency key on the wire: a 502 returned AFTER the
31
- * insert commits means the re-post writes a SECOND row. A response of any status proves the server
32
- * was reached, and that is a different question from whether it liked the payload.
28
+ * treats `false` as "still owed" and re-posts. At the time neither body carried a client id, so
29
+ * there was NO idempotency key on the wire on either surface: a 502 returned AFTER the insert
30
+ * commits meant the re-post wrote a SECOND row. A response of any status proves the server was
31
+ * reached, and that is a different question from whether it liked the payload which is why the
32
+ * three values stay, on both surfaces, whatever the wire gains.
33
33
  *
34
34
  * ── THE RESIDUAL, STATED HONESTLY ─────────────────────────────────────────────────────────────
35
35
  *
36
36
  * `unsent` is not proof the server never got it. A connection dropped after the request was written
37
37
  * — or after the row committed — surfaces as a thrown/rejected `fetch`, exactly like an offline
38
- * device. Retrying only on `unsent` is therefore SAFER, not SAFE. Closing that window needs a
39
- * CLIENT-MINTED IDEMPOTENCY KEY the server upserts on, which is a server change and not something
40
- * the kit can fake. Until it exists, prefer losing a row over inventing one: a lost detractor is a
41
- * gap in the data, a duplicated one is a lie in the data.
38
+ * device. Closing that window needs a client-minted idempotency key the server upserts on, and AS
39
+ * OF 2026-08-19 THE TWO SURFACES DIFFER do not read this paragraph as one fact about both:
40
+ *
41
+ * REVIEWS closed. `CreateReviewRequest` takes an optional `idempotency_key` and the server
42
+ * upserts a derived row on it, so a `ReviewGate` re-post merges instead of duplicating. It is a
43
+ * capability, not a guarantee: the server honours the key only when the post also carries a
44
+ * unit to scope it to (`meta.device_key`, else `session_id`), so a caller that strips both is
45
+ * back to the old behaviour.
46
+ * • QUESTIONNAIRES — still open, and still exactly as this paragraph used to read for both. The
47
+ * response body carries no client id and the server has no idempotency path for it at all, so
48
+ * retrying only on `unsent` there is SAFER, not SAFE. Prefer losing a row over inventing one: a
49
+ * lost detractor is a gap in the data, a duplicated one is a lie in the data.
42
50
  */
43
51
  type SubmitResult = "accepted" | "rejected" | "unsent";
44
52
 
@@ -119,6 +127,13 @@ interface ReviewSubmission {
119
127
  contact?: string;
120
128
  session_id?: string;
121
129
  source?: string;
130
+ /**
131
+ * OPTIONAL client-minted key for the two-phase submit: two posts carrying the same key upsert ONE
132
+ * row instead of creating two. The server honours it ONLY when the post also carries a resolvable
133
+ * user — `meta.device_key`, else `session_id` — and silently ignores it otherwise, so it always
134
+ * travels with one of those. One key per gate impression, stable across that impression's posts.
135
+ */
136
+ idempotency_key?: string;
122
137
  meta?: Record<string, unknown>;
123
138
  }
124
139
  /**
@@ -18,8 +18,13 @@ interface TargetRect {
18
18
  }
19
19
  /**
20
20
  * The host-injected gate storage. SYNCHRONOUS on purpose — a coachmark's
21
- * "seen?" gate must resolve during render with no async flash (otherwise a ring
22
- * momentarily paints before the read resolves and says "already seen"). MMKV's
21
+ * "seen?" gate must resolve during render. This is ENFORCED, fail closed: a
22
+ * probe on first read catches an adapter whose `getItem` returns a thenable
23
+ * (AsyncStorage handed straight in) and substitutes a closed adapter — every
24
+ * gate reads "seen", nothing shows, nothing fires, one dev warning names the
25
+ * fix (see `validateGateStorage` in ./runtime). A raw async adapter would
26
+ * otherwise report every gate unseen forever: tours replay each open, and a
27
+ * review gate with `minSessions` 0/1 re-fires each open. MMKV's
23
28
  * `getString`/`set` fit as-is; an in-memory `Map` wrapper works for tests. This
24
29
  * is deliberately separate from the async `WireOnboardingStorage` used for
25
30
  * session persistence.
@@ -18,8 +18,13 @@ interface TargetRect {
18
18
  }
19
19
  /**
20
20
  * The host-injected gate storage. SYNCHRONOUS on purpose — a coachmark's
21
- * "seen?" gate must resolve during render with no async flash (otherwise a ring
22
- * momentarily paints before the read resolves and says "already seen"). MMKV's
21
+ * "seen?" gate must resolve during render. This is ENFORCED, fail closed: a
22
+ * probe on first read catches an adapter whose `getItem` returns a thenable
23
+ * (AsyncStorage handed straight in) and substitutes a closed adapter — every
24
+ * gate reads "seen", nothing shows, nothing fires, one dev warning names the
25
+ * fix (see `validateGateStorage` in ./runtime). A raw async adapter would
26
+ * otherwise report every gate unseen forever: tours replay each open, and a
27
+ * review gate with `minSessions` 0/1 re-fires each open. MMKV's
23
28
  * `getString`/`set` fit as-is; an in-memory `Map` wrapper works for tests. This
24
29
  * is deliberately separate from the async `WireOnboardingStorage` used for
25
30
  * session persistence.
@@ -1,5 +1,5 @@
1
1
  import { O as OnboardingTheme } from './types-BKfpdZzX.js';
2
- import { C as CoachmarkStorage } from './types-BcmagF6K.js';
2
+ import { C as CoachmarkStorage } from './types-D_0B0yay.js';
3
3
  import { a as WireFeatures, b as WireFeaturesConfig } from './types-BpwiRpA8.js';
4
4
 
5
5
  /**
@@ -25,20 +25,28 @@ import { a as WireFeatures, b as WireFeaturesConfig } from './types-BpwiRpA8.js'
25
25
  *
26
26
  * `submitReview` was a boolean for exactly one unpublished release, and the boolean WAS the bug.
27
27
  * `false` meant both "nothing reached the server" and "the server answered non-2xx", and the caller
28
- * treats `false` as "still owed" and re-posts. But the server mints its own row id
29
- * (`create_review` / `_new_id()`), and neither `CreateReviewRequest` nor the questionnaire response
30
- * body carries a client id, so there is NO idempotency key on the wire: a 502 returned AFTER the
31
- * insert commits means the re-post writes a SECOND row. A response of any status proves the server
32
- * was reached, and that is a different question from whether it liked the payload.
28
+ * treats `false` as "still owed" and re-posts. At the time neither body carried a client id, so
29
+ * there was NO idempotency key on the wire on either surface: a 502 returned AFTER the insert
30
+ * commits meant the re-post wrote a SECOND row. A response of any status proves the server was
31
+ * reached, and that is a different question from whether it liked the payload which is why the
32
+ * three values stay, on both surfaces, whatever the wire gains.
33
33
  *
34
34
  * ── THE RESIDUAL, STATED HONESTLY ─────────────────────────────────────────────────────────────
35
35
  *
36
36
  * `unsent` is not proof the server never got it. A connection dropped after the request was written
37
37
  * — or after the row committed — surfaces as a thrown/rejected `fetch`, exactly like an offline
38
- * device. Retrying only on `unsent` is therefore SAFER, not SAFE. Closing that window needs a
39
- * CLIENT-MINTED IDEMPOTENCY KEY the server upserts on, which is a server change and not something
40
- * the kit can fake. Until it exists, prefer losing a row over inventing one: a lost detractor is a
41
- * gap in the data, a duplicated one is a lie in the data.
38
+ * device. Closing that window needs a client-minted idempotency key the server upserts on, and AS
39
+ * OF 2026-08-19 THE TWO SURFACES DIFFER do not read this paragraph as one fact about both:
40
+ *
41
+ * REVIEWS closed. `CreateReviewRequest` takes an optional `idempotency_key` and the server
42
+ * upserts a derived row on it, so a `ReviewGate` re-post merges instead of duplicating. It is a
43
+ * capability, not a guarantee: the server honours the key only when the post also carries a
44
+ * unit to scope it to (`meta.device_key`, else `session_id`), so a caller that strips both is
45
+ * back to the old behaviour.
46
+ * • QUESTIONNAIRES — still open, and still exactly as this paragraph used to read for both. The
47
+ * response body carries no client id and the server has no idempotency path for it at all, so
48
+ * retrying only on `unsent` there is SAFER, not SAFE. Prefer losing a row over inventing one: a
49
+ * lost detractor is a gap in the data, a duplicated one is a lie in the data.
42
50
  */
43
51
  type SubmitResult = "accepted" | "rejected" | "unsent";
44
52
 
@@ -119,6 +127,13 @@ interface ReviewSubmission {
119
127
  contact?: string;
120
128
  session_id?: string;
121
129
  source?: string;
130
+ /**
131
+ * OPTIONAL client-minted key for the two-phase submit: two posts carrying the same key upsert ONE
132
+ * row instead of creating two. The server honours it ONLY when the post also carries a resolvable
133
+ * user — `meta.device_key`, else `session_id` — and silently ignores it otherwise, so it always
134
+ * travels with one of those. One key per gate impression, stable across that impression's posts.
135
+ */
136
+ idempotency_key?: string;
122
137
  meta?: Record<string, unknown>;
123
138
  }
124
139
  /**
package/metro/index.js CHANGED
@@ -71,11 +71,16 @@ const SINGLE_INSTANCE_DEPS = ['react', 'react-native', 'wireai-rn', 'zod'];
71
71
  * only thing holding it up.
72
72
  *
73
73
  * ONLY modules read through a guarded require/import belong here. A module the kit imports
74
- * STATICALLY at module scope (react-native-reanimated,
75
- * @blazejkustra/react-native-onboarding) must NOT be stubbed: an empty module would turn a loud
76
- * build-time "Unable to resolve module" into a silent `undefined` component that crashes at
77
- * render, which is strictly worse. `test/canary/metroResolution.test.tsx` keeps this list in sync
78
- * with the source by asking Metro which deps it marks optional.
74
+ * STATICALLY at module scope must NOT be stubbed: an empty module would turn a loud build-time
75
+ * "Unable to resolve module" into a silent `undefined` component that crashes at render, which is
76
+ * strictly worse. That exclusion used to name `react-native-reanimated` and
77
+ * `@blazejkustra/react-native-onboarding` as of 0.14.3 both are guarded (`coachmarks/reanimated.ts`,
78
+ * `showcase/blazejOnboarding.ts`) and so they belong on the list, not in the exclusion. The kit now
79
+ * has NO statically-imported optional peer: every module it can live without is reached through a
80
+ * guard, and everything it imports statically (react, react-native, react-native-safe-area-context,
81
+ * wireai-rn, zod) is a REQUIRED peer whose absence must stay a hard build error.
82
+ * `test/canary/metroResolution.test.tsx` keeps this list in sync with the source by asking Metro
83
+ * which deps it marks optional.
79
84
  */
80
85
  const OPTIONAL_MODULES = [
81
86
  // src/icons/expoIcons.ts — icon families; absent → WireIcon renders null.
@@ -91,6 +96,12 @@ const OPTIONAL_MODULES = [
91
96
  'expo-haptics',
92
97
  // src/coachmarks/expoBlur.ts — the coachmark spotlight frost; absent → plain dimmed scrim.
93
98
  'expo-blur',
99
+ // src/coachmarks/reanimated.ts — the coachmark/showcase animation engine; absent → the overlay
100
+ // and the gesture glyph render statically, at their resting frame.
101
+ 'react-native-reanimated',
102
+ // src/showcase/blazejOnboarding.ts — the FeatureShowcase slide pager; absent → the showcase
103
+ // renders null and calls onDone, so the host's flow still advances.
104
+ '@blazejkustra/react-native-onboarding',
94
105
  ];
95
106
 
96
107
  function withWireOnboarding(config, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wireai/activation",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "description": "Premium, fully-themable drop-in AI onboarding kit for React Native / Expo, on top of the open-source wireai-rn SDK.",
6
6
  "author": "Malik Chohra <malik@getwireai.com>",
@@ -135,7 +135,10 @@ export const WireOnboarding: React.FC<WireOnboardingProps> = ({
135
135
  // per-install `device_key` the analytics surfaces auto-mint and persist — so the default wiring
136
136
  // produces a joined funnel instead of a silent zero. Three conditions, all necessary:
137
137
  //
138
- // • `missingJoinKey` — a host-supplied key ALWAYS wins, verbatim, and is never touched.
138
+ // • `missingJoinKey` — a host-supplied key ALWAYS wins, verbatim, and is never touched. What
139
+ // COUNTS as supplied is `resolveIdentity`'s predicate above: a string with at least one
140
+ // non-whitespace character. A non-string or an all-whitespace `device_key` yields no record,
141
+ // so it reads as absent here and the auto key is merged over it.
139
142
  // • `autoJoinKey !== false` — the documented opt-out for a host that genuinely wants an
140
143
  // unlinked onboarding session; opting out restores the pre-0.12.2 behavior exactly.
141
144
  // • `storage` that actually WORKS — without persistence the auto id is process-scoped, so every
@@ -1,6 +1,8 @@
1
1
  import React, { useEffect, useMemo, type ReactNode } from "react";
2
2
  import { StyleSheet, View } from "react-native";
3
- import Animated, {
3
+
4
+ import {
5
+ Animated,
4
6
  useAnimatedStyle,
5
7
  useReducedMotion,
6
8
  useSharedValue,
@@ -8,15 +10,22 @@ import Animated, {
8
10
  withRepeat,
9
11
  withSequence,
10
12
  withTiming,
11
- } from "react-native-reanimated";
12
-
13
+ } from "./reanimated";
13
14
  import type { GestureKind } from "./types";
14
15
 
15
16
  /**
16
- * Abstract gesture pictogram shown beside a coachmark tooltip. Pure reanimated
17
- * every animation runs on the UI thread (shared values + worklets), never
18
- * setState, so it costs nothing on the JS thread. Honors Reduce Motion by
19
- * dropping the motion loop and rendering the glyph static.
17
+ * Abstract gesture pictogram shown beside a coachmark tooltip. With the OPTIONAL
18
+ * `react-native-reanimated` peer installed every animation runs on the UI thread
19
+ * (shared values + worklets), never setState, so it costs nothing on the JS
20
+ * thread. Honors Reduce Motion by dropping the motion loop and rendering the
21
+ * glyph static.
22
+ *
23
+ * The peer is reached through the guarded lazy require in `reanimated.ts`, never
24
+ * a static import, so `@wireai/activation/coachmarks` (and `/showcase`, which
25
+ * reaches this component through FeatureShowcase) builds on a host that never
26
+ * installed it. Absent, the glyph renders STATICALLY at its resting frame — the
27
+ * same capsules, ring, dot and motion track, at full opacity and no travel, with
28
+ * one render instead of a UI-thread loop. The pictogram never disappears.
20
29
  *
21
30
  * NO drawn-hand anatomy. Swipes render the standard two-finger swipe glyph: two
22
31
  * rounded vertical capsules (an abstract index + middle finger, slightly
@@ -6,7 +6,12 @@ import {
6
6
  useWindowDimensions,
7
7
  View,
8
8
  } from "react-native";
9
- import Animated, {
9
+
10
+ import { useOnboardingTheme } from "../theme/ThemeContext";
11
+ import { resolveBlurView } from "./expoBlur";
12
+ import { GestureHint } from "./GestureHint";
13
+ import {
14
+ Animated,
10
15
  FadeIn,
11
16
  FadeOut,
12
17
  useAnimatedProps,
@@ -16,11 +21,7 @@ import Animated, {
16
21
  withRepeat,
17
22
  withSequence,
18
23
  withTiming,
19
- } from "react-native-reanimated";
20
-
21
- import { useOnboardingTheme } from "../theme/ThemeContext";
22
- import { resolveBlurView } from "./expoBlur";
23
- import { GestureHint } from "./GestureHint";
24
+ } from "./reanimated";
24
25
  import type { GestureKind, Placement, TargetRect } from "./types";
25
26
 
26
27
  const BLUR_INTENSITY = 26;
@@ -55,14 +56,19 @@ export interface SpotlightOverlayProps {
55
56
  * root (above the tab bar) via CoachmarkOverlayHost.
56
57
  *
57
58
  * Performance: one BlurView (mounted only while a step is visible), the ring
58
- * glow is a single reanimated view, and all animation runs on the UI thread.
59
- * Reduce Motion drops the pulse.
59
+ * glow is a single animated view, and with `react-native-reanimated` installed
60
+ * every animation runs on the UI thread. Reduce Motion drops the pulse.
60
61
  *
61
- * `expo-blur` is OPTIONAL. It is resolved through a guarded lazy require (see
62
- * `expoBlur.ts`) and the animated component is built once on first use, so the
63
- * subpath never carries a static top-level import of the peer. When it is not
64
- * installed the frost drops to an equivalent dimmed scrim — a decorative frost
65
- * missing must never blank a tour, so the ring, copy and gestures are unchanged.
62
+ * BOTH native peers are OPTIONAL, and each is resolved through a guarded lazy
63
+ * require, so the subpath never carries a static top-level import of either:
64
+ * `expo-blur` (`expoBlur.ts`) absent, the frost drops to an equivalent
65
+ * dimmed scrim. The animated component is built once, on first use.
66
+ * `react-native-reanimated` (`reanimated.ts`) absent, the overlay renders
67
+ * STATICALLY at its resting frame: the ring sits at scale 1 (no pulse), the
68
+ * blur at full BLUR_INTENSITY (no fade-in), and the whole overlay appears
69
+ * and leaves without the fade. Nothing moves; everything renders.
70
+ * A missing decoration must never blank a tour, so the ring, tooltip, copy and
71
+ * gestures are identical in all four combinations.
66
72
  */
67
73
  const SpotlightOverlayComponent: React.FC<SpotlightOverlayProps> = ({
68
74
  message,
@@ -2,8 +2,13 @@
2
2
  * @wireai/activation/coachmarks — the performance-first guided-tour engine.
3
3
  *
4
4
  * Subpath entry, kept OUT of the main barrel so the core kit stays dependency-
5
- * free: importing this pulls in the optional peers `react-native-reanimated` and
6
- * `expo-blur`. The app declares WHERE things anchor (useCoachmarkAnchor) and
5
+ * free. Importing this pulls in NO native peer: `react-native-reanimated` and
6
+ * `expo-blur` are both OPTIONAL and both reached through guarded lazy requires
7
+ * (`reanimated.ts`, `expoBlur.ts`). Installed, you get UI-thread animation and a
8
+ * frosted spotlight; absent, the tour still renders — statically, and with a
9
+ * plain dimmed scrim instead of the frost. (This doc used to say the subpath
10
+ * "pulls in" both peers, which was true until 0.14.2 / 0.14.3 removed the static
11
+ * imports.) The app declares WHERE things anchor (useCoachmarkAnchor) and
7
12
  * WHICH tour plays (useCoachmarkTour); the kit owns the animation, measuring,
8
13
  * blur, ring, gesture hand, and one-overlay-at-a-time queue.
9
14
  *