@wireai/activation 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/AGENTS.md +29 -5
  2. package/CHANGELOG.md +202 -6
  3. package/README.md +105 -10
  4. package/dist/analytics/index.d.mts +6 -6
  5. package/dist/analytics/index.d.ts +6 -6
  6. package/dist/analytics/index.js +447 -49
  7. package/dist/analytics/index.js.map +1 -1
  8. package/dist/analytics/index.mjs +447 -49
  9. package/dist/analytics/index.mjs.map +1 -1
  10. package/dist/coachmarks/index.d.mts +2 -2
  11. package/dist/coachmarks/index.d.ts +2 -2
  12. package/dist/coachmarks/index.js.map +1 -1
  13. package/dist/coachmarks/index.mjs.map +1 -1
  14. package/dist/{currentSession-f7LWcdWG.d.ts → currentSession-61dcm3V-.d.ts} +10 -2
  15. package/dist/{currentSession-d9CrBxwe.d.mts → currentSession-Bs2JfTJ8.d.mts} +10 -2
  16. package/dist/{decision-BzbiKwk3.d.mts → decision-Bl_M2y3r.d.mts} +1 -1
  17. package/dist/{decision-plDEOCkt.d.ts → decision-Cau5KmP6.d.ts} +1 -1
  18. package/dist/index.d.mts +558 -15
  19. package/dist/index.d.ts +558 -15
  20. package/dist/index.js +1144 -375
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +969 -207
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/questionnaire/index.d.mts +69 -23
  25. package/dist/questionnaire/index.d.ts +69 -23
  26. package/dist/questionnaire/index.js +643 -172
  27. package/dist/questionnaire/index.js.map +1 -1
  28. package/dist/questionnaire/index.mjs +608 -137
  29. package/dist/questionnaire/index.mjs.map +1 -1
  30. package/dist/reviews/index.d.mts +24 -12
  31. package/dist/reviews/index.d.ts +24 -12
  32. package/dist/reviews/index.js +507 -173
  33. package/dist/reviews/index.js.map +1 -1
  34. package/dist/reviews/index.mjs +462 -129
  35. package/dist/reviews/index.mjs.map +1 -1
  36. package/dist/showcase/index.d.mts +2 -2
  37. package/dist/showcase/index.d.ts +2 -2
  38. package/dist/showcase/index.js.map +1 -1
  39. package/dist/showcase/index.mjs.map +1 -1
  40. package/dist/transport-B31G0Cib.d.ts +128 -0
  41. package/dist/transport-Bzb-bcB2.d.mts +128 -0
  42. package/dist/{types-GL_hQ0TN.d.mts → types-BcmagF6K.d.mts} +1 -1
  43. package/dist/{types-GL_hQ0TN.d.ts → types-BcmagF6K.d.ts} +1 -1
  44. package/dist/{types-BhpXJGlg.d.ts → types-Buj9Lw9t.d.ts} +15 -3
  45. package/dist/{types-CMuOexw0.d.mts → types-CKFhyrMu.d.mts} +1 -1
  46. package/dist/{types-CMuOexw0.d.ts → types-CKFhyrMu.d.ts} +1 -1
  47. package/dist/{types-A6pTxIZV.d.mts → types-CNUqMK0D.d.mts} +15 -3
  48. package/metro/index.d.ts +18 -1
  49. package/metro/index.js +80 -0
  50. package/package.json +9 -1
  51. package/src/WireOnboarding.tsx +4 -0
  52. package/src/analytics/currentSession.ts +1 -1
  53. package/src/cards/CardGridSelectCard.tsx +253 -0
  54. package/src/cards/ChipSelectCard.tsx +1 -1
  55. package/src/cards/SelectionCard.tsx +15 -19
  56. package/src/cards/index.ts +13 -1
  57. package/src/cards/optionSchema.ts +65 -0
  58. package/src/coachmarks/types.ts +1 -1
  59. package/src/components/CardHandoff.tsx +39 -7
  60. package/src/components/DoneCheck.tsx +94 -0
  61. package/src/components/ModalPaneHost.tsx +141 -0
  62. package/src/components/StepDots.tsx +112 -0
  63. package/src/device/appVersion.ts +29 -8
  64. package/src/device/deviceModel.ts +21 -8
  65. package/src/features/cache.ts +1 -1
  66. package/src/haptics/expo-haptics.d.ts +31 -0
  67. package/src/haptics/haptics.ts +58 -0
  68. package/src/icons/IconRegistry.tsx +53 -0
  69. package/src/icons/WireIcon.tsx +85 -0
  70. package/src/icons/expoIcons.ts +165 -0
  71. package/src/icons/index.ts +18 -0
  72. package/src/icons/vocabulary.ts +154 -0
  73. package/src/index.ts +20 -0
  74. package/src/questionnaire/QuestionnaireGate.tsx +315 -164
  75. package/src/questionnaire/decision.ts +4 -2
  76. package/src/questionnaire/index.ts +3 -2
  77. package/src/questionnaire/steps.ts +261 -0
  78. package/src/questionnaire/types.ts +33 -12
  79. package/src/reviews/ReviewGate.tsx +250 -152
  80. package/src/reviews/index.ts +7 -3
  81. package/src/reviews/transport.ts +119 -3
  82. package/src/reviews/types.ts +13 -0
  83. package/src/session/persistedSession.ts +1 -1
  84. package/src/session-analytics/lifecycle.ts +1 -1
  85. package/src/session-analytics/useSessionStart.ts +2 -2
  86. package/src/showcase/FeatureShowcase.tsx +1 -1
  87. package/src/showcase/showcaseColors.ts +1 -1
  88. package/src/theme/mergeThemeOver.ts +27 -0
  89. package/src/types.ts +8 -0
  90. package/dist/transport-BeO_Brcu.d.mts +0 -40
  91. package/dist/transport-DLpd1v5_.d.ts +0 -40
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var React4 = require('react');
3
+ var React8 = require('react');
4
4
  var reactNative = require('react-native');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var reactNativeSafeAreaContext = require('react-native-safe-area-context');
7
7
 
8
8
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
9
 
10
- var React4__default = /*#__PURE__*/_interopDefault(React4);
10
+ var React8__default = /*#__PURE__*/_interopDefault(React8);
11
11
 
12
12
  // src/questionnaire/QuestionnaireGate.tsx
13
13
 
@@ -60,9 +60,9 @@ var defaultOnboardingTheme = {
60
60
  var mergeTheme = (override) => {
61
61
  return defaultOnboardingTheme;
62
62
  };
63
- var OnboardingThemeContext = React4.createContext(null);
63
+ var OnboardingThemeContext = React8.createContext(null);
64
64
  var useOnboardingTheme = () => {
65
- const ctx = React4.useContext(OnboardingThemeContext);
65
+ const ctx = React8.useContext(OnboardingThemeContext);
66
66
  return ctx != null ? ctx : mergeTheme();
67
67
  };
68
68
 
@@ -94,16 +94,28 @@ var buttonLabelStyle = (f) => {
94
94
  };
95
95
 
96
96
  // src/motion/motionSpec.ts
97
+ var WIRE_BEZIER = [0.16, 1, 0.3, 1];
97
98
  var REDUCED_FADE_MS = 120;
98
99
  var BUTTON_PRESS_SCALE = 0.975;
99
100
  var REDUCED_PRESS_OPACITY = 0.85;
100
101
  var BUTTON_PRESS_SPRING = { friction: 8, tension: 240 };
102
+ var SELECT_FILL_MS = 180;
103
+ var CARD_ENTER_MS = 220;
104
+ var CARD_EXIT_MS = 180;
105
+ var CARD_EXIT_LEAD_MS = 80;
106
+ var CARD_SLIDE_PX = 26;
107
+ var CARD_ENTER_SCALE_FROM = 0.96;
108
+ var CARD_SPRING = { damping: 14, stiffness: 180, mass: 1 };
109
+ var LOADER_HANDOFF_FADE_MS = 120;
110
+ var STATUS_POP_SPRING = { friction: 6, tension: 200 };
111
+ var STATUS_POP_SCALE_FROM = 0.4;
101
112
  var durationScale = 1;
102
113
  var scaledMs = (ms) => Math.round(ms * durationScale);
114
+ var stateChangeDuration = (reduced, durationMs) => reduced ? 0 : durationMs;
103
115
  var lastKnown = false;
104
116
  var useReducedMotion = () => {
105
- const [reduced, setReduced] = React4.useState(lastKnown);
106
- React4.useEffect(() => {
117
+ const [reduced, setReduced] = React8.useState(lastKnown);
118
+ React8.useEffect(() => {
107
119
  let mounted = true;
108
120
  reactNative.AccessibilityInfo.isReduceMotionEnabled().then((value) => {
109
121
  lastKnown = value;
@@ -131,8 +143,8 @@ var _Button = ({
131
143
  const t = useOnboardingTheme();
132
144
  const reduced = useReducedMotion();
133
145
  const isPrimary = variant === "primary";
134
- const pressT = React4.useRef(new reactNative.Animated.Value(0)).current;
135
- const pressTo = React4.useCallback(
146
+ const pressT = React8.useRef(new reactNative.Animated.Value(0)).current;
147
+ const pressTo = React8.useCallback(
136
148
  (to) => {
137
149
  if (reduced) {
138
150
  reactNative.Animated.timing(pressT, {
@@ -151,16 +163,16 @@ var _Button = ({
151
163
  },
152
164
  [pressT, reduced]
153
165
  );
154
- const lastPress = React4.useRef(0);
155
- const handlePress = React4.useCallback(() => {
166
+ const lastPress = React8.useRef(0);
167
+ const handlePress = React8.useCallback(() => {
156
168
  if (!onPress) return;
157
169
  const now = Date.now();
158
170
  if (now - lastPress.current < 500) return;
159
171
  lastPress.current = now;
160
172
  onPress();
161
173
  }, [onPress]);
162
- const handlePressIn = React4.useCallback(() => pressTo(1), [pressTo]);
163
- const handlePressOut = React4.useCallback(() => pressTo(0), [pressTo]);
174
+ const handlePressIn = React8.useCallback(() => pressTo(1), [pressTo]);
175
+ const handlePressOut = React8.useCallback(() => pressTo(0), [pressTo]);
164
176
  const scale = pressT.interpolate({
165
177
  inputRange: [0, 1],
166
178
  outputRange: [1, reduced ? 1 : BUTTON_PRESS_SCALE]
@@ -211,7 +223,7 @@ var _Button = ({
211
223
  }
212
224
  );
213
225
  };
214
- var Button = React4__default.default.memo(_Button);
226
+ var Button = React8__default.default.memo(_Button);
215
227
  var styles = reactNative.StyleSheet.create({
216
228
  base: { alignItems: "center", justifyContent: "center" },
217
229
  fill: { width: "100%" }
@@ -228,13 +240,13 @@ var CENTERED_MODAL_ANIM = {
228
240
  /** Peak dim of the backdrop at progress 1. */
229
241
  backdropOpacity: 0.55
230
242
  };
231
- var _CenteredModal = React4.forwardRef(
243
+ var _CenteredModal = React8.forwardRef(
232
244
  ({ theme, dismissible = true, onDismiss, children }, ref) => {
233
- const progress = React4.useRef(new reactNative.Animated.Value(0)).current;
234
- const [open, setOpen] = React4.useState(true);
235
- const closingRef = React4.useRef(false);
245
+ const progress = React8.useRef(new reactNative.Animated.Value(0)).current;
246
+ const [open, setOpen] = React8.useState(true);
247
+ const closingRef = React8.useRef(false);
236
248
  const reduced = useReducedMotion();
237
- React4.useEffect(() => {
249
+ React8.useEffect(() => {
238
250
  const anim = reduced ? reactNative.Animated.timing(progress, {
239
251
  toValue: 1,
240
252
  duration: REDUCED_FADE_MS,
@@ -248,7 +260,7 @@ var _CenteredModal = React4.forwardRef(
248
260
  anim.start();
249
261
  return () => anim.stop();
250
262
  }, [progress, reduced]);
251
- const close = React4.useCallback(() => {
263
+ const close = React8.useCallback(() => {
252
264
  if (closingRef.current) return;
253
265
  closingRef.current = true;
254
266
  reactNative.Animated.timing(progress, {
@@ -261,15 +273,15 @@ var _CenteredModal = React4.forwardRef(
261
273
  onDismiss();
262
274
  });
263
275
  }, [progress, onDismiss, reduced]);
264
- React4.useImperativeHandle(ref, () => ({ close }), [close]);
265
- React4.useEffect(() => {
276
+ React8.useImperativeHandle(ref, () => ({ close }), [close]);
277
+ React8.useEffect(() => {
266
278
  const sub = reactNative.BackHandler.addEventListener("hardwareBackPress", () => {
267
279
  if (dismissible) close();
268
280
  return true;
269
281
  });
270
282
  return () => sub.remove();
271
283
  }, [dismissible, close]);
272
- const onBackdrop = React4.useCallback(() => {
284
+ const onBackdrop = React8.useCallback(() => {
273
285
  if (dismissible) close();
274
286
  }, [dismissible, close]);
275
287
  const backdropOpacity = progress.interpolate({
@@ -331,7 +343,7 @@ var _CenteredModal = React4.forwardRef(
331
343
  }
332
344
  );
333
345
  _CenteredModal.displayName = "CenteredModal";
334
- var CenteredModal = React4__default.default.memo(_CenteredModal);
346
+ var CenteredModal = React8__default.default.memo(_CenteredModal);
335
347
  var styles2 = reactNative.StyleSheet.create({
336
348
  fill: { flex: 1 },
337
349
  backdrop: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#000" },
@@ -347,6 +359,319 @@ var styles2 = reactNative.StyleSheet.create({
347
359
  elevation: 12
348
360
  }
349
361
  });
362
+ var _DoneCheck = ({ theme, size = 48 }) => {
363
+ const reduced = useReducedMotion();
364
+ const pop = React8.useRef(new reactNative.Animated.Value(reduced ? 1 : 0)).current;
365
+ React8.useEffect(() => {
366
+ if (reduced) {
367
+ pop.setValue(1);
368
+ return;
369
+ }
370
+ const anim = reactNative.Animated.spring(pop, {
371
+ toValue: 1,
372
+ friction: STATUS_POP_SPRING.friction,
373
+ tension: STATUS_POP_SPRING.tension,
374
+ useNativeDriver: true
375
+ });
376
+ anim.start();
377
+ return () => anim.stop();
378
+ }, [pop, reduced]);
379
+ const scale = pop.interpolate({
380
+ inputRange: [0, 1],
381
+ outputRange: [STATUS_POP_SCALE_FROM, 1]
382
+ });
383
+ return /* @__PURE__ */ jsxRuntime.jsx(
384
+ reactNative.Animated.View,
385
+ {
386
+ accessibilityElementsHidden: true,
387
+ importantForAccessibility: "no-hide-descendants",
388
+ style: [
389
+ styles3.ring,
390
+ {
391
+ width: size,
392
+ height: size,
393
+ borderRadius: theme.radius.full,
394
+ backgroundColor: theme.colors.success,
395
+ opacity: pop,
396
+ transform: [{ scale }]
397
+ }
398
+ ],
399
+ children: /* @__PURE__ */ jsxRuntime.jsx(
400
+ reactNative.Animated.Text,
401
+ {
402
+ style: [
403
+ styles3.glyph,
404
+ { color: theme.colors.onPrimary, fontSize: Math.round(size * 0.5), fontFamily: theme.fonts.bold }
405
+ ],
406
+ children: "\u2713"
407
+ }
408
+ )
409
+ }
410
+ );
411
+ };
412
+ var DoneCheck = React8__default.default.memo(_DoneCheck);
413
+ DoneCheck.displayName = "DoneCheck";
414
+ var styles3 = reactNative.StyleSheet.create({
415
+ ring: { alignItems: "center", justifyContent: "center", alignSelf: "center" },
416
+ glyph: { includeFontPadding: false, textAlign: "center" }
417
+ });
418
+ var easeWire = reactNative.Easing.bezier(...WIRE_BEZIER);
419
+ var _CardHandoff = ({
420
+ transitionKey,
421
+ variant = "slide",
422
+ direction = "forward",
423
+ layout = "fill",
424
+ children
425
+ }) => {
426
+ const reduced = useReducedMotion();
427
+ const [leaving, setLeaving] = React8.useState(null);
428
+ const enterT = React8.useRef(new reactNative.Animated.Value(0)).current;
429
+ const exitT = React8.useRef(new reactNative.Animated.Value(0)).current;
430
+ const prev = React8.useRef({ key: transitionKey, node: children, variant });
431
+ const running = React8.useRef({});
432
+ const mounted = React8.useRef(false);
433
+ React8.useEffect(() => {
434
+ var _a, _b;
435
+ const last = prev.current;
436
+ prev.current = { key: transitionKey, node: children, variant };
437
+ if (!mounted.current) {
438
+ mounted.current = true;
439
+ running.current.enter = startEnter(enterT, variant, reduced, 0);
440
+ return;
441
+ }
442
+ if (last.key === transitionKey) return;
443
+ (_a = running.current.enter) == null ? void 0 : _a.stop();
444
+ (_b = running.current.exit) == null ? void 0 : _b.stop();
445
+ setLeaving(last);
446
+ enterT.setValue(0);
447
+ exitT.setValue(0);
448
+ const exitDuration = reduced ? REDUCED_FADE_MS : last.variant === "fade" ? LOADER_HANDOFF_FADE_MS : CARD_EXIT_MS;
449
+ const exit = reactNative.Animated.timing(exitT, {
450
+ toValue: 1,
451
+ duration: scaledMs(exitDuration),
452
+ easing: easeWire,
453
+ useNativeDriver: true
454
+ });
455
+ running.current.exit = exit;
456
+ exit.start();
457
+ const enter = startEnter(
458
+ enterT,
459
+ variant,
460
+ reduced,
461
+ reduced ? 0 : CARD_EXIT_LEAD_MS,
462
+ () => setLeaving(null)
463
+ );
464
+ running.current.enter = enter;
465
+ });
466
+ React8.useEffect(
467
+ () => () => {
468
+ var _a, _b;
469
+ (_a = running.current.enter) == null ? void 0 : _a.stop();
470
+ (_b = running.current.exit) == null ? void 0 : _b.stop();
471
+ },
472
+ []
473
+ );
474
+ const enterStyle = {
475
+ opacity: enterT,
476
+ transform: transformFor(variant, enterT, reduced, "enter", direction)
477
+ };
478
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
479
+ leaving ? /* @__PURE__ */ jsxRuntime.jsx(
480
+ reactNative.Animated.View,
481
+ {
482
+ pointerEvents: "none",
483
+ style: [
484
+ reactNative.StyleSheet.absoluteFill,
485
+ {
486
+ opacity: exitT.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }),
487
+ // The CURRENT direction drives BOTH layers: one transition, one geography.
488
+ transform: transformFor(leaving.variant, exitT, reduced, "exit", direction)
489
+ }
490
+ ],
491
+ children: leaving.node
492
+ }
493
+ ) : null,
494
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.View, { style: [layout === "fill" ? styles4.fill : null, enterStyle], children }, variant)
495
+ ] });
496
+ };
497
+ var startEnter = (value, variant, reduced, delay, onDone) => {
498
+ value.setValue(0);
499
+ const anim = !reduced && variant === "spring" ? reactNative.Animated.spring(value, {
500
+ toValue: 1,
501
+ delay: scaledMs(delay),
502
+ damping: CARD_SPRING.damping,
503
+ stiffness: CARD_SPRING.stiffness,
504
+ mass: CARD_SPRING.mass,
505
+ useNativeDriver: true
506
+ }) : reactNative.Animated.timing(value, {
507
+ toValue: 1,
508
+ delay: scaledMs(delay),
509
+ duration: scaledMs(
510
+ reduced ? REDUCED_FADE_MS : variant === "fade" ? LOADER_HANDOFF_FADE_MS : CARD_ENTER_MS
511
+ ),
512
+ easing: easeWire,
513
+ useNativeDriver: true
514
+ });
515
+ anim.start(({ finished }) => {
516
+ if (finished) onDone == null ? void 0 : onDone();
517
+ });
518
+ return anim;
519
+ };
520
+ var transformFor = (variant, t, reduced, layer, travel) => {
521
+ if (reduced || variant === "fade") return [];
522
+ if (variant === "spring" && layer === "enter") {
523
+ return [{ scale: t.interpolate({ inputRange: [0, 1], outputRange: [CARD_ENTER_SCALE_FROM, 1] }) }];
524
+ }
525
+ const slide = travel === "forward" ? CARD_SLIDE_PX : -CARD_SLIDE_PX;
526
+ return [
527
+ {
528
+ translateX: t.interpolate({
529
+ inputRange: [0, 1],
530
+ outputRange: layer === "enter" ? [slide, 0] : [0, -slide]
531
+ })
532
+ }
533
+ ];
534
+ };
535
+ var CardHandoff = React8__default.default.memo(_CardHandoff);
536
+ var styles4 = reactNative.StyleSheet.create({
537
+ fill: { flex: 1 }
538
+ });
539
+ var easeWire2 = reactNative.Easing.bezier(...WIRE_BEZIER);
540
+ var _ModalPaneHost = ({
541
+ paneKey,
542
+ direction = "forward",
543
+ header,
544
+ children
545
+ }) => {
546
+ const reduced = useReducedMotion();
547
+ const height = React8.useRef(new reactNative.Animated.Value(0)).current;
548
+ const measured = React8.useRef(null);
549
+ const [sized, setSized] = React8.useState(false);
550
+ const paneChanged = React8.useRef(false);
551
+ const running = React8.useRef(null);
552
+ React8.useEffect(() => {
553
+ paneChanged.current = true;
554
+ }, [paneKey]);
555
+ React8.useEffect(() => () => {
556
+ var _a;
557
+ return (_a = running.current) == null ? void 0 : _a.stop();
558
+ }, []);
559
+ const onLayout = React8.useCallback(
560
+ (event) => {
561
+ var _a;
562
+ const next = Math.round(event.nativeEvent.layout.height);
563
+ if (!next) return;
564
+ const previous = measured.current;
565
+ if (previous !== null && Math.abs(previous - next) < 1) return;
566
+ const first = previous === null;
567
+ const ease = !first && !reduced && paneChanged.current;
568
+ paneChanged.current = false;
569
+ measured.current = next;
570
+ (_a = running.current) == null ? void 0 : _a.stop();
571
+ if (!ease) {
572
+ height.setValue(next);
573
+ if (first) setSized(true);
574
+ return;
575
+ }
576
+ const anim = reactNative.Animated.timing(height, {
577
+ toValue: next,
578
+ duration: scaledMs(CARD_ENTER_MS),
579
+ easing: easeWire2,
580
+ // height is layout, not transform — see the header note.
581
+ useNativeDriver: false
582
+ });
583
+ running.current = anim;
584
+ anim.start();
585
+ },
586
+ [height, reduced]
587
+ );
588
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.View, { style: sized ? { height, overflow: "hidden" } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { onLayout, style: styles5.pane, children: [
589
+ header,
590
+ /* @__PURE__ */ jsxRuntime.jsx(CardHandoff, { transitionKey: paneKey, direction, layout: "auto", variant: "slide", children })
591
+ ] }) });
592
+ };
593
+ var ModalPaneHost = React8__default.default.memo(_ModalPaneHost);
594
+ ModalPaneHost.displayName = "ModalPaneHost";
595
+ var styles5 = reactNative.StyleSheet.create({
596
+ pane: { width: "100%" }
597
+ });
598
+ var easeWire3 = reactNative.Easing.bezier(...WIRE_BEZIER);
599
+ var Dot = ({ theme, filled, size }) => {
600
+ const reduced = useReducedMotion();
601
+ const fill = React8.useRef(new reactNative.Animated.Value(filled ? 1 : 0)).current;
602
+ React8.useEffect(() => {
603
+ const anim = reactNative.Animated.timing(fill, {
604
+ toValue: filled ? 1 : 0,
605
+ duration: scaledMs(stateChangeDuration(reduced, SELECT_FILL_MS)),
606
+ easing: easeWire3,
607
+ useNativeDriver: true
608
+ });
609
+ anim.start();
610
+ return () => anim.stop();
611
+ }, [fill, filled, reduced]);
612
+ return /* @__PURE__ */ jsxRuntime.jsx(
613
+ reactNative.View,
614
+ {
615
+ style: {
616
+ width: size,
617
+ height: size,
618
+ borderRadius: theme.radius.full,
619
+ backgroundColor: theme.colors.border
620
+ },
621
+ children: /* @__PURE__ */ jsxRuntime.jsx(
622
+ reactNative.Animated.View,
623
+ {
624
+ style: [
625
+ reactNative.StyleSheet.absoluteFill,
626
+ {
627
+ borderRadius: theme.radius.full,
628
+ backgroundColor: theme.colors.primary,
629
+ opacity: fill
630
+ }
631
+ ]
632
+ }
633
+ )
634
+ }
635
+ );
636
+ };
637
+ var _StepDots = ({ theme, count, index }) => /* @__PURE__ */ jsxRuntime.jsx(
638
+ reactNative.View,
639
+ {
640
+ style: [styles6.row, { gap: theme.spacing.sm }],
641
+ accessibilityRole: "progressbar",
642
+ accessibilityLabel: `Step ${index + 1} of ${count}`,
643
+ accessibilityValue: { min: 1, max: count, now: index + 1 },
644
+ children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: /* @__PURE__ */ jsxRuntime.jsx(Dot, { theme, filled: i <= index, size: theme.spacing.sm }) }, i))
645
+ }
646
+ );
647
+ var StepDots = React8__default.default.memo(_StepDots);
648
+ StepDots.displayName = "StepDots";
649
+ var styles6 = reactNative.StyleSheet.create({
650
+ row: { flexDirection: "row", alignItems: "center", justifyContent: "center" }
651
+ });
652
+
653
+ // src/haptics/haptics.ts
654
+ var play = async (tone) => {
655
+ try {
656
+ const mod = await import('expo-haptics');
657
+ if (!mod) return false;
658
+ if (tone === "selection") {
659
+ await mod.selectionAsync();
660
+ return true;
661
+ }
662
+ if (tone === "light") {
663
+ await mod.impactAsync(mod.ImpactFeedbackStyle.Light);
664
+ return true;
665
+ }
666
+ await mod.notificationAsync(mod.NotificationFeedbackType.Success);
667
+ return true;
668
+ } catch {
669
+ return false;
670
+ }
671
+ };
672
+ var playHaptic = (tone) => {
673
+ void play(tone);
674
+ };
350
675
 
351
676
  // src/reviews/feedbackForm.ts
352
677
  var FEEDBACK_MIN_LINES = 4;
@@ -361,6 +686,18 @@ var feedbackInputHeights = (fonts, verticalPadding = 0) => {
361
686
  };
362
687
  };
363
688
 
689
+ // src/theme/mergeThemeOver.ts
690
+ var mergeThemeOver = (base, override) => {
691
+ if (!override) return base;
692
+ return {
693
+ colors: { ...base.colors, ...override.colors },
694
+ fonts: { ...base.fonts, ...override.fonts },
695
+ radius: { ...base.radius, ...override.radius },
696
+ spacing: { ...base.spacing, ...override.spacing },
697
+ button: { ...base.button, ...override.button }
698
+ };
699
+ };
700
+
364
701
  // src/reviews/decision.ts
365
702
  var decideReview = (local, decision) => decision != null ? decision : local;
366
703
  var resolveRules = (config) => {
@@ -408,6 +745,92 @@ var buildQuestionnaireSubmission = (input) => {
408
745
  };
409
746
  };
410
747
 
748
+ // src/questionnaire/steps.ts
749
+ var REQUIRED_STEPS = {
750
+ opinion: false,
751
+ improve: false,
752
+ suggestions: true
753
+ };
754
+ var DEFAULT_STEP_LABELS = {
755
+ opinion: "What do you think about the app?",
756
+ improve: "How can we improve?",
757
+ suggestions: "Any suggestions?"
758
+ };
759
+ var STEP_PLACEHOLDERS = {
760
+ opinion: "e.g. it feels fast and clear",
761
+ improve: "e.g. fewer steps to start",
762
+ suggestions: "e.g. a dark theme"
763
+ };
764
+ var buildQuestionnaireSteps = (questionnaire) => {
765
+ var _a, _b, _c;
766
+ return [
767
+ {
768
+ key: "opinion",
769
+ label: (_a = questionnaire.opinion_label) != null ? _a : DEFAULT_STEP_LABELS.opinion,
770
+ placeholder: STEP_PLACEHOLDERS.opinion,
771
+ required: REQUIRED_STEPS.opinion
772
+ },
773
+ {
774
+ key: "improve",
775
+ label: (_b = questionnaire.improve_label) != null ? _b : DEFAULT_STEP_LABELS.improve,
776
+ placeholder: STEP_PLACEHOLDERS.improve,
777
+ required: REQUIRED_STEPS.improve
778
+ },
779
+ {
780
+ key: "suggestions",
781
+ label: (_c = questionnaire.suggestions_label) != null ? _c : DEFAULT_STEP_LABELS.suggestions,
782
+ placeholder: STEP_PLACEHOLDERS.suggestions,
783
+ required: REQUIRED_STEPS.suggestions
784
+ }
785
+ ];
786
+ };
787
+ var initialStepState = () => ({
788
+ index: 0,
789
+ drafts: {},
790
+ direction: "forward",
791
+ done: false
792
+ });
793
+ var currentStep = (steps, state) => steps[state.index];
794
+ var isLastStep = (steps, index) => index >= steps.length - 1;
795
+ var canGoBack = (state) => state.index > 0;
796
+ var canSkipStep = (steps, state) => {
797
+ var _a, _b;
798
+ return !((_b = (_a = steps[state.index]) == null ? void 0 : _a.required) != null ? _b : false);
799
+ };
800
+ var canCommitStep = (steps, state, value) => {
801
+ const step = steps[state.index];
802
+ if (!step) return false;
803
+ return !step.required || value.trim().length > 0;
804
+ };
805
+ var stashDraft = (steps, state, value) => {
806
+ const step = steps[state.index];
807
+ if (!step) return state;
808
+ return { ...state, drafts: { ...state.drafts, [step.key]: value } };
809
+ };
810
+ var commitStep = (steps, state, value) => {
811
+ const step = steps[state.index];
812
+ if (!step) return state;
813
+ if (!canCommitStep(steps, state, value)) return state;
814
+ const { drafts } = stashDraft(steps, state, value);
815
+ const last = isLastStep(steps, state.index);
816
+ return {
817
+ index: last ? state.index : state.index + 1,
818
+ drafts,
819
+ direction: "forward",
820
+ done: last
821
+ };
822
+ };
823
+ var skipStep = (steps, state) => commitStep(steps, state, "");
824
+ var goBack = (state) => state.index <= 0 ? state : { ...state, index: state.index - 1, direction: "backward" };
825
+ var toAnswers = (drafts) => {
826
+ var _a, _b, _c;
827
+ return {
828
+ opinion: (_a = drafts.opinion) != null ? _a : "",
829
+ improve: (_b = drafts.improve) != null ? _b : "",
830
+ suggestions: (_c = drafts.suggestions) != null ? _c : ""
831
+ };
832
+ };
833
+
411
834
  // src/questionnaire/transport.ts
412
835
  var submitQuestionnaireResponse = (target, id, submission) => {
413
836
  if (!(target == null ? void 0 : target.serverUrl) || !id) return;
@@ -444,16 +867,6 @@ var fetchQuestionnaireDecision = async (target, options = {}) => {
444
867
  return null;
445
868
  }
446
869
  };
447
- var mergeThemeOver = (base, override) => {
448
- if (!override) return base;
449
- return {
450
- colors: { ...base.colors, ...override.colors },
451
- fonts: { ...base.fonts, ...override.fonts },
452
- radius: { ...base.radius, ...override.radius },
453
- spacing: { ...base.spacing, ...override.spacing },
454
- button: { ...base.button, ...override.button }
455
- };
456
- };
457
870
  var _QuestionnaireGate = ({
458
871
  questionnaire,
459
872
  target,
@@ -465,150 +878,208 @@ var _QuestionnaireGate = ({
465
878
  theme: themeOverride,
466
879
  dismissible = true
467
880
  }) => {
468
- var _a, _b, _c, _d;
881
+ var _a, _b, _c;
469
882
  const active = useOnboardingTheme();
470
- const t = React4.useMemo(() => mergeThemeOver(active, themeOverride), [active, themeOverride]);
471
- const modalRef = React4__default.default.useRef(null);
883
+ const t = React8.useMemo(() => mergeThemeOver(active, themeOverride), [active, themeOverride]);
884
+ const modalRef = React8.useRef(null);
472
885
  const id = questionnaire.id;
473
- const [phase, setPhase] = React4.useState("form");
474
- const [opinion, setOpinion] = React4.useState("");
475
- const [improve, setImprove] = React4.useState("");
476
- const [suggestions, setSuggestions] = React4.useState("");
477
- const shownRef = React4__default.default.useRef(false);
478
- const onShownRef = React4__default.default.useRef(onShown);
479
- const onEventRef = React4__default.default.useRef(onEvent);
886
+ const steps = React8.useMemo(
887
+ () => buildQuestionnaireSteps({
888
+ opinion_label: questionnaire.opinion_label,
889
+ improve_label: questionnaire.improve_label,
890
+ suggestions_label: questionnaire.suggestions_label
891
+ }),
892
+ [questionnaire.opinion_label, questionnaire.improve_label, questionnaire.suggestions_label]
893
+ );
894
+ const [phase, setPhase] = React8.useState("form");
895
+ const [state, setState] = React8.useState(initialStepState);
896
+ const stateRef = React8.useRef(state);
897
+ stateRef.current = state;
898
+ const draftRef = React8.useRef("");
899
+ const [answered, setAnswered] = React8.useState(false);
900
+ const answeredRef = React8.useRef(false);
901
+ const seedDraft = React8.useCallback((value) => {
902
+ draftRef.current = value;
903
+ const has = value.trim().length > 0;
904
+ answeredRef.current = has;
905
+ setAnswered(has);
906
+ }, []);
907
+ const handleChangeText = React8.useCallback((text) => {
908
+ draftRef.current = text;
909
+ const has = text.trim().length > 0;
910
+ if (has === answeredRef.current) return;
911
+ answeredRef.current = has;
912
+ setAnswered(has);
913
+ }, []);
914
+ const shownRef = React8.useRef(false);
915
+ const onShownRef = React8.useRef(onShown);
916
+ const onEventRef = React8.useRef(onEvent);
480
917
  onShownRef.current = onShown;
481
918
  onEventRef.current = onEvent;
482
- React4__default.default.useEffect(() => {
919
+ React8__default.default.useEffect(() => {
483
920
  var _a2, _b2;
484
921
  if (shownRef.current) return;
485
922
  shownRef.current = true;
486
923
  (_a2 = onShownRef.current) == null ? void 0 : _a2.call(onShownRef);
487
924
  (_b2 = onEventRef.current) == null ? void 0 : _b2.call(onEventRef, { name: "questionnaire_prompt_shown", id });
488
925
  }, []);
489
- const resolveTimer = React4__default.default.useRef(null);
490
- React4__default.default.useEffect(() => {
926
+ const resolveTimer = React8.useRef(null);
927
+ React8__default.default.useEffect(() => {
491
928
  return () => {
492
929
  if (resolveTimer.current) clearTimeout(resolveTimer.current);
493
930
  };
494
931
  }, []);
495
- const resolve = React4.useCallback(() => {
496
- onResolved == null ? void 0 : onResolved();
497
- }, [onResolved]);
498
- const canSend = opinion.trim().length > 0;
499
- const send = React4.useCallback(() => {
500
- if (!canSend) return;
501
- const submission = buildQuestionnaireSubmission({
502
- answers: { opinion, improve, suggestions },
503
- sessionId,
504
- meta
505
- });
506
- submitQuestionnaireResponse(target, id, submission);
507
- onEvent == null ? void 0 : onEvent({ name: "questionnaire_submitted", id });
508
- setPhase("thanks");
509
- if (resolveTimer.current) clearTimeout(resolveTimer.current);
510
- resolveTimer.current = setTimeout(() => {
511
- var _a2;
512
- (_a2 = modalRef.current) == null ? void 0 : _a2.close();
513
- }, 1500);
514
- }, [canSend, opinion, improve, suggestions, sessionId, meta, target, id, onEvent]);
515
- const handleDismissTap = React4.useCallback(() => {
932
+ const submittedRef = React8.useRef(false);
933
+ const handleDismissed = React8.useCallback(() => {
516
934
  var _a2;
517
- onEvent == null ? void 0 : onEvent({ name: "questionnaire_dismissed", id });
518
- (_a2 = modalRef.current) == null ? void 0 : _a2.close();
519
- }, [onEvent, id]);
520
- const title = (_a = questionnaire.title) != null ? _a : "How's it going so far?";
521
- const opinionLabel = (_b = questionnaire.opinion_label) != null ? _b : "What do you think about the app?";
522
- const improveLabel = (_c = questionnaire.improve_label) != null ? _c : "How can we improve? (optional)";
523
- const suggestionsLabel = (_d = questionnaire.suggestions_label) != null ? _d : "Any suggestions? (optional)";
524
- const opinionBox = feedbackInputHeights(t.fonts, t.spacing.md);
525
- const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
526
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [headingStyle(t.fonts), { color: t.colors.text }], children: title }),
527
- phase === "form" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
528
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "A few words is plenty. Only the first box is required." }),
529
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: opinionLabel }),
530
- /* @__PURE__ */ jsxRuntime.jsx(
531
- reactNative.TextInput,
532
- {
533
- value: opinion,
534
- onChangeText: setOpinion,
535
- placeholder: "e.g. it feels fast and clear",
536
- placeholderTextColor: t.colors.textMuted,
537
- multiline: true,
538
- textAlignVertical: "top",
539
- style: [
540
- bodyStyle(t.fonts),
541
- styles3.input,
542
- {
543
- color: t.colors.text,
544
- backgroundColor: t.colors.background,
545
- borderRadius: t.radius.md,
546
- borderColor: t.colors.border,
547
- padding: t.spacing.md,
548
- minHeight: opinionBox.minHeight,
549
- maxHeight: opinionBox.maxHeight
550
- }
551
- ]
552
- }
553
- ),
554
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: improveLabel }),
555
- /* @__PURE__ */ jsxRuntime.jsx(
556
- reactNative.TextInput,
557
- {
558
- value: improve,
559
- onChangeText: setImprove,
560
- placeholder: "e.g. fewer steps to start",
561
- placeholderTextColor: t.colors.textMuted,
562
- style: [
563
- bodyStyle(t.fonts),
564
- styles3.input,
565
- {
566
- color: t.colors.text,
567
- backgroundColor: t.colors.background,
568
- borderRadius: t.radius.md,
569
- borderColor: t.colors.border,
570
- paddingHorizontal: t.spacing.md,
571
- paddingVertical: t.spacing.sm,
572
- minHeight: t.button.height
573
- }
574
- ]
575
- }
576
- ),
577
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: suggestionsLabel }),
578
- /* @__PURE__ */ jsxRuntime.jsx(
579
- reactNative.TextInput,
580
- {
581
- value: suggestions,
582
- onChangeText: setSuggestions,
583
- placeholder: "e.g. a dark theme",
584
- placeholderTextColor: t.colors.textMuted,
585
- style: [
586
- bodyStyle(t.fonts),
587
- styles3.input,
588
- {
589
- color: t.colors.text,
590
- backgroundColor: t.colors.background,
591
- borderRadius: t.radius.md,
592
- borderColor: t.colors.border,
593
- paddingHorizontal: t.spacing.md,
594
- paddingVertical: t.spacing.sm,
595
- minHeight: t.button.height
596
- }
597
- ]
598
- }
599
- ),
600
- /* @__PURE__ */ jsxRuntime.jsx(Button, { title: "Send", onPress: send, variant: "primary", fullWidth: true, disabled: !canSend }),
601
- dismissible && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: handleDismissTap, hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), styles3.link, { color: t.colors.textMuted }], children: "Not now" }) })
602
- ] }),
603
- phase === "thanks" && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: "Thank you. This helps us make it better." })
935
+ if (!submittedRef.current) {
936
+ (_a2 = onEventRef.current) == null ? void 0 : _a2.call(onEventRef, { name: "questionnaire_dismissed", id });
937
+ }
938
+ onResolved == null ? void 0 : onResolved();
939
+ }, [id, onResolved]);
940
+ const finish = React8.useCallback(
941
+ (drafts) => {
942
+ const submission = buildQuestionnaireSubmission({
943
+ answers: toAnswers(drafts),
944
+ sessionId,
945
+ meta
946
+ });
947
+ submitQuestionnaireResponse(target, id, submission);
948
+ submittedRef.current = true;
949
+ onEvent == null ? void 0 : onEvent({ name: "questionnaire_submitted", id });
950
+ playHaptic("success");
951
+ setPhase("thanks");
952
+ if (resolveTimer.current) clearTimeout(resolveTimer.current);
953
+ resolveTimer.current = setTimeout(() => {
954
+ var _a2;
955
+ (_a2 = modalRef.current) == null ? void 0 : _a2.close();
956
+ }, 1500);
957
+ },
958
+ [sessionId, meta, target, id, onEvent]
959
+ );
960
+ const applyTransition = React8.useCallback(
961
+ (next) => {
962
+ var _a2, _b2, _c2;
963
+ if (next === stateRef.current) return;
964
+ setState(next);
965
+ if (next.done) {
966
+ finish(next.drafts);
967
+ return;
968
+ }
969
+ seedDraft((_c2 = next.drafts[(_b2 = (_a2 = steps[next.index]) == null ? void 0 : _a2.key) != null ? _b2 : "opinion"]) != null ? _c2 : "");
970
+ playHaptic("light");
971
+ },
972
+ [finish, seedDraft, steps]
973
+ );
974
+ const handleNext = React8.useCallback(() => {
975
+ applyTransition(commitStep(steps, stateRef.current, draftRef.current));
976
+ }, [applyTransition, steps]);
977
+ const handleSkip = React8.useCallback(() => {
978
+ applyTransition(skipStep(steps, stateRef.current));
979
+ }, [applyTransition, steps]);
980
+ const handleBack = React8.useCallback(() => {
981
+ var _a2, _b2, _c2;
982
+ const next = goBack(stashDraft(steps, stateRef.current, draftRef.current));
983
+ setState(next);
984
+ seedDraft((_c2 = next.drafts[(_b2 = (_a2 = steps[next.index]) == null ? void 0 : _a2.key) != null ? _b2 : "opinion"]) != null ? _c2 : "");
985
+ playHaptic("light");
986
+ }, [seedDraft, steps]);
987
+ const step = currentStep(steps, state);
988
+ const last = isLastStep(steps, state.index);
989
+ const canSkip = canSkipStep(steps, state);
990
+ const sendDisabled = ((_a = step == null ? void 0 : step.required) != null ? _a : false) && !answered;
991
+ const answerBox = feedbackInputHeights(t.fonts, t.spacing.md);
992
+ const linkStyle = [captionStyle(t.fonts), styles7.link, { color: t.colors.textMuted }];
993
+ const header = phase === "form" ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles7.header, { marginBottom: t.spacing.md }], children: [
994
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles7.headerSide, children: canGoBack(state) && /* @__PURE__ */ jsxRuntime.jsx(
995
+ reactNative.Pressable,
996
+ {
997
+ onPress: handleBack,
998
+ hitSlop: 12,
999
+ accessibilityRole: "button",
1000
+ accessibilityLabel: "Back to the previous question",
1001
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [headingStyle(t.fonts), styles7.backGlyph, { color: t.colors.textMuted }], children: "\u2039" })
1002
+ }
1003
+ ) }),
1004
+ /* @__PURE__ */ jsxRuntime.jsx(StepDots, { theme: t, count: steps.length, index: state.index }),
1005
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles7.headerSide })
1006
+ ] }) : null;
1007
+ const formPane = step ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: t.spacing.md }, children: [
1008
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [headingStyle(t.fonts), { color: t.colors.text }], children: step.label }),
1009
+ /* @__PURE__ */ jsxRuntime.jsx(
1010
+ reactNative.TextInput,
1011
+ {
1012
+ defaultValue: (_b = state.drafts[step.key]) != null ? _b : "",
1013
+ onChangeText: handleChangeText,
1014
+ placeholder: step.placeholder,
1015
+ placeholderTextColor: t.colors.textMuted,
1016
+ accessibilityLabel: step.label,
1017
+ multiline: true,
1018
+ textAlignVertical: "top",
1019
+ style: [
1020
+ bodyStyle(t.fonts),
1021
+ styles7.input,
1022
+ {
1023
+ color: t.colors.text,
1024
+ backgroundColor: t.colors.background,
1025
+ borderRadius: t.radius.md,
1026
+ borderColor: t.colors.border,
1027
+ padding: t.spacing.md,
1028
+ minHeight: answerBox.minHeight,
1029
+ maxHeight: answerBox.maxHeight
1030
+ }
1031
+ ]
1032
+ },
1033
+ step.key
1034
+ ),
1035
+ /* @__PURE__ */ jsxRuntime.jsx(
1036
+ Button,
1037
+ {
1038
+ title: last ? "Send" : "Next",
1039
+ onPress: handleNext,
1040
+ variant: "primary",
1041
+ fullWidth: true,
1042
+ disabled: sendDisabled
1043
+ }
1044
+ ),
1045
+ canSkip && /* @__PURE__ */ jsxRuntime.jsx(
1046
+ reactNative.Pressable,
1047
+ {
1048
+ onPress: handleSkip,
1049
+ hitSlop: 8,
1050
+ accessibilityRole: "button",
1051
+ accessibilityLabel: "Skip this question",
1052
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: linkStyle, children: "Skip step" })
1053
+ }
1054
+ )
1055
+ ] }) : null;
1056
+ const thanksPane = /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { gap: t.spacing.md }, children: [
1057
+ /* @__PURE__ */ jsxRuntime.jsx(DoneCheck, { theme: t }),
1058
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [headingStyle(t.fonts), styles7.centerText, { color: t.colors.text }], children: "Thank you" }),
1059
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [bodyStyle(t.fonts), styles7.centerText, { color: t.colors.textMuted }], children: "This helps us make it better." })
604
1060
  ] });
605
- return /* @__PURE__ */ jsxRuntime.jsx(CenteredModal, { ref: modalRef, theme: t, dismissible, onDismiss: resolve, children: content });
1061
+ return /* @__PURE__ */ jsxRuntime.jsx(CenteredModal, { ref: modalRef, theme: t, dismissible, onDismiss: handleDismissed, children: /* @__PURE__ */ jsxRuntime.jsx(
1062
+ ModalPaneHost,
1063
+ {
1064
+ paneKey: phase === "thanks" ? "thanks" : (_c = step == null ? void 0 : step.key) != null ? _c : "done",
1065
+ direction: state.direction,
1066
+ header,
1067
+ children: phase === "thanks" ? thanksPane : formPane
1068
+ }
1069
+ ) });
606
1070
  };
607
- var QuestionnaireGate = React4__default.default.memo(_QuestionnaireGate);
1071
+ var QuestionnaireGate = React8__default.default.memo(_QuestionnaireGate);
608
1072
  QuestionnaireGate.displayName = "QuestionnaireGate";
609
- var styles3 = reactNative.StyleSheet.create({
1073
+ var styles7 = reactNative.StyleSheet.create({
610
1074
  input: { borderWidth: 1 },
611
- link: { textAlign: "center", textDecorationLine: "underline" }
1075
+ link: { textAlign: "center", textDecorationLine: "underline" },
1076
+ centerText: { textAlign: "center" },
1077
+ header: { flexDirection: "row", alignItems: "center" },
1078
+ // Equal flex on both sides keeps the dots optically centred whether Back is drawn or not.
1079
+ // `flex-start` matters: without it the back Pressable stretches across the whole left
1080
+ // third of the card, so a tap anywhere near the dots would navigate back.
1081
+ headerSide: { flex: 1, alignItems: "flex-start" },
1082
+ backGlyph: { includeFontPadding: false }
612
1083
  });
613
1084
 
614
1085
  // src/coachmarks/runtime.ts
@@ -750,16 +1221,16 @@ var fetchWireFeatures = async (config) => {
750
1221
 
751
1222
  // src/features/useWireFeatures.ts
752
1223
  var useStableValue = (value, isEqual) => {
753
- const ref = React4.useRef(value);
1224
+ const ref = React8.useRef(value);
754
1225
  if (!isEqual(ref.current, value)) ref.current = value;
755
1226
  return ref.current;
756
1227
  };
757
1228
  var sameFetchKey = (a, b) => (a == null ? void 0 : a.serverUrl) === (b == null ? void 0 : b.serverUrl) && (a == null ? void 0 : a.apiKey) === (b == null ? void 0 : b.apiKey) && (a == null ? void 0 : a.appId) === (b == null ? void 0 : b.appId);
758
1229
  var useWireFeatures = (config) => {
759
1230
  const stableConfig = useStableValue(config, sameFetchKey);
760
- const [flags, setFlags] = React4.useState(defaultWireFeatures);
1231
+ const [flags, setFlags] = React8.useState(defaultWireFeatures);
761
1232
  const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
762
- React4.useEffect(() => {
1233
+ React8.useEffect(() => {
763
1234
  if (!canFetch) {
764
1235
  setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
765
1236
  return;
@@ -778,10 +1249,10 @@ var useWireFeatures = (config) => {
778
1249
  var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
779
1250
  var globalObj = global;
780
1251
  if (!globalObj[CONTEXT_SYMBOL]) {
781
- globalObj[CONTEXT_SYMBOL] = React4.createContext(null);
1252
+ globalObj[CONTEXT_SYMBOL] = React8.createContext(null);
782
1253
  }
783
1254
  var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
784
- var useWireFeaturesContext = () => React4.useContext(WireFeaturesContext);
1255
+ var useWireFeaturesContext = () => React8.useContext(WireFeaturesContext);
785
1256
  var useResolvedFeatures = (options) => {
786
1257
  var _a, _b, _c;
787
1258
  const ctx = useWireFeaturesContext();
@@ -838,7 +1309,7 @@ var questionnaireSessionsKey = (id) => `wire_questionnaire_${id}_sessions`;
838
1309
 
839
1310
  // src/questionnaire/useQuestionnaireGate.ts
840
1311
  var useStableValue2 = (value, isEqual) => {
841
- const ref = React4.useRef(value);
1312
+ const ref = React8.useRef(value);
842
1313
  if (!isEqual(ref.current, value)) ref.current = value;
843
1314
  return ref.current;
844
1315
  };
@@ -861,7 +1332,7 @@ var useQuestionnaireGate = ({
861
1332
  );
862
1333
  const lastKey = questionnaireLastShownKey(config.id);
863
1334
  const sessionsKey = questionnaireSessionsKey(config.id);
864
- const sessions = React4.useState(() => {
1335
+ const sessions = React8.useState(() => {
865
1336
  const store = resolveStorage(storage);
866
1337
  const next = readInt(store, sessionsKey) + 1;
867
1338
  writeInt(store, sessionsKey, next);
@@ -870,8 +1341,8 @@ var useQuestionnaireGate = ({
870
1341
  const hasServerDecision = decision != null;
871
1342
  const timeoutMs = config.timeoutFallbackMs;
872
1343
  const waits = !hasServerDecision && timeoutMs != null && timeoutMs > 0;
873
- const [elapsed, setElapsed] = React4.useState(!waits);
874
- React4.useEffect(() => {
1344
+ const [elapsed, setElapsed] = React8.useState(!waits);
1345
+ React8.useEffect(() => {
875
1346
  if (!waits) {
876
1347
  setElapsed(true);
877
1348
  return;
@@ -879,7 +1350,7 @@ var useQuestionnaireGate = ({
879
1350
  const timer = setTimeout(() => setElapsed(true), timeoutMs);
880
1351
  return () => clearTimeout(timer);
881
1352
  }, [waits, timeoutMs]);
882
- const { visible, verdict } = React4.useMemo(() => {
1353
+ const { visible, verdict } = React8.useMemo(() => {
883
1354
  if (!featureEnabled)
884
1355
  return {
885
1356
  visible: false,
@@ -916,16 +1387,16 @@ var useQuestionnaireGate = ({
916
1387
  storage,
917
1388
  isTesting
918
1389
  ]);
919
- const shownRef = React4.useRef(false);
920
- const markShown = React4.useCallback(() => {
1390
+ const shownRef = React8.useRef(false);
1391
+ const markShown = React8.useCallback(() => {
921
1392
  if (shownRef.current) return;
922
1393
  shownRef.current = true;
923
1394
  writeInt(resolveStorage(storage), lastKey, Date.now());
924
1395
  }, [storage, lastKey]);
925
- const markResolved = React4.useCallback(() => {
1396
+ const markResolved = React8.useCallback(() => {
926
1397
  markSeenGate(seenKey, storage, isTesting);
927
1398
  }, [seenKey, storage, isTesting]);
928
- return React4.useMemo(
1399
+ return React8.useMemo(
929
1400
  () => ({ visible, decision: verdict, markShown, markResolved }),
930
1401
  [visible, verdict, markShown, markResolved]
931
1402
  );