@wireai/activation 0.12.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 (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.js CHANGED
@@ -1519,17 +1519,17 @@ var buildEventsRequest = (target, events) => {
1519
1519
  }
1520
1520
  };
1521
1521
  var warnOnSkippedEvents = (res) => {
1522
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
1523
+ if (typeof console === "undefined" || !console.warn) return;
1522
1524
  try {
1523
1525
  const json = res == null ? void 0 : res.json;
1524
1526
  if (typeof json !== "function") return;
1525
1527
  void Promise.resolve(json.call(res)).then((body) => {
1526
1528
  const skipped = body == null ? void 0 : body.skipped;
1527
1529
  if (typeof skipped !== "number" || skipped <= 0) return;
1528
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1529
- console.warn(
1530
- `[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.`
1531
- );
1532
- }
1530
+ console.warn(
1531
+ `[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.`
1532
+ );
1533
1533
  }).catch(() => {
1534
1534
  });
1535
1535
  } catch {
@@ -3317,6 +3317,82 @@ var collectDeviceContext = () => {
3317
3317
  return ctx;
3318
3318
  };
3319
3319
 
3320
+ // src/context/deviceId.ts
3321
+ var AUTO_DEVICE_ID_PREFIX = "wdev_";
3322
+ var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
3323
+ var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
3324
+ var mintDeviceId = () => {
3325
+ const time = Date.now().toString(36);
3326
+ return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
3327
+ };
3328
+ var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
3329
+ var deviceKeyGlobal = globalThis;
3330
+ var autoDeviceKeyRegistry = () => {
3331
+ const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
3332
+ if (existing) return existing;
3333
+ const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
3334
+ deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
3335
+ return created;
3336
+ };
3337
+ var startHydration = (registry, appId, storage, minted) => {
3338
+ if (!registry.pending) registry.pending = /* @__PURE__ */ new Map();
3339
+ const existing = registry.pending.get(appId);
3340
+ if (existing) return existing;
3341
+ const slot = deviceIdStorageKey(appId);
3342
+ const settled = () => {
3343
+ var _a2;
3344
+ return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
3345
+ };
3346
+ let run;
3347
+ try {
3348
+ run = Promise.resolve(storage.getItem(slot)).then((saved) => {
3349
+ const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
3350
+ if (persisted) {
3351
+ registry.keys.set(appId, persisted);
3352
+ return persisted;
3353
+ }
3354
+ return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
3355
+ }).catch(settled);
3356
+ } catch {
3357
+ run = Promise.resolve(settled());
3358
+ }
3359
+ registry.pending.set(appId, run);
3360
+ return run;
3361
+ };
3362
+ var resolveAutoDeviceKey = (opts = {}) => {
3363
+ var _a2, _b;
3364
+ const registry = autoDeviceKeyRegistry();
3365
+ const appId = (_a2 = opts.appId) != null ? _a2 : "default";
3366
+ let id = registry.keys.get(appId);
3367
+ if (!id) {
3368
+ id = mintDeviceId();
3369
+ registry.keys.set(appId, id);
3370
+ }
3371
+ const storage = opts.storage;
3372
+ if (storage && !registry.hydrating.has(appId)) {
3373
+ registry.hydrating.add(appId);
3374
+ void startHydration(registry, appId, storage, id);
3375
+ }
3376
+ return (_b = registry.keys.get(appId)) != null ? _b : id;
3377
+ };
3378
+ var hydrateAutoDeviceKey = async (opts = {}) => {
3379
+ var _a2, _b, _c;
3380
+ const id = resolveAutoDeviceKey(opts);
3381
+ if (!opts.storage) return id;
3382
+ const registry = autoDeviceKeyRegistry();
3383
+ const appId = (_a2 = opts.appId) != null ? _a2 : "default";
3384
+ const pending = (_b = registry.pending) == null ? void 0 : _b.get(appId);
3385
+ if (pending) await pending;
3386
+ return (_c = registry.keys.get(appId)) != null ? _c : id;
3387
+ };
3388
+ var resetAutoDeviceKeys = () => {
3389
+ var _a2;
3390
+ const registry = autoDeviceKeyRegistry();
3391
+ registry.keys.clear();
3392
+ registry.hydrating.clear();
3393
+ (_a2 = registry.pending) == null ? void 0 : _a2.clear();
3394
+ };
3395
+
3320
3396
  // src/analytics/currentSession.ts
3321
3397
  var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
3322
3398
  "@wireai/activation:currentSessionId"
@@ -3442,6 +3518,105 @@ var identifyOnboarding = async (opts) => {
3442
3518
  );
3443
3519
  return true;
3444
3520
  };
3521
+
3522
+ // src/context/userContext.ts
3523
+ var RESERVED_USER_CONTEXT_KEYS = [
3524
+ "device_key",
3525
+ "app_version",
3526
+ "app_build",
3527
+ "network_type",
3528
+ "session_count",
3529
+ "returning",
3530
+ "platform",
3531
+ "user_email",
3532
+ "user_email_hashed"
3533
+ ];
3534
+ var EXTRA_KEY_PREFIX = "custom.";
3535
+ var isWireScalar = (value) => {
3536
+ const t = typeof value;
3537
+ if (t === "string" || t === "boolean") return true;
3538
+ if (t === "number") return Number.isFinite(value);
3539
+ return false;
3540
+ };
3541
+ var hashEmailFnv1a = (email) => {
3542
+ const normalized = email.trim().toLowerCase();
3543
+ let hash = 2166136261;
3544
+ for (let i = 0; i < normalized.length; i++) {
3545
+ hash ^= normalized.charCodeAt(i);
3546
+ hash = Math.imul(hash, 16777619);
3547
+ }
3548
+ return (hash >>> 0).toString(16).padStart(8, "0");
3549
+ };
3550
+ var cleanString = (value) => {
3551
+ if (typeof value !== "string") return void 0;
3552
+ const trimmed = value.trim();
3553
+ return trimmed.length > 0 ? trimmed : void 0;
3554
+ };
3555
+ var namespaceExtra = (extra) => {
3556
+ const out = {};
3557
+ if (!extra || typeof extra !== "object") return out;
3558
+ for (const [key, value] of Object.entries(extra)) {
3559
+ const cleanKey = cleanString(key);
3560
+ if (!cleanKey) continue;
3561
+ if (!isWireScalar(value)) continue;
3562
+ out[`${EXTRA_KEY_PREFIX}${cleanKey}`] = value;
3563
+ }
3564
+ return out;
3565
+ };
3566
+ var analyticsUserIdStorageKey = (appId) => `wireai:analytics:userId:${appId != null ? appId : "default"}`;
3567
+ var clearPiiFromContext = (ctx = {}) => {
3568
+ const rest = {};
3569
+ if (typeof ctx.appVersion === "string") rest.appVersion = ctx.appVersion;
3570
+ if (typeof ctx.deviceKey === "string") rest.deviceKey = ctx.deviceKey;
3571
+ return rest;
3572
+ };
3573
+ var clearUserContext = async (opts = {}) => {
3574
+ const storage = opts.storage;
3575
+ if (!storage) return;
3576
+ try {
3577
+ await storage.removeItem(analyticsUserIdStorageKey(opts.appId));
3578
+ } catch {
3579
+ }
3580
+ };
3581
+ var activationJoinContext = (deviceKey) => {
3582
+ var _a2;
3583
+ return (_a2 = resolveUserContext({ deviceKey }).userContext) != null ? _a2 : {};
3584
+ };
3585
+ var resolveUserContext = (ctx = {}, opts = {}) => {
3586
+ var _a2;
3587
+ const result = {};
3588
+ const bucket = {};
3589
+ const userId = sanitizeUserId(ctx.userId);
3590
+ if (userId) result.userId = userId;
3591
+ const deviceKey = cleanString(ctx.deviceKey);
3592
+ if (deviceKey) {
3593
+ result.deviceKey = deviceKey;
3594
+ bucket.device_key = deviceKey;
3595
+ }
3596
+ const appVersion = (_a2 = cleanString(ctx.appVersion)) != null ? _a2 : cleanString(opts.autoAppVersion);
3597
+ if (appVersion) {
3598
+ result.appVersion = appVersion;
3599
+ bucket.app_version = appVersion;
3600
+ }
3601
+ const email = cleanString(ctx.userEmail);
3602
+ if (email) {
3603
+ if (ctx.hashEmail) {
3604
+ bucket.user_email = hashEmailFnv1a(email);
3605
+ bucket.user_email_hashed = true;
3606
+ } else {
3607
+ bucket.user_email = email;
3608
+ }
3609
+ }
3610
+ Object.assign(bucket, namespaceExtra(ctx.extra));
3611
+ if (Object.keys(bucket).length > 0) result.userContext = bucket;
3612
+ return result;
3613
+ };
3614
+ var warnInDev2 = (message) => {
3615
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
3616
+ console.warn(message);
3617
+ }
3618
+ };
3619
+ var AUTO_JOIN_HYDRATION_TIMEOUT_MS = 1500;
3445
3620
  var WireOnboarding = ({
3446
3621
  config,
3447
3622
  theme,
@@ -3464,7 +3639,8 @@ var WireOnboarding = ({
3464
3639
  persistKey,
3465
3640
  retainSessionOnComplete,
3466
3641
  userContext,
3467
- userId
3642
+ userId,
3643
+ autoJoinKey = true
3468
3644
  }) => {
3469
3645
  var _a2, _b, _c;
3470
3646
  const boundUserId = React18.useMemo(() => sanitizeUserId(userId), [userId]);
@@ -3477,9 +3653,50 @@ var WireOnboarding = ({
3477
3653
  () => userContextKey ? JSON.parse(userContextKey) : void 0,
3478
3654
  [userContextKey]
3479
3655
  );
3656
+ const missingJoinKey = typeof (userContextStable == null ? void 0 : userContextStable.device_key) !== "string" || !userContextStable.device_key.trim();
3657
+ const autoJoinPossible = Boolean(storage) && autoJoinKey !== false;
3658
+ const wantsAutoJoin = missingJoinKey && autoJoinPossible;
3659
+ const [autoJoinValue, setAutoJoinValue] = React18.useState(void 0);
3660
+ React18.useEffect(() => {
3661
+ if (!wantsAutoJoin || autoJoinValue !== void 0 || !storage) return;
3662
+ let cancelled = false;
3663
+ const timer = setTimeout(() => {
3664
+ if (!cancelled) setAutoJoinValue(null);
3665
+ }, AUTO_JOIN_HYDRATION_TIMEOUT_MS);
3666
+ void hydrateAutoDeviceKey({ appId: config.appId, storage }).then((key) => {
3667
+ if (cancelled) return;
3668
+ clearTimeout(timer);
3669
+ setAutoJoinValue(key || null);
3670
+ });
3671
+ return () => {
3672
+ cancelled = true;
3673
+ clearTimeout(timer);
3674
+ };
3675
+ }, [wantsAutoJoin, autoJoinValue, config.appId, storage]);
3676
+ const injectedJoinKey = wantsAutoJoin && typeof autoJoinValue === "string" ? autoJoinValue : void 0;
3677
+ const autoJoinPending = wantsAutoJoin && autoJoinValue === void 0;
3678
+ const effectiveUserContext = React18.useMemo(
3679
+ () => injectedJoinKey ? { ...userContextStable, ...activationJoinContext(injectedJoinKey) } : userContextStable,
3680
+ [userContextStable, injectedJoinKey]
3681
+ );
3682
+ const warnMissingJoinKey = missingJoinKey && !injectedJoinKey && !autoJoinPending;
3683
+ 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.";
3684
+ React18.useEffect(() => {
3685
+ if (!warnMissingJoinKey) return;
3686
+ warnInDev2(
3687
+ "[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
3688
+ );
3689
+ }, [warnMissingJoinKey, autoJoinReason]);
3690
+ const misspelledJoinKey = missingJoinKey && typeof (userContextStable == null ? void 0 : userContextStable.deviceKey) === "string" && Boolean(userContextStable.deviceKey.trim());
3691
+ React18.useEffect(() => {
3692
+ if (!misspelledJoinKey || !injectedJoinKey) return;
3693
+ warnInDev2(
3694
+ "[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."
3695
+ );
3696
+ }, [misspelledJoinKey, injectedJoinKey]);
3480
3697
  const clientContext = React18.useMemo(
3481
- () => ({ device, userContext: userContextStable, userId: boundUserId }),
3482
- [device, userContextStable, boundUserId]
3698
+ () => ({ device, userContext: effectiveUserContext, userId: boundUserId }),
3699
+ [device, effectiveUserContext, boundUserId]
3483
3700
  );
3484
3701
  const [session, setSession] = React18.useState(
3485
3702
  () => storage ? null : { id: makeSessionId(), resumed: false }
@@ -3526,7 +3743,7 @@ var WireOnboarding = ({
3526
3743
  // Device snapshot + host-injected user context ride the session-start metadata so the
3527
3744
  // backend can segment the funnel. Old servers ignore these unknown keys (backward compat).
3528
3745
  device,
3529
- ...userContextStable ? { userContext: userContextStable } : {},
3746
+ ...effectiveUserContext ? { userContext: effectiveUserContext } : {},
3530
3747
  // The session-start user id (from the ref) rides the session-start metadata so the
3531
3748
  // server binds the session to a real user at creation. Reading the ref — not
3532
3749
  // `boundUserId` — keeps this memo off the userId dependency, so a mid-session change
@@ -3535,7 +3752,7 @@ var WireOnboarding = ({
3535
3752
  },
3536
3753
  timeoutMs: 6e4
3537
3754
  };
3538
- }, [config.serverUrl, config.appId, config.apiKey, metadataStable, sessionId, componentsStable, device, userContextStable]);
3755
+ }, [config.serverUrl, config.appId, config.apiKey, metadataStable, sessionId, componentsStable, device, effectiveUserContext]);
3539
3756
  const reportTarget = React18.useMemo(
3540
3757
  () => ({ serverUrl: config.serverUrl, apiKey: config.apiKey }),
3541
3758
  [config.serverUrl, config.apiKey]
@@ -3557,7 +3774,7 @@ var WireOnboarding = ({
3557
3774
  });
3558
3775
  }, [session, sessionId, boundUserId, reportTarget]);
3559
3776
  const cards = components != null ? components : onboardingComponents;
3560
- if (!session) {
3777
+ if (!session || autoJoinPending) {
3561
3778
  return /* @__PURE__ */ jsxRuntime.jsx(OnboardingThemeProvider, { theme, children: /* @__PURE__ */ jsxRuntime.jsx(
3562
3779
  LoadingScreen,
3563
3780
  {
@@ -4023,16 +4240,37 @@ var useResolvedFeatures = (options) => {
4023
4240
  };
4024
4241
 
4025
4242
  // src/config/wireConfigFromEnv.ts
4243
+ var WIRE_ENV_VARS = [
4244
+ "EXPO_PUBLIC_WIREAI_API_KEY",
4245
+ "EXPO_PUBLIC_WIREAI_SERVER_URL",
4246
+ "EXPO_PUBLIC_WIREAI_APP_ID"
4247
+ ];
4248
+ var warnInDev3 = (message) => {
4249
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
4250
+ console.warn(message);
4251
+ }
4252
+ };
4026
4253
  var wireConfigFromEnv = (overrides) => {
4027
- var _a2, _b, _c, _d;
4254
+ var _a2, _b, _c, _d, _e;
4028
4255
  const apiKey = (_a2 = overrides == null ? void 0 : overrides.apiKey) != null ? _a2 : process.env.EXPO_PUBLIC_WIREAI_API_KEY;
4029
4256
  const serverUrl = (_b = overrides == null ? void 0 : overrides.serverUrl) != null ? _b : process.env.EXPO_PUBLIC_WIREAI_SERVER_URL;
4030
4257
  const appId = (_d = (_c = overrides == null ? void 0 : overrides.appId) != null ? _c : process.env.EXPO_PUBLIC_WIREAI_APP_ID) != null ? _d : "default";
4031
- if (!apiKey || !serverUrl) return null;
4258
+ if (!apiKey || !serverUrl) {
4259
+ const missing = [
4260
+ apiKey ? void 0 : "EXPO_PUBLIC_WIREAI_API_KEY",
4261
+ serverUrl ? void 0 : "EXPO_PUBLIC_WIREAI_SERVER_URL"
4262
+ ].filter(Boolean);
4263
+ warnInDev3(
4264
+ `[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.`
4265
+ );
4266
+ return null;
4267
+ }
4268
+ const appVersion = (_e = overrides == null ? void 0 : overrides.appVersion) != null ? _e : detectAppVersion();
4032
4269
  return {
4033
4270
  apiKey,
4034
4271
  serverUrl,
4035
4272
  appId,
4273
+ ...appVersion ? { appVersion } : {},
4036
4274
  ...(overrides == null ? void 0 : overrides.metadata) ? { metadata: overrides.metadata } : {}
4037
4275
  };
4038
4276
  };
@@ -4093,149 +4331,6 @@ var attributionMetadata = (a) => {
4093
4331
  return { attribution };
4094
4332
  };
4095
4333
 
4096
- // src/context/userContext.ts
4097
- var RESERVED_USER_CONTEXT_KEYS = [
4098
- "device_key",
4099
- "app_version",
4100
- "app_build",
4101
- "network_type",
4102
- "session_count",
4103
- "returning",
4104
- "platform",
4105
- "user_email",
4106
- "user_email_hashed"
4107
- ];
4108
- var EXTRA_KEY_PREFIX = "custom.";
4109
- var isWireScalar = (value) => {
4110
- const t = typeof value;
4111
- if (t === "string" || t === "boolean") return true;
4112
- if (t === "number") return Number.isFinite(value);
4113
- return false;
4114
- };
4115
- var hashEmailFnv1a = (email) => {
4116
- const normalized = email.trim().toLowerCase();
4117
- let hash = 2166136261;
4118
- for (let i = 0; i < normalized.length; i++) {
4119
- hash ^= normalized.charCodeAt(i);
4120
- hash = Math.imul(hash, 16777619);
4121
- }
4122
- return (hash >>> 0).toString(16).padStart(8, "0");
4123
- };
4124
- var cleanString = (value) => {
4125
- if (typeof value !== "string") return void 0;
4126
- const trimmed = value.trim();
4127
- return trimmed.length > 0 ? trimmed : void 0;
4128
- };
4129
- var namespaceExtra = (extra) => {
4130
- const out = {};
4131
- if (!extra || typeof extra !== "object") return out;
4132
- for (const [key, value] of Object.entries(extra)) {
4133
- const cleanKey = cleanString(key);
4134
- if (!cleanKey) continue;
4135
- if (!isWireScalar(value)) continue;
4136
- out[`${EXTRA_KEY_PREFIX}${cleanKey}`] = value;
4137
- }
4138
- return out;
4139
- };
4140
- var analyticsUserIdStorageKey = (appId) => `wireai:analytics:userId:${appId != null ? appId : "default"}`;
4141
- var clearPiiFromContext = (ctx = {}) => {
4142
- const rest = {};
4143
- if (typeof ctx.appVersion === "string") rest.appVersion = ctx.appVersion;
4144
- if (typeof ctx.deviceKey === "string") rest.deviceKey = ctx.deviceKey;
4145
- return rest;
4146
- };
4147
- var clearUserContext = async (opts = {}) => {
4148
- const storage = opts.storage;
4149
- if (!storage) return;
4150
- try {
4151
- await storage.removeItem(analyticsUserIdStorageKey(opts.appId));
4152
- } catch {
4153
- }
4154
- };
4155
- var activationJoinContext = (deviceKey) => {
4156
- var _a2;
4157
- return (_a2 = resolveUserContext({ deviceKey }).userContext) != null ? _a2 : {};
4158
- };
4159
- var resolveUserContext = (ctx = {}, opts = {}) => {
4160
- var _a2;
4161
- const result = {};
4162
- const bucket = {};
4163
- const userId = sanitizeUserId(ctx.userId);
4164
- if (userId) result.userId = userId;
4165
- const deviceKey = cleanString(ctx.deviceKey);
4166
- if (deviceKey) {
4167
- result.deviceKey = deviceKey;
4168
- bucket.device_key = deviceKey;
4169
- }
4170
- const appVersion = (_a2 = cleanString(ctx.appVersion)) != null ? _a2 : cleanString(opts.autoAppVersion);
4171
- if (appVersion) {
4172
- result.appVersion = appVersion;
4173
- bucket.app_version = appVersion;
4174
- }
4175
- const email = cleanString(ctx.userEmail);
4176
- if (email) {
4177
- if (ctx.hashEmail) {
4178
- bucket.user_email = hashEmailFnv1a(email);
4179
- bucket.user_email_hashed = true;
4180
- } else {
4181
- bucket.user_email = email;
4182
- }
4183
- }
4184
- Object.assign(bucket, namespaceExtra(ctx.extra));
4185
- if (Object.keys(bucket).length > 0) result.userContext = bucket;
4186
- return result;
4187
- };
4188
-
4189
- // src/context/deviceId.ts
4190
- var AUTO_DEVICE_ID_PREFIX = "wdev_";
4191
- var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
4192
- var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
4193
- var mintDeviceId = () => {
4194
- const time = Date.now().toString(36);
4195
- return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
4196
- };
4197
- var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
4198
- var deviceKeyGlobal = globalThis;
4199
- var autoDeviceKeyRegistry = () => {
4200
- const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
4201
- if (existing) return existing;
4202
- const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
4203
- deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
4204
- return created;
4205
- };
4206
- var resolveAutoDeviceKey = (opts = {}) => {
4207
- var _a2, _b;
4208
- const registry = autoDeviceKeyRegistry();
4209
- const appId = (_a2 = opts.appId) != null ? _a2 : "default";
4210
- let id = registry.keys.get(appId);
4211
- if (!id) {
4212
- id = mintDeviceId();
4213
- registry.keys.set(appId, id);
4214
- }
4215
- const storage = opts.storage;
4216
- if (storage && !registry.hydrating.has(appId)) {
4217
- registry.hydrating.add(appId);
4218
- const slot = deviceIdStorageKey(appId);
4219
- const minted = id;
4220
- try {
4221
- void Promise.resolve(storage.getItem(slot)).then((saved) => {
4222
- const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
4223
- if (persisted) registry.keys.set(appId, persisted);
4224
- else void Promise.resolve(storage.setItem(slot, minted)).catch(() => {
4225
- });
4226
- }).catch(() => {
4227
- });
4228
- } catch {
4229
- }
4230
- }
4231
- return (_b = registry.keys.get(appId)) != null ? _b : id;
4232
- };
4233
- var resetAutoDeviceKeys = () => {
4234
- const registry = autoDeviceKeyRegistry();
4235
- registry.keys.clear();
4236
- registry.hydrating.clear();
4237
- };
4238
-
4239
4334
  // src/activation/revalidation.ts
4240
4335
  var REVALIDATION_SLOT = /* @__PURE__ */ Symbol.for(
4241
4336
  "@wireai/activation:activationRevalidation"
@@ -4288,18 +4383,19 @@ var createWireActivation = (config) => {
4288
4383
  storage: explicitDeviceKey ? void 0 : config.storage
4289
4384
  };
4290
4385
  if (!explicitDeviceKey) resolveAutoDeviceKey(autoDeviceKeyOptions);
4386
+ const detectedAppVersion = detectAppVersion();
4291
4387
  const applyContext = (event) => {
4292
- var _a3, _b2;
4388
+ var _a3, _b2, _c;
4293
4389
  const resolved = resolveUserContext(
4294
4390
  // `??` is lazy on purpose: an explicit key must never even touch the auto registry.
4295
4391
  {
4296
4392
  ...(_a3 = config.userContext) != null ? _a3 : {},
4297
4393
  deviceKey: explicitDeviceKey != null ? explicitDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions)
4298
4394
  },
4299
- { autoAppVersion: config.appVersion }
4395
+ { autoAppVersion: (_b2 = config.appVersion) != null ? _b2 : detectedAppVersion }
4300
4396
  );
4301
4397
  if (resolved.userContext) {
4302
- event.user_context = { ...resolved.userContext, ...(_b2 = event.user_context) != null ? _b2 : {} };
4398
+ event.user_context = { ...resolved.userContext, ...(_c = event.user_context) != null ? _c : {} };
4303
4399
  }
4304
4400
  if (resolved.userId && !event.user_id) event.user_id = resolved.userId;
4305
4401
  };
@@ -4562,6 +4658,12 @@ var useSessionStart = (config, options = {}) => {
4562
4658
  const latest = React18.useRef({ config, options });
4563
4659
  latest.current = { config, options };
4564
4660
  React18.useEffect(() => {
4661
+ const resolveDeviceKey = (cfg, opts) => {
4662
+ const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
4663
+ if (host) return host;
4664
+ if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
4665
+ return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
4666
+ };
4565
4667
  const fire = () => {
4566
4668
  var _a2, _b;
4567
4669
  const { config: cfg, options: opts } = latest.current;
@@ -4574,7 +4676,7 @@ var useSessionStart = (config, options = {}) => {
4574
4676
  target,
4575
4677
  // A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
4576
4678
  userId: opts.userId,
4577
- deviceKey: opts.deviceKey,
4679
+ deviceKey: resolveDeviceKey(cfg, opts),
4578
4680
  sessionCount: opts.sessionCount,
4579
4681
  appVersion: (_b = cfg.appVersion) != null ? _b : device.appVersion,
4580
4682
  platform: reactNative.Platform.OS,
@@ -4908,15 +5010,14 @@ var useLifecycleEvents = (config, options = {}) => {
4908
5010
  latest.current = { config, options };
4909
5011
  const queueRef = React18.useRef(void 0);
4910
5012
  React18.useEffect(() => {
4911
- var _a2;
4912
5013
  const resolveSink = () => {
4913
- var _a3, _b;
5014
+ var _a2, _b;
4914
5015
  const { config: cfg, options: opts } = latest.current;
4915
5016
  if (opts.sink) return opts.sink;
4916
5017
  if (!(cfg == null ? void 0 : cfg.serverUrl)) return void 0;
4917
5018
  if (!queueRef.current) {
4918
5019
  queueRef.current = createEventQueue({
4919
- target: { serverUrl: cfg.serverUrl, apiKey: (_a3 = cfg.apiKey) != null ? _a3 : "" },
5020
+ target: { serverUrl: cfg.serverUrl, apiKey: (_a2 = cfg.apiKey) != null ? _a2 : "" },
4920
5021
  storage: cfg.storage,
4921
5022
  // Dedicated key so the hook's internal queue never collides with a host's main queue.
4922
5023
  storageKey: `wireai:evtq:lifecycle:${(_b = cfg.appId) != null ? _b : "default"}`,
@@ -4926,8 +5027,8 @@ var useLifecycleEvents = (config, options = {}) => {
4926
5027
  return queueRef.current.enqueue;
4927
5028
  };
4928
5029
  const targetOf = (cfg) => {
4929
- var _a3;
4930
- return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a3 = cfg.apiKey) != null ? _a3 : "" } : void 0;
5030
+ var _a2;
5031
+ return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a2 = cfg.apiKey) != null ? _a2 : "" } : void 0;
4931
5032
  };
4932
5033
  const resolveDeviceKey = (cfg, opts) => {
4933
5034
  const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
@@ -4937,7 +5038,7 @@ var useLifecycleEvents = (config, options = {}) => {
4937
5038
  };
4938
5039
  const mountOpenSessionId = makeSessionId();
4939
5040
  const fireSession = (sessionId) => {
4940
- var _a3;
5041
+ var _a2;
4941
5042
  const { config: cfg, options: opts } = latest.current;
4942
5043
  if (opts.enabled === false) return;
4943
5044
  if (!(cfg == null ? void 0 : cfg.serverUrl) && !opts.sink) return;
@@ -4950,33 +5051,43 @@ var useLifecycleEvents = (config, options = {}) => {
4950
5051
  userId: opts.userId,
4951
5052
  deviceKey: resolveDeviceKey(cfg, opts),
4952
5053
  sessionCount: opts.sessionCount,
4953
- appVersion: (_a3 = cfg == null ? void 0 : cfg.appVersion) != null ? _a3 : device.appVersion,
5054
+ appVersion: (_a2 = cfg == null ? void 0 : cfg.appVersion) != null ? _a2 : device.appVersion,
4954
5055
  platform: reactNative.Platform.OS,
4955
5056
  device,
4956
5057
  meta: opts.meta
4957
5058
  });
4958
5059
  };
4959
- fireSession(mountOpenSessionId);
4960
- {
5060
+ const fireMountOpen = () => {
5061
+ var _a2;
5062
+ fireSession(mountOpenSessionId);
4961
5063
  const { config: cfg, options: opts } = latest.current;
4962
- if (opts.enabled !== false) {
4963
- const device = collectDeviceContext();
4964
- if (cfg == null ? void 0 : cfg.appVersion) device.appVersion = cfg.appVersion;
4965
- reportFirstOpen({
4966
- target: targetOf(cfg),
4967
- sink: resolveSink(),
4968
- sessionId: mountOpenSessionId,
4969
- storage: cfg == null ? void 0 : cfg.storage,
4970
- appId: cfg == null ? void 0 : cfg.appId,
4971
- userId: opts.userId,
4972
- deviceKey: resolveDeviceKey(cfg, opts),
4973
- sessionCount: opts.sessionCount,
4974
- appVersion: (_a2 = cfg == null ? void 0 : cfg.appVersion) != null ? _a2 : device.appVersion,
4975
- platform: reactNative.Platform.OS,
4976
- device,
4977
- meta: opts.meta
4978
- });
4979
- }
5064
+ if (opts.enabled === false) return;
5065
+ const device = collectDeviceContext();
5066
+ if (cfg == null ? void 0 : cfg.appVersion) device.appVersion = cfg.appVersion;
5067
+ reportFirstOpen({
5068
+ target: targetOf(cfg),
5069
+ sink: resolveSink(),
5070
+ sessionId: mountOpenSessionId,
5071
+ storage: cfg == null ? void 0 : cfg.storage,
5072
+ appId: cfg == null ? void 0 : cfg.appId,
5073
+ userId: opts.userId,
5074
+ deviceKey: resolveDeviceKey(cfg, opts),
5075
+ sessionCount: opts.sessionCount,
5076
+ appVersion: (_a2 = cfg == null ? void 0 : cfg.appVersion) != null ? _a2 : device.appVersion,
5077
+ platform: reactNative.Platform.OS,
5078
+ device,
5079
+ meta: opts.meta
5080
+ });
5081
+ };
5082
+ let cancelled = false;
5083
+ const { config: mountCfg, options: mountOpts } = latest.current;
5084
+ const hostKey = typeof mountOpts.deviceKey === "string" && mountOpts.deviceKey.trim() ? mountOpts.deviceKey : void 0;
5085
+ if (!hostKey && (mountCfg == null ? void 0 : mountCfg.storage)) {
5086
+ void hydrateAutoDeviceKey({ appId: mountCfg.appId, storage: mountCfg.storage }).then(() => {
5087
+ if (!cancelled) fireMountOpen();
5088
+ });
5089
+ } else {
5090
+ fireMountOpen();
4980
5091
  }
4981
5092
  let backgroundedAt = null;
4982
5093
  const onChange = (state) => {
@@ -4992,6 +5103,7 @@ var useLifecycleEvents = (config, options = {}) => {
4992
5103
  };
4993
5104
  const sub = reactNative.AppState.addEventListener("change", onChange);
4994
5105
  return () => {
5106
+ cancelled = true;
4995
5107
  if (sub && typeof sub.remove === "function") sub.remove();
4996
5108
  };
4997
5109
  }, []);
@@ -5030,6 +5142,7 @@ exports.StatusCard = StatusCard;
5030
5142
  exports.StepProgress = StepProgress;
5031
5143
  exports.TextInputCard = TextInputCard;
5032
5144
  exports.USER_ID_MAX_LENGTH = USER_ID_MAX_LENGTH;
5145
+ exports.WIRE_ENV_VARS = WIRE_ENV_VARS;
5033
5146
  exports.WIRE_ICON_GLYPHS = WIRE_ICON_GLYPHS;
5034
5147
  exports.WIRE_ICON_NAMES = WIRE_ICON_NAMES;
5035
5148
  exports.WIRE_ONBOARDING_EVENTS = WIRE_ONBOARDING_EVENTS;
@@ -5066,6 +5179,7 @@ exports.firstOpenStorageKey = firstOpenStorageKey;
5066
5179
  exports.getActivationRevalidationVersion = getActivationRevalidationVersion;
5067
5180
  exports.getCurrentSessionId = getCurrentSessionId;
5068
5181
  exports.hashEmailFnv1a = hashEmailFnv1a;
5182
+ exports.hydrateAutoDeviceKey = hydrateAutoDeviceKey;
5069
5183
  exports.identifyOnboarding = identifyOnboarding;
5070
5184
  exports.isFeaturesFresh = isFeaturesFresh;
5071
5185
  exports.isOnboardingEnabled = isOnboardingEnabled;