@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
@@ -1,9 +1,9 @@
1
1
  import React__default from 'react';
2
2
  import { O as OnboardingTheme } from '../types-BKfpdZzX.mjs';
3
- import { C as CoachmarkStorage } from '../types-BcmagF6K.mjs';
3
+ import { C as CoachmarkStorage } from '../types-D_0B0yay.mjs';
4
4
  import { a as WireFeatures, b as WireFeaturesConfig } from '../types-BpwiRpA8.mjs';
5
- import { S as SubmitResult } from '../types-Cju-1_jT.mjs';
6
- export { G as GateRules, a as GateSignals, e as evaluateGate, r as resolveRules } from '../decision-Bgo17oH7.mjs';
5
+ import { S as SubmitResult } from '../types-Byx306Kv.mjs';
6
+ export { G as GateRules, a as GateSignals, e as evaluateGate, r as resolveRules } from '../decision-yBj2AyPW.mjs';
7
7
 
8
8
  /**
9
9
  * The firing verdict - the SAME contract on both sides of the seam. `decideQuestionnaire`
@@ -246,19 +246,6 @@ declare const buildQuestionnaireSubmission: (input: {
246
246
  meta?: QuestionnaireMeta;
247
247
  }) => QuestionnaireSubmission;
248
248
 
249
- /**
250
- * transport.ts - kit → Wire server requests for the questionnaire module, never throwing
251
- * (a pre-onboarding questionnaire must NEVER break the app). Mirrors `submitReview`: a thin
252
- * fetch wrapper, Bearer tenant key, and — since 0.13.3 — the SAME three-outcome delivery ack.
253
- *
254
- * • submitQuestionnaireResponse → POST {serverUrl}/v1/questionnaires/{id}/responses
255
- * • fetchQuestionnaireDecision → GET {serverUrl}/v1/questionnaires/decision (best-effort)
256
- *
257
- * The decision fetch is HARD fail-open on the "never show" side: an unreachable endpoint, a
258
- * non-2xx (incl. 404), a missing `fetch`, or bad JSON all resolve to null, so a host wiring
259
- * pure server-directed firing simply shows nothing - exactly the reviews decision seam.
260
- */
261
-
262
249
  /**
263
250
  * What became of a questionnaire POST. The SHARED verdict — identical values, identical meaning,
264
251
  * identical retry rule as `ReviewSubmitResult`. The reasoning lives once, in `utils/submitResult.ts`.
@@ -1,9 +1,9 @@
1
1
  import React__default from 'react';
2
2
  import { O as OnboardingTheme } from '../types-BKfpdZzX.js';
3
- import { C as CoachmarkStorage } from '../types-BcmagF6K.js';
3
+ import { C as CoachmarkStorage } from '../types-D_0B0yay.js';
4
4
  import { a as WireFeatures, b as WireFeaturesConfig } from '../types-BpwiRpA8.js';
5
- import { S as SubmitResult } from '../types-h2BZvl1t.js';
6
- export { G as GateRules, a as GateSignals, e as evaluateGate, r as resolveRules } from '../decision-Bkh_LigV.js';
5
+ import { S as SubmitResult } from '../types-tdATL5z0.js';
6
+ export { G as GateRules, a as GateSignals, e as evaluateGate, r as resolveRules } from '../decision-3vWLuBlO.js';
7
7
 
8
8
  /**
9
9
  * The firing verdict - the SAME contract on both sides of the seam. `decideQuestionnaire`
@@ -246,19 +246,6 @@ declare const buildQuestionnaireSubmission: (input: {
246
246
  meta?: QuestionnaireMeta;
247
247
  }) => QuestionnaireSubmission;
248
248
 
249
- /**
250
- * transport.ts - kit → Wire server requests for the questionnaire module, never throwing
251
- * (a pre-onboarding questionnaire must NEVER break the app). Mirrors `submitReview`: a thin
252
- * fetch wrapper, Bearer tenant key, and — since 0.13.3 — the SAME three-outcome delivery ack.
253
- *
254
- * • submitQuestionnaireResponse → POST {serverUrl}/v1/questionnaires/{id}/responses
255
- * • fetchQuestionnaireDecision → GET {serverUrl}/v1/questionnaires/decision (best-effort)
256
- *
257
- * The decision fetch is HARD fail-open on the "never show" side: an unreachable endpoint, a
258
- * non-2xx (incl. 404), a missing `fetch`, or bad JSON all resolve to null, so a host wiring
259
- * pure server-directed firing simply shows nothing - exactly the reviews decision seam.
260
- */
261
-
262
249
  /**
263
250
  * What became of a questionnaire POST. The SHARED verdict — identical values, identical meaning,
264
251
  * identical retry rule as `ReviewSubmitResult`. The reasoning lives once, in `utils/submitResult.ts`.
@@ -843,6 +843,28 @@ var toAnswers = (drafts) => {
843
843
  };
844
844
  };
845
845
 
846
+ // src/utils/withDeadline.ts
847
+ var DEADLINE_EXPIRED = /* @__PURE__ */ Symbol("wire-deadline-expired");
848
+ var DEFAULT_DEADLINE_MS = 15e3;
849
+ var withDeadline = async (exchange, timeoutMs = DEFAULT_DEADLINE_MS) => {
850
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
851
+ let timer;
852
+ const expired = new Promise((resolve) => {
853
+ timer = setTimeout(() => {
854
+ controller == null ? void 0 : controller.abort();
855
+ resolve(DEADLINE_EXPIRED);
856
+ }, timeoutMs);
857
+ });
858
+ const running = exchange(controller == null ? void 0 : controller.signal);
859
+ try {
860
+ return await Promise.race([running, expired]);
861
+ } finally {
862
+ clearTimeout(timer);
863
+ void running.catch(() => {
864
+ });
865
+ }
866
+ };
867
+
846
868
  // src/questionnaire/transport.ts
847
869
  var submitQuestionnaireResponse = async (target, id, submission) => {
848
870
  if (!(target == null ? void 0 : target.serverUrl) || !id) return "unsent";
@@ -851,11 +873,15 @@ var submitQuestionnaireResponse = async (target, id, submission) => {
851
873
  const url = `${base}/v1/questionnaires/${encodeURIComponent(id)}/responses`;
852
874
  const headers = { "Content-Type": "application/json" };
853
875
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
854
- const res = await fetch(url, {
855
- method: "POST",
856
- headers,
857
- body: JSON.stringify(submission)
858
- });
876
+ const res = await withDeadline(
877
+ (signal) => fetch(url, {
878
+ method: "POST",
879
+ headers,
880
+ body: JSON.stringify(submission),
881
+ signal
882
+ })
883
+ );
884
+ if (res === DEADLINE_EXPIRED) return "unsent";
859
885
  if (!res) return "unsent";
860
886
  return res.ok ? "accepted" : "rejected";
861
887
  } catch {
@@ -873,9 +899,12 @@ var fetchQuestionnaireDecision = async (target, options = {}) => {
873
899
  const url = `${base}/v1/questionnaires/decision${qs ? `?${qs}` : ""}`;
874
900
  const headers = {};
875
901
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
876
- const res = await fetch(url, { headers });
877
- if (!res || !res.ok) return null;
878
- const json = await res.json();
902
+ const json = await withDeadline(async (signal) => {
903
+ const res = await fetch(url, { headers, signal });
904
+ if (!res || !res.ok) return null;
905
+ return await res.json();
906
+ });
907
+ if (json === DEADLINE_EXPIRED) return null;
879
908
  if (!json || typeof json.fire !== "boolean") return null;
880
909
  return json;
881
910
  } catch {
@@ -954,6 +983,10 @@ var _QuestionnaireGate = ({
954
983
  }, [id, onResolved]);
955
984
  const postedRef = React8.useRef(false);
956
985
  const unackedRef = React8.useRef(null);
986
+ const recoverOnSettleRef = React8.useRef(false);
987
+ const recoveredRef = React8.useRef(false);
988
+ const postOnceRef = React8.useRef(() => {
989
+ });
957
990
  const postOnce = React8.useCallback(
958
991
  (body) => {
959
992
  if (postedRef.current) return;
@@ -962,6 +995,10 @@ var _QuestionnaireGate = ({
962
995
  void submitQuestionnaireResponse(target, id, body).then((result) => {
963
996
  if (result === "unsent") {
964
997
  postedRef.current = false;
998
+ if (recoverOnSettleRef.current && !recoveredRef.current) {
999
+ recoveredRef.current = true;
1000
+ postOnceRef.current(body);
1001
+ }
965
1002
  return;
966
1003
  }
967
1004
  unackedRef.current = null;
@@ -969,11 +1006,16 @@ var _QuestionnaireGate = ({
969
1006
  },
970
1007
  [target, id]
971
1008
  );
1009
+ postOnceRef.current = postOnce;
972
1010
  const recoverRef = React8.useRef(() => {
973
1011
  });
974
1012
  recoverRef.current = () => {
975
1013
  const undelivered = unackedRef.current;
976
- if (undelivered && !postedRef.current) postOnce(undelivered);
1014
+ if (undelivered && !postedRef.current) {
1015
+ postOnce(undelivered);
1016
+ return;
1017
+ }
1018
+ if (postedRef.current && undelivered) recoverOnSettleRef.current = true;
977
1019
  };
978
1020
  React8__default.default.useEffect(() => () => recoverRef.current(), []);
979
1021
  const finish = React8.useCallback(
@@ -1122,6 +1164,39 @@ var styles7 = reactNative.StyleSheet.create({
1122
1164
  backGlyph: { includeFontPadding: false }
1123
1165
  });
1124
1166
 
1167
+ // src/analytics/reportClientEvent.ts
1168
+ var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1169
+
1170
+ // src/analytics/currentSession.ts
1171
+ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
1172
+ "@wireai/activation:currentSessionId"
1173
+ );
1174
+ var globalSlot = globalThis;
1175
+ var SESSION_ID_LISTENERS_SLOT = /* @__PURE__ */ Symbol.for(
1176
+ "@wireai/activation:currentSessionIdListeners"
1177
+ );
1178
+ var listenerSlot = globalThis;
1179
+ var listeners = () => {
1180
+ const existing = listenerSlot[SESSION_ID_LISTENERS_SLOT];
1181
+ if (existing) return existing;
1182
+ const created = /* @__PURE__ */ new Set();
1183
+ listenerSlot[SESSION_ID_LISTENERS_SLOT] = created;
1184
+ return created;
1185
+ };
1186
+ var subscribeCurrentSessionId = (listener) => {
1187
+ const set = listeners();
1188
+ set.add(listener);
1189
+ return () => {
1190
+ set.delete(listener);
1191
+ };
1192
+ };
1193
+ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
1194
+ var MINTED_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
1195
+ "@wireai/activation:mintedSessionId"
1196
+ );
1197
+ var mintedSlot = globalThis;
1198
+ var isMintedSessionId = (id) => id !== void 0 && id === mintedSlot[MINTED_SESSION_ID_SLOT];
1199
+
1125
1200
  // src/coachmarks/runtime.ts
1126
1201
  var COACHMARK_RUNTIME_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:coachmarkRuntime");
1127
1202
  var runtimeGlobal = globalThis;
@@ -1135,9 +1210,54 @@ var coachmarkRuntime = () => {
1135
1210
  var getCoachmarkStorage = () => coachmarkRuntime().storage;
1136
1211
  var isCoachmarkTesting = () => coachmarkRuntime().testing;
1137
1212
  var SEEN_VALUE = "1";
1213
+ var GATE_PROBE_KEY = "wire_gate_probe";
1214
+ var ASYNC_GATE_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
1215
+ "@wireai/activation:asyncGateStorageWarned"
1216
+ );
1217
+ var asyncGateWarnGlobal = globalThis;
1218
+ var closedGateStorage = Object.freeze({
1219
+ getItem: () => SEEN_VALUE,
1220
+ setItem: () => {
1221
+ }
1222
+ });
1223
+ var syncGateAdapters = /* @__PURE__ */ new WeakSet();
1224
+ var asyncGateAdapters = /* @__PURE__ */ new WeakSet();
1225
+ var warnAsyncGateStorage = () => {
1226
+ if (asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT]) return;
1227
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
1228
+ if (typeof console === "undefined" || !console.warn) return;
1229
+ asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT] = true;
1230
+ console.warn(
1231
+ "[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."
1232
+ );
1233
+ };
1234
+ var validateGateStorage = (storage) => {
1235
+ if (!storage) return null;
1236
+ if (syncGateAdapters.has(storage)) return storage;
1237
+ if (asyncGateAdapters.has(storage)) return closedGateStorage;
1238
+ let probed;
1239
+ try {
1240
+ probed = storage.getItem(GATE_PROBE_KEY);
1241
+ } catch {
1242
+ syncGateAdapters.add(storage);
1243
+ return storage;
1244
+ }
1245
+ if (probed != null && typeof probed.then === "function") {
1246
+ asyncGateAdapters.add(storage);
1247
+ try {
1248
+ probed.then(void 0, () => {
1249
+ });
1250
+ } catch {
1251
+ }
1252
+ warnAsyncGateStorage();
1253
+ return closedGateStorage;
1254
+ }
1255
+ syncGateAdapters.add(storage);
1256
+ return storage;
1257
+ };
1138
1258
  var hasSeenGate = (key, storageOverride, testingOverride) => {
1139
1259
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return false;
1140
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
1260
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
1141
1261
  if (!storage) return false;
1142
1262
  try {
1143
1263
  return storage.getItem(key) === SEEN_VALUE;
@@ -1147,7 +1267,7 @@ var hasSeenGate = (key, storageOverride, testingOverride) => {
1147
1267
  };
1148
1268
  var markSeenGate = (key, storageOverride, testingOverride) => {
1149
1269
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return;
1150
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
1270
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
1151
1271
  if (!storage) return;
1152
1272
  try {
1153
1273
  storage.setItem(key, SEEN_VALUE);
@@ -1235,17 +1355,31 @@ var failOpen = async (storage, key) => {
1235
1355
  const cached = await readCachedFeatures(storage, key);
1236
1356
  return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
1237
1357
  };
1238
- var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
1358
+ var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
1359
+ var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
1239
1360
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1240
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
1241
- try {
1242
- return await fetch(url, {
1361
+ let timer;
1362
+ const expired = new Promise((resolve) => {
1363
+ timer = setTimeout(() => {
1364
+ controller == null ? void 0 : controller.abort();
1365
+ resolve(NO_ANSWER);
1366
+ }, timeoutMs);
1367
+ });
1368
+ const exchange = (async () => {
1369
+ const res = await fetch(url, {
1243
1370
  method: "GET",
1244
1371
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1245
1372
  signal: controller == null ? void 0 : controller.signal
1246
1373
  });
1374
+ if (!res || !res.ok) return NO_ANSWER;
1375
+ return await res.json();
1376
+ })();
1377
+ try {
1378
+ return await Promise.race([exchange, expired]);
1247
1379
  } finally {
1248
1380
  clearTimeout(timer);
1381
+ void exchange.catch(() => {
1382
+ });
1249
1383
  }
1250
1384
  };
1251
1385
  var fetchWireFeatures = async (config) => {
@@ -1255,9 +1389,8 @@ var fetchWireFeatures = async (config) => {
1255
1389
  const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
1256
1390
  const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
1257
1391
  try {
1258
- const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
1259
- if (!res || !res.ok) return failOpen(storage, key);
1260
- const json = await res.json();
1392
+ const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
1393
+ if (json === NO_ANSWER) return failOpen(storage, key);
1261
1394
  const features = parseWireFeatures(json);
1262
1395
  if (storage) writeCachedFeatures(storage, key, features);
1263
1396
  return features;
@@ -1273,39 +1406,51 @@ var useStableValue = (value, isEqual) => {
1273
1406
  return ref.current;
1274
1407
  };
1275
1408
  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);
1276
- var useWireFeatures = (config) => {
1409
+ var useWireFeaturesState = (config) => {
1277
1410
  const stableConfig = useStableValue(config, sameFetchKey);
1278
1411
  const [flags, setFlags] = React8.useState(defaultWireFeatures);
1279
1412
  const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
1413
+ const [settled, setSettled] = React8.useState(!canFetch);
1280
1414
  React8.useEffect(() => {
1281
1415
  if (!canFetch) {
1282
1416
  setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
1417
+ setSettled(true);
1283
1418
  return;
1284
1419
  }
1285
1420
  let alive = true;
1286
1421
  void fetchWireFeatures(stableConfig).then((next) => {
1287
1422
  if (!alive) return;
1288
1423
  setFlags((prev) => featuresEqual(prev, next) ? prev : next);
1424
+ setSettled(true);
1289
1425
  });
1290
1426
  return () => {
1291
1427
  alive = false;
1292
1428
  };
1293
1429
  }, [canFetch, stableConfig]);
1294
- return flags;
1430
+ return React8.useMemo(() => ({ flags, settled }), [flags, settled]);
1295
1431
  };
1296
1432
  var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
1297
- var globalObj = globalThis;
1298
- if (!globalObj[CONTEXT_SYMBOL]) {
1299
- globalObj[CONTEXT_SYMBOL] = React8.createContext(null);
1433
+ var featuresContextGlobal = globalThis;
1434
+ if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
1435
+ featuresContextGlobal[CONTEXT_SYMBOL] = React8.createContext(null);
1436
+ }
1437
+ var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
1438
+ var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
1439
+ var settledContextGlobal = globalThis;
1440
+ if (!settledContextGlobal[SETTLED_SYMBOL]) {
1441
+ settledContextGlobal[SETTLED_SYMBOL] = React8.createContext(true);
1300
1442
  }
1301
- var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
1443
+ var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
1302
1444
  var useWireFeaturesContext = () => React8.useContext(WireFeaturesContext);
1303
- var useResolvedFeatures = (options) => {
1445
+ var useResolvedFeaturesState = (options) => {
1304
1446
  var _a, _b, _c;
1305
1447
  const ctx = useWireFeaturesContext();
1448
+ const ctxSettled = React8.useContext(WireFeaturesSettledContext);
1306
1449
  const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
1307
- const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
1308
- return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
1450
+ const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
1451
+ const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
1452
+ const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
1453
+ return React8.useMemo(() => ({ flags, settled }), [flags, settled]);
1309
1454
  };
1310
1455
 
1311
1456
  // src/reviews/equality.ts
@@ -1329,18 +1474,8 @@ var sameDecision = (a, b) => {
1329
1474
  return a.fire === b.fire && a.reason === b.reason;
1330
1475
  };
1331
1476
 
1332
- // src/analytics/reportClientEvent.ts
1333
- var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1334
-
1335
- // src/analytics/currentSession.ts
1336
- var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
1337
- "@wireai/activation:currentSessionId"
1338
- );
1339
- var globalSlot = globalThis;
1340
- var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
1341
-
1342
1477
  // src/reviews/runtime.ts
1343
- var resolveStorage = (override) => override != null ? override : getCoachmarkStorage();
1478
+ var resolveStorage = (override) => validateGateStorage(override != null ? override : getCoachmarkStorage());
1344
1479
  var NO_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
1345
1480
  "@wireai/activation:gateStorageWarned"
1346
1481
  );
@@ -1385,18 +1520,23 @@ var PROCESS_OPEN_ID_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:proces
1385
1520
  var openIdGlobal = globalThis;
1386
1521
  var currentOpenId = () => {
1387
1522
  const live = getCurrentSessionId();
1523
+ const liveMinted = isMintedSessionId(live);
1388
1524
  const pin = openIdGlobal[PROCESS_OPEN_ID_SLOT];
1389
1525
  if (!pin) {
1390
1526
  const openId = live != null ? live : makeSessionId();
1391
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live };
1527
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live, observedMinted: liveMinted };
1392
1528
  return openId;
1393
1529
  }
1394
1530
  if (!live || live === pin.observed) return pin.openId;
1395
- if (pin.observed === void 0) {
1396
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: pin.openId, observed: live };
1531
+ if (pin.observed === void 0 || pin.observedMinted) {
1532
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = {
1533
+ openId: pin.openId,
1534
+ observed: live,
1535
+ observedMinted: liveMinted
1536
+ };
1397
1537
  return pin.openId;
1398
1538
  }
1399
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live };
1539
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live, observedMinted: liveMinted };
1400
1540
  return live;
1401
1541
  };
1402
1542
  var bumpSessionCount = (storage, sessionsKey, openKey, openId = currentOpenId()) => {
@@ -1437,7 +1577,11 @@ var useQuestionnaireGate = ({
1437
1577
  }) => {
1438
1578
  const config = useStableValue2(configProp, shallowEqual);
1439
1579
  const decision = useStableValue2(decisionProp, (a, b) => sameDecision(a, b));
1440
- const featureEnabled = useResolvedFeatures({ flags: features, config: featuresConfig }).questionnaire.enabled;
1580
+ const { flags: resolvedFeatures, settled: featuresSettled } = useResolvedFeaturesState({
1581
+ flags: features,
1582
+ config: featuresConfig
1583
+ });
1584
+ const featureEnabled = resolvedFeatures.questionnaire.enabled;
1441
1585
  const testing = isTesting != null ? isTesting : isCoachmarkTesting();
1442
1586
  const seenKey = questionnaireSeenKey(
1443
1587
  config.id,
@@ -1446,11 +1590,22 @@ var useQuestionnaireGate = ({
1446
1590
  const lastKey = questionnaireLastShownKey(config.id);
1447
1591
  const sessionsKey = questionnaireSessionsKey(config.id);
1448
1592
  const sessionOpenKey = questionnaireSessionOpenKey(config.id);
1449
- const sessions = React8.useState(() => {
1593
+ const openSessionId = React8.useSyncExternalStore(
1594
+ subscribeCurrentSessionId,
1595
+ getCurrentSessionId,
1596
+ getCurrentSessionId
1597
+ );
1598
+ const storageRef = React8.useRef(storage);
1599
+ storageRef.current = storage;
1600
+ const [sessions, setSessions] = React8.useState(() => {
1450
1601
  const store = resolveStorage(storage);
1451
1602
  warnMissingGateStorage(store, "questionnaire");
1452
1603
  return bumpSessionCount(store, sessionsKey, sessionOpenKey);
1453
- })[0];
1604
+ });
1605
+ React8.useEffect(() => {
1606
+ const next = bumpSessionCount(resolveStorage(storageRef.current), sessionsKey, sessionOpenKey);
1607
+ setSessions((prev) => prev === next ? prev : next);
1608
+ }, [openSessionId, sessionsKey, sessionOpenKey]);
1454
1609
  const hasServerDecision = decision != null;
1455
1610
  const timeoutMs = config.timeoutFallbackMs;
1456
1611
  const waits = !hasServerDecision && timeoutMs != null && timeoutMs > 0;
@@ -1484,10 +1639,11 @@ var useQuestionnaireGate = ({
1484
1639
  });
1485
1640
  const resolved = decideQuestionnaire(local, decision);
1486
1641
  const alreadySeen = hasSeenGate(seenKey, storage, isTesting);
1487
- const ready = hasServerDecision || elapsed;
1642
+ const ready = (hasServerDecision || elapsed) && featuresSettled;
1488
1643
  return { visible: ready && resolved.fire && !alreadySeen, verdict: resolved };
1489
1644
  }, [
1490
1645
  featureEnabled,
1646
+ featuresSettled,
1491
1647
  testing,
1492
1648
  config,
1493
1649
  decision,