@wireai/activation 0.12.0 → 0.12.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 (53) hide show
  1. package/AGENTS.md +13 -10
  2. package/CHANGELOG.md +130 -0
  3. package/INTEGRATION_PROMPT.md +14 -3
  4. package/README.md +40 -2
  5. package/dist/analytics/index.d.mts +5 -4
  6. package/dist/analytics/index.d.ts +5 -4
  7. package/dist/analytics/index.js +39 -24
  8. package/dist/analytics/index.js.map +1 -1
  9. package/dist/analytics/index.mjs +39 -24
  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-DsSDHqor.d.mts → currentSession-BlCeDP0f.d.mts} +59 -5
  18. package/dist/{currentSession-D6RiVtc8.d.ts → currentSession-BxEB37xt.d.ts} +59 -5
  19. package/dist/index.d.mts +10 -20
  20. package/dist/index.d.ts +10 -20
  21. package/dist/index.js +301 -187
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +300 -188
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/questionnaire/index.js +41 -19
  26. package/dist/questionnaire/index.js.map +1 -1
  27. package/dist/questionnaire/index.mjs +41 -19
  28. package/dist/questionnaire/index.mjs.map +1 -1
  29. package/dist/reviews/index.js +41 -19
  30. package/dist/reviews/index.js.map +1 -1
  31. package/dist/reviews/index.mjs +41 -19
  32. package/dist/reviews/index.mjs.map +1 -1
  33. package/dist/showcase/index.js +15 -6
  34. package/dist/showcase/index.js.map +1 -1
  35. package/dist/showcase/index.mjs +15 -6
  36. package/dist/showcase/index.mjs.map +1 -1
  37. package/llms.txt +2 -0
  38. package/package.json +1 -1
  39. package/src/WireOnboarding.tsx +140 -5
  40. package/src/activation/wireActivation.ts +8 -1
  41. package/src/analytics/analyticsFacade.ts +15 -11
  42. package/src/analytics/reportClientEvent.ts +11 -7
  43. package/src/coachmarks/runtime.ts +53 -17
  44. package/src/config/wireConfigFromEnv.ts +46 -2
  45. package/src/context/deviceId.ts +82 -18
  46. package/src/index.ts +9 -1
  47. package/src/questionnaire/runtime.ts +1 -0
  48. package/src/questionnaire/useQuestionnaireGate.ts +8 -3
  49. package/src/reviews/runtime.ts +68 -7
  50. package/src/reviews/useReviewGate.ts +8 -3
  51. package/src/session-analytics/useLifecycleEvents.ts +57 -27
  52. package/src/session-analytics/useSessionStart.ts +37 -1
  53. package/src/types.ts +41 -4
package/dist/index.mjs CHANGED
@@ -1513,17 +1513,17 @@ var buildEventsRequest = (target, events) => {
1513
1513
  }
1514
1514
  };
1515
1515
  var warnOnSkippedEvents = (res) => {
1516
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
1517
+ if (typeof console === "undefined" || !console.warn) return;
1516
1518
  try {
1517
1519
  const json = res == null ? void 0 : res.json;
1518
1520
  if (typeof json !== "function") return;
1519
1521
  void Promise.resolve(json.call(res)).then((body) => {
1520
1522
  const skipped = body == null ? void 0 : body.skipped;
1521
1523
  if (typeof skipped !== "number" || skipped <= 0) return;
1522
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1523
- console.warn(
1524
- `[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.`
1525
- );
1526
- }
1524
+ console.warn(
1525
+ `[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.`
1526
+ );
1527
1527
  }).catch(() => {
1528
1528
  });
1529
1529
  } catch {
@@ -3311,6 +3311,82 @@ var collectDeviceContext = () => {
3311
3311
  return ctx;
3312
3312
  };
3313
3313
 
3314
+ // src/context/deviceId.ts
3315
+ var AUTO_DEVICE_ID_PREFIX = "wdev_";
3316
+ var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
3317
+ var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
3318
+ var mintDeviceId = () => {
3319
+ const time = Date.now().toString(36);
3320
+ return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
3321
+ };
3322
+ var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
3323
+ var deviceKeyGlobal = globalThis;
3324
+ var autoDeviceKeyRegistry = () => {
3325
+ const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
3326
+ if (existing) return existing;
3327
+ const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
3328
+ deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
3329
+ return created;
3330
+ };
3331
+ var startHydration = (registry, appId, storage, minted) => {
3332
+ if (!registry.pending) registry.pending = /* @__PURE__ */ new Map();
3333
+ const existing = registry.pending.get(appId);
3334
+ if (existing) return existing;
3335
+ const slot = deviceIdStorageKey(appId);
3336
+ const settled = () => {
3337
+ var _a2;
3338
+ return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
3339
+ };
3340
+ let run;
3341
+ try {
3342
+ run = Promise.resolve(storage.getItem(slot)).then((saved) => {
3343
+ const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
3344
+ if (persisted) {
3345
+ registry.keys.set(appId, persisted);
3346
+ return persisted;
3347
+ }
3348
+ return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
3349
+ }).catch(settled);
3350
+ } catch {
3351
+ run = Promise.resolve(settled());
3352
+ }
3353
+ registry.pending.set(appId, run);
3354
+ return run;
3355
+ };
3356
+ var resolveAutoDeviceKey = (opts = {}) => {
3357
+ var _a2, _b;
3358
+ const registry = autoDeviceKeyRegistry();
3359
+ const appId = (_a2 = opts.appId) != null ? _a2 : "default";
3360
+ let id = registry.keys.get(appId);
3361
+ if (!id) {
3362
+ id = mintDeviceId();
3363
+ registry.keys.set(appId, id);
3364
+ }
3365
+ const storage = opts.storage;
3366
+ if (storage && !registry.hydrating.has(appId)) {
3367
+ registry.hydrating.add(appId);
3368
+ void startHydration(registry, appId, storage, id);
3369
+ }
3370
+ return (_b = registry.keys.get(appId)) != null ? _b : id;
3371
+ };
3372
+ var hydrateAutoDeviceKey = async (opts = {}) => {
3373
+ var _a2, _b, _c;
3374
+ const id = resolveAutoDeviceKey(opts);
3375
+ if (!opts.storage) return id;
3376
+ const registry = autoDeviceKeyRegistry();
3377
+ const appId = (_a2 = opts.appId) != null ? _a2 : "default";
3378
+ const pending = (_b = registry.pending) == null ? void 0 : _b.get(appId);
3379
+ if (pending) await pending;
3380
+ return (_c = registry.keys.get(appId)) != null ? _c : id;
3381
+ };
3382
+ var resetAutoDeviceKeys = () => {
3383
+ var _a2;
3384
+ const registry = autoDeviceKeyRegistry();
3385
+ registry.keys.clear();
3386
+ registry.hydrating.clear();
3387
+ (_a2 = registry.pending) == null ? void 0 : _a2.clear();
3388
+ };
3389
+
3314
3390
  // src/analytics/currentSession.ts
3315
3391
  var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
3316
3392
  "@wireai/activation:currentSessionId"
@@ -3436,6 +3512,105 @@ var identifyOnboarding = async (opts) => {
3436
3512
  );
3437
3513
  return true;
3438
3514
  };
3515
+
3516
+ // src/context/userContext.ts
3517
+ var RESERVED_USER_CONTEXT_KEYS = [
3518
+ "device_key",
3519
+ "app_version",
3520
+ "app_build",
3521
+ "network_type",
3522
+ "session_count",
3523
+ "returning",
3524
+ "platform",
3525
+ "user_email",
3526
+ "user_email_hashed"
3527
+ ];
3528
+ var EXTRA_KEY_PREFIX = "custom.";
3529
+ var isWireScalar = (value) => {
3530
+ const t = typeof value;
3531
+ if (t === "string" || t === "boolean") return true;
3532
+ if (t === "number") return Number.isFinite(value);
3533
+ return false;
3534
+ };
3535
+ var hashEmailFnv1a = (email) => {
3536
+ const normalized = email.trim().toLowerCase();
3537
+ let hash = 2166136261;
3538
+ for (let i = 0; i < normalized.length; i++) {
3539
+ hash ^= normalized.charCodeAt(i);
3540
+ hash = Math.imul(hash, 16777619);
3541
+ }
3542
+ return (hash >>> 0).toString(16).padStart(8, "0");
3543
+ };
3544
+ var cleanString = (value) => {
3545
+ if (typeof value !== "string") return void 0;
3546
+ const trimmed = value.trim();
3547
+ return trimmed.length > 0 ? trimmed : void 0;
3548
+ };
3549
+ var namespaceExtra = (extra) => {
3550
+ const out = {};
3551
+ if (!extra || typeof extra !== "object") return out;
3552
+ for (const [key, value] of Object.entries(extra)) {
3553
+ const cleanKey = cleanString(key);
3554
+ if (!cleanKey) continue;
3555
+ if (!isWireScalar(value)) continue;
3556
+ out[`${EXTRA_KEY_PREFIX}${cleanKey}`] = value;
3557
+ }
3558
+ return out;
3559
+ };
3560
+ var analyticsUserIdStorageKey = (appId) => `wireai:analytics:userId:${appId != null ? appId : "default"}`;
3561
+ var clearPiiFromContext = (ctx = {}) => {
3562
+ const rest = {};
3563
+ if (typeof ctx.appVersion === "string") rest.appVersion = ctx.appVersion;
3564
+ if (typeof ctx.deviceKey === "string") rest.deviceKey = ctx.deviceKey;
3565
+ return rest;
3566
+ };
3567
+ var clearUserContext = async (opts = {}) => {
3568
+ const storage = opts.storage;
3569
+ if (!storage) return;
3570
+ try {
3571
+ await storage.removeItem(analyticsUserIdStorageKey(opts.appId));
3572
+ } catch {
3573
+ }
3574
+ };
3575
+ var activationJoinContext = (deviceKey) => {
3576
+ var _a2;
3577
+ return (_a2 = resolveUserContext({ deviceKey }).userContext) != null ? _a2 : {};
3578
+ };
3579
+ var resolveUserContext = (ctx = {}, opts = {}) => {
3580
+ var _a2;
3581
+ const result = {};
3582
+ const bucket = {};
3583
+ const userId = sanitizeUserId(ctx.userId);
3584
+ if (userId) result.userId = userId;
3585
+ const deviceKey = cleanString(ctx.deviceKey);
3586
+ if (deviceKey) {
3587
+ result.deviceKey = deviceKey;
3588
+ bucket.device_key = deviceKey;
3589
+ }
3590
+ const appVersion = (_a2 = cleanString(ctx.appVersion)) != null ? _a2 : cleanString(opts.autoAppVersion);
3591
+ if (appVersion) {
3592
+ result.appVersion = appVersion;
3593
+ bucket.app_version = appVersion;
3594
+ }
3595
+ const email = cleanString(ctx.userEmail);
3596
+ if (email) {
3597
+ if (ctx.hashEmail) {
3598
+ bucket.user_email = hashEmailFnv1a(email);
3599
+ bucket.user_email_hashed = true;
3600
+ } else {
3601
+ bucket.user_email = email;
3602
+ }
3603
+ }
3604
+ Object.assign(bucket, namespaceExtra(ctx.extra));
3605
+ if (Object.keys(bucket).length > 0) result.userContext = bucket;
3606
+ return result;
3607
+ };
3608
+ var warnInDev2 = (message) => {
3609
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
3610
+ console.warn(message);
3611
+ }
3612
+ };
3613
+ var AUTO_JOIN_HYDRATION_TIMEOUT_MS = 1500;
3439
3614
  var WireOnboarding = ({
3440
3615
  config,
3441
3616
  theme,
@@ -3458,7 +3633,8 @@ var WireOnboarding = ({
3458
3633
  persistKey,
3459
3634
  retainSessionOnComplete,
3460
3635
  userContext,
3461
- userId
3636
+ userId,
3637
+ autoJoinKey = true
3462
3638
  }) => {
3463
3639
  var _a2, _b, _c;
3464
3640
  const boundUserId = useMemo(() => sanitizeUserId(userId), [userId]);
@@ -3471,9 +3647,50 @@ var WireOnboarding = ({
3471
3647
  () => userContextKey ? JSON.parse(userContextKey) : void 0,
3472
3648
  [userContextKey]
3473
3649
  );
3650
+ const missingJoinKey = typeof (userContextStable == null ? void 0 : userContextStable.device_key) !== "string" || !userContextStable.device_key.trim();
3651
+ const autoJoinPossible = Boolean(storage) && autoJoinKey !== false;
3652
+ const wantsAutoJoin = missingJoinKey && autoJoinPossible;
3653
+ const [autoJoinValue, setAutoJoinValue] = useState(void 0);
3654
+ useEffect(() => {
3655
+ if (!wantsAutoJoin || autoJoinValue !== void 0 || !storage) return;
3656
+ let cancelled = false;
3657
+ const timer = setTimeout(() => {
3658
+ if (!cancelled) setAutoJoinValue(null);
3659
+ }, AUTO_JOIN_HYDRATION_TIMEOUT_MS);
3660
+ void hydrateAutoDeviceKey({ appId: config.appId, storage }).then((key) => {
3661
+ if (cancelled) return;
3662
+ clearTimeout(timer);
3663
+ setAutoJoinValue(key || null);
3664
+ });
3665
+ return () => {
3666
+ cancelled = true;
3667
+ clearTimeout(timer);
3668
+ };
3669
+ }, [wantsAutoJoin, autoJoinValue, config.appId, storage]);
3670
+ const injectedJoinKey = wantsAutoJoin && typeof autoJoinValue === "string" ? autoJoinValue : void 0;
3671
+ const autoJoinPending = wantsAutoJoin && autoJoinValue === void 0;
3672
+ const effectiveUserContext = useMemo(
3673
+ () => injectedJoinKey ? { ...userContextStable, ...activationJoinContext(injectedJoinKey) } : userContextStable,
3674
+ [userContextStable, injectedJoinKey]
3675
+ );
3676
+ const warnMissingJoinKey = missingJoinKey && !injectedJoinKey && !autoJoinPending;
3677
+ const autoJoinReason = autoJoinKey === false ? "you passed autoJoinKey={false}." : !storage ? "it got no `storage` prop, and without persistence its key would be different on every launch, which corrupts min_sessions instead of merely leaving the join empty." : "your `storage` adapter did not answer in time.";
3678
+ useEffect(() => {
3679
+ if (!warnMissingJoinKey) return;
3680
+ warnInDev2(
3681
+ "[wireai] <WireOnboarding> got no user_context.device_key, so this onboarding session can never be joined to the app's later events and the `activated` funnel will read zero. Pass userContext={activationJoinContext(deviceKey)} \u2014 and if your app owns no device id, userContext={activationJoinContext(resolveAutoDeviceKey({ appId, storage }))} returns the SAME id the analytics side stamps. Never hand-write userContext={{ deviceKey }}. The kit did NOT auto-inject its own key here because " + autoJoinReason
3682
+ );
3683
+ }, [warnMissingJoinKey, autoJoinReason]);
3684
+ const misspelledJoinKey = missingJoinKey && typeof (userContextStable == null ? void 0 : userContextStable.deviceKey) === "string" && Boolean(userContextStable.deviceKey.trim());
3685
+ useEffect(() => {
3686
+ if (!misspelledJoinKey || !injectedJoinKey) return;
3687
+ warnInDev2(
3688
+ "[wireai] <WireOnboarding> got userContext={{ deviceKey }}, which is NOT the wire key \u2014 the server's device lookup reads `device_key`. The kit auto-injected its own device_key so this session is at least self-consistent, but your app's own events carry YOUR id, so the two still will not join and the `activated` funnel stays zero. Pass userContext={activationJoinContext(deviceKey)} instead."
3689
+ );
3690
+ }, [misspelledJoinKey, injectedJoinKey]);
3474
3691
  const clientContext = useMemo(
3475
- () => ({ device, userContext: userContextStable, userId: boundUserId }),
3476
- [device, userContextStable, boundUserId]
3692
+ () => ({ device, userContext: effectiveUserContext, userId: boundUserId }),
3693
+ [device, effectiveUserContext, boundUserId]
3477
3694
  );
3478
3695
  const [session, setSession] = useState(
3479
3696
  () => storage ? null : { id: makeSessionId(), resumed: false }
@@ -3520,7 +3737,7 @@ var WireOnboarding = ({
3520
3737
  // Device snapshot + host-injected user context ride the session-start metadata so the
3521
3738
  // backend can segment the funnel. Old servers ignore these unknown keys (backward compat).
3522
3739
  device,
3523
- ...userContextStable ? { userContext: userContextStable } : {},
3740
+ ...effectiveUserContext ? { userContext: effectiveUserContext } : {},
3524
3741
  // The session-start user id (from the ref) rides the session-start metadata so the
3525
3742
  // server binds the session to a real user at creation. Reading the ref — not
3526
3743
  // `boundUserId` — keeps this memo off the userId dependency, so a mid-session change
@@ -3529,7 +3746,7 @@ var WireOnboarding = ({
3529
3746
  },
3530
3747
  timeoutMs: 6e4
3531
3748
  };
3532
- }, [config.serverUrl, config.appId, config.apiKey, metadataStable, sessionId, componentsStable, device, userContextStable]);
3749
+ }, [config.serverUrl, config.appId, config.apiKey, metadataStable, sessionId, componentsStable, device, effectiveUserContext]);
3533
3750
  const reportTarget = useMemo(
3534
3751
  () => ({ serverUrl: config.serverUrl, apiKey: config.apiKey }),
3535
3752
  [config.serverUrl, config.apiKey]
@@ -3551,7 +3768,7 @@ var WireOnboarding = ({
3551
3768
  });
3552
3769
  }, [session, sessionId, boundUserId, reportTarget]);
3553
3770
  const cards = components != null ? components : onboardingComponents;
3554
- if (!session) {
3771
+ if (!session || autoJoinPending) {
3555
3772
  return /* @__PURE__ */ jsx(OnboardingThemeProvider, { theme, children: /* @__PURE__ */ jsx(
3556
3773
  LoadingScreen,
3557
3774
  {
@@ -4017,16 +4234,37 @@ var useResolvedFeatures = (options) => {
4017
4234
  };
4018
4235
 
4019
4236
  // src/config/wireConfigFromEnv.ts
4237
+ var WIRE_ENV_VARS = [
4238
+ "EXPO_PUBLIC_WIREAI_API_KEY",
4239
+ "EXPO_PUBLIC_WIREAI_SERVER_URL",
4240
+ "EXPO_PUBLIC_WIREAI_APP_ID"
4241
+ ];
4242
+ var warnInDev3 = (message) => {
4243
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
4244
+ console.warn(message);
4245
+ }
4246
+ };
4020
4247
  var wireConfigFromEnv = (overrides) => {
4021
- var _a2, _b, _c, _d;
4248
+ var _a2, _b, _c, _d, _e;
4022
4249
  const apiKey = (_a2 = overrides == null ? void 0 : overrides.apiKey) != null ? _a2 : process.env.EXPO_PUBLIC_WIREAI_API_KEY;
4023
4250
  const serverUrl = (_b = overrides == null ? void 0 : overrides.serverUrl) != null ? _b : process.env.EXPO_PUBLIC_WIREAI_SERVER_URL;
4024
4251
  const appId = (_d = (_c = overrides == null ? void 0 : overrides.appId) != null ? _c : process.env.EXPO_PUBLIC_WIREAI_APP_ID) != null ? _d : "default";
4025
- if (!apiKey || !serverUrl) return null;
4252
+ if (!apiKey || !serverUrl) {
4253
+ const missing = [
4254
+ apiKey ? void 0 : "EXPO_PUBLIC_WIREAI_API_KEY",
4255
+ serverUrl ? void 0 : "EXPO_PUBLIC_WIREAI_SERVER_URL"
4256
+ ].filter(Boolean);
4257
+ warnInDev3(
4258
+ `[wireai] wireConfigFromEnv() returned null: ${missing.join(" and ")} ${missing.length > 1 ? "are" : "is"} missing. The kit is now FULLY DISABLED (no onboarding, no analytics, no gates) and nothing else will report an error. Set the var(s) in your env / EAS secrets.`
4259
+ );
4260
+ return null;
4261
+ }
4262
+ const appVersion = (_e = overrides == null ? void 0 : overrides.appVersion) != null ? _e : detectAppVersion();
4026
4263
  return {
4027
4264
  apiKey,
4028
4265
  serverUrl,
4029
4266
  appId,
4267
+ ...appVersion ? { appVersion } : {},
4030
4268
  ...(overrides == null ? void 0 : overrides.metadata) ? { metadata: overrides.metadata } : {}
4031
4269
  };
4032
4270
  };
@@ -4087,149 +4325,6 @@ var attributionMetadata = (a) => {
4087
4325
  return { attribution };
4088
4326
  };
4089
4327
 
4090
- // src/context/userContext.ts
4091
- var RESERVED_USER_CONTEXT_KEYS = [
4092
- "device_key",
4093
- "app_version",
4094
- "app_build",
4095
- "network_type",
4096
- "session_count",
4097
- "returning",
4098
- "platform",
4099
- "user_email",
4100
- "user_email_hashed"
4101
- ];
4102
- var EXTRA_KEY_PREFIX = "custom.";
4103
- var isWireScalar = (value) => {
4104
- const t = typeof value;
4105
- if (t === "string" || t === "boolean") return true;
4106
- if (t === "number") return Number.isFinite(value);
4107
- return false;
4108
- };
4109
- var hashEmailFnv1a = (email) => {
4110
- const normalized = email.trim().toLowerCase();
4111
- let hash = 2166136261;
4112
- for (let i = 0; i < normalized.length; i++) {
4113
- hash ^= normalized.charCodeAt(i);
4114
- hash = Math.imul(hash, 16777619);
4115
- }
4116
- return (hash >>> 0).toString(16).padStart(8, "0");
4117
- };
4118
- var cleanString = (value) => {
4119
- if (typeof value !== "string") return void 0;
4120
- const trimmed = value.trim();
4121
- return trimmed.length > 0 ? trimmed : void 0;
4122
- };
4123
- var namespaceExtra = (extra) => {
4124
- const out = {};
4125
- if (!extra || typeof extra !== "object") return out;
4126
- for (const [key, value] of Object.entries(extra)) {
4127
- const cleanKey = cleanString(key);
4128
- if (!cleanKey) continue;
4129
- if (!isWireScalar(value)) continue;
4130
- out[`${EXTRA_KEY_PREFIX}${cleanKey}`] = value;
4131
- }
4132
- return out;
4133
- };
4134
- var analyticsUserIdStorageKey = (appId) => `wireai:analytics:userId:${appId != null ? appId : "default"}`;
4135
- var clearPiiFromContext = (ctx = {}) => {
4136
- const rest = {};
4137
- if (typeof ctx.appVersion === "string") rest.appVersion = ctx.appVersion;
4138
- if (typeof ctx.deviceKey === "string") rest.deviceKey = ctx.deviceKey;
4139
- return rest;
4140
- };
4141
- var clearUserContext = async (opts = {}) => {
4142
- const storage = opts.storage;
4143
- if (!storage) return;
4144
- try {
4145
- await storage.removeItem(analyticsUserIdStorageKey(opts.appId));
4146
- } catch {
4147
- }
4148
- };
4149
- var activationJoinContext = (deviceKey) => {
4150
- var _a2;
4151
- return (_a2 = resolveUserContext({ deviceKey }).userContext) != null ? _a2 : {};
4152
- };
4153
- var resolveUserContext = (ctx = {}, opts = {}) => {
4154
- var _a2;
4155
- const result = {};
4156
- const bucket = {};
4157
- const userId = sanitizeUserId(ctx.userId);
4158
- if (userId) result.userId = userId;
4159
- const deviceKey = cleanString(ctx.deviceKey);
4160
- if (deviceKey) {
4161
- result.deviceKey = deviceKey;
4162
- bucket.device_key = deviceKey;
4163
- }
4164
- const appVersion = (_a2 = cleanString(ctx.appVersion)) != null ? _a2 : cleanString(opts.autoAppVersion);
4165
- if (appVersion) {
4166
- result.appVersion = appVersion;
4167
- bucket.app_version = appVersion;
4168
- }
4169
- const email = cleanString(ctx.userEmail);
4170
- if (email) {
4171
- if (ctx.hashEmail) {
4172
- bucket.user_email = hashEmailFnv1a(email);
4173
- bucket.user_email_hashed = true;
4174
- } else {
4175
- bucket.user_email = email;
4176
- }
4177
- }
4178
- Object.assign(bucket, namespaceExtra(ctx.extra));
4179
- if (Object.keys(bucket).length > 0) result.userContext = bucket;
4180
- return result;
4181
- };
4182
-
4183
- // src/context/deviceId.ts
4184
- var AUTO_DEVICE_ID_PREFIX = "wdev_";
4185
- var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
4186
- var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
4187
- var mintDeviceId = () => {
4188
- const time = Date.now().toString(36);
4189
- return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
4190
- };
4191
- var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
4192
- var deviceKeyGlobal = globalThis;
4193
- var autoDeviceKeyRegistry = () => {
4194
- const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
4195
- if (existing) return existing;
4196
- const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
4197
- deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
4198
- return created;
4199
- };
4200
- var resolveAutoDeviceKey = (opts = {}) => {
4201
- var _a2, _b;
4202
- const registry = autoDeviceKeyRegistry();
4203
- const appId = (_a2 = opts.appId) != null ? _a2 : "default";
4204
- let id = registry.keys.get(appId);
4205
- if (!id) {
4206
- id = mintDeviceId();
4207
- registry.keys.set(appId, id);
4208
- }
4209
- const storage = opts.storage;
4210
- if (storage && !registry.hydrating.has(appId)) {
4211
- registry.hydrating.add(appId);
4212
- const slot = deviceIdStorageKey(appId);
4213
- const minted = id;
4214
- try {
4215
- void Promise.resolve(storage.getItem(slot)).then((saved) => {
4216
- const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
4217
- if (persisted) registry.keys.set(appId, persisted);
4218
- else void Promise.resolve(storage.setItem(slot, minted)).catch(() => {
4219
- });
4220
- }).catch(() => {
4221
- });
4222
- } catch {
4223
- }
4224
- }
4225
- return (_b = registry.keys.get(appId)) != null ? _b : id;
4226
- };
4227
- var resetAutoDeviceKeys = () => {
4228
- const registry = autoDeviceKeyRegistry();
4229
- registry.keys.clear();
4230
- registry.hydrating.clear();
4231
- };
4232
-
4233
4328
  // src/activation/revalidation.ts
4234
4329
  var REVALIDATION_SLOT = /* @__PURE__ */ Symbol.for(
4235
4330
  "@wireai/activation:activationRevalidation"
@@ -4282,18 +4377,19 @@ var createWireActivation = (config) => {
4282
4377
  storage: explicitDeviceKey ? void 0 : config.storage
4283
4378
  };
4284
4379
  if (!explicitDeviceKey) resolveAutoDeviceKey(autoDeviceKeyOptions);
4380
+ const detectedAppVersion = detectAppVersion();
4285
4381
  const applyContext = (event) => {
4286
- var _a3, _b2;
4382
+ var _a3, _b2, _c;
4287
4383
  const resolved = resolveUserContext(
4288
4384
  // `??` is lazy on purpose: an explicit key must never even touch the auto registry.
4289
4385
  {
4290
4386
  ...(_a3 = config.userContext) != null ? _a3 : {},
4291
4387
  deviceKey: explicitDeviceKey != null ? explicitDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions)
4292
4388
  },
4293
- { autoAppVersion: config.appVersion }
4389
+ { autoAppVersion: (_b2 = config.appVersion) != null ? _b2 : detectedAppVersion }
4294
4390
  );
4295
4391
  if (resolved.userContext) {
4296
- event.user_context = { ...resolved.userContext, ...(_b2 = event.user_context) != null ? _b2 : {} };
4392
+ event.user_context = { ...resolved.userContext, ...(_c = event.user_context) != null ? _c : {} };
4297
4393
  }
4298
4394
  if (resolved.userId && !event.user_id) event.user_id = resolved.userId;
4299
4395
  };
@@ -4556,6 +4652,12 @@ var useSessionStart = (config, options = {}) => {
4556
4652
  const latest = useRef({ config, options });
4557
4653
  latest.current = { config, options };
4558
4654
  useEffect(() => {
4655
+ const resolveDeviceKey = (cfg, opts) => {
4656
+ const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
4657
+ if (host) return host;
4658
+ if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
4659
+ return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
4660
+ };
4559
4661
  const fire = () => {
4560
4662
  var _a2, _b;
4561
4663
  const { config: cfg, options: opts } = latest.current;
@@ -4568,7 +4670,7 @@ var useSessionStart = (config, options = {}) => {
4568
4670
  target,
4569
4671
  // A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
4570
4672
  userId: opts.userId,
4571
- deviceKey: opts.deviceKey,
4673
+ deviceKey: resolveDeviceKey(cfg, opts),
4572
4674
  sessionCount: opts.sessionCount,
4573
4675
  appVersion: (_b = cfg.appVersion) != null ? _b : device.appVersion,
4574
4676
  platform: Platform.OS,
@@ -4902,15 +5004,14 @@ var useLifecycleEvents = (config, options = {}) => {
4902
5004
  latest.current = { config, options };
4903
5005
  const queueRef = useRef(void 0);
4904
5006
  useEffect(() => {
4905
- var _a2;
4906
5007
  const resolveSink = () => {
4907
- var _a3, _b;
5008
+ var _a2, _b;
4908
5009
  const { config: cfg, options: opts } = latest.current;
4909
5010
  if (opts.sink) return opts.sink;
4910
5011
  if (!(cfg == null ? void 0 : cfg.serverUrl)) return void 0;
4911
5012
  if (!queueRef.current) {
4912
5013
  queueRef.current = createEventQueue({
4913
- target: { serverUrl: cfg.serverUrl, apiKey: (_a3 = cfg.apiKey) != null ? _a3 : "" },
5014
+ target: { serverUrl: cfg.serverUrl, apiKey: (_a2 = cfg.apiKey) != null ? _a2 : "" },
4914
5015
  storage: cfg.storage,
4915
5016
  // Dedicated key so the hook's internal queue never collides with a host's main queue.
4916
5017
  storageKey: `wireai:evtq:lifecycle:${(_b = cfg.appId) != null ? _b : "default"}`,
@@ -4920,8 +5021,8 @@ var useLifecycleEvents = (config, options = {}) => {
4920
5021
  return queueRef.current.enqueue;
4921
5022
  };
4922
5023
  const targetOf = (cfg) => {
4923
- var _a3;
4924
- return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a3 = cfg.apiKey) != null ? _a3 : "" } : void 0;
5024
+ var _a2;
5025
+ return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a2 = cfg.apiKey) != null ? _a2 : "" } : void 0;
4925
5026
  };
4926
5027
  const resolveDeviceKey = (cfg, opts) => {
4927
5028
  const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
@@ -4931,7 +5032,7 @@ var useLifecycleEvents = (config, options = {}) => {
4931
5032
  };
4932
5033
  const mountOpenSessionId = makeSessionId();
4933
5034
  const fireSession = (sessionId) => {
4934
- var _a3;
5035
+ var _a2;
4935
5036
  const { config: cfg, options: opts } = latest.current;
4936
5037
  if (opts.enabled === false) return;
4937
5038
  if (!(cfg == null ? void 0 : cfg.serverUrl) && !opts.sink) return;
@@ -4944,33 +5045,43 @@ var useLifecycleEvents = (config, options = {}) => {
4944
5045
  userId: opts.userId,
4945
5046
  deviceKey: resolveDeviceKey(cfg, opts),
4946
5047
  sessionCount: opts.sessionCount,
4947
- appVersion: (_a3 = cfg == null ? void 0 : cfg.appVersion) != null ? _a3 : device.appVersion,
5048
+ appVersion: (_a2 = cfg == null ? void 0 : cfg.appVersion) != null ? _a2 : device.appVersion,
4948
5049
  platform: Platform.OS,
4949
5050
  device,
4950
5051
  meta: opts.meta
4951
5052
  });
4952
5053
  };
4953
- fireSession(mountOpenSessionId);
4954
- {
5054
+ const fireMountOpen = () => {
5055
+ var _a2;
5056
+ fireSession(mountOpenSessionId);
4955
5057
  const { config: cfg, options: opts } = latest.current;
4956
- if (opts.enabled !== false) {
4957
- const device = collectDeviceContext();
4958
- if (cfg == null ? void 0 : cfg.appVersion) device.appVersion = cfg.appVersion;
4959
- reportFirstOpen({
4960
- target: targetOf(cfg),
4961
- sink: resolveSink(),
4962
- sessionId: mountOpenSessionId,
4963
- storage: cfg == null ? void 0 : cfg.storage,
4964
- appId: cfg == null ? void 0 : cfg.appId,
4965
- userId: opts.userId,
4966
- deviceKey: resolveDeviceKey(cfg, opts),
4967
- sessionCount: opts.sessionCount,
4968
- appVersion: (_a2 = cfg == null ? void 0 : cfg.appVersion) != null ? _a2 : device.appVersion,
4969
- platform: Platform.OS,
4970
- device,
4971
- meta: opts.meta
4972
- });
4973
- }
5058
+ if (opts.enabled === false) return;
5059
+ const device = collectDeviceContext();
5060
+ if (cfg == null ? void 0 : cfg.appVersion) device.appVersion = cfg.appVersion;
5061
+ reportFirstOpen({
5062
+ target: targetOf(cfg),
5063
+ sink: resolveSink(),
5064
+ sessionId: mountOpenSessionId,
5065
+ storage: cfg == null ? void 0 : cfg.storage,
5066
+ appId: cfg == null ? void 0 : cfg.appId,
5067
+ userId: opts.userId,
5068
+ deviceKey: resolveDeviceKey(cfg, opts),
5069
+ sessionCount: opts.sessionCount,
5070
+ appVersion: (_a2 = cfg == null ? void 0 : cfg.appVersion) != null ? _a2 : device.appVersion,
5071
+ platform: Platform.OS,
5072
+ device,
5073
+ meta: opts.meta
5074
+ });
5075
+ };
5076
+ let cancelled = false;
5077
+ const { config: mountCfg, options: mountOpts } = latest.current;
5078
+ const hostKey = typeof mountOpts.deviceKey === "string" && mountOpts.deviceKey.trim() ? mountOpts.deviceKey : void 0;
5079
+ if (!hostKey && (mountCfg == null ? void 0 : mountCfg.storage)) {
5080
+ void hydrateAutoDeviceKey({ appId: mountCfg.appId, storage: mountCfg.storage }).then(() => {
5081
+ if (!cancelled) fireMountOpen();
5082
+ });
5083
+ } else {
5084
+ fireMountOpen();
4974
5085
  }
4975
5086
  let backgroundedAt = null;
4976
5087
  const onChange = (state) => {
@@ -4986,11 +5097,12 @@ var useLifecycleEvents = (config, options = {}) => {
4986
5097
  };
4987
5098
  const sub = AppState.addEventListener("change", onChange);
4988
5099
  return () => {
5100
+ cancelled = true;
4989
5101
  if (sub && typeof sub.remove === "function") sub.remove();
4990
5102
  };
4991
5103
  }, []);
4992
5104
  };
4993
5105
 
4994
- export { AUTO_DEVICE_ID_PREFIX, AnimatedSparkle, BACKGROUND_SESSION_MS, CardGridSelectCard, CardHandoff, CenteredModal, ChipSelectCard, CompletionView, DEFAULT_FEATURES_TTL_MS, DEFAULT_SESSION_TTL_MS, DemoOnboarding, DoneBlock, EXTRA_KEY_PREFIX, ErrorBlock, FIRST_OPEN_EVENT, IconRegistryProvider, IllustrationProvider, InterstitialCard, LoadingBlock, LoadingScreen, NumberStepperCard, Button as OnboardingButton, OnboardingFlow, OnboardingScaffold, OnboardingThemeProvider, PLAN_TIER_CONTEXT_KEY, RESERVED_USER_CONTEXT_KEYS, SESSION_STARTED_EVENT, SelectionCard, StatusCard, StepProgress, TextInputCard, USER_ID_MAX_LENGTH, WIRE_ICON_GLYPHS, WIRE_ICON_NAMES, WIRE_ONBOARDING_EVENTS, WIRE_PURCHASE_EVENTS, WireFeaturesProvider, WireIcon, WireOnboarding, activationJoinContext, activeEntitlement, analyticsUserIdStorageKey, attributionMetadata, bumpActivationRevalidation, clearPersistedSession, clearPiiFromContext, clearUserContext, collectDeviceContext, createRevenueCatBridge, createWireActivation, defaultIllustrations, defaultOnboardingTheme, defaultWireFeatures, deriveAnswers, describeEntitlement, describeFailure, describePackage, detectAppVersion, detectNativeModel, deviceIdStorageKey, ensureCurrentSessionId, featuresCacheKey, featuresEqual, fetchWireFeatures, firstOpenStorageKey, getActivationRevalidationVersion, getCurrentSessionId, hashEmailFnv1a, identifyOnboarding, isFeaturesFresh, isOnboardingEnabled, isUserCancelled, isWireScalar, loadPersistedSession, looksLikeEmail, lookupIconGlyph, makeSessionId, mergeTheme, mintDeviceId, motionSpec_exports as motionSpec, namespaceExtra, onboardingComponents, parseWireFeatures, peekPersistedSession, readCachedFeatures, readProgress, reportClientEvent, reportClientEventAwait, reportClientEvents, reportClientEventsAwait, reportFirstOpen, reportSessionStart, resetActivationRevalidation, resetAutoDeviceKeys, resetCurrentSessionId, resetFirstOpenLatch, resetSessionStartGuard, resolveAutoDeviceKey, resolvePlanTier, resolveUserContext, sanitizeUserId, savePersistedSession, sessionStorageKey, setCurrentSessionId, subscribeActivationRevalidation, themeFromBrand, toAnalyticsEvent, useActivationRevalidation, useHostIcon, useIllustration, useLifecycleEvents, useOnboardingTheme, useReducedMotion, useResolvedFeatures, useSessionStart, useWireActivation, useWireFeatures, useWireFeaturesContext, wireConfigFromEnv, wireLifecycleEvents, writeCachedFeatures };
5106
+ export { AUTO_DEVICE_ID_PREFIX, AnimatedSparkle, BACKGROUND_SESSION_MS, CardGridSelectCard, CardHandoff, CenteredModal, ChipSelectCard, CompletionView, DEFAULT_FEATURES_TTL_MS, DEFAULT_SESSION_TTL_MS, DemoOnboarding, DoneBlock, EXTRA_KEY_PREFIX, ErrorBlock, FIRST_OPEN_EVENT, IconRegistryProvider, IllustrationProvider, InterstitialCard, LoadingBlock, LoadingScreen, NumberStepperCard, Button as OnboardingButton, OnboardingFlow, OnboardingScaffold, OnboardingThemeProvider, PLAN_TIER_CONTEXT_KEY, RESERVED_USER_CONTEXT_KEYS, SESSION_STARTED_EVENT, SelectionCard, StatusCard, StepProgress, TextInputCard, USER_ID_MAX_LENGTH, WIRE_ENV_VARS, WIRE_ICON_GLYPHS, WIRE_ICON_NAMES, WIRE_ONBOARDING_EVENTS, WIRE_PURCHASE_EVENTS, WireFeaturesProvider, WireIcon, WireOnboarding, activationJoinContext, activeEntitlement, analyticsUserIdStorageKey, attributionMetadata, bumpActivationRevalidation, clearPersistedSession, clearPiiFromContext, clearUserContext, collectDeviceContext, createRevenueCatBridge, createWireActivation, defaultIllustrations, defaultOnboardingTheme, defaultWireFeatures, deriveAnswers, describeEntitlement, describeFailure, describePackage, detectAppVersion, detectNativeModel, deviceIdStorageKey, ensureCurrentSessionId, featuresCacheKey, featuresEqual, fetchWireFeatures, firstOpenStorageKey, getActivationRevalidationVersion, getCurrentSessionId, hashEmailFnv1a, hydrateAutoDeviceKey, identifyOnboarding, isFeaturesFresh, isOnboardingEnabled, isUserCancelled, isWireScalar, loadPersistedSession, looksLikeEmail, lookupIconGlyph, makeSessionId, mergeTheme, mintDeviceId, motionSpec_exports as motionSpec, namespaceExtra, onboardingComponents, parseWireFeatures, peekPersistedSession, readCachedFeatures, readProgress, reportClientEvent, reportClientEventAwait, reportClientEvents, reportClientEventsAwait, reportFirstOpen, reportSessionStart, resetActivationRevalidation, resetAutoDeviceKeys, resetCurrentSessionId, resetFirstOpenLatch, resetSessionStartGuard, resolveAutoDeviceKey, resolvePlanTier, resolveUserContext, sanitizeUserId, savePersistedSession, sessionStorageKey, setCurrentSessionId, subscribeActivationRevalidation, themeFromBrand, toAnalyticsEvent, useActivationRevalidation, useHostIcon, useIllustration, useLifecycleEvents, useOnboardingTheme, useReducedMotion, useResolvedFeatures, useSessionStart, useWireActivation, useWireFeatures, useWireFeaturesContext, wireConfigFromEnv, wireLifecycleEvents, writeCachedFeatures };
4995
5107
  //# sourceMappingURL=index.mjs.map
4996
5108
  //# sourceMappingURL=index.mjs.map