@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
@@ -411,6 +411,23 @@ var buildEventsRequest = (target, events) => {
411
411
  return null;
412
412
  }
413
413
  };
414
+ var warnOnSkippedEvents = (res) => {
415
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
416
+ if (typeof console === "undefined" || !console.warn) return;
417
+ try {
418
+ const json = res == null ? void 0 : res.json;
419
+ if (typeof json !== "function") return;
420
+ void Promise.resolve(json.call(res)).then((body) => {
421
+ const skipped = body == null ? void 0 : body.skipped;
422
+ if (typeof skipped !== "number" || skipped <= 0) return;
423
+ console.warn(
424
+ `[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.`
425
+ );
426
+ }).catch(() => {
427
+ });
428
+ } catch {
429
+ }
430
+ };
414
431
  var reportClientEvents = (target, events) => {
415
432
  try {
416
433
  const req = buildEventsRequest(target, events);
@@ -433,15 +450,54 @@ var reportClientEventsAwait = async (target, events) => {
433
450
  };
434
451
  var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
435
452
 
453
+ // src/analytics/currentSession.ts
454
+ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
455
+ "@wireai/activation:currentSessionId"
456
+ );
457
+ var globalSlot = globalThis;
458
+ var setCurrentSessionId = (id) => {
459
+ if (typeof id === "string" && id.length > 0) {
460
+ globalSlot[CURRENT_SESSION_ID_SLOT] = id;
461
+ }
462
+ };
463
+ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
464
+ var resetCurrentSessionId = () => {
465
+ globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
466
+ };
467
+ var warnInDev = (message) => {
468
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
469
+ console.warn(message);
470
+ return true;
471
+ }
472
+ return false;
473
+ };
474
+ 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.";
475
+ var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
476
+ "@wireai/activation:currentSessionIdMintWarned"
477
+ );
478
+ var warnSlot = globalThis;
479
+ var ensureCurrentSessionId = () => {
480
+ const existing = globalSlot[CURRENT_SESSION_ID_SLOT];
481
+ if (typeof existing === "string" && existing.length > 0) return existing;
482
+ const minted = makeSessionId();
483
+ globalSlot[CURRENT_SESSION_ID_SLOT] = minted;
484
+ if (!warnSlot[MINT_WARNED_SLOT] && warnInDev(MINT_WARNING)) {
485
+ warnSlot[MINT_WARNED_SLOT] = true;
486
+ }
487
+ return minted;
488
+ };
489
+
436
490
  // src/reviews/transport.ts
437
491
  var reportAppEvent = (target, name2, options = {}) => {
492
+ var _a2;
438
493
  if (!(target == null ? void 0 : target.serverUrl) || !name2) return;
439
494
  try {
495
+ const supplied = (_a2 = options.sessionId) != null ? _a2 : "";
440
496
  const event = {
441
497
  event_type: "app_event",
442
- question_key: name2
498
+ question_key: name2,
499
+ session_id: supplied.trim().length > 0 ? supplied : ensureCurrentSessionId()
443
500
  };
444
- if (options.sessionId) event.session_id = options.sessionId;
445
501
  if (options.deviceKey) event.user_context = { device_key: options.deviceKey };
446
502
  if (options.meta && Object.keys(options.meta).length > 0) {
447
503
  event.meta = JSON.stringify(options.meta);
@@ -877,6 +933,7 @@ var createEventQueue = (options) => {
877
933
  const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
878
934
  try {
879
935
  const res = await fetch(req.url, { ...req.init, signal: controller == null ? void 0 : controller.signal });
936
+ warnOnSkippedEvents(res);
880
937
  return !!(res && res.ok);
881
938
  } catch {
882
939
  return false;
@@ -955,21 +1012,6 @@ var createEventQueue = (options) => {
955
1012
  return { enqueue, flush, notifyOnline, size };
956
1013
  };
957
1014
 
958
- // src/analytics/currentSession.ts
959
- var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
960
- "@wireai/activation:currentSessionId"
961
- );
962
- var globalSlot = globalThis;
963
- var setCurrentSessionId = (id) => {
964
- if (typeof id === "string" && id.length > 0) {
965
- globalSlot[CURRENT_SESSION_ID_SLOT] = id;
966
- }
967
- };
968
- var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
969
- var resetCurrentSessionId = () => {
970
- globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
971
- };
972
-
973
1015
  // src/identity/userIdentity.ts
974
1016
  var USER_ID_MAX_LENGTH = 128;
975
1017
  var sanitizeUserId = (raw) => {
@@ -1066,34 +1108,90 @@ var mintDeviceId = () => {
1066
1108
  const time = Date.now().toString(36);
1067
1109
  return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
1068
1110
  };
1111
+ var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
1112
+ var deviceKeyGlobal = globalThis;
1113
+ var autoDeviceKeyRegistry = () => {
1114
+ const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
1115
+ if (existing) return existing;
1116
+ const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
1117
+ deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
1118
+ return created;
1119
+ };
1120
+ var startHydration = (registry, appId, storage, minted) => {
1121
+ if (!registry.pending) registry.pending = /* @__PURE__ */ new Map();
1122
+ const existing = registry.pending.get(appId);
1123
+ if (existing) return existing;
1124
+ const slot = deviceIdStorageKey(appId);
1125
+ const settled = () => {
1126
+ var _a2;
1127
+ return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
1128
+ };
1129
+ let run;
1130
+ try {
1131
+ run = Promise.resolve(storage.getItem(slot)).then((saved) => {
1132
+ const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1133
+ if (persisted) {
1134
+ registry.keys.set(appId, persisted);
1135
+ return persisted;
1136
+ }
1137
+ return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
1138
+ }).catch(settled);
1139
+ } catch {
1140
+ run = Promise.resolve(settled());
1141
+ }
1142
+ registry.pending.set(appId, run);
1143
+ return run;
1144
+ };
1145
+ var resolveAutoDeviceKey = (opts = {}) => {
1146
+ var _a2, _b;
1147
+ const registry = autoDeviceKeyRegistry();
1148
+ const appId = (_a2 = opts.appId) != null ? _a2 : "default";
1149
+ let id = registry.keys.get(appId);
1150
+ if (!id) {
1151
+ id = mintDeviceId();
1152
+ registry.keys.set(appId, id);
1153
+ }
1154
+ const storage = opts.storage;
1155
+ if (storage && !registry.hydrating.has(appId)) {
1156
+ registry.hydrating.add(appId);
1157
+ void startHydration(registry, appId, storage, id);
1158
+ }
1159
+ return (_b = registry.keys.get(appId)) != null ? _b : id;
1160
+ };
1161
+ var resetAutoDeviceKeys = () => {
1162
+ var _a2;
1163
+ const registry = autoDeviceKeyRegistry();
1164
+ registry.keys.clear();
1165
+ registry.hydrating.clear();
1166
+ (_a2 = registry.pending) == null ? void 0 : _a2.clear();
1167
+ };
1069
1168
 
1070
1169
  // src/analytics/analyticsFacade.ts
1071
- var warnInDev = (message) => {
1170
+ var warnInDev2 = (message) => {
1072
1171
  if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1073
1172
  console.warn(message);
1074
1173
  }
1075
1174
  };
1076
1175
  var createAnalytics = (config, options = {}) => {
1077
- var _a2, _b, _c, _d;
1078
- const instanceSessionId = (_a2 = config.sessionId) != null ? _a2 : makeSessionId();
1176
+ var _a2, _b, _c;
1079
1177
  const resolveSessionId = () => {
1080
- var _a3, _b2;
1081
- return (_b2 = (_a3 = config.sessionId) != null ? _a3 : getCurrentSessionId()) != null ? _b2 : instanceSessionId;
1178
+ var _a3;
1179
+ return (_a3 = config.sessionId) != null ? _a3 : ensureCurrentSessionId();
1082
1180
  };
1083
- let userContext = { ...(_b = config.userContext) != null ? _b : {} };
1084
- const hostDeviceKeyAtInit = typeof ((_c = config.userContext) == null ? void 0 : _c.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1085
- let autoDeviceKey = mintDeviceId();
1086
- if (config.storage && !hostDeviceKeyAtInit) {
1087
- const storage = config.storage;
1088
- const deviceKey = deviceIdStorageKey(config.appId);
1089
- void storage.getItem(deviceKey).then((saved) => {
1090
- const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1091
- if (persisted) autoDeviceKey = persisted;
1092
- else void storage.setItem(deviceKey, autoDeviceKey).catch(() => {
1093
- });
1094
- }).catch(() => {
1095
- });
1181
+ if (config.sessionId) {
1182
+ warnInDev2(
1183
+ "[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."
1184
+ );
1096
1185
  }
1186
+ const detectedAppVersion = detectAppVersion();
1187
+ let userContext = { ...(_a2 = config.userContext) != null ? _a2 : {} };
1188
+ const hostDeviceKeyAtInit = typeof ((_b = config.userContext) == null ? void 0 : _b.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1189
+ const autoDeviceKeyOptions = {
1190
+ appId: config.appId,
1191
+ // A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).
1192
+ storage: hostDeviceKeyAtInit ? void 0 : config.storage
1193
+ };
1194
+ if (!hostDeviceKeyAtInit) resolveAutoDeviceKey(autoDeviceKeyOptions);
1097
1195
  const envelope = () => {
1098
1196
  var _a3;
1099
1197
  return buildContextEnvelope({
@@ -1110,7 +1208,7 @@ var createAnalytics = (config, options = {}) => {
1110
1208
  envelope,
1111
1209
  ...options
1112
1210
  });
1113
- let boundUserId = sanitizeUserId((_d = config.userContext) == null ? void 0 : _d.userId);
1211
+ let boundUserId = sanitizeUserId((_c = config.userContext) == null ? void 0 : _c.userId);
1114
1212
  const storageKey = analyticsUserIdStorageKey(config.appId);
1115
1213
  if (config.storage) {
1116
1214
  void config.storage.getItem(storageKey).then((saved) => {
@@ -1119,20 +1217,21 @@ var createAnalytics = (config, options = {}) => {
1119
1217
  });
1120
1218
  }
1121
1219
  const applyContext = (event) => {
1122
- var _a3;
1220
+ var _a3, _b2;
1123
1221
  const hostDeviceKey = typeof userContext.deviceKey === "string" && userContext.deviceKey.trim() ? userContext.deviceKey : void 0;
1124
1222
  const resolved = resolveUserContext(
1125
- { ...userContext, deviceKey: hostDeviceKey != null ? hostDeviceKey : autoDeviceKey },
1126
- { autoAppVersion: config.appVersion }
1223
+ // `??` is lazy on purpose: a host-supplied key must never even touch the auto registry.
1224
+ { ...userContext, deviceKey: hostDeviceKey != null ? hostDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions) },
1225
+ { autoAppVersion: (_a3 = config.appVersion) != null ? _a3 : detectedAppVersion }
1127
1226
  );
1128
1227
  if (resolved.userContext) {
1129
- event.user_context = { ...resolved.userContext, ...(_a3 = event.user_context) != null ? _a3 : {} };
1228
+ event.user_context = { ...resolved.userContext, ...(_b2 = event.user_context) != null ? _b2 : {} };
1130
1229
  }
1131
1230
  if (boundUserId && !event.user_id) event.user_id = boundUserId;
1132
1231
  };
1133
1232
  const guardUserId = (clean) => {
1134
1233
  if (config.allowEmailAsUserId || !looksLikeEmail(clean)) return clean;
1135
- warnInDev(
1234
+ warnInDev2(
1136
1235
  "[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."
1137
1236
  );
1138
1237
  return void 0;
@@ -1215,6 +1314,7 @@ var createAnalytics = (config, options = {}) => {
1215
1314
  };
1216
1315
  };
1217
1316
  var useAnalytics = (config, options = {}) => {
1317
+ var _a2;
1218
1318
  const ref = useRef(void 0);
1219
1319
  const prevKeys = useRef("");
1220
1320
  const currentKeys = `${config.serverUrl}|${config.apiKey}|${config.appId}`;
@@ -1222,9 +1322,14 @@ var useAnalytics = (config, options = {}) => {
1222
1322
  prevKeys.current = currentKeys;
1223
1323
  ref.current = createAnalytics(config, options);
1224
1324
  }
1325
+ const hostDeviceKey = typeof ((_a2 = config.userContext) == null ? void 0 : _a2.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1326
+ useEffect(() => {
1327
+ var _a3;
1328
+ if (hostDeviceKey) (_a3 = ref.current) == null ? void 0 : _a3.setUserContext({ deviceKey: hostDeviceKey });
1329
+ }, [hostDeviceKey]);
1225
1330
  return ref.current;
1226
1331
  };
1227
1332
 
1228
- export { WIRE_ONBOARDING_EVENTS, analyticsUserIdStorageKey, buildContextEnvelope, clearPiiFromContext, clearUserContext, createAnalytics, createEventQueue, createScreenTracker, getActiveRouteName, getCurrentSessionId, looksLikeEmail, makeSessionId, reportAppEvent, reportClientEvent, reportClientEventAwait, reportClientEvents, reportClientEventsAwait, resetCurrentSessionId, screenTrackingHandler, setCurrentSessionId, toAnalyticsEvent, useAnalytics, useScreenTracking };
1333
+ export { AUTO_DEVICE_ID_PREFIX, WIRE_ONBOARDING_EVENTS, analyticsUserIdStorageKey, buildContextEnvelope, clearPiiFromContext, clearUserContext, createAnalytics, createEventQueue, createScreenTracker, deviceIdStorageKey, ensureCurrentSessionId, getActiveRouteName, getCurrentSessionId, looksLikeEmail, makeSessionId, reportAppEvent, reportClientEvent, reportClientEventAwait, reportClientEvents, reportClientEventsAwait, resetAutoDeviceKeys, resetCurrentSessionId, resolveAutoDeviceKey, screenTrackingHandler, setCurrentSessionId, toAnalyticsEvent, useAnalytics, useScreenTracking };
1229
1334
  //# sourceMappingURL=index.mjs.map
1230
1335
  //# sourceMappingURL=index.mjs.map