@wireai/activation 0.15.0 → 0.16.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 +95 -20
  2. package/CHANGELOG.md +707 -0
  3. package/INTEGRATION_PROMPT.md +61 -23
  4. package/README.md +100 -25
  5. package/dist/analytics/index.d.mts +32 -10
  6. package/dist/analytics/index.d.ts +32 -10
  7. package/dist/analytics/index.js +288 -127
  8. package/dist/analytics/index.js.map +1 -1
  9. package/dist/analytics/index.mjs +288 -127
  10. package/dist/analytics/index.mjs.map +1 -1
  11. package/dist/coachmarks/index.d.mts +14 -0
  12. package/dist/coachmarks/index.d.ts +14 -0
  13. package/dist/coachmarks/index.js +73 -20
  14. package/dist/coachmarks/index.js.map +1 -1
  15. package/dist/coachmarks/index.mjs +73 -20
  16. package/dist/coachmarks/index.mjs.map +1 -1
  17. package/dist/{currentSession-orZy5p1e.d.mts → currentSession-Bz7G6lno.d.mts} +25 -35
  18. package/dist/{currentSession-CFSRZ2wg.d.ts → currentSession-z-CZ55ad.d.ts} +25 -35
  19. package/dist/index.d.mts +5 -2
  20. package/dist/index.d.ts +5 -2
  21. package/dist/index.js +125 -36
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +125 -36
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/questionnaire/index.d.mts +0 -13
  26. package/dist/questionnaire/index.d.ts +0 -13
  27. package/dist/questionnaire/index.js +154 -43
  28. package/dist/questionnaire/index.js.map +1 -1
  29. package/dist/questionnaire/index.mjs +155 -44
  30. package/dist/questionnaire/index.mjs.map +1 -1
  31. package/dist/reviews/index.js +159 -91
  32. package/dist/reviews/index.js.map +1 -1
  33. package/dist/reviews/index.mjs +160 -92
  34. package/dist/reviews/index.mjs.map +1 -1
  35. package/dist/showcase/index.js +59 -18
  36. package/dist/showcase/index.js.map +1 -1
  37. package/dist/showcase/index.mjs +60 -19
  38. package/dist/showcase/index.mjs.map +1 -1
  39. package/llms.txt +9 -9
  40. package/package.json +6 -9
  41. package/src/analytics/currentSession.ts +141 -4
  42. package/src/analytics/index.ts +6 -1
  43. package/src/analytics/reportClientEvent.ts +19 -10
  44. package/src/analytics/useAnalytics.ts +74 -15
  45. package/src/analytics/wireDoctor.ts +152 -7
  46. package/src/coachmarks/CoachmarkProvider.tsx +26 -5
  47. package/src/coachmarks/runtime.ts +53 -0
  48. package/src/coachmarks/useCoachmarkTour.ts +51 -1
  49. package/src/context/deviceId.ts +49 -15
  50. package/src/features/WireFeaturesProvider.tsx +72 -12
  51. package/src/features/fetchWireFeatures.ts +49 -11
  52. package/src/features/useWireFeatures.ts +39 -3
  53. package/src/identity/identityRecord.ts +15 -2
  54. package/src/questionnaire/QuestionnaireGate.tsx +40 -1
  55. package/src/questionnaire/transport.ts +22 -8
  56. package/src/questionnaire/useQuestionnaireGate.ts +58 -7
  57. package/src/reviews/ReviewGate.tsx +39 -0
  58. package/src/reviews/idempotency.ts +38 -0
  59. package/src/reviews/runtime.ts +39 -10
  60. package/src/reviews/transport.ts +22 -8
  61. package/src/reviews/useReviewGate.ts +57 -7
  62. package/src/session-analytics/lifecycle.ts +16 -0
  63. package/src/session-analytics/useLifecycleEvents.ts +30 -2
  64. package/src/session-analytics/useSessionStart.ts +22 -2
  65. package/src/showcase/FeatureShowcase.tsx +50 -3
  66. package/src/types.ts +5 -4
  67. package/src/utils/withDeadline.ts +70 -0
@@ -29,6 +29,11 @@ interface CoachmarkProviderProps {
29
29
  * pre-resolved `features`, OR a `featuresConfig` (serverUrl + apiKey) to lazily fetch once, OR
30
30
  * mount a `WireFeaturesProvider` above (context is read automatically). Omit all three and
31
31
  * coachmarks stay on (fail-open) — zero behavior change for hosts that never adopt flags.
32
+ *
33
+ * While a fetch is still in flight, a NEW tour does not arm (so a tenant who switched the module
34
+ * off gets no first impression), and a tour already in flight is never torn down. Nothing is
35
+ * spent by that wait: no once-gate is consumed, so an ON answer plays the tour in full. A failed
36
+ * fetch is an answer — fail-open is unchanged.
32
37
  */
33
38
  features?: WireFeatures;
34
39
  /** Lazy-fetch config for the flags, used when `features` is absent and no provider is above. */
@@ -130,6 +135,15 @@ interface UseCoachmarkTourOptions {
130
135
  * `show()` is a no-op while the flag is off, so a tour that carried on would paint nothing, could
131
136
  * not be tapped to advance, and would never reach a terminal state.
132
137
  *
138
+ * ARM vs TERMINAL — the third gate, and why it is not the second one. Until the tenant's flags
139
+ * land, that kill switch reads ON because nobody has asked yet, so a tenant who turned coachmarks
140
+ * OFF still got the first tour of every cold start. The wait CANNOT be expressed by writing the
141
+ * kill switch false, precisely because that flag is terminal: it would end the tour of every
142
+ * ENABLED tenant on every cold start, a worse defect than the one being fixed. So an UNANSWERED
143
+ * verdict (`areCoachmarksResolved`, written by CoachmarkProvider from the features' `settled`)
144
+ * blocks only the ARM below — a tour already in flight is never touched by it — and costs nothing
145
+ * while it waits: no timer, no overlay, no once-gate spent.
146
+ *
133
147
  * IMPORTANT: `steps` MUST be a stable (memoized) array. If a new array identity
134
148
  * is passed on every render the drive effect re-runs and re-shows the current
135
149
  * step (wasteful anchor re-resolves / overlay churn), and analytics can
@@ -29,6 +29,11 @@ interface CoachmarkProviderProps {
29
29
  * pre-resolved `features`, OR a `featuresConfig` (serverUrl + apiKey) to lazily fetch once, OR
30
30
  * mount a `WireFeaturesProvider` above (context is read automatically). Omit all three and
31
31
  * coachmarks stay on (fail-open) — zero behavior change for hosts that never adopt flags.
32
+ *
33
+ * While a fetch is still in flight, a NEW tour does not arm (so a tenant who switched the module
34
+ * off gets no first impression), and a tour already in flight is never torn down. Nothing is
35
+ * spent by that wait: no once-gate is consumed, so an ON answer plays the tour in full. A failed
36
+ * fetch is an answer — fail-open is unchanged.
32
37
  */
33
38
  features?: WireFeatures;
34
39
  /** Lazy-fetch config for the flags, used when `features` is absent and no provider is above. */
@@ -130,6 +135,15 @@ interface UseCoachmarkTourOptions {
130
135
  * `show()` is a no-op while the flag is off, so a tour that carried on would paint nothing, could
131
136
  * not be tapped to advance, and would never reach a terminal state.
132
137
  *
138
+ * ARM vs TERMINAL — the third gate, and why it is not the second one. Until the tenant's flags
139
+ * land, that kill switch reads ON because nobody has asked yet, so a tenant who turned coachmarks
140
+ * OFF still got the first tour of every cold start. The wait CANNOT be expressed by writing the
141
+ * kill switch false, precisely because that flag is terminal: it would end the tour of every
142
+ * ENABLED tenant on every cold start, a worse defect than the one being fixed. So an UNANSWERED
143
+ * verdict (`areCoachmarksResolved`, written by CoachmarkProvider from the features' `settled`)
144
+ * blocks only the ARM below — a tour already in flight is never touched by it — and costs nothing
145
+ * while it waits: no timer, no overlay, no once-gate spent.
146
+ *
133
147
  * IMPORTANT: `steps` MUST be a stable (memoized) array. If a new array identity
134
148
  * is passed on every render the drive effect re-runs and re-shows the current
135
149
  * step (wasteful anchor re-resolves / overlay churn), and analytics can
@@ -95,17 +95,31 @@ var failOpen = async (storage, key) => {
95
95
  const cached3 = await readCachedFeatures(storage, key);
96
96
  return (_a = cached3 == null ? void 0 : cached3.features) != null ? _a : defaultWireFeatures;
97
97
  };
98
- var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
98
+ var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
99
+ var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
99
100
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
100
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
101
- try {
102
- return await fetch(url, {
101
+ let timer;
102
+ const expired = new Promise((resolve) => {
103
+ timer = setTimeout(() => {
104
+ controller == null ? void 0 : controller.abort();
105
+ resolve(NO_ANSWER);
106
+ }, timeoutMs);
107
+ });
108
+ const exchange = (async () => {
109
+ const res = await fetch(url, {
103
110
  method: "GET",
104
111
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
105
112
  signal: controller == null ? void 0 : controller.signal
106
113
  });
114
+ if (!res || !res.ok) return NO_ANSWER;
115
+ return await res.json();
116
+ })();
117
+ try {
118
+ return await Promise.race([exchange, expired]);
107
119
  } finally {
108
120
  clearTimeout(timer);
121
+ void exchange.catch(() => {
122
+ });
109
123
  }
110
124
  };
111
125
  var fetchWireFeatures = async (config) => {
@@ -115,9 +129,8 @@ var fetchWireFeatures = async (config) => {
115
129
  const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
116
130
  const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
117
131
  try {
118
- const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
119
- if (!res || !res.ok) return failOpen(storage, key);
120
- const json = await res.json();
132
+ const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
133
+ if (json === NO_ANSWER) return failOpen(storage, key);
121
134
  const features = parseWireFeatures(json);
122
135
  if (storage) writeCachedFeatures(storage, key, features);
123
136
  return features;
@@ -133,39 +146,51 @@ var useStableValue = (value, isEqual) => {
133
146
  return ref.current;
134
147
  };
135
148
  var sameFetchKey = (a, b) => (a == null ? void 0 : a.serverUrl) === (b == null ? void 0 : b.serverUrl) && (a == null ? void 0 : a.apiKey) === (b == null ? void 0 : b.apiKey) && (a == null ? void 0 : a.appId) === (b == null ? void 0 : b.appId);
136
- var useWireFeatures = (config) => {
149
+ var useWireFeaturesState = (config) => {
137
150
  const stableConfig = useStableValue(config, sameFetchKey);
138
151
  const [flags, setFlags] = React3.useState(defaultWireFeatures);
139
152
  const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
153
+ const [settled, setSettled] = React3.useState(!canFetch);
140
154
  React3.useEffect(() => {
141
155
  if (!canFetch) {
142
156
  setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
157
+ setSettled(true);
143
158
  return;
144
159
  }
145
160
  let alive = true;
146
161
  void fetchWireFeatures(stableConfig).then((next) => {
147
162
  if (!alive) return;
148
163
  setFlags((prev) => featuresEqual(prev, next) ? prev : next);
164
+ setSettled(true);
149
165
  });
150
166
  return () => {
151
167
  alive = false;
152
168
  };
153
169
  }, [canFetch, stableConfig]);
154
- return flags;
170
+ return React3.useMemo(() => ({ flags, settled }), [flags, settled]);
155
171
  };
156
172
  var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
157
- var globalObj = globalThis;
158
- if (!globalObj[CONTEXT_SYMBOL]) {
159
- globalObj[CONTEXT_SYMBOL] = React3.createContext(null);
173
+ var featuresContextGlobal = globalThis;
174
+ if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
175
+ featuresContextGlobal[CONTEXT_SYMBOL] = React3.createContext(null);
160
176
  }
161
- var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
177
+ var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
178
+ var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
179
+ var settledContextGlobal = globalThis;
180
+ if (!settledContextGlobal[SETTLED_SYMBOL]) {
181
+ settledContextGlobal[SETTLED_SYMBOL] = React3.createContext(true);
182
+ }
183
+ var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
162
184
  var useWireFeaturesContext = () => React3.useContext(WireFeaturesContext);
163
- var useResolvedFeatures = (options) => {
185
+ var useResolvedFeaturesState = (options) => {
164
186
  var _a, _b, _c;
165
187
  const ctx = useWireFeaturesContext();
188
+ const ctxSettled = React3.useContext(WireFeaturesSettledContext);
166
189
  const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
167
- const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
168
- return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
190
+ const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
191
+ const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
192
+ const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
193
+ return React3.useMemo(() => ({ flags, settled }), [flags, settled]);
169
194
  };
170
195
 
171
196
  // src/coachmarks/runtime.ts
@@ -201,6 +226,17 @@ var setCoachmarksEnabled = (value) => {
201
226
  notifyCoachmarksEnabled();
202
227
  };
203
228
  var areCoachmarksEnabled = () => coachmarkRuntime().coachmarksEnabled;
229
+ var setCoachmarksResolved = (value) => {
230
+ var _a;
231
+ const runtime = coachmarkRuntime();
232
+ if (((_a = runtime.coachmarksResolved) != null ? _a : true) === value) return;
233
+ runtime.coachmarksResolved = value;
234
+ notifyCoachmarksEnabled();
235
+ };
236
+ var areCoachmarksResolved = () => {
237
+ var _a;
238
+ return (_a = coachmarkRuntime().coachmarksResolved) != null ? _a : true;
239
+ };
204
240
  var subscribeCoachmarksEnabled = (listener) => {
205
241
  const listeners2 = enabledListeners();
206
242
  listeners2.add(listener);
@@ -1013,22 +1049,28 @@ var CoachmarkProvider = ({
1013
1049
  featuresConfig,
1014
1050
  children
1015
1051
  }) => {
1016
- const flags = useResolvedFeatures({ flags: features, config: featuresConfig });
1017
- const coachmarksOn = flags.coachmarks.enabled;
1052
+ const { flags, settled } = useResolvedFeaturesState({
1053
+ flags: features,
1054
+ config: featuresConfig
1055
+ });
1056
+ const coachmarksOn = settled ? flags.coachmarks.enabled : true;
1018
1057
  setCoachmarkStorage(storage);
1019
1058
  setCoachmarkTesting(isTestingCoachmark);
1059
+ setCoachmarksResolved(settled);
1020
1060
  setCoachmarksEnabled(coachmarksOn);
1021
1061
  React3.useEffect(() => {
1022
1062
  setCoachmarkStorage(storage);
1023
1063
  setCoachmarkTesting(isTestingCoachmark);
1064
+ setCoachmarksResolved(settled);
1024
1065
  setCoachmarksEnabled(coachmarksOn);
1025
1066
  return () => {
1026
1067
  if (getCoachmarkStorage() === storage) {
1027
1068
  setCoachmarkStorage(null);
1028
1069
  }
1029
1070
  setCoachmarksEnabled(true);
1071
+ setCoachmarksResolved(true);
1030
1072
  };
1031
- }, [storage, isTestingCoachmark, coachmarksOn]);
1073
+ }, [storage, isTestingCoachmark, coachmarksOn, settled]);
1032
1074
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1033
1075
  children,
1034
1076
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1131,6 +1173,11 @@ var useCoachmarkTour = (steps, {
1131
1173
  areCoachmarksEnabled,
1132
1174
  areCoachmarksEnabled
1133
1175
  );
1176
+ const coachmarksResolved = React3.useSyncExternalStore(
1177
+ subscribeCoachmarksEnabled,
1178
+ areCoachmarksResolved,
1179
+ areCoachmarksResolved
1180
+ );
1134
1181
  const exit = React3.useCallback(
1135
1182
  (writeGate) => {
1136
1183
  if (finishedRef.current) return;
@@ -1145,14 +1192,16 @@ var useCoachmarkTour = (steps, {
1145
1192
  const finish = React3.useCallback(() => exit(true), [exit]);
1146
1193
  React3.useEffect(() => {
1147
1194
  if (!enabled || startedRef.current) return void 0;
1195
+ if (steps.length === 0) return void 0;
1148
1196
  if (!coachmarksOn) return void 0;
1197
+ if (!coachmarksResolved) return void 0;
1149
1198
  if (showOnce && hasSeenGate(coachmarkGateKey(tourId))) return void 0;
1150
1199
  const timer = setTimeout(() => {
1151
1200
  startedRef.current = true;
1152
1201
  setActiveIndex(0);
1153
1202
  }, startDelayMs);
1154
1203
  return () => clearTimeout(timer);
1155
- }, [enabled, coachmarksOn, startDelayMs, showOnce, tourId]);
1204
+ }, [enabled, coachmarksOn, coachmarksResolved, startDelayMs, showOnce, tourId, steps.length]);
1156
1205
  React3.useEffect(() => {
1157
1206
  if (activeIndex === null) return void 0;
1158
1207
  if (!coachmarksOn) {
@@ -1163,6 +1212,10 @@ var useCoachmarkTour = (steps, {
1163
1212
  if (startedRef.current && !finishedRef.current) coachmarkOverlay.hide();
1164
1213
  return void 0;
1165
1214
  }
1215
+ if (steps.length === 0) {
1216
+ exit(false);
1217
+ return void 0;
1218
+ }
1166
1219
  if (activeIndex >= steps.length) {
1167
1220
  finish();
1168
1221
  return void 0;