@wireai/activation 0.12.2 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +259 -1
  3. package/README.md +87 -3
  4. package/dist/analytics/index.d.mts +2 -2
  5. package/dist/analytics/index.d.ts +2 -2
  6. package/dist/analytics/index.js +174 -36
  7. package/dist/analytics/index.js.map +1 -1
  8. package/dist/analytics/index.mjs +174 -37
  9. package/dist/analytics/index.mjs.map +1 -1
  10. package/dist/{currentSession-BlCeDP0f.d.mts → currentSession-ClkLjcJ0.d.mts} +456 -19
  11. package/dist/{currentSession-BxEB37xt.d.ts → currentSession-DOVZEWJl.d.ts} +456 -19
  12. package/dist/index.d.mts +197 -16
  13. package/dist/index.d.ts +197 -16
  14. package/dist/index.js +1056 -390
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +836 -193
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/questionnaire/index.js.map +1 -1
  19. package/dist/questionnaire/index.mjs.map +1 -1
  20. package/dist/reviews/index.js +20 -7
  21. package/dist/reviews/index.js.map +1 -1
  22. package/dist/reviews/index.mjs +20 -7
  23. package/dist/reviews/index.mjs.map +1 -1
  24. package/package.json +1 -1
  25. package/src/OnboardingFlow.tsx +141 -3
  26. package/src/WireOnboarding.tsx +178 -34
  27. package/src/activation/wireActivation.ts +13 -7
  28. package/src/analytics/analyticsEvent.ts +16 -1
  29. package/src/analytics/analyticsFacade.ts +11 -10
  30. package/src/analytics/currentSession.ts +6 -20
  31. package/src/analytics/eventQueue.ts +71 -1
  32. package/src/analytics/index.ts +1 -1
  33. package/src/analytics/reportClientEvent.ts +157 -38
  34. package/src/cards/PermissionCard.tsx +438 -0
  35. package/src/cards/index.ts +7 -0
  36. package/src/config/wireConfigFromEnv.ts +1 -10
  37. package/src/context/deviceId.ts +77 -16
  38. package/src/context/userContext.ts +4 -15
  39. package/src/identity/identityRecord.ts +123 -0
  40. package/src/identity/userIdentity.ts +45 -9
  41. package/src/illustrations/defaultIllustrations.tsx +44 -3
  42. package/src/index.ts +44 -4
  43. package/src/permissions/index.ts +64 -0
  44. package/src/permissions/permissionCopy.ts +87 -0
  45. package/src/permissions/permissionEvents.ts +76 -0
  46. package/src/permissions/permissionMemory.ts +88 -0
  47. package/src/permissions/placement.ts +88 -0
  48. package/src/permissions/types.ts +131 -0
  49. package/src/session/persistedSession.ts +10 -3
  50. package/src/session-analytics/useLifecycleEvents.ts +10 -1
  51. package/src/types.ts +77 -1
  52. package/src/utils/deriveAnswers.ts +6 -2
  53. package/src/utils/readProgress.ts +4 -0
  54. package/src/utils/warnInDev.ts +33 -0
  55. package/src/components/DoneBlock.tsx +0 -37
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { StyleSheet, Easing, Animated, BackHandler, Modal, Pressable, Text, View, TouchableOpacity, KeyboardAvoidingView, ScrollView, useWindowDimensions, TextInput, Image, AccessibilityInfo, Platform, Dimensions, I18nManager, AppState, NativeModules, TurboModuleRegistry } from 'react-native';
2
- import React18, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext, useSyncExternalStore } from 'react';
2
+ import React19, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext, useSyncExternalStore } from 'react';
3
3
  import { useWireAIThread, useWireAIAction, ComponentRenderer, WireAIProvider } from 'wireai-rn';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
5
  import { SafeAreaView } from 'react-native-safe-area-context';
@@ -484,7 +484,7 @@ var useIllustration = (name2) => {
484
484
  if (!name2) return void 0;
485
485
  return registry[name2];
486
486
  };
487
- var Glyph = ({ children }) => {
487
+ var Disc = ({ children }) => {
488
488
  const t = useOnboardingTheme();
489
489
  return /* @__PURE__ */ jsx(
490
490
  View,
@@ -493,11 +493,23 @@ var Glyph = ({ children }) => {
493
493
  styles.disc,
494
494
  { backgroundColor: t.colors.primarySoft, borderRadius: t.radius.full }
495
495
  ],
496
- children: /* @__PURE__ */ jsx(Text, { style: [styles.glyphText, { color: t.colors.primary }], allowFontScaling: false, children })
496
+ children
497
497
  }
498
498
  );
499
499
  };
500
+ var Glyph = ({ children }) => {
501
+ const t = useOnboardingTheme();
502
+ return /* @__PURE__ */ jsx(Disc, { children: /* @__PURE__ */ jsx(Text, { style: [styles.glyphText, { color: t.colors.primary }], allowFontScaling: false, children }) });
503
+ };
500
504
  var MomentumGlyph = () => /* @__PURE__ */ jsx(Glyph, { children: "\u2197" });
505
+ var NotificationsGlyph = () => {
506
+ const t = useOnboardingTheme();
507
+ return /* @__PURE__ */ jsx(Disc, { children: /* @__PURE__ */ jsxs(View, { style: styles.bell, children: [
508
+ /* @__PURE__ */ jsx(View, { style: [styles.bellBody, { backgroundColor: t.colors.primary }] }),
509
+ /* @__PURE__ */ jsx(View, { style: [styles.bellRim, { backgroundColor: t.colors.primary }] }),
510
+ /* @__PURE__ */ jsx(View, { style: [styles.bellClapper, { backgroundColor: t.colors.primary }] })
511
+ ] }) });
512
+ };
501
513
  var BeforeAfterGlyph = () => {
502
514
  const t = useOnboardingTheme();
503
515
  return /* @__PURE__ */ jsxs(View, { style: styles.row, children: [
@@ -524,7 +536,8 @@ var BeforeAfterGlyph = () => {
524
536
  };
525
537
  var defaultIllustrations = {
526
538
  momentum: /* @__PURE__ */ jsx(MomentumGlyph, {}),
527
- "before-after": /* @__PURE__ */ jsx(BeforeAfterGlyph, {})
539
+ "before-after": /* @__PURE__ */ jsx(BeforeAfterGlyph, {}),
540
+ notifications: /* @__PURE__ */ jsx(NotificationsGlyph, {})
528
541
  };
529
542
  var styles = StyleSheet.create({
530
543
  disc: {
@@ -552,7 +565,12 @@ var styles = StyleSheet.create({
552
565
  arrow: {
553
566
  fontSize: 28,
554
567
  fontWeight: "700"
555
- }
568
+ },
569
+ // The bell: a domed body over a wider rim, with the clapper hanging below it.
570
+ bell: { alignItems: "center", justifyContent: "center" },
571
+ bellBody: { width: 34, height: 30, borderTopLeftRadius: 17, borderTopRightRadius: 17 },
572
+ bellRim: { width: 44, height: 5, borderRadius: 3, marginTop: 2 },
573
+ bellClapper: { width: 9, height: 9, borderRadius: 5, marginTop: 3 }
556
574
  });
557
575
  var EMPTY_REGISTRY2 = {};
558
576
  var IconContext = createContext(EMPTY_REGISTRY2);
@@ -802,7 +820,7 @@ var _AnimatedSparkle = ({ size = 30, color, variant = "spin" }) => {
802
820
  }
803
821
  ) });
804
822
  };
805
- var AnimatedSparkle = React18.memo(_AnimatedSparkle);
823
+ var AnimatedSparkle = React19.memo(_AnimatedSparkle);
806
824
  var styles2 = StyleSheet.create({
807
825
  box: { alignItems: "center", justifyContent: "center" },
808
826
  glyph: {
@@ -905,7 +923,7 @@ var _LoadingBlock = ({
905
923
  )
906
924
  ] });
907
925
  };
908
- var LoadingBlock = React18.memo(_LoadingBlock);
926
+ var LoadingBlock = React19.memo(_LoadingBlock);
909
927
  var styles3 = StyleSheet.create({
910
928
  center: { flex: 1, alignItems: "center", justifyContent: "center" },
911
929
  // Absolutely-positioned label column, pinned just under the centered stage: its top
@@ -950,7 +968,7 @@ var _LoadingScreen = ({ title, hint }) => {
950
968
  }
951
969
  );
952
970
  };
953
- var LoadingScreen = React18.memo(_LoadingScreen);
971
+ var LoadingScreen = React19.memo(_LoadingScreen);
954
972
  var styles4 = StyleSheet.create({
955
973
  flex: { flex: 1 }
956
974
  });
@@ -990,7 +1008,7 @@ var _StepProgress = ({ step, complete, approxScreens }) => {
990
1008
  }
991
1009
  ) });
992
1010
  };
993
- var StepProgress = React18.memo(_StepProgress);
1011
+ var StepProgress = React19.memo(_StepProgress);
994
1012
  var styles5 = StyleSheet.create({
995
1013
  track: { height: 6, width: "100%", overflow: "hidden" },
996
1014
  fill: { height: 6 }
@@ -1030,7 +1048,7 @@ var _OnboardingScaffold = ({
1030
1048
  }
1031
1049
  );
1032
1050
  };
1033
- var OnboardingScaffold = React18.memo(_OnboardingScaffold);
1051
+ var OnboardingScaffold = React19.memo(_OnboardingScaffold);
1034
1052
  function makeStyles(t) {
1035
1053
  return StyleSheet.create({
1036
1054
  flex: { flex: 1 },
@@ -1131,7 +1149,7 @@ var _Button = ({
1131
1149
  }
1132
1150
  );
1133
1151
  };
1134
- var Button = React18.memo(_Button);
1152
+ var Button = React19.memo(_Button);
1135
1153
  var styles6 = StyleSheet.create({
1136
1154
  base: { alignItems: "center", justifyContent: "center" },
1137
1155
  fill: { width: "100%" }
@@ -1149,7 +1167,7 @@ var _ErrorBlock = ({
1149
1167
  onRetry ? /* @__PURE__ */ jsx(Button, { title: retryLabel, onPress: onRetry, variant: "outline" }) : null
1150
1168
  ] });
1151
1169
  };
1152
- var ErrorBlock = React18.memo(_ErrorBlock);
1170
+ var ErrorBlock = React19.memo(_ErrorBlock);
1153
1171
  var styles7 = StyleSheet.create({
1154
1172
  center: { flex: 1, alignItems: "center", justifyContent: "center" }
1155
1173
  });
@@ -1195,7 +1213,7 @@ var _CardLayout = ({
1195
1213
  footer ? /* @__PURE__ */ jsx(View, { style: styles18.footer, children: footer }) : null
1196
1214
  ] });
1197
1215
  };
1198
- var CardLayout = React18.memo(_CardLayout);
1216
+ var CardLayout = React19.memo(_CardLayout);
1199
1217
  function makeStyles2(t) {
1200
1218
  return StyleSheet.create({
1201
1219
  fill: { flex: 1 },
@@ -1320,7 +1338,7 @@ var _CompletionView = ({
1320
1338
  }
1321
1339
  );
1322
1340
  };
1323
- var CompletionView = React18.memo(_CompletionView);
1341
+ var CompletionView = React19.memo(_CompletionView);
1324
1342
  var styles8 = StyleSheet.create({
1325
1343
  center: { width: "100%", alignItems: "center", justifyContent: "center" },
1326
1344
  badgeStage: {
@@ -1450,7 +1468,7 @@ var transformFor = (variant, t, reduced, layer, travel) => {
1450
1468
  }
1451
1469
  ];
1452
1470
  };
1453
- var CardHandoff = React18.memo(_CardHandoff);
1471
+ var CardHandoff = React19.memo(_CardHandoff);
1454
1472
  var styles9 = StyleSheet.create({
1455
1473
  fill: { flex: 1 }
1456
1474
  });
@@ -1474,7 +1492,7 @@ var deriveAnswers = (messages) => {
1474
1492
  const props = (_a2 = r.props) != null ? _a2 : {};
1475
1493
  const progress = props.progress;
1476
1494
  const questionText = (_c = (_b = props.title) != null ? _b : props.label) != null ? _c : props.question;
1477
- const key = (progress == null ? void 0 : progress.key) || questionText;
1495
+ const key = (progress == null ? void 0 : progress.slot_id) || (progress == null ? void 0 : progress.key) || questionText;
1478
1496
  const reply = messages[i + 1];
1479
1497
  if (key && (reply == null ? void 0 : reply.role) === "user") {
1480
1498
  out[key] = tryParse(stripPrefix(reply.content));
@@ -1495,45 +1513,71 @@ var readProgress = (response) => {
1495
1513
  step: typeof p.step === "number" ? p.step : void 0,
1496
1514
  total: typeof p.total === "number" ? p.total : void 0,
1497
1515
  key: typeof p.key === "string" ? p.key : void 0,
1516
+ // Whitelisted the same way as every other field: an old backend simply omits it.
1517
+ slot_id: typeof p.slot_id === "string" ? p.slot_id : void 0,
1498
1518
  skippable: typeof p.skippable === "boolean" ? p.skippable : void 0
1499
1519
  };
1500
1520
  };
1501
1521
 
1502
1522
  // src/analytics/reportClientEvent.ts
1503
1523
  var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1524
+ var toWireEvents = (events) => events.map((event) => {
1525
+ if (typeof event.ts !== "number") return event;
1526
+ const { ts, ...rest } = event;
1527
+ if (!Number.isFinite(ts)) return rest;
1528
+ try {
1529
+ return { ...rest, ts: new Date(ts).toISOString() };
1530
+ } catch {
1531
+ return rest;
1532
+ }
1533
+ });
1504
1534
  var buildEventsRequest = (target, events) => {
1505
1535
  if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return null;
1506
1536
  try {
1507
1537
  const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
1508
1538
  const headers = { "Content-Type": "application/json" };
1509
1539
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
1510
- return { url, init: { method: "POST", headers, body: JSON.stringify({ events }) } };
1540
+ const body = JSON.stringify({ events: toWireEvents(events) });
1541
+ return { url, init: { method: "POST", headers, body } };
1511
1542
  } catch {
1512
1543
  return null;
1513
1544
  }
1514
1545
  };
1515
- var warnOnSkippedEvents = (res) => {
1516
- if (typeof __DEV__ === "undefined" || !__DEV__) return;
1517
- if (typeof console === "undefined" || !console.warn) return;
1546
+ var readEventsAck = async (res) => {
1518
1547
  try {
1519
1548
  const json = res == null ? void 0 : res.json;
1520
- if (typeof json !== "function") return;
1521
- void Promise.resolve(json.call(res)).then((body) => {
1522
- const skipped = body == null ? void 0 : body.skipped;
1523
- if (typeof skipped !== "number" || skipped <= 0) return;
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
- }).catch(() => {
1528
- });
1549
+ if (typeof json !== "function") return void 0;
1550
+ const body = await Promise.resolve(json.call(res));
1551
+ const skipped = body == null ? void 0 : body.skipped;
1552
+ if (typeof skipped !== "number" || !Number.isFinite(skipped)) return void 0;
1553
+ const reasons = Array.isArray(body == null ? void 0 : body.errors) ? body.errors.map((e) => {
1554
+ const entry = e;
1555
+ const reason = entry == null ? void 0 : entry.reason;
1556
+ if (typeof reason !== "string") return void 0;
1557
+ const field = entry == null ? void 0 : entry.field;
1558
+ return typeof field === "string" && field.length > 0 ? `${reason} (field: ${field})` : reason;
1559
+ }).filter((r) => typeof r === "string") : [];
1560
+ return { written: typeof (body == null ? void 0 : body.written) === "number" ? body.written : void 0, skipped, reasons };
1529
1561
  } catch {
1562
+ return void 0;
1530
1563
  }
1531
1564
  };
1565
+ 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.");
1566
+ var warnOnSkippedEvents = (res) => {
1567
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
1568
+ if (typeof console === "undefined" || !console.warn) return;
1569
+ void readEventsAck(res).then((ack) => {
1570
+ if (!ack || ack.skipped <= 0) return;
1571
+ console.warn(describeDiscarded(ack));
1572
+ });
1573
+ };
1532
1574
  var reportClientEvents = (target, events) => {
1533
1575
  try {
1534
1576
  const req = buildEventsRequest(target, events);
1535
1577
  if (!req) return;
1536
- void fetch(req.url, req.init).catch(() => {
1578
+ void fetch(req.url, req.init).then((res) => {
1579
+ warnOnSkippedEvents(res);
1580
+ }).catch(() => {
1537
1581
  });
1538
1582
  } catch {
1539
1583
  }
@@ -1544,7 +1588,13 @@ var reportClientEventsAwait = async (target, events) => {
1544
1588
  const req = buildEventsRequest(target, events);
1545
1589
  if (!req) return false;
1546
1590
  const res = await fetch(req.url, req.init);
1547
- return Boolean(res && res.ok);
1591
+ if (!res || !res.ok) return false;
1592
+ const ack = await readEventsAck(res);
1593
+ if (!ack || ack.skipped <= 0) return true;
1594
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1595
+ console.warn(describeDiscarded(ack));
1596
+ }
1597
+ return false;
1548
1598
  } catch {
1549
1599
  return false;
1550
1600
  }
@@ -1576,6 +1626,382 @@ var sendPreview = (target, { sessionId, userMessage }) => {
1576
1626
  } catch {
1577
1627
  }
1578
1628
  };
1629
+
1630
+ // src/permissions/permissionEvents.ts
1631
+ var WIRE_PERMISSION_EVENTS = {
1632
+ /** The priming screen became visible. The denominator for every rate below. */
1633
+ screenShown: "wire_permission_screen_shown",
1634
+ /** The user tapped the primary, so the OS dialog is about to open. The rationale worked. */
1635
+ primerAccepted: "wire_permission_primer_accepted",
1636
+ /** The OS granted it. */
1637
+ granted: "wire_permission_granted",
1638
+ /** The OS refused it (a permanently blocked answer reports here too, with `status: "blocked"`). */
1639
+ denied: "wire_permission_denied",
1640
+ /** The user took the secondary. The one native prompt was NOT spent. */
1641
+ skipped: "wire_permission_skipped",
1642
+ /** A blocked user was redirected to the OS settings page. */
1643
+ settingsOpened: "wire_permission_settings_opened"
1644
+ };
1645
+ var permissionEventName = (stage) => {
1646
+ switch (stage) {
1647
+ case "shown":
1648
+ return WIRE_PERMISSION_EVENTS.screenShown;
1649
+ case "accepted":
1650
+ return WIRE_PERMISSION_EVENTS.primerAccepted;
1651
+ case "granted":
1652
+ return WIRE_PERMISSION_EVENTS.granted;
1653
+ case "denied":
1654
+ return WIRE_PERMISSION_EVENTS.denied;
1655
+ case "skipped":
1656
+ return WIRE_PERMISSION_EVENTS.skipped;
1657
+ case "settings":
1658
+ return WIRE_PERMISSION_EVENTS.settingsOpened;
1659
+ default: {
1660
+ const _exhaustive = stage;
1661
+ return _exhaustive;
1662
+ }
1663
+ }
1664
+ };
1665
+ var permissionEventProps = (permission, status) => status ? { permission, status } : { permission };
1666
+ var normalizePermissionStatus = (value) => value === "granted" || value === "denied" || value === "blocked" ? value : "denied";
1667
+
1668
+ // src/haptics/haptics.ts
1669
+ var play = async (tone) => {
1670
+ try {
1671
+ const mod = await import('expo-haptics');
1672
+ if (!mod) return false;
1673
+ if (tone === "selection") ;
1674
+ if (tone === "light") ;
1675
+ await mod.notificationAsync(mod.NotificationFeedbackType.Success);
1676
+ return true;
1677
+ } catch {
1678
+ return false;
1679
+ }
1680
+ };
1681
+ var playHaptic = (tone) => {
1682
+ void play(tone);
1683
+ };
1684
+
1685
+ // src/utils/warnInDev.ts
1686
+ var warnInDev = (message) => {
1687
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1688
+ console.warn(message);
1689
+ return true;
1690
+ }
1691
+ return false;
1692
+ };
1693
+ var easeWire4 = Easing.bezier(...WIRE_BEZIER);
1694
+ var RISE_PX = 10;
1695
+ var PERMISSION_CARD_NAME = "PermissionCard";
1696
+ var REQUEST_WATCHDOG_MS = 9e4;
1697
+ var schema = z.object({
1698
+ permission: z.string().describe("Which OS permission this screen primes, e.g. 'notifications'"),
1699
+ title: z.string().describe("Headline for the ask"),
1700
+ message: z.string().describe("Why the app needs it, in the user's terms"),
1701
+ primaryLabel: z.string().describe("Primary button, the only control that can open the OS dialog"),
1702
+ secondaryLabel: z.string().describe("Secondary button, advances without spending the OS prompt"),
1703
+ blockedTitle: z.string().optional().describe("Headline once the permission is permanently refused"),
1704
+ blockedMessage: z.string().optional().describe("Rationale for the settings route"),
1705
+ settingsLabel: z.string().optional().describe("Primary button label on the blocked route"),
1706
+ continueLabel: z.string().optional().describe("Primary button label when there is nothing to ask"),
1707
+ illustration: z.string().optional().describe("Name of an app-provided illustration (defaults to the permission name)")
1708
+ });
1709
+ var _PermissionCard = ({
1710
+ permission,
1711
+ title,
1712
+ message,
1713
+ primaryLabel,
1714
+ secondaryLabel,
1715
+ blockedTitle,
1716
+ blockedMessage,
1717
+ settingsLabel,
1718
+ continueLabel,
1719
+ illustration,
1720
+ request,
1721
+ getStatus,
1722
+ openSettings,
1723
+ onStage,
1724
+ onSettled
1725
+ }) => {
1726
+ const t = useOnboardingTheme();
1727
+ const reduced = useReducedMotion();
1728
+ const art = useIllustration(illustration != null ? illustration : permission);
1729
+ const [status, setStatus] = useState(void 0);
1730
+ const [busy, setBusy] = useState(false);
1731
+ const settledRef = useRef(false);
1732
+ const acceptedRef = useRef(false);
1733
+ const watchdogRef = useRef(null);
1734
+ const clearWatchdog = useCallback(() => {
1735
+ if (watchdogRef.current) {
1736
+ clearTimeout(watchdogRef.current);
1737
+ watchdogRef.current = null;
1738
+ }
1739
+ }, []);
1740
+ useEffect(() => clearWatchdog, [clearWatchdog]);
1741
+ const onStageRef = useRef(onStage);
1742
+ onStageRef.current = onStage;
1743
+ const onSettledRef = useRef(onSettled);
1744
+ onSettledRef.current = onSettled;
1745
+ const getStatusRef = useRef(getStatus);
1746
+ getStatusRef.current = getStatus;
1747
+ const shownRef = useRef(false);
1748
+ useEffect(() => {
1749
+ var _a2;
1750
+ if (shownRef.current) return;
1751
+ shownRef.current = true;
1752
+ (_a2 = onStageRef.current) == null ? void 0 : _a2.call(onStageRef, "shown");
1753
+ }, []);
1754
+ useEffect(() => {
1755
+ const probe = getStatusRef.current;
1756
+ if (!probe) return;
1757
+ let cancelled = false;
1758
+ try {
1759
+ void Promise.resolve(probe()).then((value) => {
1760
+ if (!cancelled) setStatus(normalizePermissionStatus(value));
1761
+ }).catch(() => {
1762
+ });
1763
+ } catch {
1764
+ }
1765
+ return () => {
1766
+ cancelled = true;
1767
+ };
1768
+ }, []);
1769
+ const requestRef = useRef(request);
1770
+ requestRef.current = request;
1771
+ useEffect(() => {
1772
+ if (requestRef.current) return;
1773
+ warnInDev(
1774
+ `[wireai] <WireOnboarding> got a permission screen for "${permission}" with no \`request\` function, so it cannot open the OS dialog and renders as a plain continue. Pass request: () => Promise<'granted' | 'denied' | 'blocked'> (5 lines around expo-notifications, see the README).`
1775
+ );
1776
+ }, [permission]);
1777
+ const settle = useCallback((outcome) => {
1778
+ var _a2;
1779
+ if (settledRef.current) return;
1780
+ settledRef.current = true;
1781
+ clearWatchdog();
1782
+ (_a2 = onSettledRef.current) == null ? void 0 : _a2.call(onSettledRef, outcome);
1783
+ }, [clearWatchdog]);
1784
+ const primaryAction = status === "blocked" ? "settings" : status === "granted" || !request ? "continue" : "ask";
1785
+ const handlePrimary = useCallback(() => {
1786
+ var _a2, _b, _c, _d;
1787
+ if (settledRef.current || busy) return;
1788
+ if (primaryAction === "settings") {
1789
+ (_a2 = onStageRef.current) == null ? void 0 : _a2.call(onStageRef, "settings", "blocked");
1790
+ try {
1791
+ void Promise.resolve(openSettings == null ? void 0 : openSettings()).catch(() => {
1792
+ });
1793
+ } catch {
1794
+ }
1795
+ settle("blocked");
1796
+ return;
1797
+ }
1798
+ if (primaryAction === "continue") {
1799
+ if (status === "granted") {
1800
+ (_b = onStageRef.current) == null ? void 0 : _b.call(onStageRef, "granted", "granted");
1801
+ settle("granted");
1802
+ } else {
1803
+ (_c = onStageRef.current) == null ? void 0 : _c.call(onStageRef, "skipped");
1804
+ settle("skipped");
1805
+ }
1806
+ return;
1807
+ }
1808
+ if (!acceptedRef.current) {
1809
+ acceptedRef.current = true;
1810
+ (_d = onStageRef.current) == null ? void 0 : _d.call(onStageRef, "accepted");
1811
+ }
1812
+ setBusy(true);
1813
+ const finish = (value) => {
1814
+ var _a3, _b2;
1815
+ if (settledRef.current) return;
1816
+ clearWatchdog();
1817
+ const resolved = normalizePermissionStatus(value);
1818
+ setBusy(false);
1819
+ setStatus(resolved);
1820
+ if (resolved === "granted") {
1821
+ playHaptic("success");
1822
+ (_a3 = onStageRef.current) == null ? void 0 : _a3.call(onStageRef, "granted", "granted");
1823
+ } else {
1824
+ (_b2 = onStageRef.current) == null ? void 0 : _b2.call(onStageRef, "denied", resolved);
1825
+ }
1826
+ settle(resolved);
1827
+ };
1828
+ clearWatchdog();
1829
+ watchdogRef.current = setTimeout(() => {
1830
+ watchdogRef.current = null;
1831
+ if (settledRef.current) return;
1832
+ setBusy(false);
1833
+ warnInDev(
1834
+ `[wireai] the \`request\` for the "${permission}" permission screen has not settled after ${Math.round(REQUEST_WATCHDOG_MS / 1e3)}s, so the kit handed the controls back rather than leaving the user on a dead-end screen. It recorded NO outcome, because a pending request is not a denial. Make sure your request resolves to 'granted' | 'denied' | 'blocked' on every branch, including the one where the user dismisses the OS dialog.`
1835
+ );
1836
+ }, REQUEST_WATCHDOG_MS);
1837
+ try {
1838
+ void Promise.resolve(request == null ? void 0 : request()).then(finish, () => finish("denied"));
1839
+ } catch {
1840
+ finish("denied");
1841
+ }
1842
+ }, [busy, primaryAction, status, request, openSettings, settle, clearWatchdog, permission]);
1843
+ const handleSecondary = useCallback(() => {
1844
+ var _a2;
1845
+ if (settledRef.current || busy) return;
1846
+ (_a2 = onStageRef.current) == null ? void 0 : _a2.call(onStageRef, "skipped");
1847
+ settle("skipped");
1848
+ }, [busy, settle]);
1849
+ const popT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
1850
+ const titleT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
1851
+ const bodyT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
1852
+ useEffect(() => {
1853
+ if (reduced) {
1854
+ popT.setValue(1);
1855
+ titleT.setValue(1);
1856
+ bodyT.setValue(1);
1857
+ return;
1858
+ }
1859
+ const rise = (value, delay) => Animated.timing(value, {
1860
+ toValue: 1,
1861
+ duration: scaledMs(INTERSTITIAL_HEAD_MS),
1862
+ delay,
1863
+ easing: easeWire4,
1864
+ useNativeDriver: true
1865
+ });
1866
+ const anims = [
1867
+ Animated.spring(popT, {
1868
+ toValue: 1,
1869
+ friction: STATUS_POP_SPRING.friction,
1870
+ tension: STATUS_POP_SPRING.tension,
1871
+ useNativeDriver: true
1872
+ }),
1873
+ rise(titleT, scaledMs(INTERSTITIAL_HEAD_STAGGER_MS)),
1874
+ rise(bodyT, scaledMs(INTERSTITIAL_HEAD_STAGGER_MS * 2))
1875
+ ];
1876
+ anims.forEach((a) => a.start());
1877
+ return () => anims.forEach((a) => a.stop());
1878
+ }, [reduced, popT, titleT, bodyT]);
1879
+ const popScale = popT.interpolate({
1880
+ inputRange: [0, 1],
1881
+ outputRange: [STATUS_POP_SCALE_FROM, 1]
1882
+ });
1883
+ const riseStyle = (value) => ({
1884
+ opacity: value,
1885
+ transform: [
1886
+ { translateY: value.interpolate({ inputRange: [0, 1], outputRange: [RISE_PX, 0] }) }
1887
+ ]
1888
+ });
1889
+ const blocked = status === "blocked";
1890
+ const shownTitle = blocked ? blockedTitle != null ? blockedTitle : title : title;
1891
+ const shownMessage = blocked ? blockedMessage != null ? blockedMessage : message : message;
1892
+ const primaryTitle = primaryAction === "settings" ? settingsLabel != null ? settingsLabel : primaryLabel : primaryAction === "continue" ? continueLabel != null ? continueLabel : primaryLabel : primaryLabel;
1893
+ const showSecondary = status !== "granted";
1894
+ return /* @__PURE__ */ jsx(
1895
+ CardLayout,
1896
+ {
1897
+ align: "center",
1898
+ footer: /* @__PURE__ */ jsxs(View, { style: [styles10.footer, { gap: t.spacing.sm }], children: [
1899
+ /* @__PURE__ */ jsx(Button, { title: primaryTitle, onPress: handlePrimary, variant: "primary", disabled: busy }),
1900
+ showSecondary ? /* @__PURE__ */ jsx(
1901
+ Button,
1902
+ {
1903
+ title: secondaryLabel,
1904
+ onPress: handleSecondary,
1905
+ variant: "outline",
1906
+ disabled: busy
1907
+ }
1908
+ ) : null
1909
+ ] }),
1910
+ children: /* @__PURE__ */ jsxs(View, { style: [styles10.center, { gap: t.spacing.md }], children: [
1911
+ art ? /* @__PURE__ */ jsx(
1912
+ Animated.View,
1913
+ {
1914
+ style: [styles10.art, { opacity: popT, transform: [{ scale: popScale }] }],
1915
+ children: art
1916
+ }
1917
+ ) : null,
1918
+ /* @__PURE__ */ jsx(Animated.View, { style: riseStyle(titleT), children: /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text, textAlign: "center" }], children: shownTitle }) }),
1919
+ /* @__PURE__ */ jsx(Animated.View, { style: riseStyle(bodyT), children: /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: shownMessage }) })
1920
+ ] })
1921
+ }
1922
+ );
1923
+ };
1924
+ var PermissionCardView = React19.memo(_PermissionCard);
1925
+ var PermissionCard = {
1926
+ name: PERMISSION_CARD_NAME,
1927
+ description: "A priming screen shown BEFORE an OS permission dialog: it explains why the app wants the permission and asks only on the primary tap. Never first, never last. The user is never blocked by it: the secondary advances the flow with the OS prompt unspent.",
1928
+ component: PermissionCardView,
1929
+ propsSchema: schema
1930
+ };
1931
+ var styles10 = StyleSheet.create({
1932
+ center: { width: "100%", alignItems: "center", justifyContent: "center" },
1933
+ art: { alignItems: "center", justifyContent: "center" },
1934
+ footer: { width: "100%", alignItems: "center" }
1935
+ });
1936
+
1937
+ // src/permissions/placement.ts
1938
+ var DEFAULT_PERMISSION_PLACEMENT = "beforeEnd";
1939
+ var normalizeAfterCard = (value) => Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
1940
+ var targetIndex = (placement) => {
1941
+ if (placement === "beforeEnd") return void 0;
1942
+ if (placement === "start") return 0;
1943
+ return normalizeAfterCard(placement.afterCard);
1944
+ };
1945
+ var isPermissionDue = (placement, position) => {
1946
+ if (position.cardIndex < 0) return false;
1947
+ const target = targetIndex(placement);
1948
+ if (target === void 0) return position.isTerminal;
1949
+ return position.cardIndex >= target || position.isTerminal;
1950
+ };
1951
+ var permissionScreenId = (screen, index) => {
1952
+ var _a2;
1953
+ return (_a2 = screen.id) != null ? _a2 : `${screen.permission}:${index}`;
1954
+ };
1955
+ var selectDuePermissionScreen = (screens, settled, position) => {
1956
+ var _a2;
1957
+ if (!screens || screens.length === 0) return void 0;
1958
+ for (let index = 0; index < screens.length; index++) {
1959
+ const screen = screens[index];
1960
+ if (!screen) continue;
1961
+ const id = permissionScreenId(screen, index);
1962
+ if (settled.includes(id)) continue;
1963
+ if (isPermissionDue((_a2 = screen.placement) != null ? _a2 : DEFAULT_PERMISSION_PLACEMENT, position)) {
1964
+ return { screen, id };
1965
+ }
1966
+ }
1967
+ return void 0;
1968
+ };
1969
+
1970
+ // src/permissions/permissionCopy.ts
1971
+ var NOTIFICATIONS_PERMISSION_COPY = {
1972
+ title: "Want a nudge at the right moment?",
1973
+ message: "Turn notifications on and we will remind you when it actually helps. No daily noise, and you can turn them off any time.",
1974
+ primaryLabel: "Enable notifications",
1975
+ secondaryLabel: "Maybe later",
1976
+ blockedTitle: "Notifications are switched off",
1977
+ blockedMessage: "Notifications are turned off for this app, so we cannot ask from here. You can switch them back on in Settings.",
1978
+ settingsLabel: "Open settings",
1979
+ continueLabel: "Continue"
1980
+ };
1981
+ var GENERIC_PERMISSION_COPY = {
1982
+ title: "One quick permission",
1983
+ message: "We need your permission for this part to work. You can change it any time.",
1984
+ primaryLabel: "Allow",
1985
+ secondaryLabel: "Maybe later",
1986
+ blockedTitle: "Permission is switched off",
1987
+ blockedMessage: "This permission is turned off for this app, so we cannot ask from here. You can switch it back on in Settings.",
1988
+ settingsLabel: "Open settings",
1989
+ continueLabel: "Continue"
1990
+ };
1991
+ var DEFAULT_PERMISSION_COPY = {
1992
+ notifications: NOTIFICATIONS_PERMISSION_COPY
1993
+ };
1994
+ var resolvePermissionCopy = (permission, overrides) => {
1995
+ var _a2;
1996
+ const base = (_a2 = DEFAULT_PERMISSION_COPY[permission]) != null ? _a2 : GENERIC_PERMISSION_COPY;
1997
+ if (!overrides) return base;
1998
+ const merged = { ...base };
1999
+ for (const key of Object.keys(overrides)) {
2000
+ const value = overrides[key];
2001
+ if (typeof value === "string" && value.length > 0) merged[key] = value;
2002
+ }
2003
+ return merged;
2004
+ };
1579
2005
  var SKIP_ONE_SENTINEL = "__wireai_skip__";
1580
2006
  var ANSWER_CALLBACKS = ["onSubmit", "onSelect", "onConfirm", "onPress"];
1581
2007
  var DEFAULT_COPY = {
@@ -1604,7 +2030,11 @@ var OnboardingFlow = ({
1604
2030
  reportTarget,
1605
2031
  sessionId,
1606
2032
  resumed = false,
1607
- clientContext
2033
+ clientContext,
2034
+ permissionScreens,
2035
+ settledPermissions,
2036
+ permissionsPending = false,
2037
+ onPermissionSettled
1608
2038
  }) => {
1609
2039
  var _a2, _b, _c, _d, _e, _f;
1610
2040
  const { messages, error, isLoading, sendMessage, reset } = useWireAIThread();
@@ -1635,6 +2065,9 @@ var OnboardingFlow = ({
1635
2065
  const [timedOut, setTimedOut] = useState(false);
1636
2066
  const [validationError, setValidationError] = useState();
1637
2067
  const [validating, setValidating] = useState(false);
2068
+ const [locallySettledPermissions, setLocallySettledPermissions] = useState([]);
2069
+ const onPermissionSettledRef = useRef(onPermissionSettled);
2070
+ onPermissionSettledRef.current = onPermissionSettled;
1638
2071
  const lastCard = useMemo(
1639
2072
  () => [...messages].reverse().find((m) => {
1640
2073
  var _a3;
@@ -1745,6 +2178,36 @@ var OnboardingFlow = ({
1745
2178
  if (isLoading) return;
1746
2179
  sendMessage(SKIP_ONE_SENTINEL);
1747
2180
  }, [isLoading, sendMessage]);
2181
+ const emitPermissionStage = useCallback(
2182
+ (permission, stage, status) => {
2183
+ var _a3, _b2, _c2, _d2;
2184
+ (_a3 = onEventRef.current) == null ? void 0 : _a3.call(onEventRef, { type: "permission", permission, stage, status });
2185
+ reportClientEvent(reportTargetRef.current, {
2186
+ event_type: "app_event",
2187
+ session_id: sessionIdRef.current,
2188
+ question_key: permissionEventName(stage),
2189
+ component: PERMISSION_CARD_NAME,
2190
+ screen_index: lastScreenIndexRef.current >= 0 ? lastScreenIndexRef.current : void 0,
2191
+ meta: JSON.stringify(permissionEventProps(permission, status)),
2192
+ device: (_b2 = clientContextRef.current) == null ? void 0 : _b2.device,
2193
+ user_context: (_c2 = clientContextRef.current) == null ? void 0 : _c2.userContext,
2194
+ user_id: (_d2 = clientContextRef.current) == null ? void 0 : _d2.userId
2195
+ });
2196
+ },
2197
+ []
2198
+ );
2199
+ const settlePermission = useCallback(
2200
+ (id, screen, outcome) => {
2201
+ var _a3, _b2;
2202
+ setLocallySettledPermissions((prev) => prev.includes(id) ? prev : [...prev, id]);
2203
+ (_a3 = onPermissionSettledRef.current) == null ? void 0 : _a3.call(onPermissionSettledRef, id, outcome);
2204
+ try {
2205
+ (_b2 = screen.onResult) == null ? void 0 : _b2.call(screen, screen.permission, outcome);
2206
+ } catch {
2207
+ }
2208
+ },
2209
+ []
2210
+ );
1748
2211
  const previewOnSelect = useCallback((value) => {
1749
2212
  const target = reportTargetRef.current;
1750
2213
  if (!(target == null ? void 0 : target.serverUrl)) return;
@@ -1764,8 +2227,8 @@ var OnboardingFlow = ({
1764
2227
  (messageId) => {
1765
2228
  var _a3;
1766
2229
  const base = makeActions(messageId);
1767
- const key = progress.key;
1768
- const validator = key ? (_a3 = validatorsRef.current) == null ? void 0 : _a3[key] : void 0;
2230
+ const validators2 = validatorsRef.current;
2231
+ 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;
1769
2232
  if (!validator) return base;
1770
2233
  const out = { ...base };
1771
2234
  for (const name2 of ANSWER_CALLBACKS) {
@@ -1790,7 +2253,7 @@ var OnboardingFlow = ({
1790
2253
  }
1791
2254
  return out;
1792
2255
  },
1793
- [makeActions, progress.key]
2256
+ [makeActions, progress.key, progress.slot_id]
1794
2257
  );
1795
2258
  if (degraded) {
1796
2259
  if (fallbackFlow !== void 0) return /* @__PURE__ */ jsx(Fragment, { children: fallbackFlow });
@@ -1815,6 +2278,36 @@ var OnboardingFlow = ({
1815
2278
  const step = (_a2 = progress.step) != null ? _a2 : renderedCount;
1816
2279
  const total = (_b = progress.total) != null ? _b : approxScreens;
1817
2280
  const isTerminal = response.action === "render" && response.component === "StatusCard";
2281
+ const duePermission = permissionsPending ? void 0 : selectDuePermissionScreen(
2282
+ permissionScreens,
2283
+ settledPermissions ? [...settledPermissions, ...locallySettledPermissions] : locallySettledPermissions,
2284
+ { cardIndex: renderedCount - 1, isTerminal }
2285
+ );
2286
+ if (duePermission) {
2287
+ const { screen, id } = duePermission;
2288
+ const copy2 = resolvePermissionCopy(screen.permission, screen.copy);
2289
+ return /* @__PURE__ */ jsx(OnboardingScaffold, { step, approxScreens: total, children: /* @__PURE__ */ jsx(CardHandoff, { transitionKey: `permission:${id}`, variant: "spring", children: /* @__PURE__ */ jsx(
2290
+ PermissionCardView,
2291
+ {
2292
+ permission: screen.permission,
2293
+ title: copy2.title,
2294
+ message: copy2.message,
2295
+ primaryLabel: copy2.primaryLabel,
2296
+ secondaryLabel: copy2.secondaryLabel,
2297
+ blockedTitle: copy2.blockedTitle,
2298
+ blockedMessage: copy2.blockedMessage,
2299
+ settingsLabel: copy2.settingsLabel,
2300
+ continueLabel: copy2.continueLabel,
2301
+ illustration: screen.illustration,
2302
+ request: screen.request,
2303
+ getStatus: screen.getStatus,
2304
+ openSettings: screen.openSettings,
2305
+ onStage: (stage, status) => emitPermissionStage(screen.permission, stage, status),
2306
+ onSettled: (outcome) => settlePermission(id, screen, outcome)
2307
+ },
2308
+ id
2309
+ ) }) });
2310
+ }
1818
2311
  if (isTerminal) {
1819
2312
  const props = (_c = response.props) != null ? _c : {};
1820
2313
  return /* @__PURE__ */ jsx(OnboardingScaffold, { step, complete: true, approxScreens: total, children: /* @__PURE__ */ jsx(
@@ -1873,8 +2366,8 @@ var OnboardingFlow = ({
1873
2366
  }
1874
2367
  );
1875
2368
  };
1876
- var easeWire4 = Easing.bezier(...WIRE_BEZIER);
1877
- var schema = z.object({
2369
+ var easeWire5 = Easing.bezier(...WIRE_BEZIER);
2370
+ var schema2 = z.object({
1878
2371
  title: z.string().describe("Question or section header, e.g. 'Which season?'"),
1879
2372
  chips: z.array(z.string()).min(1).describe("List of option labels shown as chips, e.g. ['Spring','Summer','Autumn','Winter']"),
1880
2373
  multiSelect: z.boolean().optional().describe("Allow multiple chips selected at once, default true"),
@@ -1905,7 +2398,7 @@ var _ChipItem = ({ chip, isSelected, multiSelect, onToggle }) => {
1905
2398
  const anim = Animated.timing(fillT, {
1906
2399
  toValue: isSelected ? 1 : 0,
1907
2400
  duration: scaledMs(stateChangeDuration(reduced, CHIP_FILL_MS)),
1908
- easing: easeWire4,
2401
+ easing: easeWire5,
1909
2402
  useNativeDriver: true
1910
2403
  });
1911
2404
  anim.start();
@@ -1945,7 +2438,7 @@ var _ChipItem = ({ chip, isSelected, multiSelect, onToggle }) => {
1945
2438
  Animated.View,
1946
2439
  {
1947
2440
  style: [
1948
- styles10.chip,
2441
+ styles11.chip,
1949
2442
  {
1950
2443
  borderRadius: t.radius.full,
1951
2444
  paddingHorizontal: t.spacing.md,
@@ -1962,7 +2455,7 @@ var _ChipItem = ({ chip, isSelected, multiSelect, onToggle }) => {
1962
2455
  pointerEvents: "none",
1963
2456
  style: [
1964
2457
  StyleSheet.absoluteFill,
1965
- styles10.fillOverlay,
2458
+ styles11.fillOverlay,
1966
2459
  {
1967
2460
  borderRadius: t.radius.full,
1968
2461
  borderColor: t.colors.primary,
@@ -1976,7 +2469,7 @@ var _ChipItem = ({ chip, isSelected, multiSelect, onToggle }) => {
1976
2469
  Animated.Text,
1977
2470
  {
1978
2471
  style: [
1979
- styles10.check,
2472
+ styles11.check,
1980
2473
  { color: t.colors.onPrimary, opacity: checkT, transform: [{ scale: checkScale }] }
1981
2474
  ],
1982
2475
  children: "\u2713"
@@ -1987,7 +2480,7 @@ var _ChipItem = ({ chip, isSelected, multiSelect, onToggle }) => {
1987
2480
  {
1988
2481
  style: [
1989
2482
  captionStyle(t.fonts),
1990
- styles10.chipLabel,
2483
+ styles11.chipLabel,
1991
2484
  { color: isSelected ? t.colors.onPrimary : t.colors.textMuted }
1992
2485
  ],
1993
2486
  children: chip
@@ -1999,7 +2492,7 @@ var _ChipItem = ({ chip, isSelected, multiSelect, onToggle }) => {
1999
2492
  }
2000
2493
  );
2001
2494
  };
2002
- var ChipItem = React18.memo(_ChipItem);
2495
+ var ChipItem = React19.memo(_ChipItem);
2003
2496
  var _ChipSelectCard = ({
2004
2497
  title,
2005
2498
  chips,
@@ -2049,7 +2542,7 @@ var _ChipSelectCard = ({
2049
2542
  disabled: selected.length === 0 || submitted
2050
2543
  }
2051
2544
  ),
2052
- children: /* @__PURE__ */ jsx(View, { style: [styles10.chipsRow, { gap: t.spacing.sm + t.spacing.xs }], children: chips.map((chip) => /* @__PURE__ */ jsx(
2545
+ children: /* @__PURE__ */ jsx(View, { style: [styles11.chipsRow, { gap: t.spacing.sm + t.spacing.xs }], children: chips.map((chip) => /* @__PURE__ */ jsx(
2053
2546
  ChipItem,
2054
2547
  {
2055
2548
  chip,
@@ -2065,18 +2558,18 @@ var _ChipSelectCard = ({
2065
2558
  var ChipSelectCard = {
2066
2559
  name: "ChipSelectCard",
2067
2560
  description: "Use for compact short labels: seasons, activities, travel styles, moods, categories. Ideal for 4\u201312 single-word or two-word options. Use SelectionCard instead when options need descriptions; use CardGridSelectCard for a smaller set (2\u20136) of visual choices that each deserve an icon.",
2068
- component: React18.memo(_ChipSelectCard),
2069
- propsSchema: schema,
2561
+ component: React19.memo(_ChipSelectCard),
2562
+ propsSchema: schema2,
2070
2563
  defaultProps: { multiSelect: true, submitLabel: "Continue" }
2071
2564
  };
2072
- var styles10 = StyleSheet.create({
2565
+ var styles11 = StyleSheet.create({
2073
2566
  chipsRow: { flexDirection: "row", flexWrap: "wrap" },
2074
2567
  chip: { borderWidth: 1.5, flexDirection: "row", alignItems: "center", gap: 6, overflow: "hidden" },
2075
2568
  fillOverlay: { borderWidth: 1.5 },
2076
2569
  chipLabel: { fontWeight: "600" },
2077
2570
  check: { fontSize: 13, fontWeight: "800", lineHeight: 15 }
2078
2571
  });
2079
- var schema2 = z.object({
2572
+ var schema3 = z.object({
2080
2573
  label: z.string().describe("Input field label / the question being asked"),
2081
2574
  hint: z.string().optional().describe(
2082
2575
  "One short line under the question giving context: what the answer is used for or why you ask. Add it whenever the question alone might be unclear."
@@ -2162,8 +2655,8 @@ var _TextInputCard = ({
2162
2655
  var TextInputCard = {
2163
2656
  name: "TextInputCard",
2164
2657
  description: "Use when the user needs to type a free-text answer: an open-ended goal, notes, feedback, a name, a destination, a search query. Multi-line by default (great for open-ended goals); set multiline:false for a short single-line field like a name. Use NumberStepperCard for numeric ranges with known bounds.",
2165
- component: React18.memo(_TextInputCard),
2166
- propsSchema: schema2,
2658
+ component: React19.memo(_TextInputCard),
2659
+ propsSchema: schema3,
2167
2660
  defaultProps: { submitLabel: "Submit", multiline: true, lines: 6 }
2168
2661
  };
2169
2662
  function makeStyles3(t) {
@@ -2318,7 +2811,7 @@ var _WireIcon = ({ name: name2, size = 20, color, requireModule }) => {
2318
2811
  return /* @__PURE__ */ jsx(
2319
2812
  View,
2320
2813
  {
2321
- style: styles11.decorative,
2814
+ style: styles12.decorative,
2322
2815
  accessible: false,
2323
2816
  importantForAccessibility: "no-hide-descendants",
2324
2817
  children: hostIcon
@@ -2332,7 +2825,7 @@ var _WireIcon = ({ name: name2, size = 20, color, requireModule }) => {
2332
2825
  return /* @__PURE__ */ jsx(
2333
2826
  View,
2334
2827
  {
2335
- style: styles11.decorative,
2828
+ style: styles12.decorative,
2336
2829
  accessible: false,
2337
2830
  importantForAccessibility: "no-hide-descendants",
2338
2831
  children: /* @__PURE__ */ jsx(
@@ -2347,9 +2840,9 @@ var _WireIcon = ({ name: name2, size = 20, color, requireModule }) => {
2347
2840
  }
2348
2841
  );
2349
2842
  };
2350
- var WireIcon = React18.memo(_WireIcon);
2843
+ var WireIcon = React19.memo(_WireIcon);
2351
2844
  WireIcon.displayName = "WireIcon";
2352
- var styles11 = StyleSheet.create({
2845
+ var styles12 = StyleSheet.create({
2353
2846
  // No size/margin of its own: the glyph decides its footprint, so an absent icon and a
2354
2847
  // present one differ by exactly the glyph — never by a wrapper.
2355
2848
  decorative: {}
@@ -2363,8 +2856,8 @@ var optionObjectSchema = z.object({
2363
2856
  var coerceOptions = (arr) => Array.isArray(arr) ? arr.map((item) => typeof item === "string" ? { value: item, label: item } : item) : arr;
2364
2857
  var optionsField = (description) => z.preprocess(coerceOptions, z.array(optionObjectSchema).min(1)).describe(description);
2365
2858
  var normalizeOptions = (options) => options.map((opt) => typeof opt === "string" ? { value: opt, label: opt } : opt);
2366
- var easeWire5 = Easing.bezier(...WIRE_BEZIER);
2367
- var schema3 = z.object({
2859
+ var easeWire6 = Easing.bezier(...WIRE_BEZIER);
2860
+ var schema4 = z.object({
2368
2861
  title: z.string().describe("Question or prompt for the user"),
2369
2862
  options: optionsField("List of choices"),
2370
2863
  submitLabel: z.string().optional().describe("Submit button label, default: Continue"),
@@ -2394,7 +2887,7 @@ var _OptionRow = ({ opt, isSelected, multiSelect, onToggle }) => {
2394
2887
  const anim = Animated.timing(selectT, {
2395
2888
  toValue: isSelected ? 1 : 0,
2396
2889
  duration: scaledMs(stateChangeDuration(reduced, SELECT_FILL_MS)),
2397
- easing: easeWire5,
2890
+ easing: easeWire6,
2398
2891
  useNativeDriver: true
2399
2892
  });
2400
2893
  anim.start();
@@ -2434,7 +2927,7 @@ var _OptionRow = ({ opt, isSelected, multiSelect, onToggle }) => {
2434
2927
  Animated.View,
2435
2928
  {
2436
2929
  style: [
2437
- styles12.option,
2930
+ styles13.option,
2438
2931
  {
2439
2932
  padding: t.spacing.md,
2440
2933
  borderRadius: t.radius.md,
@@ -2451,7 +2944,7 @@ var _OptionRow = ({ opt, isSelected, multiSelect, onToggle }) => {
2451
2944
  pointerEvents: "none",
2452
2945
  style: [
2453
2946
  StyleSheet.absoluteFill,
2454
- styles12.selectedOverlay,
2947
+ styles13.selectedOverlay,
2455
2948
  {
2456
2949
  borderRadius: t.radius.md,
2457
2950
  borderColor: t.colors.primary,
@@ -2465,7 +2958,7 @@ var _OptionRow = ({ opt, isSelected, multiSelect, onToggle }) => {
2465
2958
  View,
2466
2959
  {
2467
2960
  style: [
2468
- styles12.indicator,
2961
+ styles13.indicator,
2469
2962
  { borderRadius: t.radius.full },
2470
2963
  isSelected ? { borderColor: t.colors.primary, backgroundColor: t.colors.primary } : { borderColor: t.colors.borderStrong, backgroundColor: "transparent" }
2471
2964
  ],
@@ -2473,7 +2966,7 @@ var _OptionRow = ({ opt, isSelected, multiSelect, onToggle }) => {
2473
2966
  Animated.Text,
2474
2967
  {
2475
2968
  style: [
2476
- styles12.check,
2969
+ styles13.check,
2477
2970
  { color: t.colors.onPrimary, opacity: checkT, transform: [{ scale: checkScale }] }
2478
2971
  ],
2479
2972
  children: "\u2713"
@@ -2489,7 +2982,7 @@ var _OptionRow = ({ opt, isSelected, multiSelect, onToggle }) => {
2489
2982
  }
2490
2983
  );
2491
2984
  };
2492
- var OptionRow = React18.memo(_OptionRow);
2985
+ var OptionRow = React19.memo(_OptionRow);
2493
2986
  var _SelectionCard = ({
2494
2987
  title,
2495
2988
  options,
@@ -2499,7 +2992,7 @@ var _SelectionCard = ({
2499
2992
  onSelectionChange
2500
2993
  }) => {
2501
2994
  const t = useOnboardingTheme();
2502
- const opts = React18.useMemo(() => normalizeOptions(options), [options]);
2995
+ const opts = React19.useMemo(() => normalizeOptions(options), [options]);
2503
2996
  const [selected, setSelected] = useState([]);
2504
2997
  const [submitted, setSubmitted] = useState(false);
2505
2998
  useEffect(() => {
@@ -2550,18 +3043,18 @@ var _SelectionCard = ({
2550
3043
  var SelectionCard = {
2551
3044
  name: "SelectionCard",
2552
3045
  description: "Use for mutually exclusive (radio) or multi-select (checkbox) choices from 2\u20138 options, as a vertical list of rows. Best when options are wordy or need reading. Options may carry an optional `icon` from the vocabulary to make the list scannable. Use ChipSelectCard instead when options are short single words; use CardGridSelectCard when the options are visual and each has an obvious icon (occasions, channels, categories).",
2553
- component: React18.memo(_SelectionCard),
2554
- propsSchema: schema3,
3046
+ component: React19.memo(_SelectionCard),
3047
+ propsSchema: schema4,
2555
3048
  defaultProps: { submitLabel: "Continue", multiSelect: false }
2556
3049
  };
2557
- var styles12 = StyleSheet.create({
3050
+ var styles13 = StyleSheet.create({
2558
3051
  option: { borderWidth: 1.5, flexDirection: "row", alignItems: "center", overflow: "hidden" },
2559
3052
  selectedOverlay: { borderWidth: 1.5 },
2560
3053
  indicator: { width: 24, height: 24, borderWidth: 2, alignItems: "center", justifyContent: "center" },
2561
3054
  check: { fontSize: 13, fontWeight: "800", lineHeight: 15 }
2562
3055
  });
2563
- var easeWire6 = Easing.bezier(...WIRE_BEZIER);
2564
- var schema4 = z.object({
3056
+ var easeWire7 = Easing.bezier(...WIRE_BEZIER);
3057
+ var schema5 = z.object({
2565
3058
  title: z.string().describe("Question or prompt for the user, e.g. 'What brings you here?'"),
2566
3059
  options: optionsField(
2567
3060
  "The choices, 2-6 of them. Give EVERY option an `icon` from the vocabulary \u2014 the icon is the point of this card, and an option without one renders as a bare label next to icon-bearing neighbours. Keep labels short (1-3 words) so they fit a half-width card."
@@ -2593,7 +3086,7 @@ var _OptionCard = ({ opt, isSelected, multiSelect, onToggle }) => {
2593
3086
  const anim = Animated.timing(selectT, {
2594
3087
  toValue: isSelected ? 1 : 0,
2595
3088
  duration: scaledMs(stateChangeDuration(reduced, SELECT_FILL_MS)),
2596
- easing: easeWire6,
3089
+ easing: easeWire7,
2597
3090
  useNativeDriver: true
2598
3091
  });
2599
3092
  anim.start();
@@ -2608,12 +3101,12 @@ var _OptionCard = ({ opt, isSelected, multiSelect, onToggle }) => {
2608
3101
  accessibilityRole: multiSelect ? "checkbox" : "radio",
2609
3102
  accessibilityState: multiSelect ? { checked: isSelected } : { selected: isSelected },
2610
3103
  accessibilityLabel: opt.label,
2611
- style: styles13.cell,
3104
+ style: styles14.cell,
2612
3105
  children: /* @__PURE__ */ jsxs(
2613
3106
  Animated.View,
2614
3107
  {
2615
3108
  style: [
2616
- styles13.card,
3109
+ styles14.card,
2617
3110
  {
2618
3111
  padding: t.spacing.md,
2619
3112
  borderRadius: t.radius.lg,
@@ -2630,7 +3123,7 @@ var _OptionCard = ({ opt, isSelected, multiSelect, onToggle }) => {
2630
3123
  pointerEvents: "none",
2631
3124
  style: [
2632
3125
  StyleSheet.absoluteFill,
2633
- styles13.selectedOverlay,
3126
+ styles14.selectedOverlay,
2634
3127
  {
2635
3128
  borderRadius: t.radius.lg,
2636
3129
  borderColor: t.colors.primary,
@@ -2648,7 +3141,7 @@ var _OptionCard = ({ opt, isSelected, multiSelect, onToggle }) => {
2648
3141
  }
2649
3142
  );
2650
3143
  };
2651
- var OptionCard = React18.memo(_OptionCard);
3144
+ var OptionCard = React19.memo(_OptionCard);
2652
3145
  var _CardGridSelectCard = ({
2653
3146
  title,
2654
3147
  options,
@@ -2658,7 +3151,7 @@ var _CardGridSelectCard = ({
2658
3151
  onSelectionChange
2659
3152
  }) => {
2660
3153
  const t = useOnboardingTheme();
2661
- const opts = React18.useMemo(() => normalizeOptions(options), [options]);
3154
+ const opts = React19.useMemo(() => normalizeOptions(options), [options]);
2662
3155
  const [selected, setSelected] = useState([]);
2663
3156
  const [submitted, setSubmitted] = useState(false);
2664
3157
  useEffect(() => {
@@ -2695,7 +3188,7 @@ var _CardGridSelectCard = ({
2695
3188
  disabled: selected.length === 0 || submitted
2696
3189
  }
2697
3190
  ),
2698
- children: /* @__PURE__ */ jsx(View, { style: [styles13.grid, { gap: t.spacing.sm }], children: opts.map((opt) => /* @__PURE__ */ jsx(
3191
+ children: /* @__PURE__ */ jsx(View, { style: [styles14.grid, { gap: t.spacing.sm }], children: opts.map((opt) => /* @__PURE__ */ jsx(
2699
3192
  OptionCard,
2700
3193
  {
2701
3194
  opt,
@@ -2711,19 +3204,19 @@ var _CardGridSelectCard = ({
2711
3204
  var CardGridSelectCard = {
2712
3205
  name: "CardGridSelectCard",
2713
3206
  description: "Use for VISUAL, icon-led choices shown as a 2-column grid of tappable cards (icon on top, short label beneath). Ideal for 2\u20136 concrete options that each have an obvious icon \u2014 occasions, channels, goals, categories \u2014 e.g. 'What brings you here?' or 'Where did you hear about us?'. Multi-select by default. Every option should carry an `icon`. Use SelectionCard instead when options need descriptions or have no natural icon; use ChipSelectCard for many short word-like tags.",
2714
- component: React18.memo(_CardGridSelectCard),
2715
- propsSchema: schema4,
3207
+ component: React19.memo(_CardGridSelectCard),
3208
+ propsSchema: schema5,
2716
3209
  defaultProps: { submitLabel: "Continue", multiSelect: true }
2717
3210
  };
2718
- var styles13 = StyleSheet.create({
3211
+ var styles14 = StyleSheet.create({
2719
3212
  grid: { flexDirection: "row", flexWrap: "wrap" },
2720
3213
  // Two per row: ~47% basis leaves room for the gap, flexGrow fills the remainder exactly.
2721
3214
  cell: { flexBasis: "47%", flexGrow: 1 },
2722
3215
  card: { borderWidth: 1.5, alignItems: "flex-start", overflow: "hidden" },
2723
3216
  selectedOverlay: { borderWidth: 1.5 }
2724
3217
  });
2725
- var easeWire7 = Easing.bezier(...WIRE_BEZIER);
2726
- var schema5 = z.object({
3218
+ var easeWire8 = Easing.bezier(...WIRE_BEZIER);
3219
+ var schema6 = z.object({
2727
3220
  status: z.enum(["success", "warning", "error", "info"]).describe("Status type"),
2728
3221
  title: z.string().describe("Status title"),
2729
3222
  message: z.string().optional().describe("Supporting message"),
@@ -2773,7 +3266,7 @@ var _StatusCard = ({ status, title, message, ctaLabel, onContinue, onPress, onSu
2773
3266
  toValue: 1,
2774
3267
  duration: scaledMs(STATUS_COPY_MS),
2775
3268
  delay: scaledMs(STATUS_COPY_STAGGER_MS),
2776
- easing: easeWire7,
3269
+ easing: easeWire8,
2777
3270
  useNativeDriver: true
2778
3271
  })
2779
3272
  ];
@@ -2795,8 +3288,8 @@ var _StatusCard = ({ status, title, message, ctaLabel, onContinue, onPress, onSu
2795
3288
  {
2796
3289
  align: "center",
2797
3290
  footer: ctaLabel ? /* @__PURE__ */ jsx(Button, { title: ctaLabel, onPress: handleContinue, variant: "primary", disabled: submitted }) : void 0,
2798
- children: /* @__PURE__ */ jsxs(View, { style: [styles14.center, { gap: t.spacing.sm }], children: [
2799
- /* @__PURE__ */ jsxs(View, { style: [styles14.titleRow, { gap: t.spacing.sm }], children: [
3291
+ children: /* @__PURE__ */ jsxs(View, { style: [styles15.center, { gap: t.spacing.sm }], children: [
3292
+ /* @__PURE__ */ jsxs(View, { style: [styles15.titleRow, { gap: t.spacing.sm }], children: [
2800
3293
  /* @__PURE__ */ jsx(
2801
3294
  Animated.Text,
2802
3295
  {
@@ -2840,14 +3333,14 @@ var _StatusCard = ({ status, title, message, ctaLabel, onContinue, onPress, onSu
2840
3333
  var StatusCard = {
2841
3334
  name: "StatusCard",
2842
3335
  description: "Use ONLY for real outcomes: booking confirmed, data saved, action completed. NEVER for 'you just viewed X' acknowledgments. Always include ctaLabel so the user can continue after seeing the status.",
2843
- component: React18.memo(_StatusCard),
2844
- propsSchema: schema5
3336
+ component: React19.memo(_StatusCard),
3337
+ propsSchema: schema6
2845
3338
  };
2846
- var styles14 = StyleSheet.create({
3339
+ var styles15 = StyleSheet.create({
2847
3340
  center: { width: "100%", alignItems: "center", justifyContent: "center" },
2848
3341
  titleRow: { flexDirection: "row", alignItems: "center", justifyContent: "center" }
2849
3342
  });
2850
- var schema6 = z.object({
3343
+ var schema7 = z.object({
2851
3344
  label: z.string().describe("What is being counted, e.g. 'Number of days' or 'Travellers'"),
2852
3345
  min: z.number().optional().describe("Minimum value, default 1"),
2853
3346
  max: z.number().optional().describe("Maximum value, default 30"),
@@ -2918,8 +3411,8 @@ var _NumberStepperCard = ({
2918
3411
  var NumberStepperCard = {
2919
3412
  name: "NumberStepperCard",
2920
3413
  description: "Use for any bounded numeric input: days, people, portions, budget tiers, ratings. Best for ranges 1\u201399. Set unit (e.g. 'days', 'people') for clarity.",
2921
- component: React18.memo(_NumberStepperCard),
2922
- propsSchema: schema6,
3414
+ component: React19.memo(_NumberStepperCard),
3415
+ propsSchema: schema7,
2923
3416
  defaultProps: { min: 1, max: 30, defaultValue: 1, step: 1, submitLabel: "Confirm" }
2924
3417
  };
2925
3418
  function makeStyles4(t) {
@@ -2930,9 +3423,9 @@ function makeStyles4(t) {
2930
3423
  valueBox: { alignItems: "center", minWidth: 80 }
2931
3424
  });
2932
3425
  }
2933
- var easeWire8 = Easing.bezier(...WIRE_BEZIER);
2934
- var RISE_PX = 10;
2935
- var schema7 = z.object({
3426
+ var easeWire9 = Easing.bezier(...WIRE_BEZIER);
3427
+ var RISE_PX2 = 10;
3428
+ var schema8 = z.object({
2936
3429
  title: z.string().describe("Short, warm headline for the value/momentum moment"),
2937
3430
  body: z.string().optional().describe("One or two supporting lines that reflect the user's answers back and build momentum"),
2938
3431
  items: z.array(z.string()).max(4).optional().describe(
@@ -2969,7 +3462,7 @@ var _WorkItem = ({
2969
3462
  toValue: 1,
2970
3463
  duration: scaledMs(INTERSTITIAL_ITEM_MS),
2971
3464
  delay: scaledMs(interstitialItemDelay(index)),
2972
- easing: easeWire8,
3465
+ easing: easeWire9,
2973
3466
  useNativeDriver: true
2974
3467
  }),
2975
3468
  Animated.spring(checkT, {
@@ -2983,7 +3476,7 @@ var _WorkItem = ({
2983
3476
  anims.forEach((a) => a.start());
2984
3477
  return () => anims.forEach((a) => a.stop());
2985
3478
  }, [reduced, index, rowT, checkT]);
2986
- const rise = rowT.interpolate({ inputRange: [0, 1], outputRange: [RISE_PX, 0] });
3479
+ const rise = rowT.interpolate({ inputRange: [0, 1], outputRange: [RISE_PX2, 0] });
2987
3480
  const checkScale = checkT.interpolate({
2988
3481
  inputRange: [0, 1],
2989
3482
  outputRange: [INTERSTITIAL_CHECK_SCALE_FROM, 1]
@@ -2991,20 +3484,20 @@ var _WorkItem = ({
2991
3484
  return /* @__PURE__ */ jsxs(
2992
3485
  Animated.View,
2993
3486
  {
2994
- style: [styles15.itemRow, { gap: t.spacing.sm + t.spacing.xs, opacity: rowT, transform: [{ translateY: rise }] }],
3487
+ style: [styles16.itemRow, { gap: t.spacing.sm + t.spacing.xs, opacity: rowT, transform: [{ translateY: rise }] }],
2995
3488
  children: [
2996
3489
  /* @__PURE__ */ jsx(
2997
3490
  View,
2998
3491
  {
2999
3492
  style: [
3000
- styles15.itemBadge,
3493
+ styles16.itemBadge,
3001
3494
  { backgroundColor: t.colors.primarySoft, borderRadius: t.radius.full }
3002
3495
  ],
3003
3496
  children: /* @__PURE__ */ jsx(
3004
3497
  Animated.Text,
3005
3498
  {
3006
3499
  style: [
3007
- styles15.itemCheck,
3500
+ styles16.itemCheck,
3008
3501
  { color: t.colors.primary, opacity: checkT, transform: [{ scale: checkScale }] }
3009
3502
  ],
3010
3503
  children: "\u2713"
@@ -3012,12 +3505,12 @@ var _WorkItem = ({
3012
3505
  )
3013
3506
  }
3014
3507
  ),
3015
- /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), styles15.itemText, { color: t.colors.text }], children: text })
3508
+ /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), styles16.itemText, { color: t.colors.text }], children: text })
3016
3509
  ]
3017
3510
  }
3018
3511
  );
3019
3512
  };
3020
- var WorkItem = React18.memo(_WorkItem);
3513
+ var WorkItem = React19.memo(_WorkItem);
3021
3514
  var _InterstitialCard = ({
3022
3515
  title,
3023
3516
  body,
@@ -3054,7 +3547,7 @@ var _InterstitialCard = ({
3054
3547
  toValue: 1,
3055
3548
  duration: scaledMs(INTERSTITIAL_HEAD_MS),
3056
3549
  delay,
3057
- easing: easeWire8,
3550
+ easing: easeWire9,
3058
3551
  useNativeDriver: true
3059
3552
  });
3060
3553
  const anims = [
@@ -3067,7 +3560,7 @@ var _InterstitialCard = ({
3067
3560
  }, [reduced, artT, titleT, bodyT]);
3068
3561
  const riseStyle = (value) => ({
3069
3562
  opacity: value,
3070
- transform: [{ translateY: value.interpolate({ inputRange: [0, 1], outputRange: [RISE_PX, 0] }) }]
3563
+ transform: [{ translateY: value.interpolate({ inputRange: [0, 1], outputRange: [RISE_PX2, 0] }) }]
3071
3564
  });
3072
3565
  const handleContinue = useCallback(() => {
3073
3566
  if (submitted || !canAdvance) return;
@@ -3090,18 +3583,18 @@ var _InterstitialCard = ({
3090
3583
  disabled: submitted || !canAdvance
3091
3584
  }
3092
3585
  ),
3093
- children: /* @__PURE__ */ jsxs(View, { style: [styles15.center, { gap: t.spacing.md }], children: [
3094
- /* @__PURE__ */ jsx(Animated.View, { style: [styles15.fullWidth, riseStyle(artT)], children: showImage ? /* @__PURE__ */ jsx(
3586
+ children: /* @__PURE__ */ jsxs(View, { style: [styles16.center, { gap: t.spacing.md }], children: [
3587
+ /* @__PURE__ */ jsx(Animated.View, { style: [styles16.fullWidth, riseStyle(artT)], children: showImage ? /* @__PURE__ */ jsx(
3095
3588
  Image,
3096
3589
  {
3097
3590
  source: { uri: imageUrl },
3098
- style: styles15.image,
3591
+ style: styles16.image,
3099
3592
  resizeMode: "contain",
3100
3593
  onError: () => setImgFailed(true)
3101
3594
  }
3102
- ) : art ? /* @__PURE__ */ jsx(View, { style: styles15.art, children: art }) : null }),
3595
+ ) : art ? /* @__PURE__ */ jsx(View, { style: styles16.art, children: art }) : null }),
3103
3596
  /* @__PURE__ */ jsx(Animated.View, { style: riseStyle(titleT), children: /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text, textAlign: "center" }], children: title }) }),
3104
- (items == null ? void 0 : items.length) ? /* @__PURE__ */ jsx(View, { style: [styles15.items, { gap: t.spacing.md, marginTop: t.spacing.sm }], children: items.map((item, i) => /* @__PURE__ */ jsx(WorkItem, { text: item, index: i, reduced }, `${item}-${i}`)) }) : body ? /* @__PURE__ */ jsx(Animated.View, { style: riseStyle(bodyT), children: /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: body }) }) : null
3597
+ (items == null ? void 0 : items.length) ? /* @__PURE__ */ jsx(View, { style: [styles16.items, { gap: t.spacing.md, marginTop: t.spacing.sm }], children: items.map((item, i) => /* @__PURE__ */ jsx(WorkItem, { text: item, index: i, reduced }, `${item}-${i}`)) }) : body ? /* @__PURE__ */ jsx(Animated.View, { style: riseStyle(bodyT), children: /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: body }) }) : null
3105
3598
  ] })
3106
3599
  }
3107
3600
  );
@@ -3109,11 +3602,11 @@ var _InterstitialCard = ({
3109
3602
  var InterstitialCard = {
3110
3603
  name: "InterstitialCard",
3111
3604
  description: "Use ONCE mid-flow (never first, never last) as a momentum/value screen between questions \u2014 an illustration + a warm line that reflects answers back, like Duolingo/Cal-AI. NOT a question; the user just taps Continue. Set `illustration` to an app-registered artwork name. Prefer `items` (2-3 short ticked lines, each reflecting a real answer) when you have the signal. Skip it if you don't have enough signal to make the line specific.",
3112
- component: React18.memo(_InterstitialCard),
3113
- propsSchema: schema7,
3605
+ component: React19.memo(_InterstitialCard),
3606
+ propsSchema: schema8,
3114
3607
  defaultProps: { ctaLabel: "Continue", illustrationFallback: "momentum" }
3115
3608
  };
3116
- var styles15 = StyleSheet.create({
3609
+ var styles16 = StyleSheet.create({
3117
3610
  center: { width: "100%", alignItems: "center", justifyContent: "center" },
3118
3611
  fullWidth: { width: "100%", alignItems: "center" },
3119
3612
  art: { alignItems: "center", justifyContent: "center" },
@@ -3311,6 +3804,33 @@ var collectDeviceContext = () => {
3311
3804
  return ctx;
3312
3805
  };
3313
3806
 
3807
+ // src/identity/identityRecord.ts
3808
+ var IDENTITY_PROVENANCE_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:identityProvenance");
3809
+ var provenanceGlobal = globalThis;
3810
+ var provenanceRegistry = () => {
3811
+ const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];
3812
+ if (existing) return existing;
3813
+ const created = { host: /* @__PURE__ */ new Map() };
3814
+ provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;
3815
+ return created;
3816
+ };
3817
+ var provenanceKey = (space, scope) => `${space}:${scope != null ? scope : "default"}`;
3818
+ var resolveIdentity = (input) => {
3819
+ var _a2;
3820
+ if (typeof input.value !== "string") return void 0;
3821
+ const value = input.value.trim();
3822
+ if (!value) return void 0;
3823
+ const durable = (_a2 = input.durable) != null ? _a2 : input.source === "host";
3824
+ if (input.source === "host") {
3825
+ provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);
3826
+ }
3827
+ return { value, space: input.space, source: input.source, durable };
3828
+ };
3829
+ var hostIdentity = (space, scope) => provenanceRegistry().host.get(provenanceKey(space, scope));
3830
+ var resetIdentityProvenance = () => {
3831
+ provenanceRegistry().host.clear();
3832
+ };
3833
+
3314
3834
  // src/context/deviceId.ts
3315
3835
  var AUTO_DEVICE_ID_PREFIX = "wdev_";
3316
3836
  var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
@@ -3333,24 +3853,37 @@ var startHydration = (registry, appId, storage, minted) => {
3333
3853
  const existing = registry.pending.get(appId);
3334
3854
  if (existing) return existing;
3335
3855
  const slot = deviceIdStorageKey(appId);
3336
- const settled = () => {
3856
+ const degraded = () => {
3337
3857
  var _a2;
3338
- return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
3858
+ return { value: (_a2 = registry.keys.get(appId)) != null ? _a2 : minted, durable: false };
3339
3859
  };
3860
+ const adopted = (value) => ({ value, durable: true });
3340
3861
  let run;
3341
3862
  try {
3342
3863
  run = Promise.resolve(storage.getItem(slot)).then((saved) => {
3343
3864
  const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
3344
3865
  if (persisted) {
3345
3866
  registry.keys.set(appId, persisted);
3346
- return persisted;
3867
+ return adopted(persisted);
3347
3868
  }
3348
- return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
3349
- }).catch(settled);
3869
+ return Promise.resolve(storage.setItem(slot, minted)).then(
3870
+ () => {
3871
+ var _a2;
3872
+ return adopted((_a2 = registry.keys.get(appId)) != null ? _a2 : minted);
3873
+ },
3874
+ degraded
3875
+ );
3876
+ }).catch(degraded);
3350
3877
  } catch {
3351
- run = Promise.resolve(settled());
3878
+ run = Promise.resolve(degraded());
3352
3879
  }
3353
3880
  registry.pending.set(appId, run);
3881
+ void run.then((outcome) => {
3882
+ var _a2;
3883
+ if (outcome.durable) return;
3884
+ (_a2 = registry.pending) == null ? void 0 : _a2.delete(appId);
3885
+ registry.hydrating.delete(appId);
3886
+ });
3354
3887
  return run;
3355
3888
  };
3356
3889
  var resolveAutoDeviceKey = (opts = {}) => {
@@ -3370,14 +3903,20 @@ var resolveAutoDeviceKey = (opts = {}) => {
3370
3903
  return (_b = registry.keys.get(appId)) != null ? _b : id;
3371
3904
  };
3372
3905
  var hydrateAutoDeviceKey = async (opts = {}) => {
3906
+ var _a2, _b;
3907
+ return (_b = (_a2 = await hydrateDeviceIdentity(opts)) == null ? void 0 : _a2.value) != null ? _b : resolveAutoDeviceKey(opts);
3908
+ };
3909
+ var hydrateDeviceIdentity = async (opts = {}) => {
3373
3910
  var _a2, _b, _c;
3374
3911
  const id = resolveAutoDeviceKey(opts);
3375
- if (!opts.storage) return id;
3376
- const registry = autoDeviceKeyRegistry();
3377
3912
  const appId = (_a2 = opts.appId) != null ? _a2 : "default";
3913
+ const record = (value, durable) => resolveIdentity({ value, space: "device", source: "auto", durable, scope: appId });
3914
+ if (!opts.storage) return record(id, false);
3915
+ const registry = autoDeviceKeyRegistry();
3378
3916
  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;
3917
+ if (!pending) return record((_c = registry.keys.get(appId)) != null ? _c : id, false);
3918
+ const outcome = await pending;
3919
+ return record(outcome.value, outcome.durable);
3381
3920
  };
3382
3921
  var resetAutoDeviceKeys = () => {
3383
3922
  var _a2;
@@ -3401,13 +3940,6 @@ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
3401
3940
  var resetCurrentSessionId = () => {
3402
3941
  globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
3403
3942
  };
3404
- var warnInDev = (message) => {
3405
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
3406
- console.warn(message);
3407
- return true;
3408
- }
3409
- return false;
3410
- };
3411
3943
  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.";
3412
3944
  var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
3413
3945
  "@wireai/activation:currentSessionIdMintWarned"
@@ -3504,27 +4036,20 @@ var identifyOnboarding = async (opts) => {
3504
4036
  contextId = stored == null ? void 0 : stored.id;
3505
4037
  }
3506
4038
  }
3507
- if (!contextId) contextId = getCurrentSessionId();
4039
+ let space = "onboarding";
4040
+ if (!contextId && opts.allowAppSessionFallback) {
4041
+ contextId = getCurrentSessionId();
4042
+ space = "app_session";
4043
+ }
3508
4044
  if (!contextId) return false;
3509
4045
  reportClientEvent(
3510
4046
  { serverUrl: opts.config.serverUrl, apiKey: opts.config.apiKey },
3511
4047
  { event_type: "identify", session_id: contextId, user_id: userId }
3512
4048
  );
3513
- return true;
4049
+ return space;
3514
4050
  };
3515
4051
 
3516
4052
  // 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
4053
  var EXTRA_KEY_PREFIX = "custom.";
3529
4054
  var isWireScalar = (value) => {
3530
4055
  const t = typeof value;
@@ -3605,9 +4130,41 @@ var resolveUserContext = (ctx = {}, opts = {}) => {
3605
4130
  if (Object.keys(bucket).length > 0) result.userContext = bucket;
3606
4131
  return result;
3607
4132
  };
3608
- var warnInDev2 = (message) => {
3609
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
3610
- console.warn(message);
4133
+
4134
+ // src/permissions/permissionMemory.ts
4135
+ var permissionStorageKey = (appId) => `wireai:permissions:${appId}`;
4136
+ var readSettledPermissions = (raw, sessionId) => {
4137
+ if (!raw || !sessionId) return [];
4138
+ try {
4139
+ const parsed = JSON.parse(raw);
4140
+ if (typeof parsed !== "object" || parsed === null) return [];
4141
+ const record = parsed;
4142
+ if (record.sessionId !== sessionId || !Array.isArray(record.ids)) return [];
4143
+ return record.ids.filter((id) => typeof id === "string" && id.length > 0);
4144
+ } catch {
4145
+ return [];
4146
+ }
4147
+ };
4148
+ var loadSettledPermissions = async (storage, key, sessionId) => {
4149
+ try {
4150
+ return readSettledPermissions(await withTimeout(storage.getItem(key), READ_TIMEOUT_MS), sessionId);
4151
+ } catch {
4152
+ return [];
4153
+ }
4154
+ };
4155
+ var saveSettledPermissions = (storage, key, sessionId, ids) => {
4156
+ try {
4157
+ const record = { sessionId, ids: [...ids] };
4158
+ void storage.setItem(key, JSON.stringify(record)).catch(() => {
4159
+ });
4160
+ } catch {
4161
+ }
4162
+ };
4163
+ var clearSettledPermissions = (storage, key) => {
4164
+ try {
4165
+ void storage.removeItem(key).catch(() => {
4166
+ });
4167
+ } catch {
3611
4168
  }
3612
4169
  };
3613
4170
  var AUTO_JOIN_HYDRATION_TIMEOUT_MS = 1500;
@@ -3634,9 +4191,10 @@ var WireOnboarding = ({
3634
4191
  retainSessionOnComplete,
3635
4192
  userContext,
3636
4193
  userId,
3637
- autoJoinKey = true
4194
+ autoJoinKey = true,
4195
+ permissionScreens
3638
4196
  }) => {
3639
- var _a2, _b, _c;
4197
+ var _a2, _b, _c, _d, _e, _f;
3640
4198
  const boundUserId = useMemo(() => sanitizeUserId(userId), [userId]);
3641
4199
  const device = useMemo(() => {
3642
4200
  const collected = collectDeviceContext();
@@ -3647,44 +4205,72 @@ var WireOnboarding = ({
3647
4205
  () => userContextKey ? JSON.parse(userContextKey) : void 0,
3648
4206
  [userContextKey]
3649
4207
  );
3650
- const missingJoinKey = typeof (userContextStable == null ? void 0 : userContextStable.device_key) !== "string" || !userContextStable.device_key.trim();
4208
+ const hostJoinIdentity = useMemo(
4209
+ () => resolveIdentity({
4210
+ value: userContextStable == null ? void 0 : userContextStable.device_key,
4211
+ space: "device",
4212
+ source: "host",
4213
+ scope: config.appId
4214
+ }),
4215
+ [userContextStable, config.appId]
4216
+ );
4217
+ const missingJoinKey = !hostJoinIdentity;
3651
4218
  const autoJoinPossible = Boolean(storage) && autoJoinKey !== false;
3652
4219
  const wantsAutoJoin = missingJoinKey && autoJoinPossible;
3653
- const [autoJoinValue, setAutoJoinValue] = useState(void 0);
4220
+ const [autoJoinOutcome, setAutoJoinOutcome] = useState(void 0);
3654
4221
  useEffect(() => {
3655
- if (!wantsAutoJoin || autoJoinValue !== void 0 || !storage) return;
4222
+ if (!wantsAutoJoin || autoJoinOutcome !== void 0 || !storage) return;
3656
4223
  let cancelled = false;
3657
4224
  const timer = setTimeout(() => {
3658
- if (!cancelled) setAutoJoinValue(null);
4225
+ if (!cancelled) setAutoJoinOutcome({ key: null, reason: "timeout" });
3659
4226
  }, AUTO_JOIN_HYDRATION_TIMEOUT_MS);
3660
- void hydrateAutoDeviceKey({ appId: config.appId, storage }).then((key) => {
4227
+ void hydrateDeviceIdentity({ appId: config.appId, storage }).then((identity) => {
3661
4228
  if (cancelled) return;
3662
4229
  clearTimeout(timer);
3663
- setAutoJoinValue(key || null);
4230
+ setAutoJoinOutcome(
4231
+ (identity == null ? void 0 : identity.durable) ? { key: identity.value } : { key: null, reason: "not-durable" }
4232
+ );
3664
4233
  });
3665
4234
  return () => {
3666
4235
  cancelled = true;
3667
4236
  clearTimeout(timer);
3668
4237
  };
3669
- }, [wantsAutoJoin, autoJoinValue, config.appId, storage]);
3670
- const injectedJoinKey = wantsAutoJoin && typeof autoJoinValue === "string" ? autoJoinValue : void 0;
3671
- const autoJoinPending = wantsAutoJoin && autoJoinValue === void 0;
4238
+ }, [wantsAutoJoin, autoJoinOutcome, config.appId, storage]);
4239
+ const injectedJoinKey = wantsAutoJoin ? (_a2 = autoJoinOutcome == null ? void 0 : autoJoinOutcome.key) != null ? _a2 : void 0 : void 0;
4240
+ const autoJoinPending = wantsAutoJoin && autoJoinOutcome === void 0;
3672
4241
  const effectiveUserContext = useMemo(
3673
4242
  () => injectedJoinKey ? { ...userContextStable, ...activationJoinContext(injectedJoinKey) } : userContextStable,
3674
4243
  [userContextStable, injectedJoinKey]
3675
4244
  );
4245
+ const frozenWireContextRef = useRef(null);
4246
+ if (!autoJoinPending && frozenWireContextRef.current === null) {
4247
+ frozenWireContextRef.current = effectiveUserContext ? JSON.stringify(effectiveUserContext) : "";
4248
+ }
4249
+ const startupUserContextKey = (_b = frozenWireContextRef.current) != null ? _b : "";
4250
+ const startupUserContext = useMemo(
4251
+ () => startupUserContextKey ? JSON.parse(startupUserContextKey) : void 0,
4252
+ [startupUserContextKey]
4253
+ );
3676
4254
  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.";
4255
+ 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.";
3678
4256
  useEffect(() => {
3679
4257
  if (!warnMissingJoinKey) return;
3680
- warnInDev2(
4258
+ warnInDev(
3681
4259
  "[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
4260
  );
3683
4261
  }, [warnMissingJoinKey, autoJoinReason]);
4262
+ const hostKeyElsewhere = missingJoinKey ? hostIdentity("device", config.appId) : void 0;
4263
+ const warnHostKeyElsewhere = Boolean(hostKeyElsewhere) && Boolean(injectedJoinKey);
4264
+ useEffect(() => {
4265
+ if (!warnHostKeyElsewhere) return;
4266
+ warnInDev(
4267
+ `[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)}.`
4268
+ );
4269
+ }, [warnHostKeyElsewhere, hostKeyElsewhere]);
3684
4270
  const misspelledJoinKey = missingJoinKey && typeof (userContextStable == null ? void 0 : userContextStable.deviceKey) === "string" && Boolean(userContextStable.deviceKey.trim());
3685
4271
  useEffect(() => {
3686
4272
  if (!misspelledJoinKey || !injectedJoinKey) return;
3687
- warnInDev2(
4273
+ warnInDev(
3688
4274
  "[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
4275
  );
3690
4276
  }, [misspelledJoinKey, injectedJoinKey]);
@@ -3696,6 +4282,7 @@ var WireOnboarding = ({
3696
4282
  () => storage ? null : { id: makeSessionId(), resumed: false }
3697
4283
  );
3698
4284
  const storageKey = persistKey != null ? persistKey : sessionStorageKey(config.appId);
4285
+ const permissionsKey = permissionStorageKey(config.appId);
3699
4286
  useEffect(() => {
3700
4287
  if (!storage || session) return;
3701
4288
  let cancelled = false;
@@ -3708,12 +4295,43 @@ var WireOnboarding = ({
3708
4295
  }, [storage, storageKey, sessionTtlMs, session]);
3709
4296
  const handleComplete = useCallback(
3710
4297
  (result) => {
3711
- if (storage && shouldClearOnComplete(retainSessionOnComplete)) clearPersistedSession(storage, storageKey);
4298
+ if (storage && shouldClearOnComplete(retainSessionOnComplete)) {
4299
+ clearPersistedSession(storage, storageKey);
4300
+ clearSettledPermissions(storage, permissionsKey);
4301
+ }
3712
4302
  onComplete(result);
3713
4303
  },
3714
- [storage, storageKey, retainSessionOnComplete, onComplete]
4304
+ [storage, storageKey, permissionsKey, retainSessionOnComplete, onComplete]
4305
+ );
4306
+ const sessionId = (_c = session == null ? void 0 : session.id) != null ? _c : "";
4307
+ const wantsPermissionMemory = Boolean(storage) && ((_d = permissionScreens == null ? void 0 : permissionScreens.length) != null ? _d : 0) > 0;
4308
+ const [settledPermissions, setSettledPermissions] = useState(void 0);
4309
+ useEffect(() => {
4310
+ if (!wantsPermissionMemory || !storage || !sessionId || settledPermissions !== void 0) return;
4311
+ let cancelled = false;
4312
+ void loadSettledPermissions(storage, permissionsKey, sessionId).then((ids) => {
4313
+ if (!cancelled) setSettledPermissions(ids);
4314
+ });
4315
+ return () => {
4316
+ cancelled = true;
4317
+ };
4318
+ }, [wantsPermissionMemory, storage, permissionsKey, sessionId, settledPermissions]);
4319
+ const settledPermissionsRef = useRef(void 0);
4320
+ settledPermissionsRef.current = settledPermissions;
4321
+ const handlePermissionSettled = useCallback(
4322
+ (id) => {
4323
+ var _a3;
4324
+ const current = (_a3 = settledPermissionsRef.current) != null ? _a3 : [];
4325
+ if (current.includes(id)) return;
4326
+ const next = [...current, id];
4327
+ settledPermissionsRef.current = next;
4328
+ setSettledPermissions(next);
4329
+ if (storage && wantsPermissionMemory && sessionId) {
4330
+ saveSettledPermissions(storage, permissionsKey, sessionId, next);
4331
+ }
4332
+ },
4333
+ [storage, wantsPermissionMemory, permissionsKey, sessionId]
3715
4334
  );
3716
- const sessionId = (_a2 = session == null ? void 0 : session.id) != null ? _a2 : "";
3717
4335
  const startupUserIdRef = useRef(boundUserId);
3718
4336
  const metadataKey = config.metadata ? JSON.stringify(config.metadata) : "";
3719
4337
  const metadataStable = useMemo(() => metadataKey ? JSON.parse(metadataKey) : void 0, [metadataKey]);
@@ -3737,7 +4355,9 @@ var WireOnboarding = ({
3737
4355
  // Device snapshot + host-injected user context ride the session-start metadata so the
3738
4356
  // backend can segment the funnel. Old servers ignore these unknown keys (backward compat).
3739
4357
  device,
3740
- ...effectiveUserContext ? { userContext: effectiveUserContext } : {},
4358
+ // The FROZEN wire context, never the live one — see the freeze note above. A change here
4359
+ // recreates the A2A adapter and drops `contextId`.
4360
+ ...startupUserContext ? { userContext: startupUserContext } : {},
3741
4361
  // The session-start user id (from the ref) rides the session-start metadata so the
3742
4362
  // server binds the session to a real user at creation. Reading the ref — not
3743
4363
  // `boundUserId` — keeps this memo off the userId dependency, so a mid-session change
@@ -3746,7 +4366,7 @@ var WireOnboarding = ({
3746
4366
  },
3747
4367
  timeoutMs: 6e4
3748
4368
  };
3749
- }, [config.serverUrl, config.appId, config.apiKey, metadataStable, sessionId, componentsStable, device, effectiveUserContext]);
4369
+ }, [config.serverUrl, config.appId, config.apiKey, metadataStable, sessionId, componentsStable, device, startupUserContext]);
3750
4370
  const reportTarget = useMemo(
3751
4371
  () => ({ serverUrl: config.serverUrl, apiKey: config.apiKey }),
3752
4372
  [config.serverUrl, config.apiKey]
@@ -3772,8 +4392,8 @@ var WireOnboarding = ({
3772
4392
  return /* @__PURE__ */ jsx(OnboardingThemeProvider, { theme, children: /* @__PURE__ */ jsx(
3773
4393
  LoadingScreen,
3774
4394
  {
3775
- title: (_b = copy == null ? void 0 : copy.restoringTitle) != null ? _b : DEFAULT_COPY.restoringTitle,
3776
- hint: (_c = copy == null ? void 0 : copy.restoringHint) != null ? _c : DEFAULT_COPY.restoringHint
4395
+ title: (_e = copy == null ? void 0 : copy.restoringTitle) != null ? _e : DEFAULT_COPY.restoringTitle,
4396
+ hint: (_f = copy == null ? void 0 : copy.restoringHint) != null ? _f : DEFAULT_COPY.restoringHint
3777
4397
  }
3778
4398
  ) });
3779
4399
  }
@@ -3794,7 +4414,11 @@ var WireOnboarding = ({
3794
4414
  reportTarget,
3795
4415
  sessionId,
3796
4416
  resumed: session.resumed,
3797
- clientContext
4417
+ clientContext,
4418
+ permissionScreens,
4419
+ settledPermissions,
4420
+ permissionsPending: wantsPermissionMemory && settledPermissions === void 0,
4421
+ onPermissionSettled: handlePermissionSettled
3798
4422
  }
3799
4423
  ) }) }) }) });
3800
4424
  };
@@ -3923,20 +4547,6 @@ var themeFromBrand = (input) => {
3923
4547
  button: { ...base.button, ...extra.button }
3924
4548
  };
3925
4549
  };
3926
- var _DoneBlock = ({
3927
- title = "You're all set",
3928
- message = "Personalizing your experience\u2026"
3929
- }) => {
3930
- const t = useOnboardingTheme();
3931
- return /* @__PURE__ */ jsxs(View, { style: [styles16.center, { gap: t.spacing.sm, padding: t.spacing.lg }], children: [
3932
- /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.success, textAlign: "center" }], children: title }),
3933
- /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: message })
3934
- ] });
3935
- };
3936
- var DoneBlock = React18.memo(_DoneBlock);
3937
- var styles16 = StyleSheet.create({
3938
- center: { flex: 1, alignItems: "center", justifyContent: "center" }
3939
- });
3940
4550
 
3941
4551
  // src/motion/centeredModalMotion.ts
3942
4552
  var CENTERED_MODAL_ANIM = {
@@ -4052,7 +4662,7 @@ var _CenteredModal = forwardRef(
4052
4662
  }
4053
4663
  );
4054
4664
  _CenteredModal.displayName = "CenteredModal";
4055
- var CenteredModal = React18.memo(_CenteredModal);
4665
+ var CenteredModal = React19.memo(_CenteredModal);
4056
4666
  var styles17 = StyleSheet.create({
4057
4667
  fill: { flex: 1 },
4058
4668
  backdrop: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#000" },
@@ -4239,11 +4849,6 @@ var WIRE_ENV_VARS = [
4239
4849
  "EXPO_PUBLIC_WIREAI_SERVER_URL",
4240
4850
  "EXPO_PUBLIC_WIREAI_APP_ID"
4241
4851
  ];
4242
- var warnInDev3 = (message) => {
4243
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
4244
- console.warn(message);
4245
- }
4246
- };
4247
4852
  var wireConfigFromEnv = (overrides) => {
4248
4853
  var _a2, _b, _c, _d, _e;
4249
4854
  const apiKey = (_a2 = overrides == null ? void 0 : overrides.apiKey) != null ? _a2 : process.env.EXPO_PUBLIC_WIREAI_API_KEY;
@@ -4254,7 +4859,7 @@ var wireConfigFromEnv = (overrides) => {
4254
4859
  apiKey ? void 0 : "EXPO_PUBLIC_WIREAI_API_KEY",
4255
4860
  serverUrl ? void 0 : "EXPO_PUBLIC_WIREAI_SERVER_URL"
4256
4861
  ].filter(Boolean);
4257
- warnInDev3(
4862
+ warnInDev(
4258
4863
  `[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
4864
  );
4260
4865
  return null;
@@ -4309,6 +4914,11 @@ var toAnalyticsEvent = (event) => {
4309
4914
  };
4310
4915
  case "fallback":
4311
4916
  return { name: WIRE_ONBOARDING_EVENTS.fallback, params: { reason: event.reason } };
4917
+ case "permission":
4918
+ return {
4919
+ name: permissionEventName(event.stage),
4920
+ params: permissionEventProps(event.permission, event.status)
4921
+ };
4312
4922
  default: {
4313
4923
  const _exhaustive = event;
4314
4924
  return _exhaustive;
@@ -4362,15 +4972,17 @@ var resetActivationRevalidation = () => {
4362
4972
  };
4363
4973
 
4364
4974
  // src/activation/wireActivation.ts
4365
- var clean = (value) => {
4366
- if (typeof value !== "string") return void 0;
4367
- const trimmed = value.trim();
4368
- return trimmed.length > 0 ? trimmed : void 0;
4369
- };
4975
+ var clean = cleanString;
4370
4976
  var createWireActivation = (config) => {
4371
4977
  var _a2, _b;
4372
4978
  const target = { serverUrl: config.serverUrl, apiKey: config.apiKey };
4373
4979
  const explicitDeviceKey = (_b = clean(config.deviceKey)) != null ? _b : clean((_a2 = config.userContext) == null ? void 0 : _a2.deviceKey);
4980
+ resolveIdentity({
4981
+ value: explicitDeviceKey,
4982
+ space: "device",
4983
+ source: "host",
4984
+ scope: config.appId
4985
+ });
4374
4986
  const autoDeviceKeyOptions = {
4375
4987
  appId: config.appId,
4376
4988
  // An explicit key opts out of minting AND persisting (unchanged contract).
@@ -4804,6 +5416,30 @@ var DEFAULTS = {
4804
5416
  maxRetries: 6
4805
5417
  };
4806
5418
  var READ_TIMEOUT_MS4 = 1500;
5419
+ var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
5420
+ var queueKeyGlobal = globalThis;
5421
+ var claimedQueueKeys = () => {
5422
+ const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
5423
+ if (existing) return existing;
5424
+ const created = /* @__PURE__ */ new Set();
5425
+ queueKeyGlobal[QUEUE_KEY_SLOT] = created;
5426
+ return created;
5427
+ };
5428
+ var claimQueueKey = (preferred, explicit) => {
5429
+ const claimed = claimedQueueKeys();
5430
+ if (explicit || !claimed.has(preferred)) {
5431
+ claimed.add(preferred);
5432
+ return preferred;
5433
+ }
5434
+ let ordinal = 2;
5435
+ while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
5436
+ const key = `${preferred}#${ordinal}`;
5437
+ claimed.add(key);
5438
+ warnInDev(
5439
+ `[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.`
5440
+ );
5441
+ return key;
5442
+ };
4807
5443
  var withTimeout4 = (p, ms) => {
4808
5444
  let timer;
4809
5445
  const timeout = new Promise((resolve) => {
@@ -4835,7 +5471,8 @@ var createEventQueue = (options) => {
4835
5471
  var _a2, _b, _c, _d, _e, _f, _g;
4836
5472
  const target = options.target;
4837
5473
  const storage = options.storage;
4838
- const key = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
5474
+ const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
5475
+ const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
4839
5476
  const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
4840
5477
  const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
4841
5478
  const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
@@ -5025,7 +5662,13 @@ var useLifecycleEvents = (config, options = {}) => {
5025
5662
  return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a2 = cfg.apiKey) != null ? _a2 : "" } : void 0;
5026
5663
  };
5027
5664
  const resolveDeviceKey = (cfg, opts) => {
5028
- const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5665
+ var _a2;
5666
+ const host = (_a2 = resolveIdentity({
5667
+ value: opts.deviceKey,
5668
+ space: "device",
5669
+ source: "host",
5670
+ scope: cfg == null ? void 0 : cfg.appId
5671
+ })) == null ? void 0 : _a2.value;
5029
5672
  if (host) return host;
5030
5673
  if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
5031
5674
  return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
@@ -5103,6 +5746,6 @@ var useLifecycleEvents = (config, options = {}) => {
5103
5746
  }, []);
5104
5747
  };
5105
5748
 
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 };
5749
+ export { AUTO_DEVICE_ID_PREFIX, AnimatedSparkle, BACKGROUND_SESSION_MS, CardGridSelectCard, CardHandoff, CenteredModal, ChipSelectCard, CompletionView, DEFAULT_FEATURES_TTL_MS, DEFAULT_PERMISSION_COPY, DEFAULT_PERMISSION_PLACEMENT, DEFAULT_SESSION_TTL_MS, DemoOnboarding, EXTRA_KEY_PREFIX, ErrorBlock, FIRST_OPEN_EVENT, GENERIC_PERMISSION_COPY, IconRegistryProvider, IllustrationProvider, InterstitialCard, LoadingBlock, LoadingScreen, NOTIFICATIONS_PERMISSION_COPY, NumberStepperCard, Button as OnboardingButton, OnboardingFlow, OnboardingScaffold, OnboardingThemeProvider, PERMISSION_CARD_NAME, PLAN_TIER_CONTEXT_KEY, PermissionCard, PermissionCardView, SESSION_STARTED_EVENT, SelectionCard, StatusCard, StepProgress, TextInputCard, USER_ID_MAX_LENGTH, WIRE_ENV_VARS, WIRE_ICON_GLYPHS, WIRE_ICON_NAMES, WIRE_ONBOARDING_EVENTS, WIRE_PERMISSION_EVENTS, WIRE_PURCHASE_EVENTS, WireFeaturesProvider, WireIcon, WireOnboarding, activationJoinContext, activeEntitlement, analyticsUserIdStorageKey, attributionMetadata, bumpActivationRevalidation, clearPersistedSession, clearPiiFromContext, clearSettledPermissions, clearUserContext, collectDeviceContext, createRevenueCatBridge, createWireActivation, defaultIllustrations, defaultOnboardingTheme, defaultWireFeatures, deriveAnswers, describeEntitlement, describeFailure, describePackage, detectAppVersion, detectNativeModel, deviceIdStorageKey, ensureCurrentSessionId, featuresCacheKey, featuresEqual, fetchWireFeatures, firstOpenStorageKey, getActivationRevalidationVersion, getCurrentSessionId, hashEmailFnv1a, hostIdentity, hydrateAutoDeviceKey, hydrateDeviceIdentity, identifyOnboarding, isFeaturesFresh, isOnboardingEnabled, isPermissionDue, isUserCancelled, isWireScalar, loadPersistedSession, loadSettledPermissions, looksLikeEmail, lookupIconGlyph, makeSessionId, mergeTheme, mintDeviceId, motionSpec_exports as motionSpec, namespaceExtra, normalizeAfterCard, normalizePermissionStatus, onboardingComponents, parseWireFeatures, peekPersistedSession, permissionEventName, permissionEventProps, permissionScreenId, permissionStorageKey, readCachedFeatures, readProgress, readSettledPermissions, reportClientEvent, reportClientEventAwait, reportClientEvents, reportClientEventsAwait, reportFirstOpen, reportSessionStart, resetActivationRevalidation, resetAutoDeviceKeys, resetCurrentSessionId, resetFirstOpenLatch, resetIdentityProvenance, resetSessionStartGuard, resolveAutoDeviceKey, resolveIdentity, resolvePermissionCopy, resolvePlanTier, resolveUserContext, sanitizeUserId, savePersistedSession, saveSettledPermissions, selectDuePermissionScreen, sessionStorageKey, setCurrentSessionId, subscribeActivationRevalidation, themeFromBrand, toAnalyticsEvent, useActivationRevalidation, useHostIcon, useIllustration, useLifecycleEvents, useOnboardingTheme, useReducedMotion, useResolvedFeatures, useSessionStart, useWireActivation, useWireFeatures, useWireFeaturesContext, wireConfigFromEnv, wireLifecycleEvents, writeCachedFeatures };
5107
5750
  //# sourceMappingURL=index.mjs.map
5108
5751
  //# sourceMappingURL=index.mjs.map