@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
@@ -402,39 +402,63 @@ var init_Constants = __esm({
402
402
 
403
403
  // src/analytics/reportClientEvent.ts
404
404
  var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
405
+ var toWireEvents = (events) => events.map((event) => {
406
+ if (typeof event.ts !== "number") return event;
407
+ const { ts, ...rest } = event;
408
+ if (!Number.isFinite(ts)) return rest;
409
+ try {
410
+ return { ...rest, ts: new Date(ts).toISOString() };
411
+ } catch {
412
+ return rest;
413
+ }
414
+ });
405
415
  var buildEventsRequest = (target, events) => {
406
416
  if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return null;
407
417
  try {
408
418
  const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
409
419
  const headers = { "Content-Type": "application/json" };
410
420
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
411
- return { url, init: { method: "POST", headers, body: JSON.stringify({ events }) } };
421
+ const body = JSON.stringify({ events: toWireEvents(events) });
422
+ return { url, init: { method: "POST", headers, body } };
412
423
  } catch {
413
424
  return null;
414
425
  }
415
426
  };
416
- var warnOnSkippedEvents = (res) => {
417
- if (typeof __DEV__ === "undefined" || !__DEV__) return;
418
- if (typeof console === "undefined" || !console.warn) return;
427
+ var readEventsAck = async (res) => {
419
428
  try {
420
429
  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
+ if (typeof json !== "function") return void 0;
431
+ const body = await Promise.resolve(json.call(res));
432
+ const skipped = body == null ? void 0 : body.skipped;
433
+ if (typeof skipped !== "number" || !Number.isFinite(skipped)) return void 0;
434
+ const reasons = Array.isArray(body == null ? void 0 : body.errors) ? body.errors.map((e) => {
435
+ const entry = e;
436
+ const reason = entry == null ? void 0 : entry.reason;
437
+ if (typeof reason !== "string") return void 0;
438
+ const field = entry == null ? void 0 : entry.field;
439
+ return typeof field === "string" && field.length > 0 ? `${reason} (field: ${field})` : reason;
440
+ }).filter((r) => typeof r === "string") : [];
441
+ return { written: typeof (body == null ? void 0 : body.written) === "number" ? body.written : void 0, skipped, reasons };
430
442
  } catch {
443
+ return void 0;
431
444
  }
432
445
  };
446
+ 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.");
447
+ var warnOnSkippedEvents = (res) => {
448
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
449
+ if (typeof console === "undefined" || !console.warn) return;
450
+ void readEventsAck(res).then((ack) => {
451
+ if (!ack || ack.skipped <= 0) return;
452
+ console.warn(describeDiscarded(ack));
453
+ });
454
+ };
433
455
  var reportClientEvents = (target, events) => {
434
456
  try {
435
457
  const req = buildEventsRequest(target, events);
436
458
  if (!req) return;
437
- void fetch(req.url, req.init).catch(() => {
459
+ void fetch(req.url, req.init).then((res) => {
460
+ warnOnSkippedEvents(res);
461
+ }).catch(() => {
438
462
  });
439
463
  } catch {
440
464
  }
@@ -445,13 +469,28 @@ var reportClientEventsAwait = async (target, events) => {
445
469
  const req = buildEventsRequest(target, events);
446
470
  if (!req) return false;
447
471
  const res = await fetch(req.url, req.init);
448
- return Boolean(res && res.ok);
472
+ if (!res || !res.ok) return false;
473
+ const ack = await readEventsAck(res);
474
+ if (!ack || ack.skipped <= 0) return true;
475
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
476
+ console.warn(describeDiscarded(ack));
477
+ }
478
+ return false;
449
479
  } catch {
450
480
  return false;
451
481
  }
452
482
  };
453
483
  var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
454
484
 
485
+ // src/utils/warnInDev.ts
486
+ var warnInDev = (message) => {
487
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
488
+ console.warn(message);
489
+ return true;
490
+ }
491
+ return false;
492
+ };
493
+
455
494
  // src/analytics/currentSession.ts
456
495
  var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
457
496
  "@wireai/activation:currentSessionId"
@@ -466,13 +505,6 @@ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
466
505
  var resetCurrentSessionId = () => {
467
506
  globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
468
507
  };
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
508
  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
509
  var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
478
510
  "@wireai/activation:currentSessionIdMintWarned"
@@ -583,6 +615,43 @@ var useScreenTracking = (navigationRef, options = {}) => {
583
615
  }, [navigationRef]);
584
616
  };
585
617
 
618
+ // src/permissions/permissionEvents.ts
619
+ var WIRE_PERMISSION_EVENTS = {
620
+ /** The priming screen became visible. The denominator for every rate below. */
621
+ screenShown: "wire_permission_screen_shown",
622
+ /** The user tapped the primary, so the OS dialog is about to open. The rationale worked. */
623
+ primerAccepted: "wire_permission_primer_accepted",
624
+ /** The OS granted it. */
625
+ granted: "wire_permission_granted",
626
+ /** The OS refused it (a permanently blocked answer reports here too, with `status: "blocked"`). */
627
+ denied: "wire_permission_denied",
628
+ /** The user took the secondary. The one native prompt was NOT spent. */
629
+ skipped: "wire_permission_skipped",
630
+ /** A blocked user was redirected to the OS settings page. */
631
+ settingsOpened: "wire_permission_settings_opened"
632
+ };
633
+ var permissionEventName = (stage) => {
634
+ switch (stage) {
635
+ case "shown":
636
+ return WIRE_PERMISSION_EVENTS.screenShown;
637
+ case "accepted":
638
+ return WIRE_PERMISSION_EVENTS.primerAccepted;
639
+ case "granted":
640
+ return WIRE_PERMISSION_EVENTS.granted;
641
+ case "denied":
642
+ return WIRE_PERMISSION_EVENTS.denied;
643
+ case "skipped":
644
+ return WIRE_PERMISSION_EVENTS.skipped;
645
+ case "settings":
646
+ return WIRE_PERMISSION_EVENTS.settingsOpened;
647
+ default: {
648
+ const _exhaustive = stage;
649
+ return _exhaustive;
650
+ }
651
+ }
652
+ };
653
+ var permissionEventProps = (permission, status) => status ? { permission, status } : { permission };
654
+
586
655
  // src/analytics/analyticsEvent.ts
587
656
  var WIRE_ONBOARDING_EVENTS = {
588
657
  started: "wire_onboarding_started",
@@ -615,6 +684,11 @@ var toAnalyticsEvent = (event) => {
615
684
  };
616
685
  case "fallback":
617
686
  return { name: WIRE_ONBOARDING_EVENTS.fallback, params: { reason: event.reason } };
687
+ case "permission":
688
+ return {
689
+ name: permissionEventName(event.stage),
690
+ params: permissionEventProps(event.permission, event.status)
691
+ };
618
692
  default: {
619
693
  const _exhaustive = event;
620
694
  return _exhaustive;
@@ -820,6 +894,31 @@ var DEFAULTS = {
820
894
  maxRetries: 6
821
895
  };
822
896
  var READ_TIMEOUT_MS = 1500;
897
+ var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
898
+ var queueKeyGlobal = globalThis;
899
+ var claimedQueueKeys = () => {
900
+ const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
901
+ if (existing) return existing;
902
+ const created = /* @__PURE__ */ new Set();
903
+ queueKeyGlobal[QUEUE_KEY_SLOT] = created;
904
+ return created;
905
+ };
906
+ var claimQueueKey = (preferred, explicit) => {
907
+ const claimed = claimedQueueKeys();
908
+ if (explicit || !claimed.has(preferred)) {
909
+ claimed.add(preferred);
910
+ return preferred;
911
+ }
912
+ let ordinal = 2;
913
+ while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
914
+ const key = `${preferred}#${ordinal}`;
915
+ claimed.add(key);
916
+ warnInDev(
917
+ `[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.`
918
+ );
919
+ return key;
920
+ };
921
+ var resetEventQueueKeys = () => claimedQueueKeys().clear();
823
922
  var withTimeout = (p, ms) => {
824
923
  let timer;
825
924
  const timeout = new Promise((resolve) => {
@@ -851,7 +950,8 @@ var createEventQueue = (options) => {
851
950
  var _a2, _b, _c, _d, _e, _f, _g;
852
951
  const target = options.target;
853
952
  const storage = options.storage;
854
- const key = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
953
+ const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
954
+ const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
855
955
  const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
856
956
  const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
857
957
  const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
@@ -1102,6 +1202,29 @@ var resolveUserContext = (ctx = {}, opts = {}) => {
1102
1202
  return result;
1103
1203
  };
1104
1204
 
1205
+ // src/identity/identityRecord.ts
1206
+ var IDENTITY_PROVENANCE_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:identityProvenance");
1207
+ var provenanceGlobal = globalThis;
1208
+ var provenanceRegistry = () => {
1209
+ const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];
1210
+ if (existing) return existing;
1211
+ const created = { host: /* @__PURE__ */ new Map() };
1212
+ provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;
1213
+ return created;
1214
+ };
1215
+ var provenanceKey = (space, scope) => `${space}:${scope != null ? scope : "default"}`;
1216
+ var resolveIdentity = (input) => {
1217
+ var _a2;
1218
+ if (typeof input.value !== "string") return void 0;
1219
+ const value = input.value.trim();
1220
+ if (!value) return void 0;
1221
+ const durable = (_a2 = input.durable) != null ? _a2 : input.source === "host";
1222
+ {
1223
+ provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);
1224
+ }
1225
+ return { value, space: input.space, source: input.source, durable };
1226
+ };
1227
+
1105
1228
  // src/context/deviceId.ts
1106
1229
  var AUTO_DEVICE_ID_PREFIX = "wdev_";
1107
1230
  var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
@@ -1124,24 +1247,37 @@ var startHydration = (registry, appId, storage, minted) => {
1124
1247
  const existing = registry.pending.get(appId);
1125
1248
  if (existing) return existing;
1126
1249
  const slot = deviceIdStorageKey(appId);
1127
- const settled = () => {
1250
+ const degraded = () => {
1128
1251
  var _a2;
1129
- return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
1252
+ return { value: (_a2 = registry.keys.get(appId)) != null ? _a2 : minted, durable: false };
1130
1253
  };
1254
+ const adopted = (value) => ({ value, durable: true });
1131
1255
  let run;
1132
1256
  try {
1133
1257
  run = Promise.resolve(storage.getItem(slot)).then((saved) => {
1134
1258
  const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1135
1259
  if (persisted) {
1136
1260
  registry.keys.set(appId, persisted);
1137
- return persisted;
1261
+ return adopted(persisted);
1138
1262
  }
1139
- return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
1140
- }).catch(settled);
1263
+ return Promise.resolve(storage.setItem(slot, minted)).then(
1264
+ () => {
1265
+ var _a2;
1266
+ return adopted((_a2 = registry.keys.get(appId)) != null ? _a2 : minted);
1267
+ },
1268
+ degraded
1269
+ );
1270
+ }).catch(degraded);
1141
1271
  } catch {
1142
- run = Promise.resolve(settled());
1272
+ run = Promise.resolve(degraded());
1143
1273
  }
1144
1274
  registry.pending.set(appId, run);
1275
+ void run.then((outcome) => {
1276
+ var _a2;
1277
+ if (outcome.durable) return;
1278
+ (_a2 = registry.pending) == null ? void 0 : _a2.delete(appId);
1279
+ registry.hydrating.delete(appId);
1280
+ });
1145
1281
  return run;
1146
1282
  };
1147
1283
  var resolveAutoDeviceKey = (opts = {}) => {
@@ -1169,11 +1305,6 @@ var resetAutoDeviceKeys = () => {
1169
1305
  };
1170
1306
 
1171
1307
  // src/analytics/analyticsFacade.ts
1172
- var warnInDev2 = (message) => {
1173
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1174
- console.warn(message);
1175
- }
1176
- };
1177
1308
  var createAnalytics = (config, options = {}) => {
1178
1309
  var _a2, _b, _c;
1179
1310
  const resolveSessionId = () => {
@@ -1181,13 +1312,19 @@ var createAnalytics = (config, options = {}) => {
1181
1312
  return (_a3 = config.sessionId) != null ? _a3 : ensureCurrentSessionId();
1182
1313
  };
1183
1314
  if (config.sessionId) {
1184
- warnInDev2(
1315
+ warnInDev(
1185
1316
  "[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
1317
  );
1187
1318
  }
1188
1319
  const detectedAppVersion = detectAppVersion();
1189
1320
  let userContext = { ...(_a2 = config.userContext) != null ? _a2 : {} };
1190
1321
  const hostDeviceKeyAtInit = typeof ((_b = config.userContext) == null ? void 0 : _b.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1322
+ resolveIdentity({
1323
+ value: hostDeviceKeyAtInit,
1324
+ space: "device",
1325
+ source: "host",
1326
+ scope: config.appId
1327
+ });
1191
1328
  const autoDeviceKeyOptions = {
1192
1329
  appId: config.appId,
1193
1330
  // A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).
@@ -1233,7 +1370,7 @@ var createAnalytics = (config, options = {}) => {
1233
1370
  };
1234
1371
  const guardUserId = (clean) => {
1235
1372
  if (config.allowEmailAsUserId || !looksLikeEmail(clean)) return clean;
1236
- warnInDev2(
1373
+ warnInDev(
1237
1374
  "[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."
1238
1375
  );
1239
1376
  return void 0;
@@ -1354,6 +1491,7 @@ exports.reportClientEvents = reportClientEvents;
1354
1491
  exports.reportClientEventsAwait = reportClientEventsAwait;
1355
1492
  exports.resetAutoDeviceKeys = resetAutoDeviceKeys;
1356
1493
  exports.resetCurrentSessionId = resetCurrentSessionId;
1494
+ exports.resetEventQueueKeys = resetEventQueueKeys;
1357
1495
  exports.resolveAutoDeviceKey = resolveAutoDeviceKey;
1358
1496
  exports.screenTrackingHandler = screenTrackingHandler;
1359
1497
  exports.setCurrentSessionId = setCurrentSessionId;