@wireai/activation 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/AGENTS.md +29 -5
  2. package/CHANGELOG.md +198 -6
  3. package/README.md +105 -10
  4. package/dist/analytics/index.d.mts +12 -8
  5. package/dist/analytics/index.d.ts +12 -8
  6. package/dist/analytics/index.js +525 -55
  7. package/dist/analytics/index.js.map +1 -1
  8. package/dist/analytics/index.mjs +525 -55
  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-CxnP7gAa.d.ts → currentSession-61dcm3V-.d.ts} +19 -3
  15. package/dist/{currentSession-BJBB7i4-.d.mts → currentSession-Bs2JfTJ8.d.mts} +19 -3
  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 +620 -13
  19. package/dist/index.d.ts +620 -13
  20. package/dist/index.js +1194 -369
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +1015 -201
  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-DzU-TqZz.d.ts +86 -0
  41. package/dist/transport-f5VVB5hH.d.mts +86 -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/analyticsFacade.ts +55 -10
  53. package/src/analytics/currentSession.ts +1 -1
  54. package/src/cards/CardGridSelectCard.tsx +253 -0
  55. package/src/cards/ChipSelectCard.tsx +1 -1
  56. package/src/cards/SelectionCard.tsx +15 -19
  57. package/src/cards/index.ts +13 -1
  58. package/src/cards/optionSchema.ts +65 -0
  59. package/src/coachmarks/types.ts +1 -1
  60. package/src/components/CardHandoff.tsx +39 -7
  61. package/src/components/DoneCheck.tsx +94 -0
  62. package/src/components/ModalPaneHost.tsx +141 -0
  63. package/src/components/StepDots.tsx +112 -0
  64. package/src/context/deviceId.ts +43 -0
  65. package/src/device/appVersion.ts +29 -8
  66. package/src/device/deviceContext.ts +14 -1
  67. package/src/device/deviceModel.ts +106 -0
  68. package/src/features/cache.ts +1 -1
  69. package/src/haptics/expo-haptics.d.ts +31 -0
  70. package/src/haptics/haptics.ts +58 -0
  71. package/src/icons/IconRegistry.tsx +53 -0
  72. package/src/icons/WireIcon.tsx +85 -0
  73. package/src/icons/expoIcons.ts +165 -0
  74. package/src/icons/index.ts +18 -0
  75. package/src/icons/vocabulary.ts +154 -0
  76. package/src/index.ts +22 -0
  77. package/src/questionnaire/QuestionnaireGate.tsx +315 -164
  78. package/src/questionnaire/decision.ts +4 -2
  79. package/src/questionnaire/index.ts +3 -2
  80. package/src/questionnaire/steps.ts +261 -0
  81. package/src/questionnaire/types.ts +33 -12
  82. package/src/reviews/ReviewGate.tsx +250 -152
  83. package/src/reviews/index.ts +7 -3
  84. package/src/reviews/transport.ts +77 -3
  85. package/src/reviews/types.ts +13 -0
  86. package/src/session/persistedSession.ts +1 -1
  87. package/src/session-analytics/lifecycle.ts +1 -1
  88. package/src/session-analytics/useSessionStart.ts +2 -2
  89. package/src/showcase/FeatureShowcase.tsx +1 -1
  90. package/src/showcase/showcaseColors.ts +1 -1
  91. package/src/theme/mergeThemeOver.ts +27 -0
  92. package/src/types.ts +8 -0
  93. package/dist/transport-BeO_Brcu.d.mts +0 -40
  94. package/dist/transport-DLpd1v5_.d.ts +0 -40
@@ -1,5 +1,5 @@
1
- import React4, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext } from 'react';
2
- import { StyleSheet, Animated, Easing, BackHandler, Modal, Pressable, Text, AccessibilityInfo, View, TextInput, Linking, Platform } from 'react-native';
1
+ import React7, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext } from 'react';
2
+ import { StyleSheet, Easing, Animated, BackHandler, Modal, Pressable, Text, View, AccessibilityInfo, TextInput, Linking, Platform } from 'react-native';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import { SafeAreaView } from 'react-native-safe-area-context';
5
5
 
@@ -88,10 +88,20 @@ var buttonLabelStyle = (f) => {
88
88
  };
89
89
 
90
90
  // src/motion/motionSpec.ts
91
+ var WIRE_BEZIER = [0.16, 1, 0.3, 1];
91
92
  var REDUCED_FADE_MS = 120;
92
93
  var BUTTON_PRESS_SCALE = 0.975;
93
94
  var REDUCED_PRESS_OPACITY = 0.85;
94
95
  var BUTTON_PRESS_SPRING = { friction: 8, tension: 240 };
96
+ var CARD_ENTER_MS = 220;
97
+ var CARD_EXIT_MS = 180;
98
+ var CARD_EXIT_LEAD_MS = 80;
99
+ var CARD_SLIDE_PX = 26;
100
+ var CARD_ENTER_SCALE_FROM = 0.96;
101
+ var CARD_SPRING = { damping: 14, stiffness: 180, mass: 1 };
102
+ var LOADER_HANDOFF_FADE_MS = 120;
103
+ var STATUS_POP_SPRING = { friction: 6, tension: 200 };
104
+ var STATUS_POP_SCALE_FROM = 0.4;
95
105
  var durationScale = 1;
96
106
  var scaledMs = (ms) => Math.round(ms * durationScale);
97
107
  var lastKnown = false;
@@ -205,11 +215,282 @@ var _Button = ({
205
215
  }
206
216
  );
207
217
  };
208
- var Button = React4.memo(_Button);
218
+ var Button = React7.memo(_Button);
209
219
  var styles = StyleSheet.create({
210
220
  base: { alignItems: "center", justifyContent: "center" },
211
221
  fill: { width: "100%" }
212
222
  });
223
+ var _DoneCheck = ({ theme, size = 48 }) => {
224
+ const reduced = useReducedMotion();
225
+ const pop = useRef(new Animated.Value(reduced ? 1 : 0)).current;
226
+ useEffect(() => {
227
+ if (reduced) {
228
+ pop.setValue(1);
229
+ return;
230
+ }
231
+ const anim = Animated.spring(pop, {
232
+ toValue: 1,
233
+ friction: STATUS_POP_SPRING.friction,
234
+ tension: STATUS_POP_SPRING.tension,
235
+ useNativeDriver: true
236
+ });
237
+ anim.start();
238
+ return () => anim.stop();
239
+ }, [pop, reduced]);
240
+ const scale = pop.interpolate({
241
+ inputRange: [0, 1],
242
+ outputRange: [STATUS_POP_SCALE_FROM, 1]
243
+ });
244
+ return /* @__PURE__ */ jsx(
245
+ Animated.View,
246
+ {
247
+ accessibilityElementsHidden: true,
248
+ importantForAccessibility: "no-hide-descendants",
249
+ style: [
250
+ styles2.ring,
251
+ {
252
+ width: size,
253
+ height: size,
254
+ borderRadius: theme.radius.full,
255
+ backgroundColor: theme.colors.success,
256
+ opacity: pop,
257
+ transform: [{ scale }]
258
+ }
259
+ ],
260
+ children: /* @__PURE__ */ jsx(
261
+ Animated.Text,
262
+ {
263
+ style: [
264
+ styles2.glyph,
265
+ { color: theme.colors.onPrimary, fontSize: Math.round(size * 0.5), fontFamily: theme.fonts.bold }
266
+ ],
267
+ children: "\u2713"
268
+ }
269
+ )
270
+ }
271
+ );
272
+ };
273
+ var DoneCheck = React7.memo(_DoneCheck);
274
+ DoneCheck.displayName = "DoneCheck";
275
+ var styles2 = StyleSheet.create({
276
+ ring: { alignItems: "center", justifyContent: "center", alignSelf: "center" },
277
+ glyph: { includeFontPadding: false, textAlign: "center" }
278
+ });
279
+ var easeWire = Easing.bezier(...WIRE_BEZIER);
280
+ var _CardHandoff = ({
281
+ transitionKey,
282
+ variant = "slide",
283
+ direction = "forward",
284
+ layout = "fill",
285
+ children
286
+ }) => {
287
+ const reduced = useReducedMotion();
288
+ const [leaving, setLeaving] = useState(null);
289
+ const enterT = useRef(new Animated.Value(0)).current;
290
+ const exitT = useRef(new Animated.Value(0)).current;
291
+ const prev = useRef({ key: transitionKey, node: children, variant });
292
+ const running = useRef({});
293
+ const mounted = useRef(false);
294
+ useEffect(() => {
295
+ var _a, _b;
296
+ const last = prev.current;
297
+ prev.current = { key: transitionKey, node: children, variant };
298
+ if (!mounted.current) {
299
+ mounted.current = true;
300
+ running.current.enter = startEnter(enterT, variant, reduced, 0);
301
+ return;
302
+ }
303
+ if (last.key === transitionKey) return;
304
+ (_a = running.current.enter) == null ? void 0 : _a.stop();
305
+ (_b = running.current.exit) == null ? void 0 : _b.stop();
306
+ setLeaving(last);
307
+ enterT.setValue(0);
308
+ exitT.setValue(0);
309
+ const exitDuration = reduced ? REDUCED_FADE_MS : last.variant === "fade" ? LOADER_HANDOFF_FADE_MS : CARD_EXIT_MS;
310
+ const exit = Animated.timing(exitT, {
311
+ toValue: 1,
312
+ duration: scaledMs(exitDuration),
313
+ easing: easeWire,
314
+ useNativeDriver: true
315
+ });
316
+ running.current.exit = exit;
317
+ exit.start();
318
+ const enter = startEnter(
319
+ enterT,
320
+ variant,
321
+ reduced,
322
+ reduced ? 0 : CARD_EXIT_LEAD_MS,
323
+ () => setLeaving(null)
324
+ );
325
+ running.current.enter = enter;
326
+ });
327
+ useEffect(
328
+ () => () => {
329
+ var _a, _b;
330
+ (_a = running.current.enter) == null ? void 0 : _a.stop();
331
+ (_b = running.current.exit) == null ? void 0 : _b.stop();
332
+ },
333
+ []
334
+ );
335
+ const enterStyle = {
336
+ opacity: enterT,
337
+ transform: transformFor(variant, enterT, reduced, "enter", direction)
338
+ };
339
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
340
+ leaving ? /* @__PURE__ */ jsx(
341
+ Animated.View,
342
+ {
343
+ pointerEvents: "none",
344
+ style: [
345
+ StyleSheet.absoluteFill,
346
+ {
347
+ opacity: exitT.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }),
348
+ // The CURRENT direction drives BOTH layers: one transition, one geography.
349
+ transform: transformFor(leaving.variant, exitT, reduced, "exit", direction)
350
+ }
351
+ ],
352
+ children: leaving.node
353
+ }
354
+ ) : null,
355
+ /* @__PURE__ */ jsx(Animated.View, { style: [layout === "fill" ? styles3.fill : null, enterStyle], children }, variant)
356
+ ] });
357
+ };
358
+ var startEnter = (value, variant, reduced, delay, onDone) => {
359
+ value.setValue(0);
360
+ const anim = !reduced && variant === "spring" ? Animated.spring(value, {
361
+ toValue: 1,
362
+ delay: scaledMs(delay),
363
+ damping: CARD_SPRING.damping,
364
+ stiffness: CARD_SPRING.stiffness,
365
+ mass: CARD_SPRING.mass,
366
+ useNativeDriver: true
367
+ }) : Animated.timing(value, {
368
+ toValue: 1,
369
+ delay: scaledMs(delay),
370
+ duration: scaledMs(
371
+ reduced ? REDUCED_FADE_MS : variant === "fade" ? LOADER_HANDOFF_FADE_MS : CARD_ENTER_MS
372
+ ),
373
+ easing: easeWire,
374
+ useNativeDriver: true
375
+ });
376
+ anim.start(({ finished }) => {
377
+ if (finished) onDone == null ? void 0 : onDone();
378
+ });
379
+ return anim;
380
+ };
381
+ var transformFor = (variant, t, reduced, layer, travel) => {
382
+ if (reduced || variant === "fade") return [];
383
+ if (variant === "spring" && layer === "enter") {
384
+ return [{ scale: t.interpolate({ inputRange: [0, 1], outputRange: [CARD_ENTER_SCALE_FROM, 1] }) }];
385
+ }
386
+ const slide = travel === "forward" ? CARD_SLIDE_PX : -CARD_SLIDE_PX;
387
+ return [
388
+ {
389
+ translateX: t.interpolate({
390
+ inputRange: [0, 1],
391
+ outputRange: layer === "enter" ? [slide, 0] : [0, -slide]
392
+ })
393
+ }
394
+ ];
395
+ };
396
+ var CardHandoff = React7.memo(_CardHandoff);
397
+ var styles3 = StyleSheet.create({
398
+ fill: { flex: 1 }
399
+ });
400
+ var easeWire2 = Easing.bezier(...WIRE_BEZIER);
401
+ var _ModalPaneHost = ({
402
+ paneKey,
403
+ direction = "forward",
404
+ header,
405
+ children
406
+ }) => {
407
+ const reduced = useReducedMotion();
408
+ const height = useRef(new Animated.Value(0)).current;
409
+ const measured = useRef(null);
410
+ const [sized, setSized] = useState(false);
411
+ const paneChanged = useRef(false);
412
+ const running = useRef(null);
413
+ useEffect(() => {
414
+ paneChanged.current = true;
415
+ }, [paneKey]);
416
+ useEffect(() => () => {
417
+ var _a;
418
+ return (_a = running.current) == null ? void 0 : _a.stop();
419
+ }, []);
420
+ const onLayout = useCallback(
421
+ (event) => {
422
+ var _a;
423
+ const next = Math.round(event.nativeEvent.layout.height);
424
+ if (!next) return;
425
+ const previous = measured.current;
426
+ if (previous !== null && Math.abs(previous - next) < 1) return;
427
+ const first = previous === null;
428
+ const ease = !first && !reduced && paneChanged.current;
429
+ paneChanged.current = false;
430
+ measured.current = next;
431
+ (_a = running.current) == null ? void 0 : _a.stop();
432
+ if (!ease) {
433
+ height.setValue(next);
434
+ if (first) setSized(true);
435
+ return;
436
+ }
437
+ const anim = Animated.timing(height, {
438
+ toValue: next,
439
+ duration: scaledMs(CARD_ENTER_MS),
440
+ easing: easeWire2,
441
+ // height is layout, not transform — see the header note.
442
+ useNativeDriver: false
443
+ });
444
+ running.current = anim;
445
+ anim.start();
446
+ },
447
+ [height, reduced]
448
+ );
449
+ return /* @__PURE__ */ jsx(Animated.View, { style: sized ? { height, overflow: "hidden" } : void 0, children: /* @__PURE__ */ jsxs(View, { onLayout, style: styles4.pane, children: [
450
+ header,
451
+ /* @__PURE__ */ jsx(CardHandoff, { transitionKey: paneKey, direction, layout: "auto", variant: "slide", children })
452
+ ] }) });
453
+ };
454
+ var ModalPaneHost = React7.memo(_ModalPaneHost);
455
+ ModalPaneHost.displayName = "ModalPaneHost";
456
+ var styles4 = StyleSheet.create({
457
+ pane: { width: "100%" }
458
+ });
459
+
460
+ // src/haptics/haptics.ts
461
+ var play = async (tone) => {
462
+ try {
463
+ const mod = await import('expo-haptics');
464
+ if (!mod) return false;
465
+ if (tone === "selection") {
466
+ await mod.selectionAsync();
467
+ return true;
468
+ }
469
+ if (tone === "light") {
470
+ await mod.impactAsync(mod.ImpactFeedbackStyle.Light);
471
+ return true;
472
+ }
473
+ await mod.notificationAsync(mod.NotificationFeedbackType.Success);
474
+ return true;
475
+ } catch {
476
+ return false;
477
+ }
478
+ };
479
+ var playHaptic = (tone) => {
480
+ void play(tone);
481
+ };
482
+
483
+ // src/theme/mergeThemeOver.ts
484
+ var mergeThemeOver = (base, override) => {
485
+ if (!override) return base;
486
+ return {
487
+ colors: { ...base.colors, ...override.colors },
488
+ fonts: { ...base.fonts, ...override.fonts },
489
+ radius: { ...base.radius, ...override.radius },
490
+ spacing: { ...base.spacing, ...override.spacing },
491
+ button: { ...base.button, ...override.button }
492
+ };
493
+ };
213
494
 
214
495
  // src/reviews/decision.ts
215
496
  var decideReview = (local, decision) => decision != null ? decision : local;
@@ -352,25 +633,25 @@ var _CenteredModal = forwardRef(
352
633
  /* @__PURE__ */ jsx(
353
634
  Animated.View,
354
635
  {
355
- style: [styles2.backdrop, { opacity: backdropOpacity }],
636
+ style: [styles5.backdrop, { opacity: backdropOpacity }],
356
637
  pointerEvents: "none"
357
638
  }
358
639
  ),
359
640
  /* @__PURE__ */ jsx(
360
641
  Pressable,
361
642
  {
362
- style: styles2.fill,
643
+ style: styles5.fill,
363
644
  onPress: onBackdrop,
364
645
  accessibilityRole: "button",
365
646
  accessibilityLabel: "Dismiss",
366
- children: /* @__PURE__ */ jsx(SafeAreaView, { style: styles2.center, pointerEvents: "box-none", children: /* @__PURE__ */ jsx(Pressable, { onPress: () => {
367
- }, style: styles2.cardWrap, children: /* @__PURE__ */ jsx(
647
+ children: /* @__PURE__ */ jsx(SafeAreaView, { style: styles5.center, pointerEvents: "box-none", children: /* @__PURE__ */ jsx(Pressable, { onPress: () => {
648
+ }, style: styles5.cardWrap, children: /* @__PURE__ */ jsx(
368
649
  Animated.View,
369
650
  {
370
651
  accessibilityViewIsModal: true,
371
652
  accessibilityRole: "alert",
372
653
  style: [
373
- styles2.card,
654
+ styles5.card,
374
655
  {
375
656
  backgroundColor: theme.colors.surface,
376
657
  borderRadius: theme.radius.lg,
@@ -391,8 +672,8 @@ var _CenteredModal = forwardRef(
391
672
  }
392
673
  );
393
674
  _CenteredModal.displayName = "CenteredModal";
394
- var CenteredModal = React4.memo(_CenteredModal);
395
- var styles2 = StyleSheet.create({
675
+ var CenteredModal = React7.memo(_CenteredModal);
676
+ var styles5 = StyleSheet.create({
396
677
  fill: { flex: 1 },
397
678
  backdrop: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#000" },
398
679
  center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
@@ -457,6 +738,26 @@ var submitReview = (target, review) => {
457
738
  } catch {
458
739
  }
459
740
  };
741
+ var fetchReviewDecision = async (target, options = {}) => {
742
+ if (!(target == null ? void 0 : target.serverUrl)) return null;
743
+ try {
744
+ const base = target.serverUrl.replace(/\/$/, "");
745
+ const params = new URLSearchParams();
746
+ if (options.sessionId) params.set("session_id", options.sessionId);
747
+ if (options.deviceKey) params.set("device_key", options.deviceKey);
748
+ const qs = params.toString();
749
+ const url = `${base}/v1/reviews/decision${qs ? `?${qs}` : ""}`;
750
+ const headers = {};
751
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
752
+ const res = await fetch(url, { headers });
753
+ if (!res || !res.ok) return null;
754
+ const json = await res.json();
755
+ if (!json || typeof json.fire !== "boolean") return null;
756
+ return json;
757
+ } catch {
758
+ return null;
759
+ }
760
+ };
460
761
  var reportAppEvent = (target, name, options = {}) => {
461
762
  if (!(target == null ? void 0 : target.serverUrl) || !name) return;
462
763
  try {
@@ -481,16 +782,6 @@ var reportAppEvent = (target, name, options = {}) => {
481
782
  } catch {
482
783
  }
483
784
  };
484
- var mergeThemeOver = (base, override) => {
485
- if (!override) return base;
486
- return {
487
- colors: { ...base.colors, ...override.colors },
488
- fonts: { ...base.fonts, ...override.fonts },
489
- radius: { ...base.radius, ...override.radius },
490
- spacing: { ...base.spacing, ...override.spacing },
491
- button: { ...base.button, ...override.button }
492
- };
493
- };
494
785
  var _ReviewGate = ({
495
786
  appName,
496
787
  store,
@@ -508,26 +799,26 @@ var _ReviewGate = ({
508
799
  const active = useOnboardingTheme();
509
800
  const t = useMemo(() => mergeThemeOver(active, themeOverride), [active, themeOverride]);
510
801
  const mode = resolvePresentation(presentation);
511
- const modalRef = React4.useRef(null);
802
+ const modalRef = React7.useRef(null);
512
803
  const [phase, setPhase] = useState("rating");
513
804
  const [stars, setStars] = useState(0);
514
805
  const [hovered, setHovered] = useState(0);
515
806
  const [feedback, setFeedback] = useState("");
516
807
  const [suggestion, setSuggestion] = useState("");
517
- const shownRef = React4.useRef(false);
518
- const onShownRef = React4.useRef(onShown);
519
- const onEventRef = React4.useRef(onEvent);
808
+ const shownRef = React7.useRef(false);
809
+ const onShownRef = React7.useRef(onShown);
810
+ const onEventRef = React7.useRef(onEvent);
520
811
  onShownRef.current = onShown;
521
812
  onEventRef.current = onEvent;
522
- React4.useEffect(() => {
813
+ React7.useEffect(() => {
523
814
  var _a, _b;
524
815
  if (shownRef.current) return;
525
816
  shownRef.current = true;
526
817
  (_a = onShownRef.current) == null ? void 0 : _a.call(onShownRef);
527
818
  (_b = onEventRef.current) == null ? void 0 : _b.call(onEventRef, { name: "review_prompt_shown", id });
528
819
  }, []);
529
- const resolveTimer = React4.useRef(null);
530
- React4.useEffect(() => {
820
+ const resolveTimer = React7.useRef(null);
821
+ React7.useEffect(() => {
531
822
  return () => {
532
823
  if (resolveTimer.current) clearTimeout(resolveTimer.current);
533
824
  };
@@ -535,6 +826,30 @@ var _ReviewGate = ({
535
826
  const resolve = useCallback(() => {
536
827
  onResolved == null ? void 0 : onResolved();
537
828
  }, [onResolved]);
829
+ const postedRef = React7.useRef(false);
830
+ const postOnce = useCallback(
831
+ (body) => {
832
+ if (postedRef.current) return;
833
+ postedRef.current = true;
834
+ submitReview(target, body);
835
+ },
836
+ [target]
837
+ );
838
+ const abandonRef = React7.useRef(() => {
839
+ });
840
+ abandonRef.current = () => {
841
+ if (postedRef.current || stars < 1) return;
842
+ postOnce(
843
+ buildReviewSubmission({
844
+ stars,
845
+ feedbackText: feedback,
846
+ suggestion,
847
+ sessionId,
848
+ meta: { ...meta, abandoned: true }
849
+ })
850
+ );
851
+ };
852
+ React7.useEffect(() => () => abandonRef.current(), []);
538
853
  const resolveWithDelay = useCallback(() => {
539
854
  if (resolveTimer.current) clearTimeout(resolveTimer.current);
540
855
  resolveTimer.current = setTimeout(() => {
@@ -549,19 +864,29 @@ var _ReviewGate = ({
549
864
  const pickRating = useCallback(
550
865
  async (value) => {
551
866
  setStars(value);
867
+ playHaptic("selection");
552
868
  onEvent == null ? void 0 : onEvent({ name: "review_rating_selected", id, stars: value });
553
869
  if (routeRating(value) === "store") {
554
870
  onEvent == null ? void 0 : onEvent({ name: "store_review_requested", id, stars: value });
555
- await requestStoreReview(store);
871
+ const route = await requestStoreReview(store);
872
+ postOnce(
873
+ buildReviewSubmission({
874
+ stars: value,
875
+ sessionId,
876
+ meta: { ...meta, store_route: route }
877
+ })
878
+ );
556
879
  setPhase("thanks");
557
880
  resolveWithDelay();
558
881
  } else {
559
882
  setPhase("feedback");
560
883
  }
561
884
  },
562
- [onEvent, id, store, resolveWithDelay]
885
+ [onEvent, id, store, postOnce, sessionId, meta, resolveWithDelay]
563
886
  );
887
+ const feedbackReady = feedback.trim().length > 0;
564
888
  const sendFeedback = useCallback(() => {
889
+ if (!feedbackReady) return;
565
890
  const body = buildReviewSubmission({
566
891
  stars,
567
892
  feedbackText: feedback,
@@ -569,11 +894,12 @@ var _ReviewGate = ({
569
894
  sessionId,
570
895
  meta
571
896
  });
572
- submitReview(target, body);
897
+ postOnce(body);
573
898
  onEvent == null ? void 0 : onEvent({ name: "review_feedback_submitted", id, stars });
899
+ playHaptic("success");
574
900
  setPhase("thanks");
575
901
  resolveWithDelay();
576
- }, [stars, feedback, suggestion, sessionId, meta, target, onEvent, id, resolveWithDelay]);
902
+ }, [feedbackReady, stars, feedback, suggestion, sessionId, meta, postOnce, onEvent, id, resolveWithDelay]);
577
903
  const dismiss = useCallback(() => {
578
904
  setPhase("thanks");
579
905
  resolveWithDelay();
@@ -583,7 +909,6 @@ var _ReviewGate = ({
583
909
  if (mode === "modal") (_a = modalRef.current) == null ? void 0 : _a.close();
584
910
  else dismiss();
585
911
  }, [mode, dismiss]);
586
- const title = phase === "feedback" ? "What should we improve?" : phase === "thanks" ? "Thank you" : appName ? `Enjoying ${appName}?` : "How's your experience?";
587
912
  const surface = {
588
913
  backgroundColor: t.colors.surface,
589
914
  borderRadius: t.radius.lg,
@@ -591,113 +916,121 @@ var _ReviewGate = ({
591
916
  gap: t.spacing.md
592
917
  };
593
918
  const feedbackBox = feedbackInputHeights(t.fonts, t.spacing.md);
594
- const content = /* @__PURE__ */ jsxs(Fragment, { children: [
595
- /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text }], children: title }),
596
- phase === "rating" && /* @__PURE__ */ jsxs(Fragment, { children: [
597
- /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "Tap a star to let us know." }),
598
- /* @__PURE__ */ jsx(View, { style: styles3.stars, children: [1, 2, 3, 4, 5].map((n) => {
599
- const filled = n <= (hovered || stars);
600
- return /* @__PURE__ */ jsx(
601
- Pressable,
602
- {
603
- onPressIn: () => setHovered(n),
604
- onPressOut: () => setHovered(0),
605
- onPress: () => void pickRating(n),
606
- accessibilityRole: "button",
607
- accessibilityLabel: `${n} star${n > 1 ? "s" : ""}`,
608
- hitSlop: 8,
609
- children: /* @__PURE__ */ jsx(
610
- Text,
611
- {
612
- style: [
613
- styles3.star,
614
- { color: filled ? t.colors.primary : t.colors.border }
615
- ],
616
- children: filled ? "\u2605" : "\u2606"
617
- }
618
- )
619
- },
620
- n
621
- );
622
- }) }),
623
- dismissible && /* @__PURE__ */ jsx(Pressable, { onPress: handleDismissTap, hitSlop: 8, children: /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), styles3.link, { color: t.colors.textMuted }], children: "Not now" }) })
624
- ] }),
625
- phase === "feedback" && /* @__PURE__ */ jsxs(Fragment, { children: [
626
- /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "A few words is plenty. Both boxes are optional, so send whatever you like." }),
627
- /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "Why this rating? (optional)" }),
628
- /* @__PURE__ */ jsx(
629
- TextInput,
630
- {
631
- value: feedback,
632
- onChangeText: setFeedback,
633
- placeholder: "e.g. too many steps",
634
- placeholderTextColor: t.colors.textMuted,
635
- multiline: true,
636
- textAlignVertical: "top",
637
- style: [
638
- bodyStyle(t.fonts),
639
- styles3.input,
640
- {
641
- color: t.colors.text,
642
- backgroundColor: t.colors.background,
643
- borderRadius: t.radius.md,
644
- borderColor: t.colors.border,
645
- padding: t.spacing.md,
646
- minHeight: feedbackBox.minHeight,
647
- maxHeight: feedbackBox.maxHeight
648
- }
649
- ]
650
- }
651
- ),
652
- /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "What can we do better? (optional)" }),
653
- /* @__PURE__ */ jsx(
654
- TextInput,
655
- {
656
- value: suggestion,
657
- onChangeText: setSuggestion,
658
- placeholder: "e.g. save my place between steps",
659
- placeholderTextColor: t.colors.textMuted,
660
- style: [
661
- bodyStyle(t.fonts),
662
- styles3.input,
663
- {
664
- color: t.colors.text,
665
- backgroundColor: t.colors.background,
666
- borderRadius: t.radius.md,
667
- borderColor: t.colors.border,
668
- paddingHorizontal: t.spacing.md,
669
- paddingVertical: t.spacing.sm,
670
- minHeight: t.button.height
671
- }
672
- ]
673
- }
674
- ),
675
- /* @__PURE__ */ jsx(
676
- Button,
919
+ const linkStyle = [captionStyle(t.fonts), styles6.link, { color: t.colors.textMuted }];
920
+ const ratingPane = /* @__PURE__ */ jsxs(View, { style: { gap: t.spacing.md }, children: [
921
+ /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text }], children: appName ? `Enjoying ${appName}?` : "How's your experience?" }),
922
+ /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "Tap a star to let us know." }),
923
+ /* @__PURE__ */ jsx(View, { style: styles6.stars, children: [1, 2, 3, 4, 5].map((n) => {
924
+ const filled = n <= (hovered || stars);
925
+ return /* @__PURE__ */ jsx(
926
+ Pressable,
677
927
  {
678
- title: "Send feedback",
679
- onPress: sendFeedback,
680
- variant: "primary",
681
- fullWidth: true
682
- }
683
- ),
684
- dismissible && /* @__PURE__ */ jsx(Pressable, { onPress: handleDismissTap, hitSlop: 8, children: /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), styles3.link, { color: t.colors.textMuted }], children: "Skip" }) })
685
- ] }),
686
- phase === "thanks" && /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: "We appreciate you. This helps us make it better." })
928
+ onPressIn: () => setHovered(n),
929
+ onPressOut: () => setHovered(0),
930
+ onPress: () => void pickRating(n),
931
+ accessibilityRole: "button",
932
+ accessibilityLabel: `${n} star${n > 1 ? "s" : ""}`,
933
+ hitSlop: 8,
934
+ children: /* @__PURE__ */ jsx(Text, { style: [styles6.star, { color: filled ? t.colors.primary : t.colors.border }], children: filled ? "\u2605" : "\u2606" })
935
+ },
936
+ n
937
+ );
938
+ }) }),
939
+ dismissible && /* @__PURE__ */ jsx(
940
+ Pressable,
941
+ {
942
+ onPress: handleDismissTap,
943
+ hitSlop: 8,
944
+ accessibilityRole: "button",
945
+ accessibilityLabel: "Dismiss the review prompt",
946
+ children: /* @__PURE__ */ jsx(Text, { style: linkStyle, children: "Not now" })
947
+ }
948
+ )
949
+ ] });
950
+ const feedbackPane = /* @__PURE__ */ jsxs(View, { style: { gap: t.spacing.md }, children: [
951
+ /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text }], children: "What should we improve?" }),
952
+ /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "A few words is plenty \u2014 tell us what went wrong and we'll fix it." }),
953
+ /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "Why this rating?" }),
954
+ /* @__PURE__ */ jsx(
955
+ TextInput,
956
+ {
957
+ value: feedback,
958
+ onChangeText: setFeedback,
959
+ placeholder: "e.g. too many steps",
960
+ placeholderTextColor: t.colors.textMuted,
961
+ accessibilityLabel: "Why this rating?",
962
+ multiline: true,
963
+ textAlignVertical: "top",
964
+ style: [
965
+ bodyStyle(t.fonts),
966
+ styles6.input,
967
+ {
968
+ color: t.colors.text,
969
+ backgroundColor: t.colors.background,
970
+ borderRadius: t.radius.md,
971
+ borderColor: t.colors.border,
972
+ padding: t.spacing.md,
973
+ minHeight: feedbackBox.minHeight,
974
+ maxHeight: feedbackBox.maxHeight
975
+ }
976
+ ]
977
+ }
978
+ ),
979
+ /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "What can we do better? (optional)" }),
980
+ /* @__PURE__ */ jsx(
981
+ TextInput,
982
+ {
983
+ value: suggestion,
984
+ onChangeText: setSuggestion,
985
+ placeholder: "e.g. save my place between steps",
986
+ placeholderTextColor: t.colors.textMuted,
987
+ accessibilityLabel: "What can we do better? (optional)",
988
+ style: [
989
+ bodyStyle(t.fonts),
990
+ styles6.input,
991
+ {
992
+ color: t.colors.text,
993
+ backgroundColor: t.colors.background,
994
+ borderRadius: t.radius.md,
995
+ borderColor: t.colors.border,
996
+ paddingHorizontal: t.spacing.md,
997
+ paddingVertical: t.spacing.sm,
998
+ minHeight: t.button.height
999
+ }
1000
+ ]
1001
+ }
1002
+ ),
1003
+ /* @__PURE__ */ jsx(
1004
+ Button,
1005
+ {
1006
+ title: "Send feedback",
1007
+ onPress: sendFeedback,
1008
+ variant: "primary",
1009
+ fullWidth: true,
1010
+ disabled: !feedbackReady
1011
+ }
1012
+ )
687
1013
  ] });
1014
+ const thanksPane = /* @__PURE__ */ jsxs(View, { style: { gap: t.spacing.md }, children: [
1015
+ /* @__PURE__ */ jsx(DoneCheck, { theme: t }),
1016
+ /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), styles6.centerText, { color: t.colors.text }], children: "Thank you" }),
1017
+ /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), styles6.centerText, { color: t.colors.textMuted }], children: "We appreciate you. This helps us make it better." })
1018
+ ] });
1019
+ const content = /* @__PURE__ */ jsx(ModalPaneHost, { paneKey: phase, children: phase === "rating" ? ratingPane : phase === "feedback" ? feedbackPane : thanksPane });
688
1020
  if (mode === "modal") {
689
1021
  return /* @__PURE__ */ jsx(CenteredModal, { ref: modalRef, theme: t, dismissible, onDismiss: resolve, children: content });
690
1022
  }
691
- return /* @__PURE__ */ jsx(View, { style: [styles3.card, surface], children: content });
1023
+ return /* @__PURE__ */ jsx(View, { style: [styles6.card, surface], children: content });
692
1024
  };
693
- var ReviewGate = React4.memo(_ReviewGate);
1025
+ var ReviewGate = React7.memo(_ReviewGate);
694
1026
  ReviewGate.displayName = "ReviewGate";
695
- var styles3 = StyleSheet.create({
1027
+ var styles6 = StyleSheet.create({
696
1028
  card: { width: "100%" },
697
1029
  stars: { flexDirection: "row", justifyContent: "center", gap: 8 },
698
1030
  star: { fontSize: 40, lineHeight: 46 },
699
1031
  input: { borderWidth: 1 },
700
- link: { textAlign: "center", textDecorationLine: "underline" }
1032
+ link: { textAlign: "center", textDecorationLine: "underline" },
1033
+ centerText: { textAlign: "center" }
701
1034
  });
702
1035
 
703
1036
  // src/coachmarks/runtime.ts
@@ -997,6 +1330,6 @@ var useReviewGate = ({
997
1330
  );
998
1331
  };
999
1332
 
1000
- export { DEFAULT_PRESENTATION, FEEDBACK_MAX_LINES, FEEDBACK_MIN_LINES, CENTERED_MODAL_ANIM as REVIEW_MODAL_ANIM, ReviewGate, CenteredModal as ReviewModal, buildReviewSubmission, decideReview, centeredModalFrame as entranceFrame, evaluateGate, feedbackInputHeights, openStoreListing, reportAppEvent, requestStoreReview, resolvePresentation, resolveRules, reviewLastShownKey, reviewSeenKey, reviewSessionsKey, routeRating, sameDecision, shallowEqual, storeUrl, submitReview, useReviewGate };
1333
+ export { DEFAULT_PRESENTATION, FEEDBACK_MAX_LINES, FEEDBACK_MIN_LINES, CENTERED_MODAL_ANIM as REVIEW_MODAL_ANIM, ReviewGate, CenteredModal as ReviewModal, buildReviewSubmission, decideReview, centeredModalFrame as entranceFrame, evaluateGate, feedbackInputHeights, fetchReviewDecision, openStoreListing, reportAppEvent, requestStoreReview, resolvePresentation, resolveRules, reviewLastShownKey, reviewSeenKey, reviewSessionsKey, routeRating, sameDecision, shallowEqual, storeUrl, submitReview, useReviewGate };
1001
1334
  //# sourceMappingURL=index.mjs.map
1002
1335
  //# sourceMappingURL=index.mjs.map