@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 React8, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext } from 'react';
1
+ import React8, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext, useSyncExternalStore } from 'react';
2
2
  import { StyleSheet, Animated, Easing, BackHandler, Modal, Pressable, Text, AccessibilityInfo, View, TextInput } from 'react-native';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import { SafeAreaView } from 'react-native-safe-area-context';
@@ -837,6 +837,28 @@ var toAnswers = (drafts) => {
837
837
  };
838
838
  };
839
839
 
840
+ // src/utils/withDeadline.ts
841
+ var DEADLINE_EXPIRED = /* @__PURE__ */ Symbol("wire-deadline-expired");
842
+ var DEFAULT_DEADLINE_MS = 15e3;
843
+ var withDeadline = async (exchange, timeoutMs = DEFAULT_DEADLINE_MS) => {
844
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
845
+ let timer;
846
+ const expired = new Promise((resolve) => {
847
+ timer = setTimeout(() => {
848
+ controller == null ? void 0 : controller.abort();
849
+ resolve(DEADLINE_EXPIRED);
850
+ }, timeoutMs);
851
+ });
852
+ const running = exchange(controller == null ? void 0 : controller.signal);
853
+ try {
854
+ return await Promise.race([running, expired]);
855
+ } finally {
856
+ clearTimeout(timer);
857
+ void running.catch(() => {
858
+ });
859
+ }
860
+ };
861
+
840
862
  // src/questionnaire/transport.ts
841
863
  var submitQuestionnaireResponse = async (target, id, submission) => {
842
864
  if (!(target == null ? void 0 : target.serverUrl) || !id) return "unsent";
@@ -845,11 +867,15 @@ var submitQuestionnaireResponse = async (target, id, submission) => {
845
867
  const url = `${base}/v1/questionnaires/${encodeURIComponent(id)}/responses`;
846
868
  const headers = { "Content-Type": "application/json" };
847
869
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
848
- const res = await fetch(url, {
849
- method: "POST",
850
- headers,
851
- body: JSON.stringify(submission)
852
- });
870
+ const res = await withDeadline(
871
+ (signal) => fetch(url, {
872
+ method: "POST",
873
+ headers,
874
+ body: JSON.stringify(submission),
875
+ signal
876
+ })
877
+ );
878
+ if (res === DEADLINE_EXPIRED) return "unsent";
853
879
  if (!res) return "unsent";
854
880
  return res.ok ? "accepted" : "rejected";
855
881
  } catch {
@@ -867,9 +893,12 @@ var fetchQuestionnaireDecision = async (target, options = {}) => {
867
893
  const url = `${base}/v1/questionnaires/decision${qs ? `?${qs}` : ""}`;
868
894
  const headers = {};
869
895
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
870
- const res = await fetch(url, { headers });
871
- if (!res || !res.ok) return null;
872
- const json = await res.json();
896
+ const json = await withDeadline(async (signal) => {
897
+ const res = await fetch(url, { headers, signal });
898
+ if (!res || !res.ok) return null;
899
+ return await res.json();
900
+ });
901
+ if (json === DEADLINE_EXPIRED) return null;
873
902
  if (!json || typeof json.fire !== "boolean") return null;
874
903
  return json;
875
904
  } catch {
@@ -948,6 +977,10 @@ var _QuestionnaireGate = ({
948
977
  }, [id, onResolved]);
949
978
  const postedRef = useRef(false);
950
979
  const unackedRef = useRef(null);
980
+ const recoverOnSettleRef = useRef(false);
981
+ const recoveredRef = useRef(false);
982
+ const postOnceRef = useRef(() => {
983
+ });
951
984
  const postOnce = useCallback(
952
985
  (body) => {
953
986
  if (postedRef.current) return;
@@ -956,6 +989,10 @@ var _QuestionnaireGate = ({
956
989
  void submitQuestionnaireResponse(target, id, body).then((result) => {
957
990
  if (result === "unsent") {
958
991
  postedRef.current = false;
992
+ if (recoverOnSettleRef.current && !recoveredRef.current) {
993
+ recoveredRef.current = true;
994
+ postOnceRef.current(body);
995
+ }
959
996
  return;
960
997
  }
961
998
  unackedRef.current = null;
@@ -963,11 +1000,16 @@ var _QuestionnaireGate = ({
963
1000
  },
964
1001
  [target, id]
965
1002
  );
1003
+ postOnceRef.current = postOnce;
966
1004
  const recoverRef = useRef(() => {
967
1005
  });
968
1006
  recoverRef.current = () => {
969
1007
  const undelivered = unackedRef.current;
970
- if (undelivered && !postedRef.current) postOnce(undelivered);
1008
+ if (undelivered && !postedRef.current) {
1009
+ postOnce(undelivered);
1010
+ return;
1011
+ }
1012
+ if (postedRef.current && undelivered) recoverOnSettleRef.current = true;
971
1013
  };
972
1014
  React8.useEffect(() => () => recoverRef.current(), []);
973
1015
  const finish = useCallback(
@@ -1116,6 +1158,39 @@ var styles7 = StyleSheet.create({
1116
1158
  backGlyph: { includeFontPadding: false }
1117
1159
  });
1118
1160
 
1161
+ // src/analytics/reportClientEvent.ts
1162
+ var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1163
+
1164
+ // src/analytics/currentSession.ts
1165
+ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
1166
+ "@wireai/activation:currentSessionId"
1167
+ );
1168
+ var globalSlot = globalThis;
1169
+ var SESSION_ID_LISTENERS_SLOT = /* @__PURE__ */ Symbol.for(
1170
+ "@wireai/activation:currentSessionIdListeners"
1171
+ );
1172
+ var listenerSlot = globalThis;
1173
+ var listeners = () => {
1174
+ const existing = listenerSlot[SESSION_ID_LISTENERS_SLOT];
1175
+ if (existing) return existing;
1176
+ const created = /* @__PURE__ */ new Set();
1177
+ listenerSlot[SESSION_ID_LISTENERS_SLOT] = created;
1178
+ return created;
1179
+ };
1180
+ var subscribeCurrentSessionId = (listener) => {
1181
+ const set = listeners();
1182
+ set.add(listener);
1183
+ return () => {
1184
+ set.delete(listener);
1185
+ };
1186
+ };
1187
+ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
1188
+ var MINTED_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
1189
+ "@wireai/activation:mintedSessionId"
1190
+ );
1191
+ var mintedSlot = globalThis;
1192
+ var isMintedSessionId = (id) => id !== void 0 && id === mintedSlot[MINTED_SESSION_ID_SLOT];
1193
+
1119
1194
  // src/coachmarks/runtime.ts
1120
1195
  var COACHMARK_RUNTIME_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:coachmarkRuntime");
1121
1196
  var runtimeGlobal = globalThis;
@@ -1274,17 +1349,31 @@ var failOpen = async (storage, key) => {
1274
1349
  const cached = await readCachedFeatures(storage, key);
1275
1350
  return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
1276
1351
  };
1277
- var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
1352
+ var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
1353
+ var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
1278
1354
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1279
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
1280
- try {
1281
- return await fetch(url, {
1355
+ let timer;
1356
+ const expired = new Promise((resolve) => {
1357
+ timer = setTimeout(() => {
1358
+ controller == null ? void 0 : controller.abort();
1359
+ resolve(NO_ANSWER);
1360
+ }, timeoutMs);
1361
+ });
1362
+ const exchange = (async () => {
1363
+ const res = await fetch(url, {
1282
1364
  method: "GET",
1283
1365
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1284
1366
  signal: controller == null ? void 0 : controller.signal
1285
1367
  });
1368
+ if (!res || !res.ok) return NO_ANSWER;
1369
+ return await res.json();
1370
+ })();
1371
+ try {
1372
+ return await Promise.race([exchange, expired]);
1286
1373
  } finally {
1287
1374
  clearTimeout(timer);
1375
+ void exchange.catch(() => {
1376
+ });
1288
1377
  }
1289
1378
  };
1290
1379
  var fetchWireFeatures = async (config) => {
@@ -1294,9 +1383,8 @@ var fetchWireFeatures = async (config) => {
1294
1383
  const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
1295
1384
  const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
1296
1385
  try {
1297
- const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
1298
- if (!res || !res.ok) return failOpen(storage, key);
1299
- const json = await res.json();
1386
+ const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
1387
+ if (json === NO_ANSWER) return failOpen(storage, key);
1300
1388
  const features = parseWireFeatures(json);
1301
1389
  if (storage) writeCachedFeatures(storage, key, features);
1302
1390
  return features;
@@ -1312,39 +1400,51 @@ var useStableValue = (value, isEqual) => {
1312
1400
  return ref.current;
1313
1401
  };
1314
1402
  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);
1315
- var useWireFeatures = (config) => {
1403
+ var useWireFeaturesState = (config) => {
1316
1404
  const stableConfig = useStableValue(config, sameFetchKey);
1317
1405
  const [flags, setFlags] = useState(defaultWireFeatures);
1318
1406
  const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
1407
+ const [settled, setSettled] = useState(!canFetch);
1319
1408
  useEffect(() => {
1320
1409
  if (!canFetch) {
1321
1410
  setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
1411
+ setSettled(true);
1322
1412
  return;
1323
1413
  }
1324
1414
  let alive = true;
1325
1415
  void fetchWireFeatures(stableConfig).then((next) => {
1326
1416
  if (!alive) return;
1327
1417
  setFlags((prev) => featuresEqual(prev, next) ? prev : next);
1418
+ setSettled(true);
1328
1419
  });
1329
1420
  return () => {
1330
1421
  alive = false;
1331
1422
  };
1332
1423
  }, [canFetch, stableConfig]);
1333
- return flags;
1424
+ return useMemo(() => ({ flags, settled }), [flags, settled]);
1334
1425
  };
1335
1426
  var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
1336
- var globalObj = globalThis;
1337
- if (!globalObj[CONTEXT_SYMBOL]) {
1338
- globalObj[CONTEXT_SYMBOL] = createContext(null);
1427
+ var featuresContextGlobal = globalThis;
1428
+ if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
1429
+ featuresContextGlobal[CONTEXT_SYMBOL] = createContext(null);
1430
+ }
1431
+ var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
1432
+ var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
1433
+ var settledContextGlobal = globalThis;
1434
+ if (!settledContextGlobal[SETTLED_SYMBOL]) {
1435
+ settledContextGlobal[SETTLED_SYMBOL] = createContext(true);
1339
1436
  }
1340
- var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
1437
+ var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
1341
1438
  var useWireFeaturesContext = () => useContext(WireFeaturesContext);
1342
- var useResolvedFeatures = (options) => {
1439
+ var useResolvedFeaturesState = (options) => {
1343
1440
  var _a, _b, _c;
1344
1441
  const ctx = useWireFeaturesContext();
1442
+ const ctxSettled = useContext(WireFeaturesSettledContext);
1345
1443
  const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
1346
- const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
1347
- return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
1444
+ const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
1445
+ const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
1446
+ const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
1447
+ return useMemo(() => ({ flags, settled }), [flags, settled]);
1348
1448
  };
1349
1449
 
1350
1450
  // src/reviews/equality.ts
@@ -1368,16 +1468,6 @@ var sameDecision = (a, b) => {
1368
1468
  return a.fire === b.fire && a.reason === b.reason;
1369
1469
  };
1370
1470
 
1371
- // src/analytics/reportClientEvent.ts
1372
- var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1373
-
1374
- // src/analytics/currentSession.ts
1375
- var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
1376
- "@wireai/activation:currentSessionId"
1377
- );
1378
- var globalSlot = globalThis;
1379
- var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
1380
-
1381
1471
  // src/reviews/runtime.ts
1382
1472
  var resolveStorage = (override) => validateGateStorage(override != null ? override : getCoachmarkStorage());
1383
1473
  var NO_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
@@ -1424,18 +1514,23 @@ var PROCESS_OPEN_ID_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:proces
1424
1514
  var openIdGlobal = globalThis;
1425
1515
  var currentOpenId = () => {
1426
1516
  const live = getCurrentSessionId();
1517
+ const liveMinted = isMintedSessionId(live);
1427
1518
  const pin = openIdGlobal[PROCESS_OPEN_ID_SLOT];
1428
1519
  if (!pin) {
1429
1520
  const openId = live != null ? live : makeSessionId();
1430
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live };
1521
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live, observedMinted: liveMinted };
1431
1522
  return openId;
1432
1523
  }
1433
1524
  if (!live || live === pin.observed) return pin.openId;
1434
- if (pin.observed === void 0) {
1435
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: pin.openId, observed: live };
1525
+ if (pin.observed === void 0 || pin.observedMinted) {
1526
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = {
1527
+ openId: pin.openId,
1528
+ observed: live,
1529
+ observedMinted: liveMinted
1530
+ };
1436
1531
  return pin.openId;
1437
1532
  }
1438
- openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live };
1533
+ openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live, observedMinted: liveMinted };
1439
1534
  return live;
1440
1535
  };
1441
1536
  var bumpSessionCount = (storage, sessionsKey, openKey, openId = currentOpenId()) => {
@@ -1476,7 +1571,11 @@ var useQuestionnaireGate = ({
1476
1571
  }) => {
1477
1572
  const config = useStableValue2(configProp, shallowEqual);
1478
1573
  const decision = useStableValue2(decisionProp, (a, b) => sameDecision(a, b));
1479
- const featureEnabled = useResolvedFeatures({ flags: features, config: featuresConfig }).questionnaire.enabled;
1574
+ const { flags: resolvedFeatures, settled: featuresSettled } = useResolvedFeaturesState({
1575
+ flags: features,
1576
+ config: featuresConfig
1577
+ });
1578
+ const featureEnabled = resolvedFeatures.questionnaire.enabled;
1480
1579
  const testing = isTesting != null ? isTesting : isCoachmarkTesting();
1481
1580
  const seenKey = questionnaireSeenKey(
1482
1581
  config.id,
@@ -1485,11 +1584,22 @@ var useQuestionnaireGate = ({
1485
1584
  const lastKey = questionnaireLastShownKey(config.id);
1486
1585
  const sessionsKey = questionnaireSessionsKey(config.id);
1487
1586
  const sessionOpenKey = questionnaireSessionOpenKey(config.id);
1488
- const sessions = useState(() => {
1587
+ const openSessionId = useSyncExternalStore(
1588
+ subscribeCurrentSessionId,
1589
+ getCurrentSessionId,
1590
+ getCurrentSessionId
1591
+ );
1592
+ const storageRef = useRef(storage);
1593
+ storageRef.current = storage;
1594
+ const [sessions, setSessions] = useState(() => {
1489
1595
  const store = resolveStorage(storage);
1490
1596
  warnMissingGateStorage(store, "questionnaire");
1491
1597
  return bumpSessionCount(store, sessionsKey, sessionOpenKey);
1492
- })[0];
1598
+ });
1599
+ useEffect(() => {
1600
+ const next = bumpSessionCount(resolveStorage(storageRef.current), sessionsKey, sessionOpenKey);
1601
+ setSessions((prev) => prev === next ? prev : next);
1602
+ }, [openSessionId, sessionsKey, sessionOpenKey]);
1493
1603
  const hasServerDecision = decision != null;
1494
1604
  const timeoutMs = config.timeoutFallbackMs;
1495
1605
  const waits = !hasServerDecision && timeoutMs != null && timeoutMs > 0;
@@ -1523,10 +1633,11 @@ var useQuestionnaireGate = ({
1523
1633
  });
1524
1634
  const resolved = decideQuestionnaire(local, decision);
1525
1635
  const alreadySeen = hasSeenGate(seenKey, storage, isTesting);
1526
- const ready = hasServerDecision || elapsed;
1636
+ const ready = (hasServerDecision || elapsed) && featuresSettled;
1527
1637
  return { visible: ready && resolved.fire && !alreadySeen, verdict: resolved };
1528
1638
  }, [
1529
1639
  featureEnabled,
1640
+ featuresSettled,
1530
1641
  testing,
1531
1642
  config,
1532
1643
  decision,