@wireai/activation 0.12.1 → 0.13.0

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 (43) hide show
  1. package/CHANGELOG.md +146 -1
  2. package/README.md +5 -3
  3. package/dist/analytics/index.d.mts +2 -2
  4. package/dist/analytics/index.d.ts +2 -2
  5. package/dist/analytics/index.js +114 -35
  6. package/dist/analytics/index.js.map +1 -1
  7. package/dist/analytics/index.mjs +114 -36
  8. package/dist/analytics/index.mjs.map +1 -1
  9. package/dist/{currentSession-BxEB37xt.d.ts → currentSession-D7zabMXK.d.ts} +161 -9
  10. package/dist/{currentSession-BlCeDP0f.d.mts → currentSession-_GynvhzT.d.mts} +161 -9
  11. package/dist/index.d.mts +5 -15
  12. package/dist/index.d.ts +5 -15
  13. package/dist/index.js +232 -135
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +229 -134
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/questionnaire/index.js.map +1 -1
  18. package/dist/questionnaire/index.mjs.map +1 -1
  19. package/dist/reviews/index.js +8 -6
  20. package/dist/reviews/index.js.map +1 -1
  21. package/dist/reviews/index.mjs +8 -6
  22. package/dist/reviews/index.mjs.map +1 -1
  23. package/package.json +1 -1
  24. package/src/OnboardingFlow.tsx +10 -3
  25. package/src/WireOnboarding.tsx +115 -32
  26. package/src/activation/wireActivation.ts +13 -7
  27. package/src/analytics/analyticsFacade.ts +11 -10
  28. package/src/analytics/currentSession.ts +6 -20
  29. package/src/analytics/eventQueue.ts +69 -1
  30. package/src/analytics/index.ts +1 -1
  31. package/src/analytics/reportClientEvent.ts +92 -29
  32. package/src/config/wireConfigFromEnv.ts +1 -10
  33. package/src/context/deviceId.ts +77 -16
  34. package/src/context/userContext.ts +4 -15
  35. package/src/identity/identityRecord.ts +123 -0
  36. package/src/identity/userIdentity.ts +45 -9
  37. package/src/index.ts +6 -4
  38. package/src/session-analytics/useLifecycleEvents.ts +10 -1
  39. package/src/types.ts +14 -0
  40. package/src/utils/deriveAnswers.ts +6 -2
  41. package/src/utils/readProgress.ts +4 -0
  42. package/src/utils/warnInDev.ts +33 -0
  43. package/src/components/DoneBlock.tsx +0 -37
package/dist/index.js CHANGED
@@ -1012,26 +1012,26 @@ var _OnboardingScaffold = ({
1012
1012
  children
1013
1013
  }) => {
1014
1014
  const t = useOnboardingTheme();
1015
- const styles18 = React18.useMemo(() => makeStyles(t), [t]);
1015
+ const styles17 = React18.useMemo(() => makeStyles(t), [t]);
1016
1016
  const hit = { top: 12, bottom: 12, left: 12, right: 12 };
1017
1017
  return /* @__PURE__ */ jsxRuntime.jsxs(
1018
1018
  reactNativeSafeAreaContext.SafeAreaView,
1019
1019
  {
1020
1020
  edges: ["top", "bottom"],
1021
- style: [styles18.flex, { backgroundColor: t.colors.background }],
1021
+ style: [styles17.flex, { backgroundColor: t.colors.background }],
1022
1022
  children: [
1023
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles18.header, { paddingHorizontal: t.spacing.md, paddingTop: t.spacing.md }], children: [
1024
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles18.progressWrap, children: /* @__PURE__ */ jsxRuntime.jsx(StepProgress, { step, complete, approxScreens }) }),
1025
- onSkip ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.TouchableOpacity, { onPress: onSkip, hitSlop: hit, style: styles18.skip, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: skipLabel }) }) : null
1023
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles17.header, { paddingHorizontal: t.spacing.md, paddingTop: t.spacing.md }], children: [
1024
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles17.progressWrap, children: /* @__PURE__ */ jsxRuntime.jsx(StepProgress, { step, complete, approxScreens }) }),
1025
+ onSkip ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.TouchableOpacity, { onPress: onSkip, hitSlop: hit, style: styles17.skip, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: skipLabel }) }) : null
1026
1026
  ] }),
1027
1027
  /* @__PURE__ */ jsxRuntime.jsx(
1028
1028
  reactNative.View,
1029
1029
  {
1030
- style: [styles18.body, { paddingHorizontal: t.spacing.md, paddingTop: t.spacing.md }],
1030
+ style: [styles17.body, { paddingHorizontal: t.spacing.md, paddingTop: t.spacing.md }],
1031
1031
  children
1032
1032
  }
1033
1033
  ),
1034
- onBack ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles18.footer, { paddingHorizontal: t.spacing.md, paddingBottom: t.spacing.sm }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.TouchableOpacity, { onPress: onBack, hitSlop: hit, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: backLabel }) }) }) : null
1034
+ onBack ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles17.footer, { paddingHorizontal: t.spacing.md, paddingBottom: t.spacing.sm }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.TouchableOpacity, { onPress: onBack, hitSlop: hit, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: backLabel }) }) }) : null
1035
1035
  ]
1036
1036
  }
1037
1037
  );
@@ -1168,10 +1168,10 @@ var _CardLayout = ({
1168
1168
  titleNode
1169
1169
  }) => {
1170
1170
  const t = useOnboardingTheme();
1171
- const styles18 = React18.useMemo(() => makeStyles2(t), [t]);
1171
+ const styles17 = React18.useMemo(() => makeStyles2(t), [t]);
1172
1172
  const hasHeader = !!title || !!subtitle || !!titleNode;
1173
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.KeyboardAvoidingView, { style: styles18.fill, behavior: "padding", children: [
1174
- hasHeader ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles18.header, children: [
1173
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.KeyboardAvoidingView, { style: styles17.fill, behavior: "padding", children: [
1174
+ hasHeader ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.header, children: [
1175
1175
  titleNode != null ? titleNode : title ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [labelStyle(t.fonts), { color: t.colors.text }], children: title }) : null,
1176
1176
  subtitle ? /* @__PURE__ */ jsxRuntime.jsx(
1177
1177
  reactNative.Text,
@@ -1187,10 +1187,10 @@ var _CardLayout = ({
1187
1187
  /* @__PURE__ */ jsxRuntime.jsx(
1188
1188
  reactNative.ScrollView,
1189
1189
  {
1190
- style: styles18.fill,
1190
+ style: styles17.fill,
1191
1191
  contentContainerStyle: [
1192
- styles18.scrollContent,
1193
- align === "center" && styles18.center
1192
+ styles17.scrollContent,
1193
+ align === "center" && styles17.center
1194
1194
  ],
1195
1195
  keyboardShouldPersistTaps: "handled",
1196
1196
  keyboardDismissMode: "on-drag",
@@ -1198,7 +1198,7 @@ var _CardLayout = ({
1198
1198
  children
1199
1199
  }
1200
1200
  ),
1201
- footer ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles18.footer, children: footer }) : null
1201
+ footer ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles17.footer, children: footer }) : null
1202
1202
  ] });
1203
1203
  };
1204
1204
  var CardLayout = React18__default.default.memo(_CardLayout);
@@ -1480,7 +1480,7 @@ var deriveAnswers = (messages) => {
1480
1480
  const props = (_a2 = r.props) != null ? _a2 : {};
1481
1481
  const progress = props.progress;
1482
1482
  const questionText = (_c = (_b = props.title) != null ? _b : props.label) != null ? _c : props.question;
1483
- const key = (progress == null ? void 0 : progress.key) || questionText;
1483
+ const key = (progress == null ? void 0 : progress.slot_id) || (progress == null ? void 0 : progress.key) || questionText;
1484
1484
  const reply = messages[i + 1];
1485
1485
  if (key && (reply == null ? void 0 : reply.role) === "user") {
1486
1486
  out[key] = tryParse(stripPrefix(reply.content));
@@ -1501,6 +1501,8 @@ var readProgress = (response) => {
1501
1501
  step: typeof p.step === "number" ? p.step : void 0,
1502
1502
  total: typeof p.total === "number" ? p.total : void 0,
1503
1503
  key: typeof p.key === "string" ? p.key : void 0,
1504
+ // Whitelisted the same way as every other field: an old backend simply omits it.
1505
+ slot_id: typeof p.slot_id === "string" ? p.slot_id : void 0,
1504
1506
  skippable: typeof p.skippable === "boolean" ? p.skippable : void 0
1505
1507
  };
1506
1508
  };
@@ -1518,28 +1520,35 @@ var buildEventsRequest = (target, events) => {
1518
1520
  return null;
1519
1521
  }
1520
1522
  };
1521
- var warnOnSkippedEvents = (res) => {
1522
- if (typeof __DEV__ === "undefined" || !__DEV__) return;
1523
- if (typeof console === "undefined" || !console.warn) return;
1523
+ var readEventsAck = async (res) => {
1524
1524
  try {
1525
1525
  const json = res == null ? void 0 : res.json;
1526
- if (typeof json !== "function") return;
1527
- void Promise.resolve(json.call(res)).then((body) => {
1528
- const skipped = body == null ? void 0 : body.skipped;
1529
- if (typeof skipped !== "number" || skipped <= 0) return;
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
- }).catch(() => {
1534
- });
1526
+ if (typeof json !== "function") return void 0;
1527
+ const body = await Promise.resolve(json.call(res));
1528
+ const skipped = body == null ? void 0 : body.skipped;
1529
+ if (typeof skipped !== "number" || !Number.isFinite(skipped)) return void 0;
1530
+ const reasons = Array.isArray(body == null ? void 0 : body.errors) ? body.errors.map((e) => e == null ? void 0 : e.reason).filter((r) => typeof r === "string") : [];
1531
+ return { written: typeof (body == null ? void 0 : body.written) === "number" ? body.written : void 0, skipped, reasons };
1535
1532
  } catch {
1533
+ return void 0;
1536
1534
  }
1537
1535
  };
1536
+ 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.");
1537
+ var warnOnSkippedEvents = (res) => {
1538
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
1539
+ if (typeof console === "undefined" || !console.warn) return;
1540
+ void readEventsAck(res).then((ack) => {
1541
+ if (!ack || ack.skipped <= 0) return;
1542
+ console.warn(describeDiscarded(ack));
1543
+ });
1544
+ };
1538
1545
  var reportClientEvents = (target, events) => {
1539
1546
  try {
1540
1547
  const req = buildEventsRequest(target, events);
1541
1548
  if (!req) return;
1542
- void fetch(req.url, req.init).catch(() => {
1549
+ void fetch(req.url, req.init).then((res) => {
1550
+ warnOnSkippedEvents(res);
1551
+ }).catch(() => {
1543
1552
  });
1544
1553
  } catch {
1545
1554
  }
@@ -1550,7 +1559,13 @@ var reportClientEventsAwait = async (target, events) => {
1550
1559
  const req = buildEventsRequest(target, events);
1551
1560
  if (!req) return false;
1552
1561
  const res = await fetch(req.url, req.init);
1553
- return Boolean(res && res.ok);
1562
+ if (!res || !res.ok) return false;
1563
+ const ack = await readEventsAck(res);
1564
+ if (!ack || ack.skipped <= 0) return true;
1565
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1566
+ console.warn(describeDiscarded(ack));
1567
+ }
1568
+ return false;
1554
1569
  } catch {
1555
1570
  return false;
1556
1571
  }
@@ -1770,8 +1785,8 @@ var OnboardingFlow = ({
1770
1785
  (messageId) => {
1771
1786
  var _a3;
1772
1787
  const base = makeActions(messageId);
1773
- const key = progress.key;
1774
- const validator = key ? (_a3 = validatorsRef.current) == null ? void 0 : _a3[key] : void 0;
1788
+ const validators2 = validatorsRef.current;
1789
+ const validator = (_a3 = progress.slot_id ? validators2 == null ? void 0 : validators2[progress.slot_id] : void 0) != null ? _a3 : progress.key ? validators2 == null ? void 0 : validators2[progress.key] : void 0;
1775
1790
  if (!validator) return base;
1776
1791
  const out = { ...base };
1777
1792
  for (const name2 of ANSWER_CALLBACKS) {
@@ -1796,7 +1811,7 @@ var OnboardingFlow = ({
1796
1811
  }
1797
1812
  return out;
1798
1813
  },
1799
- [makeActions, progress.key]
1814
+ [makeActions, progress.key, progress.slot_id]
1800
1815
  );
1801
1816
  if (degraded) {
1802
1817
  if (fallbackFlow !== void 0) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallbackFlow });
@@ -2107,7 +2122,7 @@ var _TextInputCard = ({
2107
2122
  lines = 6
2108
2123
  }) => {
2109
2124
  const t = useOnboardingTheme();
2110
- const styles18 = React18.useMemo(() => makeStyles3(t), [t]);
2125
+ const styles17 = React18.useMemo(() => makeStyles3(t), [t]);
2111
2126
  const { height: screenHeight } = reactNative.useWindowDimensions();
2112
2127
  const [value, setValue] = React18.useState("");
2113
2128
  const [submitted, setSubmitted] = React18.useState(false);
@@ -2134,7 +2149,7 @@ var _TextInputCard = ({
2134
2149
  disabled: !value.trim() || locked
2135
2150
  }
2136
2151
  ),
2137
- children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles18.row, children: [
2152
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.row, children: [
2138
2153
  /* @__PURE__ */ jsxRuntime.jsx(
2139
2154
  reactNative.TextInput,
2140
2155
  {
@@ -2148,7 +2163,7 @@ var _TextInputCard = ({
2148
2163
  textAlignVertical: multiline ? "top" : "center",
2149
2164
  style: [
2150
2165
  bodyStyle(t.fonts),
2151
- styles18.input,
2166
+ styles17.input,
2152
2167
  {
2153
2168
  color: t.colors.text,
2154
2169
  backgroundColor: t.colors.surface,
@@ -2874,7 +2889,7 @@ var _NumberStepperCard = ({
2874
2889
  onSubmit
2875
2890
  }) => {
2876
2891
  const t = useOnboardingTheme();
2877
- const styles18 = React18.useMemo(() => makeStyles4(t), [t]);
2892
+ const styles17 = React18.useMemo(() => makeStyles4(t), [t]);
2878
2893
  const safeMin = safeNum(min, 1);
2879
2894
  const safeMaxRaw = safeNum(max, 30);
2880
2895
  const safeMax = safeMaxRaw < safeMin ? safeMin : safeMaxRaw;
@@ -2898,7 +2913,7 @@ var _NumberStepperCard = ({
2898
2913
  const atMin = value <= safeMin;
2899
2914
  const atMax = value >= safeMax;
2900
2915
  const stepBtnStyle = (disabled) => [
2901
- styles18.stepBtn,
2916
+ styles17.stepBtn,
2902
2917
  { borderRadius: t.radius.full, backgroundColor: t.colors.background, borderColor: disabled ? t.colors.border : t.colors.primary }
2903
2918
  ];
2904
2919
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -2907,10 +2922,10 @@ var _NumberStepperCard = ({
2907
2922
  title: label,
2908
2923
  align: "center",
2909
2924
  footer: /* @__PURE__ */ jsxRuntime.jsx(Button, { title: submitLabel, onPress: handleSubmit, variant: "primary", disabled: submitted }),
2910
- children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles18.wrap, children: [
2911
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles18.stepper, { gap: t.spacing.lg }], children: [
2925
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.wrap, children: [
2926
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles17.stepper, { gap: t.spacing.lg }], children: [
2912
2927
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: atMin || submitted ? void 0 : handleDecrement, disabled: atMin || submitted, style: stepBtnStyle(atMin), children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [headingStyle(t.fonts), { color: atMin ? t.colors.textMuted : t.colors.primary }], children: "\u2212" }) }),
2913
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles18.valueBox, children: [
2928
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.valueBox, children: [
2914
2929
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [headingStyle(t.fonts), { color: t.colors.primary }], children: String(value) }),
2915
2930
  unit ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: unit }) : null
2916
2931
  ] }),
@@ -3317,6 +3332,33 @@ var collectDeviceContext = () => {
3317
3332
  return ctx;
3318
3333
  };
3319
3334
 
3335
+ // src/identity/identityRecord.ts
3336
+ var IDENTITY_PROVENANCE_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:identityProvenance");
3337
+ var provenanceGlobal = globalThis;
3338
+ var provenanceRegistry = () => {
3339
+ const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];
3340
+ if (existing) return existing;
3341
+ const created = { host: /* @__PURE__ */ new Map() };
3342
+ provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;
3343
+ return created;
3344
+ };
3345
+ var provenanceKey = (space, scope) => `${space}:${scope != null ? scope : "default"}`;
3346
+ var resolveIdentity = (input) => {
3347
+ var _a2;
3348
+ if (typeof input.value !== "string") return void 0;
3349
+ const value = input.value.trim();
3350
+ if (!value) return void 0;
3351
+ const durable = (_a2 = input.durable) != null ? _a2 : input.source === "host";
3352
+ if (input.source === "host") {
3353
+ provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);
3354
+ }
3355
+ return { value, space: input.space, source: input.source, durable };
3356
+ };
3357
+ var hostIdentity = (space, scope) => provenanceRegistry().host.get(provenanceKey(space, scope));
3358
+ var resetIdentityProvenance = () => {
3359
+ provenanceRegistry().host.clear();
3360
+ };
3361
+
3320
3362
  // src/context/deviceId.ts
3321
3363
  var AUTO_DEVICE_ID_PREFIX = "wdev_";
3322
3364
  var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
@@ -3339,24 +3381,37 @@ var startHydration = (registry, appId, storage, minted) => {
3339
3381
  const existing = registry.pending.get(appId);
3340
3382
  if (existing) return existing;
3341
3383
  const slot = deviceIdStorageKey(appId);
3342
- const settled = () => {
3384
+ const degraded = () => {
3343
3385
  var _a2;
3344
- return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
3386
+ return { value: (_a2 = registry.keys.get(appId)) != null ? _a2 : minted, durable: false };
3345
3387
  };
3388
+ const adopted = (value) => ({ value, durable: true });
3346
3389
  let run;
3347
3390
  try {
3348
3391
  run = Promise.resolve(storage.getItem(slot)).then((saved) => {
3349
3392
  const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
3350
3393
  if (persisted) {
3351
3394
  registry.keys.set(appId, persisted);
3352
- return persisted;
3395
+ return adopted(persisted);
3353
3396
  }
3354
- return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
3355
- }).catch(settled);
3397
+ return Promise.resolve(storage.setItem(slot, minted)).then(
3398
+ () => {
3399
+ var _a2;
3400
+ return adopted((_a2 = registry.keys.get(appId)) != null ? _a2 : minted);
3401
+ },
3402
+ degraded
3403
+ );
3404
+ }).catch(degraded);
3356
3405
  } catch {
3357
- run = Promise.resolve(settled());
3406
+ run = Promise.resolve(degraded());
3358
3407
  }
3359
3408
  registry.pending.set(appId, run);
3409
+ void run.then((outcome) => {
3410
+ var _a2;
3411
+ if (outcome.durable) return;
3412
+ (_a2 = registry.pending) == null ? void 0 : _a2.delete(appId);
3413
+ registry.hydrating.delete(appId);
3414
+ });
3360
3415
  return run;
3361
3416
  };
3362
3417
  var resolveAutoDeviceKey = (opts = {}) => {
@@ -3376,14 +3431,20 @@ var resolveAutoDeviceKey = (opts = {}) => {
3376
3431
  return (_b = registry.keys.get(appId)) != null ? _b : id;
3377
3432
  };
3378
3433
  var hydrateAutoDeviceKey = async (opts = {}) => {
3434
+ var _a2, _b;
3435
+ return (_b = (_a2 = await hydrateDeviceIdentity(opts)) == null ? void 0 : _a2.value) != null ? _b : resolveAutoDeviceKey(opts);
3436
+ };
3437
+ var hydrateDeviceIdentity = async (opts = {}) => {
3379
3438
  var _a2, _b, _c;
3380
3439
  const id = resolveAutoDeviceKey(opts);
3381
- if (!opts.storage) return id;
3382
- const registry = autoDeviceKeyRegistry();
3383
3440
  const appId = (_a2 = opts.appId) != null ? _a2 : "default";
3441
+ const record = (value, durable) => resolveIdentity({ value, space: "device", source: "auto", durable, scope: appId });
3442
+ if (!opts.storage) return record(id, false);
3443
+ const registry = autoDeviceKeyRegistry();
3384
3444
  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;
3445
+ if (!pending) return record((_c = registry.keys.get(appId)) != null ? _c : id, false);
3446
+ const outcome = await pending;
3447
+ return record(outcome.value, outcome.durable);
3387
3448
  };
3388
3449
  var resetAutoDeviceKeys = () => {
3389
3450
  var _a2;
@@ -3393,6 +3454,15 @@ var resetAutoDeviceKeys = () => {
3393
3454
  (_a2 = registry.pending) == null ? void 0 : _a2.clear();
3394
3455
  };
3395
3456
 
3457
+ // src/utils/warnInDev.ts
3458
+ var warnInDev = (message) => {
3459
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
3460
+ console.warn(message);
3461
+ return true;
3462
+ }
3463
+ return false;
3464
+ };
3465
+
3396
3466
  // src/analytics/currentSession.ts
3397
3467
  var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
3398
3468
  "@wireai/activation:currentSessionId"
@@ -3407,13 +3477,6 @@ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
3407
3477
  var resetCurrentSessionId = () => {
3408
3478
  globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
3409
3479
  };
3410
- var warnInDev = (message) => {
3411
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
3412
- console.warn(message);
3413
- return true;
3414
- }
3415
- return false;
3416
- };
3417
3480
  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.";
3418
3481
  var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
3419
3482
  "@wireai/activation:currentSessionIdMintWarned"
@@ -3510,27 +3573,20 @@ var identifyOnboarding = async (opts) => {
3510
3573
  contextId = stored == null ? void 0 : stored.id;
3511
3574
  }
3512
3575
  }
3513
- if (!contextId) contextId = getCurrentSessionId();
3576
+ let space = "onboarding";
3577
+ if (!contextId && opts.allowAppSessionFallback) {
3578
+ contextId = getCurrentSessionId();
3579
+ space = "app_session";
3580
+ }
3514
3581
  if (!contextId) return false;
3515
3582
  reportClientEvent(
3516
3583
  { serverUrl: opts.config.serverUrl, apiKey: opts.config.apiKey },
3517
3584
  { event_type: "identify", session_id: contextId, user_id: userId }
3518
3585
  );
3519
- return true;
3586
+ return space;
3520
3587
  };
3521
3588
 
3522
3589
  // 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
3590
  var EXTRA_KEY_PREFIX = "custom.";
3535
3591
  var isWireScalar = (value) => {
3536
3592
  const t = typeof value;
@@ -3611,11 +3667,6 @@ var resolveUserContext = (ctx = {}, opts = {}) => {
3611
3667
  if (Object.keys(bucket).length > 0) result.userContext = bucket;
3612
3668
  return result;
3613
3669
  };
3614
- var warnInDev2 = (message) => {
3615
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
3616
- console.warn(message);
3617
- }
3618
- };
3619
3670
  var AUTO_JOIN_HYDRATION_TIMEOUT_MS = 1500;
3620
3671
  var WireOnboarding = ({
3621
3672
  config,
@@ -3642,7 +3693,7 @@ var WireOnboarding = ({
3642
3693
  userId,
3643
3694
  autoJoinKey = true
3644
3695
  }) => {
3645
- var _a2, _b, _c;
3696
+ var _a2, _b, _c, _d, _e;
3646
3697
  const boundUserId = React18.useMemo(() => sanitizeUserId(userId), [userId]);
3647
3698
  const device = React18.useMemo(() => {
3648
3699
  const collected = collectDeviceContext();
@@ -3653,44 +3704,72 @@ var WireOnboarding = ({
3653
3704
  () => userContextKey ? JSON.parse(userContextKey) : void 0,
3654
3705
  [userContextKey]
3655
3706
  );
3656
- const missingJoinKey = typeof (userContextStable == null ? void 0 : userContextStable.device_key) !== "string" || !userContextStable.device_key.trim();
3707
+ const hostJoinIdentity = React18.useMemo(
3708
+ () => resolveIdentity({
3709
+ value: userContextStable == null ? void 0 : userContextStable.device_key,
3710
+ space: "device",
3711
+ source: "host",
3712
+ scope: config.appId
3713
+ }),
3714
+ [userContextStable, config.appId]
3715
+ );
3716
+ const missingJoinKey = !hostJoinIdentity;
3657
3717
  const autoJoinPossible = Boolean(storage) && autoJoinKey !== false;
3658
3718
  const wantsAutoJoin = missingJoinKey && autoJoinPossible;
3659
- const [autoJoinValue, setAutoJoinValue] = React18.useState(void 0);
3719
+ const [autoJoinOutcome, setAutoJoinOutcome] = React18.useState(void 0);
3660
3720
  React18.useEffect(() => {
3661
- if (!wantsAutoJoin || autoJoinValue !== void 0 || !storage) return;
3721
+ if (!wantsAutoJoin || autoJoinOutcome !== void 0 || !storage) return;
3662
3722
  let cancelled = false;
3663
3723
  const timer = setTimeout(() => {
3664
- if (!cancelled) setAutoJoinValue(null);
3724
+ if (!cancelled) setAutoJoinOutcome({ key: null, reason: "timeout" });
3665
3725
  }, AUTO_JOIN_HYDRATION_TIMEOUT_MS);
3666
- void hydrateAutoDeviceKey({ appId: config.appId, storage }).then((key) => {
3726
+ void hydrateDeviceIdentity({ appId: config.appId, storage }).then((identity) => {
3667
3727
  if (cancelled) return;
3668
3728
  clearTimeout(timer);
3669
- setAutoJoinValue(key || null);
3729
+ setAutoJoinOutcome(
3730
+ (identity == null ? void 0 : identity.durable) ? { key: identity.value } : { key: null, reason: "not-durable" }
3731
+ );
3670
3732
  });
3671
3733
  return () => {
3672
3734
  cancelled = true;
3673
3735
  clearTimeout(timer);
3674
3736
  };
3675
- }, [wantsAutoJoin, autoJoinValue, config.appId, storage]);
3676
- const injectedJoinKey = wantsAutoJoin && typeof autoJoinValue === "string" ? autoJoinValue : void 0;
3677
- const autoJoinPending = wantsAutoJoin && autoJoinValue === void 0;
3737
+ }, [wantsAutoJoin, autoJoinOutcome, config.appId, storage]);
3738
+ const injectedJoinKey = wantsAutoJoin ? (_a2 = autoJoinOutcome == null ? void 0 : autoJoinOutcome.key) != null ? _a2 : void 0 : void 0;
3739
+ const autoJoinPending = wantsAutoJoin && autoJoinOutcome === void 0;
3678
3740
  const effectiveUserContext = React18.useMemo(
3679
3741
  () => injectedJoinKey ? { ...userContextStable, ...activationJoinContext(injectedJoinKey) } : userContextStable,
3680
3742
  [userContextStable, injectedJoinKey]
3681
3743
  );
3744
+ const frozenWireContextRef = React18.useRef(null);
3745
+ if (!autoJoinPending && frozenWireContextRef.current === null) {
3746
+ frozenWireContextRef.current = effectiveUserContext ? JSON.stringify(effectiveUserContext) : "";
3747
+ }
3748
+ const startupUserContextKey = (_b = frozenWireContextRef.current) != null ? _b : "";
3749
+ const startupUserContext = React18.useMemo(
3750
+ () => startupUserContextKey ? JSON.parse(startupUserContextKey) : void 0,
3751
+ [startupUserContextKey]
3752
+ );
3682
3753
  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.";
3754
+ 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." : (autoJoinOutcome == null ? void 0 : autoJoinOutcome.key) === null && autoJoinOutcome.reason === "not-durable" ? "your `storage` adapter answered but could not persist the key (the read or the write failed), so the key would be different on every launch, which corrupts min_sessions instead of merely leaving the join empty. Check the adapter you passed as `storage`." : "your `storage` adapter did not answer in time.";
3684
3755
  React18.useEffect(() => {
3685
3756
  if (!warnMissingJoinKey) return;
3686
- warnInDev2(
3757
+ warnInDev(
3687
3758
  "[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
3759
  );
3689
3760
  }, [warnMissingJoinKey, autoJoinReason]);
3761
+ const hostKeyElsewhere = missingJoinKey ? hostIdentity("device", config.appId) : void 0;
3762
+ const warnHostKeyElsewhere = Boolean(hostKeyElsewhere) && Boolean(injectedJoinKey);
3763
+ React18.useEffect(() => {
3764
+ if (!warnHostKeyElsewhere) return;
3765
+ warnInDev(
3766
+ `[wireai] <WireOnboarding> got no user_context.device_key, so the kit injected its own \u2014 but another surface in this app was constructed WITH a host-supplied device key ("${hostKeyElsewhere}"). Your app events carry that id and this onboarding session carries the kit's, so the two land in disjoint id spaces and the \`activated\` funnel still reads zero. Pass the SAME id here: userContext={activationJoinContext(deviceKey)}.`
3767
+ );
3768
+ }, [warnHostKeyElsewhere, hostKeyElsewhere]);
3690
3769
  const misspelledJoinKey = missingJoinKey && typeof (userContextStable == null ? void 0 : userContextStable.deviceKey) === "string" && Boolean(userContextStable.deviceKey.trim());
3691
3770
  React18.useEffect(() => {
3692
3771
  if (!misspelledJoinKey || !injectedJoinKey) return;
3693
- warnInDev2(
3772
+ warnInDev(
3694
3773
  "[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
3774
  );
3696
3775
  }, [misspelledJoinKey, injectedJoinKey]);
@@ -3719,7 +3798,7 @@ var WireOnboarding = ({
3719
3798
  },
3720
3799
  [storage, storageKey, retainSessionOnComplete, onComplete]
3721
3800
  );
3722
- const sessionId = (_a2 = session == null ? void 0 : session.id) != null ? _a2 : "";
3801
+ const sessionId = (_c = session == null ? void 0 : session.id) != null ? _c : "";
3723
3802
  const startupUserIdRef = React18.useRef(boundUserId);
3724
3803
  const metadataKey = config.metadata ? JSON.stringify(config.metadata) : "";
3725
3804
  const metadataStable = React18.useMemo(() => metadataKey ? JSON.parse(metadataKey) : void 0, [metadataKey]);
@@ -3743,7 +3822,9 @@ var WireOnboarding = ({
3743
3822
  // Device snapshot + host-injected user context ride the session-start metadata so the
3744
3823
  // backend can segment the funnel. Old servers ignore these unknown keys (backward compat).
3745
3824
  device,
3746
- ...effectiveUserContext ? { userContext: effectiveUserContext } : {},
3825
+ // The FROZEN wire context, never the live one — see the freeze note above. A change here
3826
+ // recreates the A2A adapter and drops `contextId`.
3827
+ ...startupUserContext ? { userContext: startupUserContext } : {},
3747
3828
  // The session-start user id (from the ref) rides the session-start metadata so the
3748
3829
  // server binds the session to a real user at creation. Reading the ref — not
3749
3830
  // `boundUserId` — keeps this memo off the userId dependency, so a mid-session change
@@ -3752,7 +3833,7 @@ var WireOnboarding = ({
3752
3833
  },
3753
3834
  timeoutMs: 6e4
3754
3835
  };
3755
- }, [config.serverUrl, config.appId, config.apiKey, metadataStable, sessionId, componentsStable, device, effectiveUserContext]);
3836
+ }, [config.serverUrl, config.appId, config.apiKey, metadataStable, sessionId, componentsStable, device, startupUserContext]);
3756
3837
  const reportTarget = React18.useMemo(
3757
3838
  () => ({ serverUrl: config.serverUrl, apiKey: config.apiKey }),
3758
3839
  [config.serverUrl, config.apiKey]
@@ -3778,8 +3859,8 @@ var WireOnboarding = ({
3778
3859
  return /* @__PURE__ */ jsxRuntime.jsx(OnboardingThemeProvider, { theme, children: /* @__PURE__ */ jsxRuntime.jsx(
3779
3860
  LoadingScreen,
3780
3861
  {
3781
- title: (_b = copy == null ? void 0 : copy.restoringTitle) != null ? _b : DEFAULT_COPY.restoringTitle,
3782
- hint: (_c = copy == null ? void 0 : copy.restoringHint) != null ? _c : DEFAULT_COPY.restoringHint
3862
+ title: (_d = copy == null ? void 0 : copy.restoringTitle) != null ? _d : DEFAULT_COPY.restoringTitle,
3863
+ hint: (_e = copy == null ? void 0 : copy.restoringHint) != null ? _e : DEFAULT_COPY.restoringHint
3783
3864
  }
3784
3865
  ) });
3785
3866
  }
@@ -3817,7 +3898,7 @@ function DemoOnboarding({
3817
3898
  const [open, setOpen] = React18.useState(false);
3818
3899
  const [runId, setRunId] = React18.useState(0);
3819
3900
  const t = React18.useMemo(() => mergeTheme(theme != null ? theme : {}), [theme]);
3820
- const styles18 = React18.useMemo(() => makeStyles5(t), [t]);
3901
+ const styles17 = React18.useMemo(() => makeStyles5(t), [t]);
3821
3902
  const openDemo = React18.useCallback(() => {
3822
3903
  setRunId((n) => n + 1);
3823
3904
  setOpen(true);
@@ -3831,7 +3912,7 @@ function DemoOnboarding({
3831
3912
  [onComplete]
3832
3913
  );
3833
3914
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3834
- renderTrigger ? renderTrigger(openDemo) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: openDemo, style: styles18.trigger, accessibilityRole: "button", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles18.triggerText, children: label }) }),
3915
+ renderTrigger ? renderTrigger(openDemo) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: openDemo, style: styles17.trigger, accessibilityRole: "button", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles17.triggerText, children: label }) }),
3835
3916
  /* @__PURE__ */ jsxRuntime.jsx(
3836
3917
  reactNative.Modal,
3837
3918
  {
@@ -3839,7 +3920,7 @@ function DemoOnboarding({
3839
3920
  animationType: "slide",
3840
3921
  presentationStyle: "fullScreen",
3841
3922
  onRequestClose: close,
3842
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles18.modal, children: config ? /* @__PURE__ */ jsxRuntime.jsx(
3923
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles17.modal, children: config ? /* @__PURE__ */ jsxRuntime.jsx(
3843
3924
  WireOnboarding,
3844
3925
  {
3845
3926
  config,
@@ -3852,10 +3933,10 @@ function DemoOnboarding({
3852
3933
  onError: close
3853
3934
  },
3854
3935
  runId
3855
- ) : /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles18.notice, children: [
3856
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles18.noticeTitle, children: "Wire AI not configured" }),
3857
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles18.noticeBody, children: "Set the Wire AI key + server URL (e.g. via wireConfigFromEnv) to demo the AI onboarding here." }),
3858
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: close, style: styles18.trigger, accessibilityRole: "button", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles18.triggerText, children: "Close" }) })
3936
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.notice, children: [
3937
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles17.noticeTitle, children: "Wire AI not configured" }),
3938
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles17.noticeBody, children: "Set the Wire AI key + server URL (e.g. via wireConfigFromEnv) to demo the AI onboarding here." }),
3939
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: close, style: styles17.trigger, accessibilityRole: "button", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles17.triggerText, children: "Close" }) })
3859
3940
  ] }) })
3860
3941
  }
3861
3942
  )
@@ -3929,20 +4010,6 @@ var themeFromBrand = (input) => {
3929
4010
  button: { ...base.button, ...extra.button }
3930
4011
  };
3931
4012
  };
3932
- var _DoneBlock = ({
3933
- title = "You're all set",
3934
- message = "Personalizing your experience\u2026"
3935
- }) => {
3936
- const t = useOnboardingTheme();
3937
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles16.center, { gap: t.spacing.sm, padding: t.spacing.lg }], children: [
3938
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [headingStyle(t.fonts), { color: t.colors.success, textAlign: "center" }], children: title }),
3939
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: message })
3940
- ] });
3941
- };
3942
- var DoneBlock = React18__default.default.memo(_DoneBlock);
3943
- var styles16 = reactNative.StyleSheet.create({
3944
- center: { flex: 1, alignItems: "center", justifyContent: "center" }
3945
- });
3946
4013
 
3947
4014
  // src/motion/centeredModalMotion.ts
3948
4015
  var CENTERED_MODAL_ANIM = {
@@ -4019,25 +4086,25 @@ var _CenteredModal = React18.forwardRef(
4019
4086
  /* @__PURE__ */ jsxRuntime.jsx(
4020
4087
  reactNative.Animated.View,
4021
4088
  {
4022
- style: [styles17.backdrop, { opacity: backdropOpacity }],
4089
+ style: [styles16.backdrop, { opacity: backdropOpacity }],
4023
4090
  pointerEvents: "none"
4024
4091
  }
4025
4092
  ),
4026
4093
  /* @__PURE__ */ jsxRuntime.jsx(
4027
4094
  reactNative.Pressable,
4028
4095
  {
4029
- style: styles17.fill,
4096
+ style: styles16.fill,
4030
4097
  onPress: onBackdrop,
4031
4098
  accessibilityRole: "button",
4032
4099
  accessibilityLabel: "Dismiss",
4033
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNativeSafeAreaContext.SafeAreaView, { style: styles17.center, pointerEvents: "box-none", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => {
4034
- }, style: styles17.cardWrap, children: /* @__PURE__ */ jsxRuntime.jsx(
4100
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNativeSafeAreaContext.SafeAreaView, { style: styles16.center, pointerEvents: "box-none", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => {
4101
+ }, style: styles16.cardWrap, children: /* @__PURE__ */ jsxRuntime.jsx(
4035
4102
  reactNative.Animated.View,
4036
4103
  {
4037
4104
  accessibilityViewIsModal: true,
4038
4105
  accessibilityRole: "alert",
4039
4106
  style: [
4040
- styles17.card,
4107
+ styles16.card,
4041
4108
  {
4042
4109
  backgroundColor: theme.colors.surface,
4043
4110
  borderRadius: theme.radius.lg,
@@ -4059,7 +4126,7 @@ var _CenteredModal = React18.forwardRef(
4059
4126
  );
4060
4127
  _CenteredModal.displayName = "CenteredModal";
4061
4128
  var CenteredModal = React18__default.default.memo(_CenteredModal);
4062
- var styles17 = reactNative.StyleSheet.create({
4129
+ var styles16 = reactNative.StyleSheet.create({
4063
4130
  fill: { flex: 1 },
4064
4131
  backdrop: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#000" },
4065
4132
  center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
@@ -4245,11 +4312,6 @@ var WIRE_ENV_VARS = [
4245
4312
  "EXPO_PUBLIC_WIREAI_SERVER_URL",
4246
4313
  "EXPO_PUBLIC_WIREAI_APP_ID"
4247
4314
  ];
4248
- var warnInDev3 = (message) => {
4249
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
4250
- console.warn(message);
4251
- }
4252
- };
4253
4315
  var wireConfigFromEnv = (overrides) => {
4254
4316
  var _a2, _b, _c, _d, _e;
4255
4317
  const apiKey = (_a2 = overrides == null ? void 0 : overrides.apiKey) != null ? _a2 : process.env.EXPO_PUBLIC_WIREAI_API_KEY;
@@ -4260,7 +4322,7 @@ var wireConfigFromEnv = (overrides) => {
4260
4322
  apiKey ? void 0 : "EXPO_PUBLIC_WIREAI_API_KEY",
4261
4323
  serverUrl ? void 0 : "EXPO_PUBLIC_WIREAI_SERVER_URL"
4262
4324
  ].filter(Boolean);
4263
- warnInDev3(
4325
+ warnInDev(
4264
4326
  `[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
4327
  );
4266
4328
  return null;
@@ -4368,15 +4430,17 @@ var resetActivationRevalidation = () => {
4368
4430
  };
4369
4431
 
4370
4432
  // src/activation/wireActivation.ts
4371
- var clean = (value) => {
4372
- if (typeof value !== "string") return void 0;
4373
- const trimmed = value.trim();
4374
- return trimmed.length > 0 ? trimmed : void 0;
4375
- };
4433
+ var clean = cleanString;
4376
4434
  var createWireActivation = (config) => {
4377
4435
  var _a2, _b;
4378
4436
  const target = { serverUrl: config.serverUrl, apiKey: config.apiKey };
4379
4437
  const explicitDeviceKey = (_b = clean(config.deviceKey)) != null ? _b : clean((_a2 = config.userContext) == null ? void 0 : _a2.deviceKey);
4438
+ resolveIdentity({
4439
+ value: explicitDeviceKey,
4440
+ space: "device",
4441
+ source: "host",
4442
+ scope: config.appId
4443
+ });
4380
4444
  const autoDeviceKeyOptions = {
4381
4445
  appId: config.appId,
4382
4446
  // An explicit key opts out of minting AND persisting (unchanged contract).
@@ -4810,6 +4874,30 @@ var DEFAULTS = {
4810
4874
  maxRetries: 6
4811
4875
  };
4812
4876
  var READ_TIMEOUT_MS4 = 1500;
4877
+ var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
4878
+ var queueKeyGlobal = globalThis;
4879
+ var claimedQueueKeys = () => {
4880
+ const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
4881
+ if (existing) return existing;
4882
+ const created = /* @__PURE__ */ new Set();
4883
+ queueKeyGlobal[QUEUE_KEY_SLOT] = created;
4884
+ return created;
4885
+ };
4886
+ var claimQueueKey = (preferred, explicit) => {
4887
+ const claimed = claimedQueueKeys();
4888
+ if (explicit || !claimed.has(preferred)) {
4889
+ claimed.add(preferred);
4890
+ return preferred;
4891
+ }
4892
+ let ordinal = 2;
4893
+ while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
4894
+ const key = `${preferred}#${ordinal}`;
4895
+ claimed.add(key);
4896
+ warnInDev(
4897
+ `[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.`
4898
+ );
4899
+ return key;
4900
+ };
4813
4901
  var withTimeout4 = (p, ms) => {
4814
4902
  let timer;
4815
4903
  const timeout = new Promise((resolve) => {
@@ -4841,7 +4929,8 @@ var createEventQueue = (options) => {
4841
4929
  var _a2, _b, _c, _d, _e, _f, _g;
4842
4930
  const target = options.target;
4843
4931
  const storage = options.storage;
4844
- const key = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
4932
+ const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
4933
+ const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
4845
4934
  const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
4846
4935
  const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
4847
4936
  const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
@@ -5031,7 +5120,13 @@ var useLifecycleEvents = (config, options = {}) => {
5031
5120
  return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a2 = cfg.apiKey) != null ? _a2 : "" } : void 0;
5032
5121
  };
5033
5122
  const resolveDeviceKey = (cfg, opts) => {
5034
- const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5123
+ var _a2;
5124
+ const host = (_a2 = resolveIdentity({
5125
+ value: opts.deviceKey,
5126
+ space: "device",
5127
+ source: "host",
5128
+ scope: cfg == null ? void 0 : cfg.appId
5129
+ })) == null ? void 0 : _a2.value;
5035
5130
  if (host) return host;
5036
5131
  if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
5037
5132
  return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
@@ -5120,7 +5215,6 @@ exports.CompletionView = CompletionView;
5120
5215
  exports.DEFAULT_FEATURES_TTL_MS = DEFAULT_FEATURES_TTL_MS;
5121
5216
  exports.DEFAULT_SESSION_TTL_MS = DEFAULT_SESSION_TTL_MS;
5122
5217
  exports.DemoOnboarding = DemoOnboarding;
5123
- exports.DoneBlock = DoneBlock;
5124
5218
  exports.EXTRA_KEY_PREFIX = EXTRA_KEY_PREFIX;
5125
5219
  exports.ErrorBlock = ErrorBlock;
5126
5220
  exports.FIRST_OPEN_EVENT = FIRST_OPEN_EVENT;
@@ -5135,7 +5229,6 @@ exports.OnboardingFlow = OnboardingFlow;
5135
5229
  exports.OnboardingScaffold = OnboardingScaffold;
5136
5230
  exports.OnboardingThemeProvider = OnboardingThemeProvider;
5137
5231
  exports.PLAN_TIER_CONTEXT_KEY = PLAN_TIER_CONTEXT_KEY;
5138
- exports.RESERVED_USER_CONTEXT_KEYS = RESERVED_USER_CONTEXT_KEYS;
5139
5232
  exports.SESSION_STARTED_EVENT = SESSION_STARTED_EVENT;
5140
5233
  exports.SelectionCard = SelectionCard;
5141
5234
  exports.StatusCard = StatusCard;
@@ -5179,7 +5272,9 @@ exports.firstOpenStorageKey = firstOpenStorageKey;
5179
5272
  exports.getActivationRevalidationVersion = getActivationRevalidationVersion;
5180
5273
  exports.getCurrentSessionId = getCurrentSessionId;
5181
5274
  exports.hashEmailFnv1a = hashEmailFnv1a;
5275
+ exports.hostIdentity = hostIdentity;
5182
5276
  exports.hydrateAutoDeviceKey = hydrateAutoDeviceKey;
5277
+ exports.hydrateDeviceIdentity = hydrateDeviceIdentity;
5183
5278
  exports.identifyOnboarding = identifyOnboarding;
5184
5279
  exports.isFeaturesFresh = isFeaturesFresh;
5185
5280
  exports.isOnboardingEnabled = isOnboardingEnabled;
@@ -5208,8 +5303,10 @@ exports.resetActivationRevalidation = resetActivationRevalidation;
5208
5303
  exports.resetAutoDeviceKeys = resetAutoDeviceKeys;
5209
5304
  exports.resetCurrentSessionId = resetCurrentSessionId;
5210
5305
  exports.resetFirstOpenLatch = resetFirstOpenLatch;
5306
+ exports.resetIdentityProvenance = resetIdentityProvenance;
5211
5307
  exports.resetSessionStartGuard = resetSessionStartGuard;
5212
5308
  exports.resolveAutoDeviceKey = resolveAutoDeviceKey;
5309
+ exports.resolveIdentity = resolveIdentity;
5213
5310
  exports.resolvePlanTier = resolvePlanTier;
5214
5311
  exports.resolveUserContext = resolveUserContext;
5215
5312
  exports.sanitizeUserId = sanitizeUserId;