@wireai/activation 0.14.3 → 0.15.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 (59) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +73 -0
  3. package/README.md +11 -7
  4. package/dist/analytics/index.d.mts +5 -5
  5. package/dist/analytics/index.d.ts +5 -5
  6. package/dist/analytics/index.js.map +1 -1
  7. package/dist/analytics/index.mjs.map +1 -1
  8. package/dist/coachmarks/index.d.mts +1 -1
  9. package/dist/coachmarks/index.d.ts +1 -1
  10. package/dist/coachmarks/index.js +47 -2
  11. package/dist/coachmarks/index.js.map +1 -1
  12. package/dist/coachmarks/index.mjs +47 -2
  13. package/dist/coachmarks/index.mjs.map +1 -1
  14. package/dist/{currentSession-CW_5Mq4O.d.ts → currentSession-CFSRZ2wg.d.ts} +10 -4
  15. package/dist/{currentSession-CUvTOchb.d.mts → currentSession-orZy5p1e.d.mts} +10 -4
  16. package/dist/{decision-Bgo17oH7.d.mts → decision-3vWLuBlO.d.ts} +11 -2
  17. package/dist/{decision-Bkh_LigV.d.ts → decision-yBj2AyPW.d.mts} +11 -2
  18. package/dist/index.d.mts +2 -2
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/questionnaire/index.d.mts +3 -3
  23. package/dist/questionnaire/index.d.ts +3 -3
  24. package/dist/questionnaire/index.js +48 -3
  25. package/dist/questionnaire/index.js.map +1 -1
  26. package/dist/questionnaire/index.mjs +48 -3
  27. package/dist/questionnaire/index.mjs.map +1 -1
  28. package/dist/reviews/index.d.mts +10 -6
  29. package/dist/reviews/index.d.ts +10 -6
  30. package/dist/reviews/index.js +167 -18
  31. package/dist/reviews/index.js.map +1 -1
  32. package/dist/reviews/index.mjs +167 -18
  33. package/dist/reviews/index.mjs.map +1 -1
  34. package/dist/showcase/index.d.mts +1 -1
  35. package/dist/showcase/index.d.ts +1 -1
  36. package/dist/showcase/index.js +47 -2
  37. package/dist/showcase/index.js.map +1 -1
  38. package/dist/showcase/index.mjs +47 -2
  39. package/dist/showcase/index.mjs.map +1 -1
  40. package/dist/{transport-j5gFfJhK.d.mts → transport-s5QxA-ci.d.mts} +18 -13
  41. package/dist/{transport-B_0SgCBe.d.ts → transport-xqqSFqxs.d.ts} +18 -13
  42. package/dist/{types-Cju-1_jT.d.mts → types-Byx306Kv.d.mts} +25 -10
  43. package/dist/{types-BcmagF6K.d.mts → types-D_0B0yay.d.mts} +7 -2
  44. package/dist/{types-BcmagF6K.d.ts → types-D_0B0yay.d.ts} +7 -2
  45. package/dist/{types-h2BZvl1t.d.ts → types-tdATL5z0.d.ts} +25 -10
  46. package/package.json +1 -1
  47. package/src/WireOnboarding.tsx +4 -1
  48. package/src/coachmarks/runtime.ts +103 -2
  49. package/src/coachmarks/types.ts +7 -2
  50. package/src/context/deviceId.ts +32 -0
  51. package/src/reviews/ReviewGate.tsx +86 -37
  52. package/src/reviews/decision.ts +11 -1
  53. package/src/reviews/idempotency.ts +71 -0
  54. package/src/reviews/index.ts +4 -2
  55. package/src/reviews/runtime.ts +5 -3
  56. package/src/reviews/transport.ts +17 -12
  57. package/src/reviews/types.ts +7 -0
  58. package/src/types.ts +10 -4
  59. package/src/utils/submitResult.ts +17 -9
@@ -1,11 +1,11 @@
1
1
  import React__default from 'react';
2
- import { R as ReviewGateProps, a as ReviewPresentation, U as UseReviewGateOptions, b as ReviewGateController, c as ReviewDecision, d as ReviewStoreConfig } from '../types-Cju-1_jT.mjs';
3
- export { e as RatingRoute, f as ReviewConfig, g as ReviewDecisionResponse, h as ReviewGateEvent, i as ReviewGateEventName, j as ReviewSubmission, k as ReviewTarget, S as SubmitResult } from '../types-Cju-1_jT.mjs';
2
+ import { R as ReviewGateProps, a as ReviewPresentation, U as UseReviewGateOptions, b as ReviewGateController, c as ReviewDecision, d as ReviewStoreConfig } from '../types-Byx306Kv.mjs';
3
+ export { e as RatingRoute, f as ReviewConfig, g as ReviewDecisionResponse, h as ReviewGateEvent, i as ReviewGateEventName, j as ReviewSubmission, k as ReviewTarget, S as SubmitResult } from '../types-Byx306Kv.mjs';
4
4
  export { C as ReviewModal, a as ReviewModalHandle, b as ReviewModalProps } from '../CenteredModal-C3qQBHsA.mjs';
5
- export { G as GateRules, a as GateSignals, b as buildReviewSubmission, d as decideReview, e as evaluateGate, r as resolveRules, c as routeRating } from '../decision-Bgo17oH7.mjs';
5
+ export { G as GateRules, a as GateSignals, b as buildReviewSubmission, d as decideReview, e as evaluateGate, r as resolveRules, c as routeRating } from '../decision-yBj2AyPW.mjs';
6
6
  import { c as OnboardingFonts } from '../types-BKfpdZzX.mjs';
7
- export { F as FetchReviewDecisionOptions, R as ReportAppEventOptions, a as ReviewSubmitResult, f as fetchReviewDecision, r as reportAppEvent, s as submitReview } from '../transport-j5gFfJhK.mjs';
8
- import '../types-BcmagF6K.mjs';
7
+ export { F as FetchReviewDecisionOptions, R as ReportAppEventOptions, a as ReviewSubmitResult, f as fetchReviewDecision, r as reportAppEvent, s as submitReview } from '../transport-s5QxA-ci.mjs';
8
+ import '../types-D_0B0yay.mjs';
9
9
  import '../types-BpwiRpA8.mjs';
10
10
 
11
11
  /**
@@ -41,7 +41,11 @@ declare const reviewSessionsKey: (id: string) => string;
41
41
  *
42
42
  * The form NEVER asks for identity (no name / email / contact field): the host already
43
43
  * identifies the user via `sessionId` + `meta` (e.g. user_id, device_key), which is all
44
- * the POST body carries alongside the rating and the free text.
44
+ * the POST body carries alongside the rating and the free text. Since 2026-08-19 the gate also
45
+ * fills in `meta.device_key` from the kit's own per-install id when the host passed none — not for
46
+ * identification, but because it is the unit the server scopes the idempotency key to, and without
47
+ * it the key is discarded. Any non-blank string the host passes still wins verbatim. See
48
+ * `idempotency.ts`.
45
49
  *
46
50
  * Motion (2026-07-16): the three phases used to swap IN PLACE with no transition. They now
47
51
  * ride `ModalPaneHost`, the same pane-slide primitive the questionnaire steps use, and the
@@ -1,11 +1,11 @@
1
1
  import React__default from 'react';
2
- import { R as ReviewGateProps, a as ReviewPresentation, U as UseReviewGateOptions, b as ReviewGateController, c as ReviewDecision, d as ReviewStoreConfig } from '../types-h2BZvl1t.js';
3
- export { e as RatingRoute, f as ReviewConfig, g as ReviewDecisionResponse, h as ReviewGateEvent, i as ReviewGateEventName, j as ReviewSubmission, k as ReviewTarget, S as SubmitResult } from '../types-h2BZvl1t.js';
2
+ import { R as ReviewGateProps, a as ReviewPresentation, U as UseReviewGateOptions, b as ReviewGateController, c as ReviewDecision, d as ReviewStoreConfig } from '../types-tdATL5z0.js';
3
+ export { e as RatingRoute, f as ReviewConfig, g as ReviewDecisionResponse, h as ReviewGateEvent, i as ReviewGateEventName, j as ReviewSubmission, k as ReviewTarget, S as SubmitResult } from '../types-tdATL5z0.js';
4
4
  export { C as ReviewModal, a as ReviewModalHandle, b as ReviewModalProps } from '../CenteredModal-Cdgns6--.js';
5
- export { G as GateRules, a as GateSignals, b as buildReviewSubmission, d as decideReview, e as evaluateGate, r as resolveRules, c as routeRating } from '../decision-Bkh_LigV.js';
5
+ export { G as GateRules, a as GateSignals, b as buildReviewSubmission, d as decideReview, e as evaluateGate, r as resolveRules, c as routeRating } from '../decision-3vWLuBlO.js';
6
6
  import { c as OnboardingFonts } from '../types-BKfpdZzX.js';
7
- export { F as FetchReviewDecisionOptions, R as ReportAppEventOptions, a as ReviewSubmitResult, f as fetchReviewDecision, r as reportAppEvent, s as submitReview } from '../transport-B_0SgCBe.js';
8
- import '../types-BcmagF6K.js';
7
+ export { F as FetchReviewDecisionOptions, R as ReportAppEventOptions, a as ReviewSubmitResult, f as fetchReviewDecision, r as reportAppEvent, s as submitReview } from '../transport-xqqSFqxs.js';
8
+ import '../types-D_0B0yay.js';
9
9
  import '../types-BpwiRpA8.js';
10
10
 
11
11
  /**
@@ -41,7 +41,11 @@ declare const reviewSessionsKey: (id: string) => string;
41
41
  *
42
42
  * The form NEVER asks for identity (no name / email / contact field): the host already
43
43
  * identifies the user via `sessionId` + `meta` (e.g. user_id, device_key), which is all
44
- * the POST body carries alongside the rating and the free text.
44
+ * the POST body carries alongside the rating and the free text. Since 2026-08-19 the gate also
45
+ * fills in `meta.device_key` from the kit's own per-install id when the host passed none — not for
46
+ * identification, but because it is the unit the server scopes the idempotency key to, and without
47
+ * it the key is discarded. Any non-blank string the host passes still wins verbatim. See
48
+ * `idempotency.ts`.
45
49
  *
46
50
  * Motion (2026-07-16): the three phases used to swap IN PLACE with no transition. They now
47
51
  * ride `ModalPaneHost`, the same pane-slide primitive the questionnaire steps use, and the
@@ -546,6 +546,7 @@ var buildReviewSubmission = (input) => {
546
546
  if (contact) body.contact = contact;
547
547
  if (input.sessionId) body.session_id = input.sessionId;
548
548
  if (input.source) body.source = input.source;
549
+ if (input.idempotencyKey) body.idempotency_key = input.idempotencyKey;
549
550
  if (input.meta && Object.keys(input.meta).length > 0) body.meta = input.meta;
550
551
  return body;
551
552
  };
@@ -563,6 +564,96 @@ var feedbackInputHeights = (fonts, verticalPadding = 0) => {
563
564
  };
564
565
  };
565
566
 
567
+ // src/context/deviceId.ts
568
+ var AUTO_DEVICE_ID_PREFIX = "wdev_";
569
+ var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
570
+ var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
571
+ var mintDeviceId = () => {
572
+ const time = Date.now().toString(36);
573
+ return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
574
+ };
575
+ var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
576
+ var deviceKeyGlobal = globalThis;
577
+ var autoDeviceKeyRegistry = () => {
578
+ const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
579
+ if (existing) return existing;
580
+ const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
581
+ deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
582
+ return created;
583
+ };
584
+ var startHydration = (registry, appId, storage, minted) => {
585
+ if (!registry.pending) registry.pending = /* @__PURE__ */ new Map();
586
+ const existing = registry.pending.get(appId);
587
+ if (existing) return existing;
588
+ const slot = deviceIdStorageKey(appId);
589
+ const degraded = () => {
590
+ var _a;
591
+ return { value: (_a = registry.keys.get(appId)) != null ? _a : minted, durable: false };
592
+ };
593
+ const adopted = (value) => ({ value, durable: true });
594
+ let run;
595
+ try {
596
+ run = Promise.resolve(storage.getItem(slot)).then((saved) => {
597
+ const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
598
+ if (persisted) {
599
+ registry.keys.set(appId, persisted);
600
+ return adopted(persisted);
601
+ }
602
+ return Promise.resolve(storage.setItem(slot, minted)).then(
603
+ () => {
604
+ var _a;
605
+ return adopted((_a = registry.keys.get(appId)) != null ? _a : minted);
606
+ },
607
+ degraded
608
+ );
609
+ }).catch(degraded);
610
+ } catch {
611
+ run = Promise.resolve(degraded());
612
+ }
613
+ registry.pending.set(appId, run);
614
+ void run.then((outcome) => {
615
+ var _a;
616
+ if (outcome.durable) return;
617
+ (_a = registry.pending) == null ? void 0 : _a.delete(appId);
618
+ registry.hydrating.delete(appId);
619
+ });
620
+ return run;
621
+ };
622
+ var resolveAutoDeviceKey = (opts = {}) => {
623
+ var _a, _b;
624
+ const registry = autoDeviceKeyRegistry();
625
+ const appId = (_a = opts.appId) != null ? _a : "default";
626
+ let id = registry.keys.get(appId);
627
+ if (!id) {
628
+ id = mintDeviceId();
629
+ registry.keys.set(appId, id);
630
+ }
631
+ const storage = opts.storage;
632
+ if (storage && !registry.hydrating.has(appId)) {
633
+ registry.hydrating.add(appId);
634
+ void startHydration(registry, appId, storage, id);
635
+ }
636
+ return (_b = registry.keys.get(appId)) != null ? _b : id;
637
+ };
638
+ var ambientAutoDeviceKey = () => {
639
+ const registry = autoDeviceKeyRegistry();
640
+ if (registry.keys.size === 0) return resolveAutoDeviceKey();
641
+ if (registry.keys.size > 1) return void 0;
642
+ return [...registry.keys.values()][0];
643
+ };
644
+
645
+ // src/reviews/idempotency.ts
646
+ var REVIEW_IDEMPOTENCY_PREFIX = "rvw_";
647
+ var mintIdempotencyKey = () => `${REVIEW_IDEMPOTENCY_PREFIX}${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
648
+ var hasDeviceKey = (meta) => {
649
+ const value = meta == null ? void 0 : meta.device_key;
650
+ return typeof value === "string" && value.trim().length > 0;
651
+ };
652
+ var createImpression = () => ({
653
+ idempotencyKey: mintIdempotencyKey(),
654
+ deviceKey: ambientAutoDeviceKey()
655
+ });
656
+
566
657
  // src/motion/centeredModalMotion.ts
567
658
  var CENTERED_MODAL_ANIM = {
568
659
  /** Spring config for the card scale/opacity entrance (mirrors CompletionView's pop). */
@@ -901,6 +992,27 @@ var _ReviewGate = ({
901
992
  const resolve = React7.useCallback(() => {
902
993
  onResolved == null ? void 0 : onResolved();
903
994
  }, [onResolved]);
995
+ const impressionRef = React7__default.default.useRef(null);
996
+ const buildBody = React7.useCallback(
997
+ (input) => {
998
+ let impression = impressionRef.current;
999
+ if (!impression) {
1000
+ impression = createImpression();
1001
+ impressionRef.current = impression;
1002
+ }
1003
+ const merged = { ...meta, ...input.extraMeta };
1004
+ if (!hasDeviceKey(merged) && impression.deviceKey) merged.device_key = impression.deviceKey;
1005
+ return buildReviewSubmission({
1006
+ stars: input.stars,
1007
+ feedbackText: input.feedbackText,
1008
+ suggestion: input.suggestion,
1009
+ sessionId,
1010
+ meta: merged,
1011
+ idempotencyKey: impression.idempotencyKey
1012
+ });
1013
+ },
1014
+ [meta, sessionId]
1015
+ );
904
1016
  const postedRef = React7__default.default.useRef(false);
905
1017
  const unackedRef = React7__default.default.useRef(null);
906
1018
  const postOnce = React7.useCallback(
@@ -928,12 +1040,11 @@ var _ReviewGate = ({
928
1040
  }
929
1041
  if (postedRef.current || stars < 1) return;
930
1042
  postOnce(
931
- buildReviewSubmission({
1043
+ buildBody({
932
1044
  stars,
933
1045
  feedbackText: feedback,
934
1046
  suggestion,
935
- sessionId,
936
- meta: { ...meta, abandoned: true }
1047
+ extraMeta: { abandoned: true }
937
1048
  })
938
1049
  );
939
1050
  };
@@ -958,10 +1069,9 @@ var _ReviewGate = ({
958
1069
  onEvent == null ? void 0 : onEvent({ name: "store_review_requested", id, stars: value });
959
1070
  const route = await requestStoreReview(store);
960
1071
  postOnce(
961
- buildReviewSubmission({
1072
+ buildBody({
962
1073
  stars: value,
963
- sessionId,
964
- meta: { ...meta, store_route: route }
1074
+ extraMeta: { store_route: route }
965
1075
  })
966
1076
  );
967
1077
  setPhase("thanks");
@@ -970,24 +1080,18 @@ var _ReviewGate = ({
970
1080
  setPhase("feedback");
971
1081
  }
972
1082
  },
973
- [onEvent, id, store, postOnce, sessionId, meta, resolveWithDelay]
1083
+ [onEvent, id, store, postOnce, buildBody, resolveWithDelay]
974
1084
  );
975
1085
  const feedbackReady = feedback.trim().length > 0;
976
1086
  const sendFeedback = React7.useCallback(() => {
977
1087
  if (!feedbackReady) return;
978
- const body = buildReviewSubmission({
979
- stars,
980
- feedbackText: feedback,
981
- suggestion,
982
- sessionId,
983
- meta
984
- });
1088
+ const body = buildBody({ stars, feedbackText: feedback, suggestion });
985
1089
  postOnce(body);
986
1090
  onEvent == null ? void 0 : onEvent({ name: "review_feedback_submitted", id, stars });
987
1091
  playHaptic("success");
988
1092
  setPhase("thanks");
989
1093
  resolveWithDelay();
990
- }, [feedbackReady, stars, feedback, suggestion, sessionId, meta, postOnce, onEvent, id, resolveWithDelay]);
1094
+ }, [feedbackReady, stars, feedback, suggestion, buildBody, postOnce, onEvent, id, resolveWithDelay]);
991
1095
  const dismiss = React7.useCallback(() => {
992
1096
  setPhase("thanks");
993
1097
  resolveWithDelay();
@@ -1134,9 +1238,54 @@ var coachmarkRuntime = () => {
1134
1238
  var getCoachmarkStorage = () => coachmarkRuntime().storage;
1135
1239
  var isCoachmarkTesting = () => coachmarkRuntime().testing;
1136
1240
  var SEEN_VALUE = "1";
1241
+ var GATE_PROBE_KEY = "wire_gate_probe";
1242
+ var ASYNC_GATE_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
1243
+ "@wireai/activation:asyncGateStorageWarned"
1244
+ );
1245
+ var asyncGateWarnGlobal = globalThis;
1246
+ var closedGateStorage = Object.freeze({
1247
+ getItem: () => SEEN_VALUE,
1248
+ setItem: () => {
1249
+ }
1250
+ });
1251
+ var syncGateAdapters = /* @__PURE__ */ new WeakSet();
1252
+ var asyncGateAdapters = /* @__PURE__ */ new WeakSet();
1253
+ var warnAsyncGateStorage = () => {
1254
+ if (asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT]) return;
1255
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
1256
+ if (typeof console === "undefined" || !console.warn) return;
1257
+ asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT] = true;
1258
+ console.warn(
1259
+ "[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."
1260
+ );
1261
+ };
1262
+ var validateGateStorage = (storage) => {
1263
+ if (!storage) return null;
1264
+ if (syncGateAdapters.has(storage)) return storage;
1265
+ if (asyncGateAdapters.has(storage)) return closedGateStorage;
1266
+ let probed;
1267
+ try {
1268
+ probed = storage.getItem(GATE_PROBE_KEY);
1269
+ } catch {
1270
+ syncGateAdapters.add(storage);
1271
+ return storage;
1272
+ }
1273
+ if (probed != null && typeof probed.then === "function") {
1274
+ asyncGateAdapters.add(storage);
1275
+ try {
1276
+ probed.then(void 0, () => {
1277
+ });
1278
+ } catch {
1279
+ }
1280
+ warnAsyncGateStorage();
1281
+ return closedGateStorage;
1282
+ }
1283
+ syncGateAdapters.add(storage);
1284
+ return storage;
1285
+ };
1137
1286
  var hasSeenGate = (key, storageOverride, testingOverride) => {
1138
1287
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return false;
1139
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
1288
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
1140
1289
  if (!storage) return false;
1141
1290
  try {
1142
1291
  return storage.getItem(key) === SEEN_VALUE;
@@ -1146,7 +1295,7 @@ var hasSeenGate = (key, storageOverride, testingOverride) => {
1146
1295
  };
1147
1296
  var markSeenGate = (key, storageOverride, testingOverride) => {
1148
1297
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return;
1149
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
1298
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
1150
1299
  if (!storage) return;
1151
1300
  try {
1152
1301
  storage.setItem(key, SEEN_VALUE);
@@ -1333,7 +1482,7 @@ var reviewSeenKey = (id, version) => version ? `wire_review_${id}_${version}_see
1333
1482
  var reviewLastShownKey = (id) => `wire_review_${id}_last`;
1334
1483
  var reviewSessionsKey = (id) => `wire_review_${id}_sessions`;
1335
1484
  var reviewSessionOpenKey = (id) => `wire_review_${id}_open`;
1336
- var resolveStorage = (override) => override != null ? override : getCoachmarkStorage();
1485
+ var resolveStorage = (override) => validateGateStorage(override != null ? override : getCoachmarkStorage());
1337
1486
  var NO_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
1338
1487
  "@wireai/activation:gateStorageWarned"
1339
1488
  );