@wireai/activation 0.12.2 → 0.13.2

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 (55) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +259 -1
  3. package/README.md +87 -3
  4. package/dist/analytics/index.d.mts +2 -2
  5. package/dist/analytics/index.d.ts +2 -2
  6. package/dist/analytics/index.js +174 -36
  7. package/dist/analytics/index.js.map +1 -1
  8. package/dist/analytics/index.mjs +174 -37
  9. package/dist/analytics/index.mjs.map +1 -1
  10. package/dist/{currentSession-BlCeDP0f.d.mts → currentSession-ClkLjcJ0.d.mts} +456 -19
  11. package/dist/{currentSession-BxEB37xt.d.ts → currentSession-DOVZEWJl.d.ts} +456 -19
  12. package/dist/index.d.mts +197 -16
  13. package/dist/index.d.ts +197 -16
  14. package/dist/index.js +1056 -390
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +836 -193
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/questionnaire/index.js.map +1 -1
  19. package/dist/questionnaire/index.mjs.map +1 -1
  20. package/dist/reviews/index.js +20 -7
  21. package/dist/reviews/index.js.map +1 -1
  22. package/dist/reviews/index.mjs +20 -7
  23. package/dist/reviews/index.mjs.map +1 -1
  24. package/package.json +1 -1
  25. package/src/OnboardingFlow.tsx +141 -3
  26. package/src/WireOnboarding.tsx +178 -34
  27. package/src/activation/wireActivation.ts +13 -7
  28. package/src/analytics/analyticsEvent.ts +16 -1
  29. package/src/analytics/analyticsFacade.ts +11 -10
  30. package/src/analytics/currentSession.ts +6 -20
  31. package/src/analytics/eventQueue.ts +71 -1
  32. package/src/analytics/index.ts +1 -1
  33. package/src/analytics/reportClientEvent.ts +157 -38
  34. package/src/cards/PermissionCard.tsx +438 -0
  35. package/src/cards/index.ts +7 -0
  36. package/src/config/wireConfigFromEnv.ts +1 -10
  37. package/src/context/deviceId.ts +77 -16
  38. package/src/context/userContext.ts +4 -15
  39. package/src/identity/identityRecord.ts +123 -0
  40. package/src/identity/userIdentity.ts +45 -9
  41. package/src/illustrations/defaultIllustrations.tsx +44 -3
  42. package/src/index.ts +44 -4
  43. package/src/permissions/index.ts +64 -0
  44. package/src/permissions/permissionCopy.ts +87 -0
  45. package/src/permissions/permissionEvents.ts +76 -0
  46. package/src/permissions/permissionMemory.ts +88 -0
  47. package/src/permissions/placement.ts +88 -0
  48. package/src/permissions/types.ts +131 -0
  49. package/src/session/persistedSession.ts +10 -3
  50. package/src/session-analytics/useLifecycleEvents.ts +10 -1
  51. package/src/types.ts +77 -1
  52. package/src/utils/deriveAnswers.ts +6 -2
  53. package/src/utils/readProgress.ts +4 -0
  54. package/src/utils/warnInDev.ts +33 -0
  55. package/src/components/DoneBlock.tsx +0 -37
@@ -400,39 +400,63 @@ var init_Constants = __esm({
400
400
 
401
401
  // src/analytics/reportClientEvent.ts
402
402
  var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
403
+ var toWireEvents = (events) => events.map((event) => {
404
+ if (typeof event.ts !== "number") return event;
405
+ const { ts, ...rest } = event;
406
+ if (!Number.isFinite(ts)) return rest;
407
+ try {
408
+ return { ...rest, ts: new Date(ts).toISOString() };
409
+ } catch {
410
+ return rest;
411
+ }
412
+ });
403
413
  var buildEventsRequest = (target, events) => {
404
414
  if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return null;
405
415
  try {
406
416
  const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
407
417
  const headers = { "Content-Type": "application/json" };
408
418
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
409
- return { url, init: { method: "POST", headers, body: JSON.stringify({ events }) } };
419
+ const body = JSON.stringify({ events: toWireEvents(events) });
420
+ return { url, init: { method: "POST", headers, body } };
410
421
  } catch {
411
422
  return null;
412
423
  }
413
424
  };
414
- var warnOnSkippedEvents = (res) => {
415
- if (typeof __DEV__ === "undefined" || !__DEV__) return;
416
- if (typeof console === "undefined" || !console.warn) return;
425
+ var readEventsAck = async (res) => {
417
426
  try {
418
427
  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
+ if (typeof json !== "function") return void 0;
429
+ const body = await Promise.resolve(json.call(res));
430
+ const skipped = body == null ? void 0 : body.skipped;
431
+ if (typeof skipped !== "number" || !Number.isFinite(skipped)) return void 0;
432
+ const reasons = Array.isArray(body == null ? void 0 : body.errors) ? body.errors.map((e) => {
433
+ const entry = e;
434
+ const reason = entry == null ? void 0 : entry.reason;
435
+ if (typeof reason !== "string") return void 0;
436
+ const field = entry == null ? void 0 : entry.field;
437
+ return typeof field === "string" && field.length > 0 ? `${reason} (field: ${field})` : reason;
438
+ }).filter((r) => typeof r === "string") : [];
439
+ return { written: typeof (body == null ? void 0 : body.written) === "number" ? body.written : void 0, skipped, reasons };
428
440
  } catch {
441
+ return void 0;
429
442
  }
430
443
  };
444
+ var describeDiscarded = (ack) => `[wireai] the server ACCEPTED the /v1/events POST but DISCARDED ${ack.skipped} event(s) (skipped in the response body) \u2014 they are gone, not retried. The server reported: skipped=${ack.skipped}${ack.written !== void 0 ? `, written=${ack.written}` : ""}` + (ack.reasons.length > 0 ? `, reasons: ${ack.reasons.join(", ")}.` : ". It gave no reason (the endpoint folds every rejection into one count), so check the server's ingest log for this request rather than guessing.");
445
+ var warnOnSkippedEvents = (res) => {
446
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
447
+ if (typeof console === "undefined" || !console.warn) return;
448
+ void readEventsAck(res).then((ack) => {
449
+ if (!ack || ack.skipped <= 0) return;
450
+ console.warn(describeDiscarded(ack));
451
+ });
452
+ };
431
453
  var reportClientEvents = (target, events) => {
432
454
  try {
433
455
  const req = buildEventsRequest(target, events);
434
456
  if (!req) return;
435
- void fetch(req.url, req.init).catch(() => {
457
+ void fetch(req.url, req.init).then((res) => {
458
+ warnOnSkippedEvents(res);
459
+ }).catch(() => {
436
460
  });
437
461
  } catch {
438
462
  }
@@ -443,13 +467,28 @@ var reportClientEventsAwait = async (target, events) => {
443
467
  const req = buildEventsRequest(target, events);
444
468
  if (!req) return false;
445
469
  const res = await fetch(req.url, req.init);
446
- return Boolean(res && res.ok);
470
+ if (!res || !res.ok) return false;
471
+ const ack = await readEventsAck(res);
472
+ if (!ack || ack.skipped <= 0) return true;
473
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
474
+ console.warn(describeDiscarded(ack));
475
+ }
476
+ return false;
447
477
  } catch {
448
478
  return false;
449
479
  }
450
480
  };
451
481
  var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
452
482
 
483
+ // src/utils/warnInDev.ts
484
+ var warnInDev = (message) => {
485
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
486
+ console.warn(message);
487
+ return true;
488
+ }
489
+ return false;
490
+ };
491
+
453
492
  // src/analytics/currentSession.ts
454
493
  var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
455
494
  "@wireai/activation:currentSessionId"
@@ -464,13 +503,6 @@ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
464
503
  var resetCurrentSessionId = () => {
465
504
  globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
466
505
  };
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
506
  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
507
  var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
476
508
  "@wireai/activation:currentSessionIdMintWarned"
@@ -581,6 +613,43 @@ var useScreenTracking = (navigationRef, options = {}) => {
581
613
  }, [navigationRef]);
582
614
  };
583
615
 
616
+ // src/permissions/permissionEvents.ts
617
+ var WIRE_PERMISSION_EVENTS = {
618
+ /** The priming screen became visible. The denominator for every rate below. */
619
+ screenShown: "wire_permission_screen_shown",
620
+ /** The user tapped the primary, so the OS dialog is about to open. The rationale worked. */
621
+ primerAccepted: "wire_permission_primer_accepted",
622
+ /** The OS granted it. */
623
+ granted: "wire_permission_granted",
624
+ /** The OS refused it (a permanently blocked answer reports here too, with `status: "blocked"`). */
625
+ denied: "wire_permission_denied",
626
+ /** The user took the secondary. The one native prompt was NOT spent. */
627
+ skipped: "wire_permission_skipped",
628
+ /** A blocked user was redirected to the OS settings page. */
629
+ settingsOpened: "wire_permission_settings_opened"
630
+ };
631
+ var permissionEventName = (stage) => {
632
+ switch (stage) {
633
+ case "shown":
634
+ return WIRE_PERMISSION_EVENTS.screenShown;
635
+ case "accepted":
636
+ return WIRE_PERMISSION_EVENTS.primerAccepted;
637
+ case "granted":
638
+ return WIRE_PERMISSION_EVENTS.granted;
639
+ case "denied":
640
+ return WIRE_PERMISSION_EVENTS.denied;
641
+ case "skipped":
642
+ return WIRE_PERMISSION_EVENTS.skipped;
643
+ case "settings":
644
+ return WIRE_PERMISSION_EVENTS.settingsOpened;
645
+ default: {
646
+ const _exhaustive = stage;
647
+ return _exhaustive;
648
+ }
649
+ }
650
+ };
651
+ var permissionEventProps = (permission, status) => status ? { permission, status } : { permission };
652
+
584
653
  // src/analytics/analyticsEvent.ts
585
654
  var WIRE_ONBOARDING_EVENTS = {
586
655
  started: "wire_onboarding_started",
@@ -613,6 +682,11 @@ var toAnalyticsEvent = (event) => {
613
682
  };
614
683
  case "fallback":
615
684
  return { name: WIRE_ONBOARDING_EVENTS.fallback, params: { reason: event.reason } };
685
+ case "permission":
686
+ return {
687
+ name: permissionEventName(event.stage),
688
+ params: permissionEventProps(event.permission, event.status)
689
+ };
616
690
  default: {
617
691
  const _exhaustive = event;
618
692
  return _exhaustive;
@@ -818,6 +892,31 @@ var DEFAULTS = {
818
892
  maxRetries: 6
819
893
  };
820
894
  var READ_TIMEOUT_MS = 1500;
895
+ var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
896
+ var queueKeyGlobal = globalThis;
897
+ var claimedQueueKeys = () => {
898
+ const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
899
+ if (existing) return existing;
900
+ const created = /* @__PURE__ */ new Set();
901
+ queueKeyGlobal[QUEUE_KEY_SLOT] = created;
902
+ return created;
903
+ };
904
+ var claimQueueKey = (preferred, explicit) => {
905
+ const claimed = claimedQueueKeys();
906
+ if (explicit || !claimed.has(preferred)) {
907
+ claimed.add(preferred);
908
+ return preferred;
909
+ }
910
+ let ordinal = 2;
911
+ while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
912
+ const key = `${preferred}#${ordinal}`;
913
+ claimed.add(key);
914
+ warnInDev(
915
+ `[wireai] a second event queue was created for the same appId, and "${preferred}" is already claimed by a live one. Two queues sharing one storage slot overwrite each other's backlog, delete each other's pending events when one drains to empty, and double-send on relaunch, so this queue was given "${key}" instead. Prefer ONE analytics instance per app; if you really need two, pass an explicit \`storageKey\` to each so the slots are yours to reason about.`
916
+ );
917
+ return key;
918
+ };
919
+ var resetEventQueueKeys = () => claimedQueueKeys().clear();
821
920
  var withTimeout = (p, ms) => {
822
921
  let timer;
823
922
  const timeout = new Promise((resolve) => {
@@ -849,7 +948,8 @@ var createEventQueue = (options) => {
849
948
  var _a2, _b, _c, _d, _e, _f, _g;
850
949
  const target = options.target;
851
950
  const storage = options.storage;
852
- const key = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
951
+ const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
952
+ const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
853
953
  const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
854
954
  const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
855
955
  const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
@@ -1100,6 +1200,29 @@ var resolveUserContext = (ctx = {}, opts = {}) => {
1100
1200
  return result;
1101
1201
  };
1102
1202
 
1203
+ // src/identity/identityRecord.ts
1204
+ var IDENTITY_PROVENANCE_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:identityProvenance");
1205
+ var provenanceGlobal = globalThis;
1206
+ var provenanceRegistry = () => {
1207
+ const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];
1208
+ if (existing) return existing;
1209
+ const created = { host: /* @__PURE__ */ new Map() };
1210
+ provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;
1211
+ return created;
1212
+ };
1213
+ var provenanceKey = (space, scope) => `${space}:${scope != null ? scope : "default"}`;
1214
+ var resolveIdentity = (input) => {
1215
+ var _a2;
1216
+ if (typeof input.value !== "string") return void 0;
1217
+ const value = input.value.trim();
1218
+ if (!value) return void 0;
1219
+ const durable = (_a2 = input.durable) != null ? _a2 : input.source === "host";
1220
+ {
1221
+ provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);
1222
+ }
1223
+ return { value, space: input.space, source: input.source, durable };
1224
+ };
1225
+
1103
1226
  // src/context/deviceId.ts
1104
1227
  var AUTO_DEVICE_ID_PREFIX = "wdev_";
1105
1228
  var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
@@ -1122,24 +1245,37 @@ var startHydration = (registry, appId, storage, minted) => {
1122
1245
  const existing = registry.pending.get(appId);
1123
1246
  if (existing) return existing;
1124
1247
  const slot = deviceIdStorageKey(appId);
1125
- const settled = () => {
1248
+ const degraded = () => {
1126
1249
  var _a2;
1127
- return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
1250
+ return { value: (_a2 = registry.keys.get(appId)) != null ? _a2 : minted, durable: false };
1128
1251
  };
1252
+ const adopted = (value) => ({ value, durable: true });
1129
1253
  let run;
1130
1254
  try {
1131
1255
  run = Promise.resolve(storage.getItem(slot)).then((saved) => {
1132
1256
  const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1133
1257
  if (persisted) {
1134
1258
  registry.keys.set(appId, persisted);
1135
- return persisted;
1259
+ return adopted(persisted);
1136
1260
  }
1137
- return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
1138
- }).catch(settled);
1261
+ return Promise.resolve(storage.setItem(slot, minted)).then(
1262
+ () => {
1263
+ var _a2;
1264
+ return adopted((_a2 = registry.keys.get(appId)) != null ? _a2 : minted);
1265
+ },
1266
+ degraded
1267
+ );
1268
+ }).catch(degraded);
1139
1269
  } catch {
1140
- run = Promise.resolve(settled());
1270
+ run = Promise.resolve(degraded());
1141
1271
  }
1142
1272
  registry.pending.set(appId, run);
1273
+ void run.then((outcome) => {
1274
+ var _a2;
1275
+ if (outcome.durable) return;
1276
+ (_a2 = registry.pending) == null ? void 0 : _a2.delete(appId);
1277
+ registry.hydrating.delete(appId);
1278
+ });
1143
1279
  return run;
1144
1280
  };
1145
1281
  var resolveAutoDeviceKey = (opts = {}) => {
@@ -1167,11 +1303,6 @@ var resetAutoDeviceKeys = () => {
1167
1303
  };
1168
1304
 
1169
1305
  // src/analytics/analyticsFacade.ts
1170
- var warnInDev2 = (message) => {
1171
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1172
- console.warn(message);
1173
- }
1174
- };
1175
1306
  var createAnalytics = (config, options = {}) => {
1176
1307
  var _a2, _b, _c;
1177
1308
  const resolveSessionId = () => {
@@ -1179,13 +1310,19 @@ var createAnalytics = (config, options = {}) => {
1179
1310
  return (_a3 = config.sessionId) != null ? _a3 : ensureCurrentSessionId();
1180
1311
  };
1181
1312
  if (config.sessionId) {
1182
- warnInDev2(
1313
+ warnInDev(
1183
1314
  "[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
1315
  );
1185
1316
  }
1186
1317
  const detectedAppVersion = detectAppVersion();
1187
1318
  let userContext = { ...(_a2 = config.userContext) != null ? _a2 : {} };
1188
1319
  const hostDeviceKeyAtInit = typeof ((_b = config.userContext) == null ? void 0 : _b.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1320
+ resolveIdentity({
1321
+ value: hostDeviceKeyAtInit,
1322
+ space: "device",
1323
+ source: "host",
1324
+ scope: config.appId
1325
+ });
1189
1326
  const autoDeviceKeyOptions = {
1190
1327
  appId: config.appId,
1191
1328
  // A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).
@@ -1231,7 +1368,7 @@ var createAnalytics = (config, options = {}) => {
1231
1368
  };
1232
1369
  const guardUserId = (clean) => {
1233
1370
  if (config.allowEmailAsUserId || !looksLikeEmail(clean)) return clean;
1234
- warnInDev2(
1371
+ warnInDev(
1235
1372
  "[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."
1236
1373
  );
1237
1374
  return void 0;
@@ -1330,6 +1467,6 @@ var useAnalytics = (config, options = {}) => {
1330
1467
  return ref.current;
1331
1468
  };
1332
1469
 
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 };
1470
+ 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, resetEventQueueKeys, resolveAutoDeviceKey, screenTrackingHandler, setCurrentSessionId, toAnalyticsEvent, useAnalytics, useScreenTracking };
1334
1471
  //# sourceMappingURL=index.mjs.map
1335
1472
  //# sourceMappingURL=index.mjs.map