@wireai/activation 0.11.0 → 0.12.1

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 (73) hide show
  1. package/AGENTS.md +62 -8
  2. package/CHANGELOG.md +188 -3
  3. package/INTEGRATION_PROMPT.md +25 -2
  4. package/README.md +112 -1
  5. package/dist/analytics/index.d.mts +18 -6
  6. package/dist/analytics/index.d.ts +18 -6
  7. package/dist/analytics/index.js +151 -41
  8. package/dist/analytics/index.js.map +1 -1
  9. package/dist/analytics/index.mjs +147 -42
  10. package/dist/analytics/index.mjs.map +1 -1
  11. package/dist/coachmarks/index.d.mts +16 -0
  12. package/dist/coachmarks/index.d.ts +16 -0
  13. package/dist/coachmarks/index.js +19 -13
  14. package/dist/coachmarks/index.js.map +1 -1
  15. package/dist/coachmarks/index.mjs +19 -13
  16. package/dist/coachmarks/index.mjs.map +1 -1
  17. package/dist/{currentSession-C0_odnIW.d.mts → currentSession-BlCeDP0f.d.mts} +145 -33
  18. package/dist/{currentSession-DdnUq2HQ.d.ts → currentSession-BxEB37xt.d.ts} +145 -33
  19. package/dist/index.d.mts +243 -53
  20. package/dist/index.d.ts +243 -53
  21. package/dist/index.js +531 -156
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +517 -157
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/questionnaire/index.d.mts +1 -1
  26. package/dist/questionnaire/index.d.ts +1 -1
  27. package/dist/questionnaire/index.js +79 -14
  28. package/dist/questionnaire/index.js.map +1 -1
  29. package/dist/questionnaire/index.mjs +79 -14
  30. package/dist/questionnaire/index.mjs.map +1 -1
  31. package/dist/reviews/index.d.mts +2 -2
  32. package/dist/reviews/index.d.ts +2 -2
  33. package/dist/reviews/index.js +103 -16
  34. package/dist/reviews/index.js.map +1 -1
  35. package/dist/reviews/index.mjs +103 -16
  36. package/dist/reviews/index.mjs.map +1 -1
  37. package/dist/showcase/index.js +15 -6
  38. package/dist/showcase/index.js.map +1 -1
  39. package/dist/showcase/index.mjs +15 -6
  40. package/dist/showcase/index.mjs.map +1 -1
  41. package/dist/{transport-BGW9uXZJ.d.mts → transport-CF_eHwzC.d.mts} +15 -1
  42. package/dist/{transport-jUJd5kxu.d.ts → transport-DsRe4epC.d.ts} +15 -1
  43. package/llms.txt +3 -0
  44. package/package.json +1 -1
  45. package/src/WireOnboarding.tsx +140 -5
  46. package/src/activation/useWireActivation.ts +12 -1
  47. package/src/activation/wireActivation.ts +44 -25
  48. package/src/analytics/analyticsFacade.ts +54 -29
  49. package/src/analytics/currentSession.ts +83 -0
  50. package/src/analytics/eventQueue.ts +9 -1
  51. package/src/analytics/index.ts +20 -1
  52. package/src/analytics/reportClientEvent.ts +42 -0
  53. package/src/analytics/screenTracking.ts +6 -1
  54. package/src/analytics/useAnalytics.ts +22 -1
  55. package/src/coachmarks/runtime.ts +53 -17
  56. package/src/config/wireConfigFromEnv.ts +46 -2
  57. package/src/context/deviceId.ts +173 -0
  58. package/src/context/userContext.ts +18 -0
  59. package/src/index.ts +54 -2
  60. package/src/questionnaire/runtime.ts +13 -2
  61. package/src/questionnaire/useQuestionnaireGate.ts +11 -4
  62. package/src/revenuecat/index.ts +55 -0
  63. package/src/revenuecat/purchaseEvents.ts +167 -0
  64. package/src/revenuecat/revenueCatBridge.ts +221 -0
  65. package/src/revenuecat/types.ts +95 -0
  66. package/src/reviews/runtime.ts +153 -1
  67. package/src/reviews/transport.ts +21 -2
  68. package/src/reviews/useReviewGate.ts +14 -4
  69. package/src/session-analytics/lifecycle.ts +9 -2
  70. package/src/session-analytics/reportSessionStart.ts +15 -4
  71. package/src/session-analytics/useLifecycleEvents.ts +83 -27
  72. package/src/session-analytics/useSessionStart.ts +37 -1
  73. package/src/types.ts +41 -4
@@ -413,6 +413,23 @@ var buildEventsRequest = (target, events) => {
413
413
  return null;
414
414
  }
415
415
  };
416
+ var warnOnSkippedEvents = (res) => {
417
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
418
+ if (typeof console === "undefined" || !console.warn) return;
419
+ try {
420
+ const json = res == null ? void 0 : res.json;
421
+ if (typeof json !== "function") return;
422
+ void Promise.resolve(json.call(res)).then((body) => {
423
+ const skipped = body == null ? void 0 : body.skipped;
424
+ if (typeof skipped !== "number" || skipped <= 0) return;
425
+ console.warn(
426
+ `[wireai] the server ACCEPTED the /v1/events POST but DISCARDED ${skipped} event(s) (skipped in the response body) \u2014 they are gone, not retried. The usual cause is an event with a missing or empty session_id.`
427
+ );
428
+ }).catch(() => {
429
+ });
430
+ } catch {
431
+ }
432
+ };
416
433
  var reportClientEvents = (target, events) => {
417
434
  try {
418
435
  const req = buildEventsRequest(target, events);
@@ -435,15 +452,54 @@ var reportClientEventsAwait = async (target, events) => {
435
452
  };
436
453
  var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
437
454
 
455
+ // src/analytics/currentSession.ts
456
+ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
457
+ "@wireai/activation:currentSessionId"
458
+ );
459
+ var globalSlot = globalThis;
460
+ var setCurrentSessionId = (id) => {
461
+ if (typeof id === "string" && id.length > 0) {
462
+ globalSlot[CURRENT_SESSION_ID_SLOT] = id;
463
+ }
464
+ };
465
+ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
466
+ var resetCurrentSessionId = () => {
467
+ globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
468
+ };
469
+ var warnInDev = (message) => {
470
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
471
+ console.warn(message);
472
+ return true;
473
+ }
474
+ return false;
475
+ };
476
+ 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.";
477
+ var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
478
+ "@wireai/activation:currentSessionIdMintWarned"
479
+ );
480
+ var warnSlot = globalThis;
481
+ var ensureCurrentSessionId = () => {
482
+ const existing = globalSlot[CURRENT_SESSION_ID_SLOT];
483
+ if (typeof existing === "string" && existing.length > 0) return existing;
484
+ const minted = makeSessionId();
485
+ globalSlot[CURRENT_SESSION_ID_SLOT] = minted;
486
+ if (!warnSlot[MINT_WARNED_SLOT] && warnInDev(MINT_WARNING)) {
487
+ warnSlot[MINT_WARNED_SLOT] = true;
488
+ }
489
+ return minted;
490
+ };
491
+
438
492
  // src/reviews/transport.ts
439
493
  var reportAppEvent = (target, name2, options = {}) => {
494
+ var _a2;
440
495
  if (!(target == null ? void 0 : target.serverUrl) || !name2) return;
441
496
  try {
497
+ const supplied = (_a2 = options.sessionId) != null ? _a2 : "";
442
498
  const event = {
443
499
  event_type: "app_event",
444
- question_key: name2
500
+ question_key: name2,
501
+ session_id: supplied.trim().length > 0 ? supplied : ensureCurrentSessionId()
445
502
  };
446
- if (options.sessionId) event.session_id = options.sessionId;
447
503
  if (options.deviceKey) event.user_context = { device_key: options.deviceKey };
448
504
  if (options.meta && Object.keys(options.meta).length > 0) {
449
505
  event.meta = JSON.stringify(options.meta);
@@ -879,6 +935,7 @@ var createEventQueue = (options) => {
879
935
  const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
880
936
  try {
881
937
  const res = await fetch(req.url, { ...req.init, signal: controller == null ? void 0 : controller.signal });
938
+ warnOnSkippedEvents(res);
882
939
  return !!(res && res.ok);
883
940
  } catch {
884
941
  return false;
@@ -957,21 +1014,6 @@ var createEventQueue = (options) => {
957
1014
  return { enqueue, flush, notifyOnline, size };
958
1015
  };
959
1016
 
960
- // src/analytics/currentSession.ts
961
- var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
962
- "@wireai/activation:currentSessionId"
963
- );
964
- var globalSlot = globalThis;
965
- var setCurrentSessionId = (id) => {
966
- if (typeof id === "string" && id.length > 0) {
967
- globalSlot[CURRENT_SESSION_ID_SLOT] = id;
968
- }
969
- };
970
- var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
971
- var resetCurrentSessionId = () => {
972
- globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
973
- };
974
-
975
1017
  // src/identity/userIdentity.ts
976
1018
  var USER_ID_MAX_LENGTH = 128;
977
1019
  var sanitizeUserId = (raw) => {
@@ -1068,34 +1110,90 @@ var mintDeviceId = () => {
1068
1110
  const time = Date.now().toString(36);
1069
1111
  return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
1070
1112
  };
1113
+ var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
1114
+ var deviceKeyGlobal = globalThis;
1115
+ var autoDeviceKeyRegistry = () => {
1116
+ const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
1117
+ if (existing) return existing;
1118
+ const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
1119
+ deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
1120
+ return created;
1121
+ };
1122
+ var startHydration = (registry, appId, storage, minted) => {
1123
+ if (!registry.pending) registry.pending = /* @__PURE__ */ new Map();
1124
+ const existing = registry.pending.get(appId);
1125
+ if (existing) return existing;
1126
+ const slot = deviceIdStorageKey(appId);
1127
+ const settled = () => {
1128
+ var _a2;
1129
+ return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
1130
+ };
1131
+ let run;
1132
+ try {
1133
+ run = Promise.resolve(storage.getItem(slot)).then((saved) => {
1134
+ const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1135
+ if (persisted) {
1136
+ registry.keys.set(appId, persisted);
1137
+ return persisted;
1138
+ }
1139
+ return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
1140
+ }).catch(settled);
1141
+ } catch {
1142
+ run = Promise.resolve(settled());
1143
+ }
1144
+ registry.pending.set(appId, run);
1145
+ return run;
1146
+ };
1147
+ var resolveAutoDeviceKey = (opts = {}) => {
1148
+ var _a2, _b;
1149
+ const registry = autoDeviceKeyRegistry();
1150
+ const appId = (_a2 = opts.appId) != null ? _a2 : "default";
1151
+ let id = registry.keys.get(appId);
1152
+ if (!id) {
1153
+ id = mintDeviceId();
1154
+ registry.keys.set(appId, id);
1155
+ }
1156
+ const storage = opts.storage;
1157
+ if (storage && !registry.hydrating.has(appId)) {
1158
+ registry.hydrating.add(appId);
1159
+ void startHydration(registry, appId, storage, id);
1160
+ }
1161
+ return (_b = registry.keys.get(appId)) != null ? _b : id;
1162
+ };
1163
+ var resetAutoDeviceKeys = () => {
1164
+ var _a2;
1165
+ const registry = autoDeviceKeyRegistry();
1166
+ registry.keys.clear();
1167
+ registry.hydrating.clear();
1168
+ (_a2 = registry.pending) == null ? void 0 : _a2.clear();
1169
+ };
1071
1170
 
1072
1171
  // src/analytics/analyticsFacade.ts
1073
- var warnInDev = (message) => {
1172
+ var warnInDev2 = (message) => {
1074
1173
  if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1075
1174
  console.warn(message);
1076
1175
  }
1077
1176
  };
1078
1177
  var createAnalytics = (config, options = {}) => {
1079
- var _a2, _b, _c, _d;
1080
- const instanceSessionId = (_a2 = config.sessionId) != null ? _a2 : makeSessionId();
1178
+ var _a2, _b, _c;
1081
1179
  const resolveSessionId = () => {
1082
- var _a3, _b2;
1083
- return (_b2 = (_a3 = config.sessionId) != null ? _a3 : getCurrentSessionId()) != null ? _b2 : instanceSessionId;
1180
+ var _a3;
1181
+ return (_a3 = config.sessionId) != null ? _a3 : ensureCurrentSessionId();
1084
1182
  };
1085
- let userContext = { ...(_b = config.userContext) != null ? _b : {} };
1086
- const hostDeviceKeyAtInit = typeof ((_c = config.userContext) == null ? void 0 : _c.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1087
- let autoDeviceKey = mintDeviceId();
1088
- if (config.storage && !hostDeviceKeyAtInit) {
1089
- const storage = config.storage;
1090
- const deviceKey = deviceIdStorageKey(config.appId);
1091
- void storage.getItem(deviceKey).then((saved) => {
1092
- const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1093
- if (persisted) autoDeviceKey = persisted;
1094
- else void storage.setItem(deviceKey, autoDeviceKey).catch(() => {
1095
- });
1096
- }).catch(() => {
1097
- });
1183
+ if (config.sessionId) {
1184
+ warnInDev2(
1185
+ "[wireai] createAnalytics({ sessionId }) PINS every event from this instance to that one frozen id and opts out of the live per-open session (app.session_started) \u2014 lifecycle analytics collapse onto a single device-scoped id. Remove it unless your host runs its own session lifecycle."
1186
+ );
1098
1187
  }
1188
+ const detectedAppVersion = detectAppVersion();
1189
+ let userContext = { ...(_a2 = config.userContext) != null ? _a2 : {} };
1190
+ const hostDeviceKeyAtInit = typeof ((_b = config.userContext) == null ? void 0 : _b.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1191
+ const autoDeviceKeyOptions = {
1192
+ appId: config.appId,
1193
+ // A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).
1194
+ storage: hostDeviceKeyAtInit ? void 0 : config.storage
1195
+ };
1196
+ if (!hostDeviceKeyAtInit) resolveAutoDeviceKey(autoDeviceKeyOptions);
1099
1197
  const envelope = () => {
1100
1198
  var _a3;
1101
1199
  return buildContextEnvelope({
@@ -1112,7 +1210,7 @@ var createAnalytics = (config, options = {}) => {
1112
1210
  envelope,
1113
1211
  ...options
1114
1212
  });
1115
- let boundUserId = sanitizeUserId((_d = config.userContext) == null ? void 0 : _d.userId);
1213
+ let boundUserId = sanitizeUserId((_c = config.userContext) == null ? void 0 : _c.userId);
1116
1214
  const storageKey = analyticsUserIdStorageKey(config.appId);
1117
1215
  if (config.storage) {
1118
1216
  void config.storage.getItem(storageKey).then((saved) => {
@@ -1121,20 +1219,21 @@ var createAnalytics = (config, options = {}) => {
1121
1219
  });
1122
1220
  }
1123
1221
  const applyContext = (event) => {
1124
- var _a3;
1222
+ var _a3, _b2;
1125
1223
  const hostDeviceKey = typeof userContext.deviceKey === "string" && userContext.deviceKey.trim() ? userContext.deviceKey : void 0;
1126
1224
  const resolved = resolveUserContext(
1127
- { ...userContext, deviceKey: hostDeviceKey != null ? hostDeviceKey : autoDeviceKey },
1128
- { autoAppVersion: config.appVersion }
1225
+ // `??` is lazy on purpose: a host-supplied key must never even touch the auto registry.
1226
+ { ...userContext, deviceKey: hostDeviceKey != null ? hostDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions) },
1227
+ { autoAppVersion: (_a3 = config.appVersion) != null ? _a3 : detectedAppVersion }
1129
1228
  );
1130
1229
  if (resolved.userContext) {
1131
- event.user_context = { ...resolved.userContext, ...(_a3 = event.user_context) != null ? _a3 : {} };
1230
+ event.user_context = { ...resolved.userContext, ...(_b2 = event.user_context) != null ? _b2 : {} };
1132
1231
  }
1133
1232
  if (boundUserId && !event.user_id) event.user_id = boundUserId;
1134
1233
  };
1135
1234
  const guardUserId = (clean) => {
1136
1235
  if (config.allowEmailAsUserId || !looksLikeEmail(clean)) return clean;
1137
- warnInDev(
1236
+ warnInDev2(
1138
1237
  "[wireai] identify() was called with an email-shaped id. A raw email must NOT be the opaque user_id (PII leak) \u2014 pass it as userContext.userEmail instead. Binding was skipped. Set allowEmailAsUserId:true on createAnalytics if your user id genuinely is an email."
1139
1238
  );
1140
1239
  return void 0;
@@ -1217,6 +1316,7 @@ var createAnalytics = (config, options = {}) => {
1217
1316
  };
1218
1317
  };
1219
1318
  var useAnalytics = (config, options = {}) => {
1319
+ var _a2;
1220
1320
  const ref = react.useRef(void 0);
1221
1321
  const prevKeys = react.useRef("");
1222
1322
  const currentKeys = `${config.serverUrl}|${config.apiKey}|${config.appId}`;
@@ -1224,9 +1324,15 @@ var useAnalytics = (config, options = {}) => {
1224
1324
  prevKeys.current = currentKeys;
1225
1325
  ref.current = createAnalytics(config, options);
1226
1326
  }
1327
+ const hostDeviceKey = typeof ((_a2 = config.userContext) == null ? void 0 : _a2.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1328
+ react.useEffect(() => {
1329
+ var _a3;
1330
+ if (hostDeviceKey) (_a3 = ref.current) == null ? void 0 : _a3.setUserContext({ deviceKey: hostDeviceKey });
1331
+ }, [hostDeviceKey]);
1227
1332
  return ref.current;
1228
1333
  };
1229
1334
 
1335
+ exports.AUTO_DEVICE_ID_PREFIX = AUTO_DEVICE_ID_PREFIX;
1230
1336
  exports.WIRE_ONBOARDING_EVENTS = WIRE_ONBOARDING_EVENTS;
1231
1337
  exports.analyticsUserIdStorageKey = analyticsUserIdStorageKey;
1232
1338
  exports.buildContextEnvelope = buildContextEnvelope;
@@ -1235,6 +1341,8 @@ exports.clearUserContext = clearUserContext;
1235
1341
  exports.createAnalytics = createAnalytics;
1236
1342
  exports.createEventQueue = createEventQueue;
1237
1343
  exports.createScreenTracker = createScreenTracker;
1344
+ exports.deviceIdStorageKey = deviceIdStorageKey;
1345
+ exports.ensureCurrentSessionId = ensureCurrentSessionId;
1238
1346
  exports.getActiveRouteName = getActiveRouteName;
1239
1347
  exports.getCurrentSessionId = getCurrentSessionId;
1240
1348
  exports.looksLikeEmail = looksLikeEmail;
@@ -1244,7 +1352,9 @@ exports.reportClientEvent = reportClientEvent;
1244
1352
  exports.reportClientEventAwait = reportClientEventAwait;
1245
1353
  exports.reportClientEvents = reportClientEvents;
1246
1354
  exports.reportClientEventsAwait = reportClientEventsAwait;
1355
+ exports.resetAutoDeviceKeys = resetAutoDeviceKeys;
1247
1356
  exports.resetCurrentSessionId = resetCurrentSessionId;
1357
+ exports.resolveAutoDeviceKey = resolveAutoDeviceKey;
1248
1358
  exports.screenTrackingHandler = screenTrackingHandler;
1249
1359
  exports.setCurrentSessionId = setCurrentSessionId;
1250
1360
  exports.toAnalyticsEvent = toAnalyticsEvent;