@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
@@ -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`.
@@ -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;
@@ -1280,17 +1355,31 @@ var failOpen = async (storage, key) => {
1280
1355
  const cached = await readCachedFeatures(storage, key);
1281
1356
  return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
1282
1357
  };
1283
- var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
1358
+ var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
1359
+ var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
1284
1360
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1285
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
1286
- try {
1287
- 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, {
1288
1370
  method: "GET",
1289
1371
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1290
1372
  signal: controller == null ? void 0 : controller.signal
1291
1373
  });
1374
+ if (!res || !res.ok) return NO_ANSWER;
1375
+ return await res.json();
1376
+ })();
1377
+ try {
1378
+ return await Promise.race([exchange, expired]);
1292
1379
  } finally {
1293
1380
  clearTimeout(timer);
1381
+ void exchange.catch(() => {
1382
+ });
1294
1383
  }
1295
1384
  };
1296
1385
  var fetchWireFeatures = async (config) => {
@@ -1300,9 +1389,8 @@ var fetchWireFeatures = async (config) => {
1300
1389
  const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
1301
1390
  const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
1302
1391
  try {
1303
- const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
1304
- if (!res || !res.ok) return failOpen(storage, key);
1305
- const json = await res.json();
1392
+ const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
1393
+ if (json === NO_ANSWER) return failOpen(storage, key);
1306
1394
  const features = parseWireFeatures(json);
1307
1395
  if (storage) writeCachedFeatures(storage, key, features);
1308
1396
  return features;
@@ -1318,39 +1406,51 @@ var useStableValue = (value, isEqual) => {
1318
1406
  return ref.current;
1319
1407
  };
1320
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);
1321
- var useWireFeatures = (config) => {
1409
+ var useWireFeaturesState = (config) => {
1322
1410
  const stableConfig = useStableValue(config, sameFetchKey);
1323
1411
  const [flags, setFlags] = React8.useState(defaultWireFeatures);
1324
1412
  const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
1413
+ const [settled, setSettled] = React8.useState(!canFetch);
1325
1414
  React8.useEffect(() => {
1326
1415
  if (!canFetch) {
1327
1416
  setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
1417
+ setSettled(true);
1328
1418
  return;
1329
1419
  }
1330
1420
  let alive = true;
1331
1421
  void fetchWireFeatures(stableConfig).then((next) => {
1332
1422
  if (!alive) return;
1333
1423
  setFlags((prev) => featuresEqual(prev, next) ? prev : next);
1424
+ setSettled(true);
1334
1425
  });
1335
1426
  return () => {
1336
1427
  alive = false;
1337
1428
  };
1338
1429
  }, [canFetch, stableConfig]);
1339
- return flags;
1430
+ return React8.useMemo(() => ({ flags, settled }), [flags, settled]);
1340
1431
  };
1341
1432
  var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
1342
- var globalObj = globalThis;
1343
- if (!globalObj[CONTEXT_SYMBOL]) {
1344
- 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);
1345
1442
  }
1346
- var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
1443
+ var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
1347
1444
  var useWireFeaturesContext = () => React8.useContext(WireFeaturesContext);
1348
- var useResolvedFeatures = (options) => {
1445
+ var useResolvedFeaturesState = (options) => {
1349
1446
  var _a, _b, _c;
1350
1447
  const ctx = useWireFeaturesContext();
1448
+ const ctxSettled = React8.useContext(WireFeaturesSettledContext);
1351
1449
  const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
1352
- const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
1353
- 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]);
1354
1454
  };
1355
1455
 
1356
1456
  // src/reviews/equality.ts
@@ -1374,16 +1474,6 @@ var sameDecision = (a, b) => {
1374
1474
  return a.fire === b.fire && a.reason === b.reason;
1375
1475
  };
1376
1476
 
1377
- // src/analytics/reportClientEvent.ts
1378
- var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1379
-
1380
- // src/analytics/currentSession.ts
1381
- var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
1382
- "@wireai/activation:currentSessionId"
1383
- );
1384
- var globalSlot = globalThis;
1385
- var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
1386
-
1387
1477
  // src/reviews/runtime.ts
1388
1478
  var resolveStorage = (override) => validateGateStorage(override != null ? override : getCoachmarkStorage());
1389
1479
  var NO_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
@@ -1430,18 +1520,23 @@ var PROCESS_OPEN_ID_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:proces
1430
1520
  var openIdGlobal = globalThis;
1431
1521
  var currentOpenId = () => {
1432
1522
  const live = getCurrentSessionId();
1523
+ const liveMinted = isMintedSessionId(live);
1433
1524
  const pin = openIdGlobal[PROCESS_OPEN_ID_SLOT];
1434
1525
  if (!pin) {
1435
1526
  const openId = live != null ? live : makeSessionId();
1436
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live };
1527
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live, observedMinted: liveMinted };
1437
1528
  return openId;
1438
1529
  }
1439
1530
  if (!live || live === pin.observed) return pin.openId;
1440
- if (pin.observed === void 0) {
1441
- 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
+ };
1442
1537
  return pin.openId;
1443
1538
  }
1444
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live };
1539
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live, observedMinted: liveMinted };
1445
1540
  return live;
1446
1541
  };
1447
1542
  var bumpSessionCount = (storage, sessionsKey, openKey, openId = currentOpenId()) => {
@@ -1482,7 +1577,11 @@ var useQuestionnaireGate = ({
1482
1577
  }) => {
1483
1578
  const config = useStableValue2(configProp, shallowEqual);
1484
1579
  const decision = useStableValue2(decisionProp, (a, b) => sameDecision(a, b));
1485
- 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;
1486
1585
  const testing = isTesting != null ? isTesting : isCoachmarkTesting();
1487
1586
  const seenKey = questionnaireSeenKey(
1488
1587
  config.id,
@@ -1491,11 +1590,22 @@ var useQuestionnaireGate = ({
1491
1590
  const lastKey = questionnaireLastShownKey(config.id);
1492
1591
  const sessionsKey = questionnaireSessionsKey(config.id);
1493
1592
  const sessionOpenKey = questionnaireSessionOpenKey(config.id);
1494
- 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(() => {
1495
1601
  const store = resolveStorage(storage);
1496
1602
  warnMissingGateStorage(store, "questionnaire");
1497
1603
  return bumpSessionCount(store, sessionsKey, sessionOpenKey);
1498
- })[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]);
1499
1609
  const hasServerDecision = decision != null;
1500
1610
  const timeoutMs = config.timeoutFallbackMs;
1501
1611
  const waits = !hasServerDecision && timeoutMs != null && timeoutMs > 0;
@@ -1529,10 +1639,11 @@ var useQuestionnaireGate = ({
1529
1639
  });
1530
1640
  const resolved = decideQuestionnaire(local, decision);
1531
1641
  const alreadySeen = hasSeenGate(seenKey, storage, isTesting);
1532
- const ready = hasServerDecision || elapsed;
1642
+ const ready = (hasServerDecision || elapsed) && featuresSettled;
1533
1643
  return { visible: ready && resolved.fire && !alreadySeen, verdict: resolved };
1534
1644
  }, [
1535
1645
  featureEnabled,
1646
+ featuresSettled,
1536
1647
  testing,
1537
1648
  config,
1538
1649
  decision,