@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
@@ -1,4 +1,4 @@
1
- import React7, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext } from 'react';
1
+ import React7, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext, useSyncExternalStore } from 'react';
2
2
  import { StyleSheet, Easing, Animated, BackHandler, Modal, Pressable, Text, View, AccessibilityInfo, TextInput, Linking, Platform } from 'react-native';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import { SafeAreaView } from 'react-native-safe-area-context';
@@ -560,7 +560,6 @@ var feedbackInputHeights = (fonts, verticalPadding = 0) => {
560
560
 
561
561
  // src/context/deviceId.ts
562
562
  var AUTO_DEVICE_ID_PREFIX = "wdev_";
563
- var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
564
563
  var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
565
564
  var mintDeviceId = () => {
566
565
  const time = Date.now().toString(36);
@@ -575,65 +574,13 @@ var autoDeviceKeyRegistry = () => {
575
574
  deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
576
575
  return created;
577
576
  };
578
- var startHydration = (registry, appId, storage, minted) => {
579
- if (!registry.pending) registry.pending = /* @__PURE__ */ new Map();
580
- const existing = registry.pending.get(appId);
581
- if (existing) return existing;
582
- const slot = deviceIdStorageKey(appId);
583
- const degraded = () => {
584
- var _a;
585
- return { value: (_a = registry.keys.get(appId)) != null ? _a : minted, durable: false };
586
- };
587
- const adopted = (value) => ({ value, durable: true });
588
- let run;
589
- try {
590
- run = Promise.resolve(storage.getItem(slot)).then((saved) => {
591
- const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
592
- if (persisted) {
593
- registry.keys.set(appId, persisted);
594
- return adopted(persisted);
595
- }
596
- return Promise.resolve(storage.setItem(slot, minted)).then(
597
- () => {
598
- var _a;
599
- return adopted((_a = registry.keys.get(appId)) != null ? _a : minted);
600
- },
601
- degraded
602
- );
603
- }).catch(degraded);
604
- } catch {
605
- run = Promise.resolve(degraded());
606
- }
607
- registry.pending.set(appId, run);
608
- void run.then((outcome) => {
609
- var _a;
610
- if (outcome.durable) return;
611
- (_a = registry.pending) == null ? void 0 : _a.delete(appId);
612
- registry.hydrating.delete(appId);
613
- });
614
- return run;
615
- };
616
- var resolveAutoDeviceKey = (opts = {}) => {
617
- var _a, _b;
618
- const registry = autoDeviceKeyRegistry();
619
- const appId = (_a = opts.appId) != null ? _a : "default";
620
- let id = registry.keys.get(appId);
621
- if (!id) {
622
- id = mintDeviceId();
623
- registry.keys.set(appId, id);
624
- }
625
- const storage = opts.storage;
626
- if (storage && !registry.hydrating.has(appId)) {
627
- registry.hydrating.add(appId);
628
- void startHydration(registry, appId, storage, id);
629
- }
630
- return (_b = registry.keys.get(appId)) != null ? _b : id;
631
- };
632
577
  var ambientAutoDeviceKey = () => {
633
578
  const registry = autoDeviceKeyRegistry();
634
- if (registry.keys.size === 0) return resolveAutoDeviceKey();
635
579
  if (registry.keys.size > 1) return void 0;
636
- return [...registry.keys.values()][0];
580
+ const sole = [...registry.keys.values()][0];
581
+ if (sole) return sole;
582
+ if (!registry.ambient) registry.ambient = mintDeviceId();
583
+ return registry.ambient;
637
584
  };
638
585
 
639
586
  // src/reviews/idempotency.ts
@@ -825,6 +772,28 @@ var requestStoreReview = async (store) => {
825
772
  return opened ? "store_url" : "none";
826
773
  };
827
774
 
775
+ // src/utils/withDeadline.ts
776
+ var DEADLINE_EXPIRED = /* @__PURE__ */ Symbol("wire-deadline-expired");
777
+ var DEFAULT_DEADLINE_MS = 15e3;
778
+ var withDeadline = async (exchange, timeoutMs = DEFAULT_DEADLINE_MS) => {
779
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
780
+ let timer;
781
+ const expired = new Promise((resolve) => {
782
+ timer = setTimeout(() => {
783
+ controller == null ? void 0 : controller.abort();
784
+ resolve(DEADLINE_EXPIRED);
785
+ }, timeoutMs);
786
+ });
787
+ const running = exchange(controller == null ? void 0 : controller.signal);
788
+ try {
789
+ return await Promise.race([running, expired]);
790
+ } finally {
791
+ clearTimeout(timer);
792
+ void running.catch(() => {
793
+ });
794
+ }
795
+ };
796
+
828
797
  // src/analytics/reportClientEvent.ts
829
798
  var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
830
799
  var toWireEvents = (events) => events.map((event) => {
@@ -866,17 +835,51 @@ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
866
835
  "@wireai/activation:currentSessionId"
867
836
  );
868
837
  var globalSlot = globalThis;
838
+ var SESSION_ID_LISTENERS_SLOT = /* @__PURE__ */ Symbol.for(
839
+ "@wireai/activation:currentSessionIdListeners"
840
+ );
841
+ var listenerSlot = globalThis;
842
+ var listeners = () => {
843
+ const existing = listenerSlot[SESSION_ID_LISTENERS_SLOT];
844
+ if (existing) return existing;
845
+ const created = /* @__PURE__ */ new Set();
846
+ listenerSlot[SESSION_ID_LISTENERS_SLOT] = created;
847
+ return created;
848
+ };
849
+ var writeCurrentSessionId = (id) => {
850
+ if (globalSlot[CURRENT_SESSION_ID_SLOT] === id) return;
851
+ globalSlot[CURRENT_SESSION_ID_SLOT] = id;
852
+ for (const listener of Array.from(listeners())) {
853
+ try {
854
+ listener();
855
+ } catch {
856
+ }
857
+ }
858
+ };
859
+ var subscribeCurrentSessionId = (listener) => {
860
+ const set = listeners();
861
+ set.add(listener);
862
+ return () => {
863
+ set.delete(listener);
864
+ };
865
+ };
869
866
  var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
870
867
  var MINT_WARNING = "[wireai] No app-open session was registered, so a session id was minted for this event (the server drops an event that has no session_id, and still answers 200). Mount useLifecycleEvents at your app root so events correlate to a real app-open.";
871
868
  var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
872
869
  "@wireai/activation:currentSessionIdMintWarned"
873
870
  );
874
871
  var warnSlot = globalThis;
872
+ var MINTED_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
873
+ "@wireai/activation:mintedSessionId"
874
+ );
875
+ var mintedSlot = globalThis;
876
+ var isMintedSessionId = (id) => id !== void 0 && id === mintedSlot[MINTED_SESSION_ID_SLOT];
875
877
  var ensureCurrentSessionId = () => {
876
878
  const existing = globalSlot[CURRENT_SESSION_ID_SLOT];
877
879
  if (typeof existing === "string" && existing.length > 0) return existing;
878
880
  const minted = makeSessionId();
879
- globalSlot[CURRENT_SESSION_ID_SLOT] = minted;
881
+ writeCurrentSessionId(minted);
882
+ mintedSlot[MINTED_SESSION_ID_SLOT] = minted;
880
883
  if (!warnSlot[MINT_WARNED_SLOT] && warnInDev(MINT_WARNING)) {
881
884
  warnSlot[MINT_WARNED_SLOT] = true;
882
885
  }
@@ -890,11 +893,15 @@ var submitReview = async (target, review) => {
890
893
  const url = `${target.serverUrl.replace(/\/$/, "")}/v1/reviews`;
891
894
  const headers = { "Content-Type": "application/json" };
892
895
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
893
- const res = await fetch(url, {
894
- method: "POST",
895
- headers,
896
- body: JSON.stringify(review)
897
- });
896
+ const res = await withDeadline(
897
+ (signal) => fetch(url, {
898
+ method: "POST",
899
+ headers,
900
+ body: JSON.stringify(review),
901
+ signal
902
+ })
903
+ );
904
+ if (res === DEADLINE_EXPIRED) return "unsent";
898
905
  if (!res) return "unsent";
899
906
  return res.ok ? "accepted" : "rejected";
900
907
  } catch {
@@ -912,9 +919,12 @@ var fetchReviewDecision = async (target, options = {}) => {
912
919
  const url = `${base}/v1/reviews/decision${qs ? `?${qs}` : ""}`;
913
920
  const headers = {};
914
921
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
915
- const res = await fetch(url, { headers });
916
- if (!res || !res.ok) return null;
917
- const json = await res.json();
922
+ const json = await withDeadline(async (signal) => {
923
+ const res = await fetch(url, { headers, signal });
924
+ if (!res || !res.ok) return null;
925
+ return await res.json();
926
+ });
927
+ if (json === DEADLINE_EXPIRED) return null;
918
928
  if (!json || typeof json.fire !== "boolean") return null;
919
929
  return json;
920
930
  } catch {
@@ -1009,6 +1019,10 @@ var _ReviewGate = ({
1009
1019
  );
1010
1020
  const postedRef = React7.useRef(false);
1011
1021
  const unackedRef = React7.useRef(null);
1022
+ const recoverOnSettleRef = React7.useRef(false);
1023
+ const recoveredRef = React7.useRef(false);
1024
+ const postOnceRef = React7.useRef(() => {
1025
+ });
1012
1026
  const postOnce = useCallback(
1013
1027
  (body) => {
1014
1028
  if (postedRef.current) return;
@@ -1017,6 +1031,10 @@ var _ReviewGate = ({
1017
1031
  void submitReview(target, body).then((result) => {
1018
1032
  if (result === "unsent") {
1019
1033
  postedRef.current = false;
1034
+ if (recoverOnSettleRef.current && !recoveredRef.current) {
1035
+ recoveredRef.current = true;
1036
+ postOnceRef.current(body);
1037
+ }
1020
1038
  return;
1021
1039
  }
1022
1040
  unackedRef.current = null;
@@ -1024,6 +1042,7 @@ var _ReviewGate = ({
1024
1042
  },
1025
1043
  [target]
1026
1044
  );
1045
+ postOnceRef.current = postOnce;
1027
1046
  const abandonRef = React7.useRef(() => {
1028
1047
  });
1029
1048
  abandonRef.current = () => {
@@ -1032,6 +1051,10 @@ var _ReviewGate = ({
1032
1051
  postOnce(undelivered);
1033
1052
  return;
1034
1053
  }
1054
+ if (postedRef.current && undelivered) {
1055
+ recoverOnSettleRef.current = true;
1056
+ return;
1057
+ }
1035
1058
  if (postedRef.current || stars < 1) return;
1036
1059
  postOnce(
1037
1060
  buildBody({
@@ -1377,17 +1400,31 @@ var failOpen = async (storage, key) => {
1377
1400
  const cached = await readCachedFeatures(storage, key);
1378
1401
  return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
1379
1402
  };
1380
- var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
1403
+ var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
1404
+ var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
1381
1405
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1382
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
1383
- try {
1384
- return await fetch(url, {
1406
+ let timer;
1407
+ const expired = new Promise((resolve) => {
1408
+ timer = setTimeout(() => {
1409
+ controller == null ? void 0 : controller.abort();
1410
+ resolve(NO_ANSWER);
1411
+ }, timeoutMs);
1412
+ });
1413
+ const exchange = (async () => {
1414
+ const res = await fetch(url, {
1385
1415
  method: "GET",
1386
1416
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1387
1417
  signal: controller == null ? void 0 : controller.signal
1388
1418
  });
1419
+ if (!res || !res.ok) return NO_ANSWER;
1420
+ return await res.json();
1421
+ })();
1422
+ try {
1423
+ return await Promise.race([exchange, expired]);
1389
1424
  } finally {
1390
1425
  clearTimeout(timer);
1426
+ void exchange.catch(() => {
1427
+ });
1391
1428
  }
1392
1429
  };
1393
1430
  var fetchWireFeatures = async (config) => {
@@ -1397,9 +1434,8 @@ var fetchWireFeatures = async (config) => {
1397
1434
  const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
1398
1435
  const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
1399
1436
  try {
1400
- const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
1401
- if (!res || !res.ok) return failOpen(storage, key);
1402
- const json = await res.json();
1437
+ const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
1438
+ if (json === NO_ANSWER) return failOpen(storage, key);
1403
1439
  const features = parseWireFeatures(json);
1404
1440
  if (storage) writeCachedFeatures(storage, key, features);
1405
1441
  return features;
@@ -1415,39 +1451,51 @@ var useStableValue = (value, isEqual) => {
1415
1451
  return ref.current;
1416
1452
  };
1417
1453
  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);
1418
- var useWireFeatures = (config) => {
1454
+ var useWireFeaturesState = (config) => {
1419
1455
  const stableConfig = useStableValue(config, sameFetchKey);
1420
1456
  const [flags, setFlags] = useState(defaultWireFeatures);
1421
1457
  const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
1458
+ const [settled, setSettled] = useState(!canFetch);
1422
1459
  useEffect(() => {
1423
1460
  if (!canFetch) {
1424
1461
  setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
1462
+ setSettled(true);
1425
1463
  return;
1426
1464
  }
1427
1465
  let alive = true;
1428
1466
  void fetchWireFeatures(stableConfig).then((next) => {
1429
1467
  if (!alive) return;
1430
1468
  setFlags((prev) => featuresEqual(prev, next) ? prev : next);
1469
+ setSettled(true);
1431
1470
  });
1432
1471
  return () => {
1433
1472
  alive = false;
1434
1473
  };
1435
1474
  }, [canFetch, stableConfig]);
1436
- return flags;
1475
+ return useMemo(() => ({ flags, settled }), [flags, settled]);
1437
1476
  };
1438
1477
  var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
1439
- var globalObj = globalThis;
1440
- if (!globalObj[CONTEXT_SYMBOL]) {
1441
- globalObj[CONTEXT_SYMBOL] = createContext(null);
1478
+ var featuresContextGlobal = globalThis;
1479
+ if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
1480
+ featuresContextGlobal[CONTEXT_SYMBOL] = createContext(null);
1481
+ }
1482
+ var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
1483
+ var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
1484
+ var settledContextGlobal = globalThis;
1485
+ if (!settledContextGlobal[SETTLED_SYMBOL]) {
1486
+ settledContextGlobal[SETTLED_SYMBOL] = createContext(true);
1442
1487
  }
1443
- var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
1488
+ var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
1444
1489
  var useWireFeaturesContext = () => useContext(WireFeaturesContext);
1445
- var useResolvedFeatures = (options) => {
1490
+ var useResolvedFeaturesState = (options) => {
1446
1491
  var _a, _b, _c;
1447
1492
  const ctx = useWireFeaturesContext();
1493
+ const ctxSettled = useContext(WireFeaturesSettledContext);
1448
1494
  const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
1449
- const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
1450
- return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
1495
+ const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
1496
+ const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
1497
+ const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
1498
+ return useMemo(() => ({ flags, settled }), [flags, settled]);
1451
1499
  };
1452
1500
 
1453
1501
  // src/reviews/equality.ts
@@ -1521,18 +1569,23 @@ var PROCESS_OPEN_ID_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:proces
1521
1569
  var openIdGlobal = globalThis;
1522
1570
  var currentOpenId = () => {
1523
1571
  const live = getCurrentSessionId();
1572
+ const liveMinted = isMintedSessionId(live);
1524
1573
  const pin = openIdGlobal[PROCESS_OPEN_ID_SLOT];
1525
1574
  if (!pin) {
1526
1575
  const openId = live != null ? live : makeSessionId();
1527
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live };
1576
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live, observedMinted: liveMinted };
1528
1577
  return openId;
1529
1578
  }
1530
1579
  if (!live || live === pin.observed) return pin.openId;
1531
- if (pin.observed === void 0) {
1532
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: pin.openId, observed: live };
1580
+ if (pin.observed === void 0 || pin.observedMinted) {
1581
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = {
1582
+ openId: pin.openId,
1583
+ observed: live,
1584
+ observedMinted: liveMinted
1585
+ };
1533
1586
  return pin.openId;
1534
1587
  }
1535
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live };
1588
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live, observedMinted: liveMinted };
1536
1589
  return live;
1537
1590
  };
1538
1591
  var bumpSessionCount = (storage, sessionsKey, openKey, openId = currentOpenId()) => {
@@ -1570,17 +1623,32 @@ var useReviewGate = ({
1570
1623
  decisionProp,
1571
1624
  (a, b) => sameDecision(a, b)
1572
1625
  );
1573
- const featureEnabled = useResolvedFeatures({ flags: features, config: featuresConfig }).review.enabled;
1626
+ const { flags: resolvedFeatures, settled: featuresSettled } = useResolvedFeaturesState({
1627
+ flags: features,
1628
+ config: featuresConfig
1629
+ });
1630
+ const featureEnabled = resolvedFeatures.review.enabled;
1574
1631
  const testing = isTesting != null ? isTesting : isCoachmarkTesting();
1575
1632
  const seenKey = reviewSeenKey(config.id, config.oncePerVersion === false ? void 0 : config.appVersion);
1576
1633
  const lastKey = reviewLastShownKey(config.id);
1577
1634
  const sessionsKey = reviewSessionsKey(config.id);
1578
1635
  const sessionOpenKey = reviewSessionOpenKey(config.id);
1579
- const sessions = useState(() => {
1636
+ const openSessionId = useSyncExternalStore(
1637
+ subscribeCurrentSessionId,
1638
+ getCurrentSessionId,
1639
+ getCurrentSessionId
1640
+ );
1641
+ const storageRef = useRef(storage);
1642
+ storageRef.current = storage;
1643
+ const [sessions, setSessions] = useState(() => {
1580
1644
  const store = resolveStorage(storage);
1581
1645
  warnMissingGateStorage(store, "review");
1582
1646
  return bumpSessionCount(store, sessionsKey, sessionOpenKey);
1583
- })[0];
1647
+ });
1648
+ useEffect(() => {
1649
+ const next = bumpSessionCount(resolveStorage(storageRef.current), sessionsKey, sessionOpenKey);
1650
+ setSessions((prev) => prev === next ? prev : next);
1651
+ }, [openSessionId, sessionsKey, sessionOpenKey]);
1584
1652
  const hasServerDecision = decision != null;
1585
1653
  const timeoutMs = config.timeoutFallbackMs;
1586
1654
  const waits = !hasServerDecision && timeoutMs != null && timeoutMs > 0;
@@ -1606,9 +1674,9 @@ var useReviewGate = ({
1606
1674
  });
1607
1675
  const resolved = decideReview(local, decision);
1608
1676
  const alreadySeen = hasSeenGate(seenKey, storage, isTesting);
1609
- const ready = hasServerDecision || elapsed;
1677
+ const ready = (hasServerDecision || elapsed) && featuresSettled;
1610
1678
  return { visible: ready && resolved.fire && !alreadySeen, verdict: resolved };
1611
- }, [featureEnabled, testing, config, decision, events, sessions, elapsed, hasServerDecision, seenKey, lastKey, storage, isTesting]);
1679
+ }, [featureEnabled, featuresSettled, testing, config, decision, events, sessions, elapsed, hasServerDecision, seenKey, lastKey, storage, isTesting]);
1612
1680
  const shownRef = useRef(false);
1613
1681
  const markShown = useCallback(() => {
1614
1682
  if (shownRef.current) return;