@wireai/activation 0.14.3 → 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 (85) hide show
  1. package/AGENTS.md +96 -21
  2. package/CHANGELOG.md +780 -0
  3. package/INTEGRATION_PROMPT.md +61 -23
  4. package/README.md +110 -31
  5. package/dist/analytics/index.d.mts +35 -13
  6. package/dist/analytics/index.d.ts +35 -13
  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 +15 -1
  12. package/dist/coachmarks/index.d.ts +15 -1
  13. package/dist/coachmarks/index.js +120 -22
  14. package/dist/coachmarks/index.js.map +1 -1
  15. package/dist/coachmarks/index.mjs +120 -22
  16. package/dist/coachmarks/index.mjs.map +1 -1
  17. package/dist/{currentSession-CUvTOchb.d.mts → currentSession-Bz7G6lno.d.mts} +35 -39
  18. package/dist/{currentSession-CW_5Mq4O.d.ts → currentSession-z-CZ55ad.d.ts} +35 -39
  19. package/dist/{decision-Bgo17oH7.d.mts → decision-3vWLuBlO.d.ts} +11 -2
  20. package/dist/{decision-Bkh_LigV.d.ts → decision-yBj2AyPW.d.mts} +11 -2
  21. package/dist/index.d.mts +5 -2
  22. package/dist/index.d.ts +5 -2
  23. package/dist/index.js +125 -36
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mjs +125 -36
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/questionnaire/index.d.mts +3 -16
  28. package/dist/questionnaire/index.d.ts +3 -16
  29. package/dist/questionnaire/index.js +202 -46
  30. package/dist/questionnaire/index.js.map +1 -1
  31. package/dist/questionnaire/index.mjs +203 -47
  32. package/dist/questionnaire/index.mjs.map +1 -1
  33. package/dist/reviews/index.d.mts +10 -6
  34. package/dist/reviews/index.d.ts +10 -6
  35. package/dist/reviews/index.js +269 -52
  36. package/dist/reviews/index.js.map +1 -1
  37. package/dist/reviews/index.mjs +270 -53
  38. package/dist/reviews/index.mjs.map +1 -1
  39. package/dist/showcase/index.d.mts +1 -1
  40. package/dist/showcase/index.d.ts +1 -1
  41. package/dist/showcase/index.js +106 -20
  42. package/dist/showcase/index.js.map +1 -1
  43. package/dist/showcase/index.mjs +107 -21
  44. package/dist/showcase/index.mjs.map +1 -1
  45. package/dist/{transport-j5gFfJhK.d.mts → transport-s5QxA-ci.d.mts} +18 -13
  46. package/dist/{transport-B_0SgCBe.d.ts → transport-xqqSFqxs.d.ts} +18 -13
  47. package/dist/{types-Cju-1_jT.d.mts → types-Byx306Kv.d.mts} +25 -10
  48. package/dist/{types-BcmagF6K.d.mts → types-D_0B0yay.d.mts} +7 -2
  49. package/dist/{types-BcmagF6K.d.ts → types-D_0B0yay.d.ts} +7 -2
  50. package/dist/{types-h2BZvl1t.d.ts → types-tdATL5z0.d.ts} +25 -10
  51. package/llms.txt +9 -9
  52. package/package.json +6 -9
  53. package/src/WireOnboarding.tsx +4 -1
  54. package/src/analytics/currentSession.ts +141 -4
  55. package/src/analytics/index.ts +6 -1
  56. package/src/analytics/reportClientEvent.ts +19 -10
  57. package/src/analytics/useAnalytics.ts +74 -15
  58. package/src/analytics/wireDoctor.ts +152 -7
  59. package/src/coachmarks/CoachmarkProvider.tsx +26 -5
  60. package/src/coachmarks/runtime.ts +156 -2
  61. package/src/coachmarks/types.ts +7 -2
  62. package/src/coachmarks/useCoachmarkTour.ts +51 -1
  63. package/src/context/deviceId.ts +72 -6
  64. package/src/features/WireFeaturesProvider.tsx +72 -12
  65. package/src/features/fetchWireFeatures.ts +49 -11
  66. package/src/features/useWireFeatures.ts +39 -3
  67. package/src/identity/identityRecord.ts +15 -2
  68. package/src/questionnaire/QuestionnaireGate.tsx +40 -1
  69. package/src/questionnaire/transport.ts +22 -8
  70. package/src/questionnaire/useQuestionnaireGate.ts +58 -7
  71. package/src/reviews/ReviewGate.tsx +125 -37
  72. package/src/reviews/decision.ts +11 -1
  73. package/src/reviews/idempotency.ts +109 -0
  74. package/src/reviews/index.ts +4 -2
  75. package/src/reviews/runtime.ts +44 -13
  76. package/src/reviews/transport.ts +39 -20
  77. package/src/reviews/types.ts +7 -0
  78. package/src/reviews/useReviewGate.ts +57 -7
  79. package/src/session-analytics/lifecycle.ts +16 -0
  80. package/src/session-analytics/useLifecycleEvents.ts +30 -2
  81. package/src/session-analytics/useSessionStart.ts +22 -2
  82. package/src/showcase/FeatureShowcase.tsx +50 -3
  83. package/src/types.ts +15 -8
  84. package/src/utils/submitResult.ts +17 -9
  85. package/src/utils/withDeadline.ts +70 -0
@@ -89,17 +89,31 @@ var failOpen = async (storage, key) => {
89
89
  const cached3 = await readCachedFeatures(storage, key);
90
90
  return (_a = cached3 == null ? void 0 : cached3.features) != null ? _a : defaultWireFeatures;
91
91
  };
92
- var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
92
+ var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
93
+ var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
93
94
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
94
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
95
- try {
96
- return await fetch(url, {
95
+ let timer;
96
+ const expired = new Promise((resolve) => {
97
+ timer = setTimeout(() => {
98
+ controller == null ? void 0 : controller.abort();
99
+ resolve(NO_ANSWER);
100
+ }, timeoutMs);
101
+ });
102
+ const exchange = (async () => {
103
+ const res = await fetch(url, {
97
104
  method: "GET",
98
105
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
99
106
  signal: controller == null ? void 0 : controller.signal
100
107
  });
108
+ if (!res || !res.ok) return NO_ANSWER;
109
+ return await res.json();
110
+ })();
111
+ try {
112
+ return await Promise.race([exchange, expired]);
101
113
  } finally {
102
114
  clearTimeout(timer);
115
+ void exchange.catch(() => {
116
+ });
103
117
  }
104
118
  };
105
119
  var fetchWireFeatures = async (config) => {
@@ -109,9 +123,8 @@ var fetchWireFeatures = async (config) => {
109
123
  const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
110
124
  const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
111
125
  try {
112
- const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
113
- if (!res || !res.ok) return failOpen(storage, key);
114
- const json = await res.json();
126
+ const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
127
+ if (json === NO_ANSWER) return failOpen(storage, key);
115
128
  const features = parseWireFeatures(json);
116
129
  if (storage) writeCachedFeatures(storage, key, features);
117
130
  return features;
@@ -127,39 +140,51 @@ var useStableValue = (value, isEqual) => {
127
140
  return ref.current;
128
141
  };
129
142
  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);
130
- var useWireFeatures = (config) => {
143
+ var useWireFeaturesState = (config) => {
131
144
  const stableConfig = useStableValue(config, sameFetchKey);
132
145
  const [flags, setFlags] = useState(defaultWireFeatures);
133
146
  const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
147
+ const [settled, setSettled] = useState(!canFetch);
134
148
  useEffect(() => {
135
149
  if (!canFetch) {
136
150
  setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
151
+ setSettled(true);
137
152
  return;
138
153
  }
139
154
  let alive = true;
140
155
  void fetchWireFeatures(stableConfig).then((next) => {
141
156
  if (!alive) return;
142
157
  setFlags((prev) => featuresEqual(prev, next) ? prev : next);
158
+ setSettled(true);
143
159
  });
144
160
  return () => {
145
161
  alive = false;
146
162
  };
147
163
  }, [canFetch, stableConfig]);
148
- return flags;
164
+ return useMemo(() => ({ flags, settled }), [flags, settled]);
149
165
  };
150
166
  var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
151
- var globalObj = globalThis;
152
- if (!globalObj[CONTEXT_SYMBOL]) {
153
- globalObj[CONTEXT_SYMBOL] = createContext(null);
167
+ var featuresContextGlobal = globalThis;
168
+ if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
169
+ featuresContextGlobal[CONTEXT_SYMBOL] = createContext(null);
170
+ }
171
+ var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
172
+ var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
173
+ var settledContextGlobal = globalThis;
174
+ if (!settledContextGlobal[SETTLED_SYMBOL]) {
175
+ settledContextGlobal[SETTLED_SYMBOL] = createContext(true);
154
176
  }
155
- var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
177
+ var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
156
178
  var useWireFeaturesContext = () => useContext(WireFeaturesContext);
157
- var useResolvedFeatures = (options) => {
179
+ var useResolvedFeaturesState = (options) => {
158
180
  var _a, _b, _c;
159
181
  const ctx = useWireFeaturesContext();
182
+ const ctxSettled = useContext(WireFeaturesSettledContext);
160
183
  const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
161
- const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
162
- return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
184
+ const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
185
+ const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
186
+ const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
187
+ return useMemo(() => ({ flags, settled }), [flags, settled]);
163
188
  };
164
189
 
165
190
  // src/coachmarks/runtime.ts
@@ -195,6 +220,17 @@ var setCoachmarksEnabled = (value) => {
195
220
  notifyCoachmarksEnabled();
196
221
  };
197
222
  var areCoachmarksEnabled = () => coachmarkRuntime().coachmarksEnabled;
223
+ var setCoachmarksResolved = (value) => {
224
+ var _a;
225
+ const runtime = coachmarkRuntime();
226
+ if (((_a = runtime.coachmarksResolved) != null ? _a : true) === value) return;
227
+ runtime.coachmarksResolved = value;
228
+ notifyCoachmarksEnabled();
229
+ };
230
+ var areCoachmarksResolved = () => {
231
+ var _a;
232
+ return (_a = coachmarkRuntime().coachmarksResolved) != null ? _a : true;
233
+ };
198
234
  var subscribeCoachmarksEnabled = (listener) => {
199
235
  const listeners2 = enabledListeners();
200
236
  listeners2.add(listener);
@@ -213,9 +249,54 @@ var isCoachmarkTesting = () => coachmarkRuntime().testing;
213
249
  var coachmarkGateKey = (tourId) => `wire_coachmark_${tourId}_seen`;
214
250
  var showcaseGateKey = (showcaseId) => `wire_showcase_${showcaseId}_seen`;
215
251
  var SEEN_VALUE = "1";
252
+ var GATE_PROBE_KEY = "wire_gate_probe";
253
+ var ASYNC_GATE_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
254
+ "@wireai/activation:asyncGateStorageWarned"
255
+ );
256
+ var asyncGateWarnGlobal = globalThis;
257
+ var closedGateStorage = Object.freeze({
258
+ getItem: () => SEEN_VALUE,
259
+ setItem: () => {
260
+ }
261
+ });
262
+ var syncGateAdapters = /* @__PURE__ */ new WeakSet();
263
+ var asyncGateAdapters = /* @__PURE__ */ new WeakSet();
264
+ var warnAsyncGateStorage = () => {
265
+ if (asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT]) return;
266
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
267
+ if (typeof console === "undefined" || !console.warn) return;
268
+ asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT] = true;
269
+ console.warn(
270
+ "[wireai] the gate storage's getItem returned a Promise \u2014 an async adapter (e.g. AsyncStorage) cannot drive the once-per-user gates, so coachmark tours, showcases and the review/questionnaire gates are all DISABLED (fail closed: nothing shows, nothing fires) until a synchronous adapter is provided. Pass a sync adapter (e.g. MMKV: `{ getItem: (k) => mmkv.getString(k) ?? null, setItem: (k, v) => mmkv.set(k, v) }`) to CoachmarkProvider or the gate's `storage` prop."
271
+ );
272
+ };
273
+ var validateGateStorage = (storage) => {
274
+ if (!storage) return null;
275
+ if (syncGateAdapters.has(storage)) return storage;
276
+ if (asyncGateAdapters.has(storage)) return closedGateStorage;
277
+ let probed;
278
+ try {
279
+ probed = storage.getItem(GATE_PROBE_KEY);
280
+ } catch {
281
+ syncGateAdapters.add(storage);
282
+ return storage;
283
+ }
284
+ if (probed != null && typeof probed.then === "function") {
285
+ asyncGateAdapters.add(storage);
286
+ try {
287
+ probed.then(void 0, () => {
288
+ });
289
+ } catch {
290
+ }
291
+ warnAsyncGateStorage();
292
+ return closedGateStorage;
293
+ }
294
+ syncGateAdapters.add(storage);
295
+ return storage;
296
+ };
216
297
  var hasSeenGate = (key, storageOverride, testingOverride) => {
217
298
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return false;
218
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
299
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
219
300
  if (!storage) return false;
220
301
  try {
221
302
  return storage.getItem(key) === SEEN_VALUE;
@@ -225,7 +306,7 @@ var hasSeenGate = (key, storageOverride, testingOverride) => {
225
306
  };
226
307
  var markSeenGate = (key, storageOverride, testingOverride) => {
227
308
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return;
228
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
309
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
229
310
  if (!storage) return;
230
311
  try {
231
312
  storage.setItem(key, SEEN_VALUE);
@@ -962,22 +1043,28 @@ var CoachmarkProvider = ({
962
1043
  featuresConfig,
963
1044
  children
964
1045
  }) => {
965
- const flags = useResolvedFeatures({ flags: features, config: featuresConfig });
966
- const coachmarksOn = flags.coachmarks.enabled;
1046
+ const { flags, settled } = useResolvedFeaturesState({
1047
+ flags: features,
1048
+ config: featuresConfig
1049
+ });
1050
+ const coachmarksOn = settled ? flags.coachmarks.enabled : true;
967
1051
  setCoachmarkStorage(storage);
968
1052
  setCoachmarkTesting(isTestingCoachmark);
1053
+ setCoachmarksResolved(settled);
969
1054
  setCoachmarksEnabled(coachmarksOn);
970
1055
  useEffect(() => {
971
1056
  setCoachmarkStorage(storage);
972
1057
  setCoachmarkTesting(isTestingCoachmark);
1058
+ setCoachmarksResolved(settled);
973
1059
  setCoachmarksEnabled(coachmarksOn);
974
1060
  return () => {
975
1061
  if (getCoachmarkStorage() === storage) {
976
1062
  setCoachmarkStorage(null);
977
1063
  }
978
1064
  setCoachmarksEnabled(true);
1065
+ setCoachmarksResolved(true);
979
1066
  };
980
- }, [storage, isTestingCoachmark, coachmarksOn]);
1067
+ }, [storage, isTestingCoachmark, coachmarksOn, settled]);
981
1068
  return /* @__PURE__ */ jsxs(Fragment, { children: [
982
1069
  children,
983
1070
  /* @__PURE__ */ jsx(
@@ -1080,6 +1167,11 @@ var useCoachmarkTour = (steps, {
1080
1167
  areCoachmarksEnabled,
1081
1168
  areCoachmarksEnabled
1082
1169
  );
1170
+ const coachmarksResolved = useSyncExternalStore(
1171
+ subscribeCoachmarksEnabled,
1172
+ areCoachmarksResolved,
1173
+ areCoachmarksResolved
1174
+ );
1083
1175
  const exit = useCallback(
1084
1176
  (writeGate) => {
1085
1177
  if (finishedRef.current) return;
@@ -1094,14 +1186,16 @@ var useCoachmarkTour = (steps, {
1094
1186
  const finish = useCallback(() => exit(true), [exit]);
1095
1187
  useEffect(() => {
1096
1188
  if (!enabled || startedRef.current) return void 0;
1189
+ if (steps.length === 0) return void 0;
1097
1190
  if (!coachmarksOn) return void 0;
1191
+ if (!coachmarksResolved) return void 0;
1098
1192
  if (showOnce && hasSeenGate(coachmarkGateKey(tourId))) return void 0;
1099
1193
  const timer = setTimeout(() => {
1100
1194
  startedRef.current = true;
1101
1195
  setActiveIndex(0);
1102
1196
  }, startDelayMs);
1103
1197
  return () => clearTimeout(timer);
1104
- }, [enabled, coachmarksOn, startDelayMs, showOnce, tourId]);
1198
+ }, [enabled, coachmarksOn, coachmarksResolved, startDelayMs, showOnce, tourId, steps.length]);
1105
1199
  useEffect(() => {
1106
1200
  if (activeIndex === null) return void 0;
1107
1201
  if (!coachmarksOn) {
@@ -1112,6 +1206,10 @@ var useCoachmarkTour = (steps, {
1112
1206
  if (startedRef.current && !finishedRef.current) coachmarkOverlay.hide();
1113
1207
  return void 0;
1114
1208
  }
1209
+ if (steps.length === 0) {
1210
+ exit(false);
1211
+ return void 0;
1212
+ }
1115
1213
  if (activeIndex >= steps.length) {
1116
1214
  finish();
1117
1215
  return void 0;