@wireai/activation 0.14.2 → 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 (67) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +23 -9
  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 +13 -5
  9. package/dist/coachmarks/index.d.ts +13 -5
  10. package/dist/coachmarks/index.js +252 -58
  11. package/dist/coachmarks/index.js.map +1 -1
  12. package/dist/coachmarks/index.mjs +204 -9
  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 +282 -48
  37. package/dist/showcase/index.js.map +1 -1
  38. package/dist/showcase/index.mjs +248 -12
  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/metro/index.js +16 -5
  47. package/package.json +1 -1
  48. package/src/WireOnboarding.tsx +4 -1
  49. package/src/coachmarks/GestureHint.tsx +16 -7
  50. package/src/coachmarks/SpotlightOverlay.tsx +19 -13
  51. package/src/coachmarks/index.ts +7 -2
  52. package/src/coachmarks/reanimated.ts +342 -0
  53. package/src/coachmarks/runtime.ts +103 -2
  54. package/src/coachmarks/types.ts +7 -2
  55. package/src/context/deviceId.ts +32 -0
  56. package/src/reviews/ReviewGate.tsx +86 -37
  57. package/src/reviews/decision.ts +11 -1
  58. package/src/reviews/idempotency.ts +71 -0
  59. package/src/reviews/index.ts +4 -2
  60. package/src/reviews/runtime.ts +5 -3
  61. package/src/reviews/transport.ts +17 -12
  62. package/src/reviews/types.ts +7 -0
  63. package/src/showcase/FeatureShowcase.tsx +25 -9
  64. package/src/showcase/blazejOnboarding.ts +136 -0
  65. package/src/showcase/index.ts +8 -4
  66. package/src/types.ts +10 -4
  67. package/src/utils/submitResult.ts +17 -9
@@ -540,6 +540,7 @@ var buildReviewSubmission = (input) => {
540
540
  if (contact) body.contact = contact;
541
541
  if (input.sessionId) body.session_id = input.sessionId;
542
542
  if (input.source) body.source = input.source;
543
+ if (input.idempotencyKey) body.idempotency_key = input.idempotencyKey;
543
544
  if (input.meta && Object.keys(input.meta).length > 0) body.meta = input.meta;
544
545
  return body;
545
546
  };
@@ -557,6 +558,96 @@ var feedbackInputHeights = (fonts, verticalPadding = 0) => {
557
558
  };
558
559
  };
559
560
 
561
+ // src/context/deviceId.ts
562
+ var AUTO_DEVICE_ID_PREFIX = "wdev_";
563
+ var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
564
+ var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
565
+ var mintDeviceId = () => {
566
+ const time = Date.now().toString(36);
567
+ return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
568
+ };
569
+ var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
570
+ var deviceKeyGlobal = globalThis;
571
+ var autoDeviceKeyRegistry = () => {
572
+ const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
573
+ if (existing) return existing;
574
+ const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
575
+ deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
576
+ return created;
577
+ };
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
+ var ambientAutoDeviceKey = () => {
633
+ const registry = autoDeviceKeyRegistry();
634
+ if (registry.keys.size === 0) return resolveAutoDeviceKey();
635
+ if (registry.keys.size > 1) return void 0;
636
+ return [...registry.keys.values()][0];
637
+ };
638
+
639
+ // src/reviews/idempotency.ts
640
+ var REVIEW_IDEMPOTENCY_PREFIX = "rvw_";
641
+ var mintIdempotencyKey = () => `${REVIEW_IDEMPOTENCY_PREFIX}${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
642
+ var hasDeviceKey = (meta) => {
643
+ const value = meta == null ? void 0 : meta.device_key;
644
+ return typeof value === "string" && value.trim().length > 0;
645
+ };
646
+ var createImpression = () => ({
647
+ idempotencyKey: mintIdempotencyKey(),
648
+ deviceKey: ambientAutoDeviceKey()
649
+ });
650
+
560
651
  // src/motion/centeredModalMotion.ts
561
652
  var CENTERED_MODAL_ANIM = {
562
653
  /** Spring config for the card scale/opacity entrance (mirrors CompletionView's pop). */
@@ -895,6 +986,27 @@ var _ReviewGate = ({
895
986
  const resolve = useCallback(() => {
896
987
  onResolved == null ? void 0 : onResolved();
897
988
  }, [onResolved]);
989
+ const impressionRef = React7.useRef(null);
990
+ const buildBody = useCallback(
991
+ (input) => {
992
+ let impression = impressionRef.current;
993
+ if (!impression) {
994
+ impression = createImpression();
995
+ impressionRef.current = impression;
996
+ }
997
+ const merged = { ...meta, ...input.extraMeta };
998
+ if (!hasDeviceKey(merged) && impression.deviceKey) merged.device_key = impression.deviceKey;
999
+ return buildReviewSubmission({
1000
+ stars: input.stars,
1001
+ feedbackText: input.feedbackText,
1002
+ suggestion: input.suggestion,
1003
+ sessionId,
1004
+ meta: merged,
1005
+ idempotencyKey: impression.idempotencyKey
1006
+ });
1007
+ },
1008
+ [meta, sessionId]
1009
+ );
898
1010
  const postedRef = React7.useRef(false);
899
1011
  const unackedRef = React7.useRef(null);
900
1012
  const postOnce = useCallback(
@@ -922,12 +1034,11 @@ var _ReviewGate = ({
922
1034
  }
923
1035
  if (postedRef.current || stars < 1) return;
924
1036
  postOnce(
925
- buildReviewSubmission({
1037
+ buildBody({
926
1038
  stars,
927
1039
  feedbackText: feedback,
928
1040
  suggestion,
929
- sessionId,
930
- meta: { ...meta, abandoned: true }
1041
+ extraMeta: { abandoned: true }
931
1042
  })
932
1043
  );
933
1044
  };
@@ -952,10 +1063,9 @@ var _ReviewGate = ({
952
1063
  onEvent == null ? void 0 : onEvent({ name: "store_review_requested", id, stars: value });
953
1064
  const route = await requestStoreReview(store);
954
1065
  postOnce(
955
- buildReviewSubmission({
1066
+ buildBody({
956
1067
  stars: value,
957
- sessionId,
958
- meta: { ...meta, store_route: route }
1068
+ extraMeta: { store_route: route }
959
1069
  })
960
1070
  );
961
1071
  setPhase("thanks");
@@ -964,24 +1074,18 @@ var _ReviewGate = ({
964
1074
  setPhase("feedback");
965
1075
  }
966
1076
  },
967
- [onEvent, id, store, postOnce, sessionId, meta, resolveWithDelay]
1077
+ [onEvent, id, store, postOnce, buildBody, resolveWithDelay]
968
1078
  );
969
1079
  const feedbackReady = feedback.trim().length > 0;
970
1080
  const sendFeedback = useCallback(() => {
971
1081
  if (!feedbackReady) return;
972
- const body = buildReviewSubmission({
973
- stars,
974
- feedbackText: feedback,
975
- suggestion,
976
- sessionId,
977
- meta
978
- });
1082
+ const body = buildBody({ stars, feedbackText: feedback, suggestion });
979
1083
  postOnce(body);
980
1084
  onEvent == null ? void 0 : onEvent({ name: "review_feedback_submitted", id, stars });
981
1085
  playHaptic("success");
982
1086
  setPhase("thanks");
983
1087
  resolveWithDelay();
984
- }, [feedbackReady, stars, feedback, suggestion, sessionId, meta, postOnce, onEvent, id, resolveWithDelay]);
1088
+ }, [feedbackReady, stars, feedback, suggestion, buildBody, postOnce, onEvent, id, resolveWithDelay]);
985
1089
  const dismiss = useCallback(() => {
986
1090
  setPhase("thanks");
987
1091
  resolveWithDelay();
@@ -1128,9 +1232,54 @@ var coachmarkRuntime = () => {
1128
1232
  var getCoachmarkStorage = () => coachmarkRuntime().storage;
1129
1233
  var isCoachmarkTesting = () => coachmarkRuntime().testing;
1130
1234
  var SEEN_VALUE = "1";
1235
+ var GATE_PROBE_KEY = "wire_gate_probe";
1236
+ var ASYNC_GATE_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
1237
+ "@wireai/activation:asyncGateStorageWarned"
1238
+ );
1239
+ var asyncGateWarnGlobal = globalThis;
1240
+ var closedGateStorage = Object.freeze({
1241
+ getItem: () => SEEN_VALUE,
1242
+ setItem: () => {
1243
+ }
1244
+ });
1245
+ var syncGateAdapters = /* @__PURE__ */ new WeakSet();
1246
+ var asyncGateAdapters = /* @__PURE__ */ new WeakSet();
1247
+ var warnAsyncGateStorage = () => {
1248
+ if (asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT]) return;
1249
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
1250
+ if (typeof console === "undefined" || !console.warn) return;
1251
+ asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT] = true;
1252
+ console.warn(
1253
+ "[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."
1254
+ );
1255
+ };
1256
+ var validateGateStorage = (storage) => {
1257
+ if (!storage) return null;
1258
+ if (syncGateAdapters.has(storage)) return storage;
1259
+ if (asyncGateAdapters.has(storage)) return closedGateStorage;
1260
+ let probed;
1261
+ try {
1262
+ probed = storage.getItem(GATE_PROBE_KEY);
1263
+ } catch {
1264
+ syncGateAdapters.add(storage);
1265
+ return storage;
1266
+ }
1267
+ if (probed != null && typeof probed.then === "function") {
1268
+ asyncGateAdapters.add(storage);
1269
+ try {
1270
+ probed.then(void 0, () => {
1271
+ });
1272
+ } catch {
1273
+ }
1274
+ warnAsyncGateStorage();
1275
+ return closedGateStorage;
1276
+ }
1277
+ syncGateAdapters.add(storage);
1278
+ return storage;
1279
+ };
1131
1280
  var hasSeenGate = (key, storageOverride, testingOverride) => {
1132
1281
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return false;
1133
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
1282
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
1134
1283
  if (!storage) return false;
1135
1284
  try {
1136
1285
  return storage.getItem(key) === SEEN_VALUE;
@@ -1140,7 +1289,7 @@ var hasSeenGate = (key, storageOverride, testingOverride) => {
1140
1289
  };
1141
1290
  var markSeenGate = (key, storageOverride, testingOverride) => {
1142
1291
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return;
1143
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
1292
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
1144
1293
  if (!storage) return;
1145
1294
  try {
1146
1295
  storage.setItem(key, SEEN_VALUE);
@@ -1327,7 +1476,7 @@ var reviewSeenKey = (id, version) => version ? `wire_review_${id}_${version}_see
1327
1476
  var reviewLastShownKey = (id) => `wire_review_${id}_last`;
1328
1477
  var reviewSessionsKey = (id) => `wire_review_${id}_sessions`;
1329
1478
  var reviewSessionOpenKey = (id) => `wire_review_${id}_open`;
1330
- var resolveStorage = (override) => override != null ? override : getCoachmarkStorage();
1479
+ var resolveStorage = (override) => validateGateStorage(override != null ? override : getCoachmarkStorage());
1331
1480
  var NO_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
1332
1481
  "@wireai/activation:gateStorageWarned"
1333
1482
  );