@wireai/activation 0.1.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 (142) hide show
  1. package/AGENTS.md +73 -0
  2. package/CHANGELOG.md +572 -0
  3. package/INTEGRATION_PROMPT.md +55 -0
  4. package/LICENSE +21 -0
  5. package/README.md +923 -0
  6. package/dist/CenteredModal-C3qQBHsA.d.mts +38 -0
  7. package/dist/CenteredModal-Cdgns6--.d.ts +38 -0
  8. package/dist/coachmarks/index.d.mts +352 -0
  9. package/dist/coachmarks/index.d.ts +352 -0
  10. package/dist/coachmarks/index.js +944 -0
  11. package/dist/coachmarks/index.js.map +1 -0
  12. package/dist/coachmarks/index.mjs +918 -0
  13. package/dist/coachmarks/index.mjs.map +1 -0
  14. package/dist/decision-CFvGY6nP.d.ts +228 -0
  15. package/dist/decision-Cl8OFYzu.d.mts +228 -0
  16. package/dist/index.d.mts +1390 -0
  17. package/dist/index.d.ts +1390 -0
  18. package/dist/index.js +3260 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/index.mjs +3186 -0
  21. package/dist/index.mjs.map +1 -0
  22. package/dist/questionnaire/index.d.mts +249 -0
  23. package/dist/questionnaire/index.d.ts +249 -0
  24. package/dist/questionnaire/index.js +922 -0
  25. package/dist/questionnaire/index.js.map +1 -0
  26. package/dist/questionnaire/index.mjs +906 -0
  27. package/dist/questionnaire/index.mjs.map +1 -0
  28. package/dist/reviews/index.d.mts +213 -0
  29. package/dist/reviews/index.d.ts +213 -0
  30. package/dist/reviews/index.js +1001 -0
  31. package/dist/reviews/index.js.map +1 -0
  32. package/dist/reviews/index.mjs +971 -0
  33. package/dist/reviews/index.mjs.map +1 -0
  34. package/dist/showcase/index.d.mts +119 -0
  35. package/dist/showcase/index.d.ts +119 -0
  36. package/dist/showcase/index.js +660 -0
  37. package/dist/showcase/index.js.map +1 -0
  38. package/dist/showcase/index.mjs +651 -0
  39. package/dist/showcase/index.mjs.map +1 -0
  40. package/dist/types-BKfpdZzX.d.mts +84 -0
  41. package/dist/types-BKfpdZzX.d.ts +84 -0
  42. package/dist/types-CMuOexw0.d.mts +116 -0
  43. package/dist/types-CMuOexw0.d.ts +116 -0
  44. package/dist/types-GL_hQ0TN.d.mts +46 -0
  45. package/dist/types-GL_hQ0TN.d.ts +46 -0
  46. package/llms.txt +36 -0
  47. package/metro/index.d.ts +32 -0
  48. package/metro/index.js +83 -0
  49. package/package.json +171 -0
  50. package/src/DemoOnboarding.tsx +133 -0
  51. package/src/OnboardingFlow.tsx +504 -0
  52. package/src/WireOnboarding.tsx +248 -0
  53. package/src/analytics/analyticsEvent.ts +69 -0
  54. package/src/analytics/reportClientEvent.ts +119 -0
  55. package/src/analytics/sendPreview.ts +73 -0
  56. package/src/attribution/attribution.ts +46 -0
  57. package/src/cards/ChipSelectCard.tsx +265 -0
  58. package/src/cards/InterstitialCard.tsx +299 -0
  59. package/src/cards/NumberStepperCard.tsx +121 -0
  60. package/src/cards/SelectionCard.tsx +270 -0
  61. package/src/cards/StatusCard.tsx +170 -0
  62. package/src/cards/TextInputCard.tsx +147 -0
  63. package/src/cards/index.ts +23 -0
  64. package/src/coachmarks/CoachmarkOverlayHost.tsx +44 -0
  65. package/src/coachmarks/CoachmarkProvider.tsx +108 -0
  66. package/src/coachmarks/GestureHint.tsx +331 -0
  67. package/src/coachmarks/SpotlightOverlay.tsx +254 -0
  68. package/src/coachmarks/coachmarkAnchorRegistry.ts +70 -0
  69. package/src/coachmarks/coachmarkOverlayStore.ts +63 -0
  70. package/src/coachmarks/index.ts +62 -0
  71. package/src/coachmarks/runtime.ts +103 -0
  72. package/src/coachmarks/selectTourSteps.ts +45 -0
  73. package/src/coachmarks/types.ts +58 -0
  74. package/src/coachmarks/useCoachmarkAnchor.ts +49 -0
  75. package/src/coachmarks/useCoachmarkTour.ts +200 -0
  76. package/src/components/AnimatedSparkle.tsx +101 -0
  77. package/src/components/Button.tsx +129 -0
  78. package/src/components/CardHandoff.tsx +212 -0
  79. package/src/components/CardLayout.tsx +103 -0
  80. package/src/components/CenteredModal.tsx +184 -0
  81. package/src/components/CompletionView.tsx +186 -0
  82. package/src/components/DoneBlock.tsx +37 -0
  83. package/src/components/ErrorBlock.tsx +42 -0
  84. package/src/components/Illustration.tsx +36 -0
  85. package/src/components/LoadingBlock.tsx +182 -0
  86. package/src/components/LoadingScreen.tsx +35 -0
  87. package/src/components/OnboardingScaffold.tsx +105 -0
  88. package/src/components/StepProgress.tsx +85 -0
  89. package/src/components/loaderChrome.ts +28 -0
  90. package/src/config/onboardingFlag.ts +39 -0
  91. package/src/config/wireConfigFromEnv.ts +52 -0
  92. package/src/device/deviceContext.ts +158 -0
  93. package/src/features/WireFeaturesProvider.tsx +81 -0
  94. package/src/features/cache.ts +92 -0
  95. package/src/features/defaults.ts +67 -0
  96. package/src/features/fetchWireFeatures.ts +86 -0
  97. package/src/features/index.ts +39 -0
  98. package/src/features/types.ts +61 -0
  99. package/src/features/useWireFeatures.ts +57 -0
  100. package/src/identity/userIdentity.ts +100 -0
  101. package/src/illustrations/defaultIllustrations.tsx +99 -0
  102. package/src/index.ts +159 -0
  103. package/src/motion/centeredModalMotion.ts +45 -0
  104. package/src/motion/motionSpec.ts +133 -0
  105. package/src/motion/useReducedMotion.ts +43 -0
  106. package/src/questionnaire/QuestionnaireGate.tsx +235 -0
  107. package/src/questionnaire/decision.ts +63 -0
  108. package/src/questionnaire/index.ts +54 -0
  109. package/src/questionnaire/runtime.ts +20 -0
  110. package/src/questionnaire/transport.ts +83 -0
  111. package/src/questionnaire/types.ts +182 -0
  112. package/src/questionnaire/useQuestionnaireGate.ts +159 -0
  113. package/src/reviews/ReviewGate.tsx +316 -0
  114. package/src/reviews/ReviewModal.tsx +13 -0
  115. package/src/reviews/decision.ts +120 -0
  116. package/src/reviews/equality.ts +54 -0
  117. package/src/reviews/expo-store-review.d.ts +16 -0
  118. package/src/reviews/feedbackForm.ts +41 -0
  119. package/src/reviews/index.ts +78 -0
  120. package/src/reviews/presentation.ts +29 -0
  121. package/src/reviews/runtime.ts +45 -0
  122. package/src/reviews/storeReview.ts +62 -0
  123. package/src/reviews/transport.ts +89 -0
  124. package/src/reviews/types.ts +172 -0
  125. package/src/reviews/useReviewGate.ts +137 -0
  126. package/src/session/persistedSession.ts +143 -0
  127. package/src/session-analytics/index.ts +18 -0
  128. package/src/session-analytics/reportSessionStart.ts +144 -0
  129. package/src/session-analytics/useSessionStart.ts +114 -0
  130. package/src/showcase/FeatureShowcase.tsx +233 -0
  131. package/src/showcase/index.ts +19 -0
  132. package/src/showcase/selectShowcaseSlides.ts +45 -0
  133. package/src/showcase/showcaseColors.ts +91 -0
  134. package/src/showcase/types.ts +74 -0
  135. package/src/theme/ThemeContext.tsx +38 -0
  136. package/src/theme/defaultTheme.ts +70 -0
  137. package/src/theme/themeFromBrand.ts +93 -0
  138. package/src/theme/types.ts +88 -0
  139. package/src/theme/typography.ts +46 -0
  140. package/src/types.ts +215 -0
  141. package/src/utils/deriveAnswers.ts +43 -0
  142. package/src/utils/readProgress.ts +33 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,3186 @@
1
+ import React14, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useContext, useMemo } from 'react';
2
+ import { useWireAIThread, useWireAIAction, ComponentRenderer, WireAIProvider } from 'wireai-rn';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+ import { StyleSheet, Easing, Animated, BackHandler, Modal, Pressable, Text, View, TouchableOpacity, KeyboardAvoidingView, ScrollView, useWindowDimensions, TextInput, Image, AccessibilityInfo, Platform, Dimensions, I18nManager, AppState } from 'react-native';
5
+ import { SafeAreaView } from 'react-native-safe-area-context';
6
+ import { z } from 'zod';
7
+
8
+ var __defProp = Object.defineProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+
14
+ // src/theme/defaultTheme.ts
15
+ var defaultOnboardingTheme = {
16
+ colors: {
17
+ background: "#FFFFFF",
18
+ surface: "#F7F7F8",
19
+ text: "#101114",
20
+ textMuted: "#6B7280",
21
+ primary: "#111111",
22
+ onPrimary: "#FFFFFF",
23
+ primarySoft: "rgba(17,17,17,0.06)",
24
+ border: "#E4E4E7",
25
+ borderStrong: "#C7C7CC",
26
+ disabled: "#EBEBEB",
27
+ success: "#16A34A",
28
+ warning: "#D97706",
29
+ error: "#DC2626",
30
+ info: "#2563EB"
31
+ },
32
+ fonts: {
33
+ regular: void 0,
34
+ medium: void 0,
35
+ bold: void 0,
36
+ baseSize: 16,
37
+ scale: 1
38
+ },
39
+ radius: {
40
+ sm: 8,
41
+ md: 12,
42
+ lg: 16,
43
+ full: 9999
44
+ },
45
+ spacing: {
46
+ xs: 4,
47
+ sm: 8,
48
+ md: 16,
49
+ lg: 24,
50
+ xl: 32
51
+ },
52
+ // Circled (pill) CTA by default; an app can pass `button: { radius: 12 }` for
53
+ // a rounded rectangle, or override height/paddingX to match its design system.
54
+ button: {
55
+ radius: 9999,
56
+ height: 52,
57
+ paddingX: 24
58
+ }
59
+ };
60
+ var mergeTheme = (override) => {
61
+ if (!override) return defaultOnboardingTheme;
62
+ return {
63
+ colors: { ...defaultOnboardingTheme.colors, ...override.colors },
64
+ fonts: { ...defaultOnboardingTheme.fonts, ...override.fonts },
65
+ radius: { ...defaultOnboardingTheme.radius, ...override.radius },
66
+ spacing: { ...defaultOnboardingTheme.spacing, ...override.spacing },
67
+ button: { ...defaultOnboardingTheme.button, ...override.button }
68
+ };
69
+ };
70
+ var OnboardingThemeContext = createContext(null);
71
+ var OnboardingThemeProvider = ({
72
+ theme,
73
+ children
74
+ }) => {
75
+ const resolved = useMemo(() => mergeTheme(theme), [theme]);
76
+ return /* @__PURE__ */ jsx(OnboardingThemeContext.Provider, { value: resolved, children });
77
+ };
78
+ var useOnboardingTheme = () => {
79
+ const ctx = useContext(OnboardingThemeContext);
80
+ return ctx != null ? ctx : mergeTheme();
81
+ };
82
+ var IllustrationContext = createContext({});
83
+ var IllustrationProvider = ({
84
+ registry,
85
+ children
86
+ }) => /* @__PURE__ */ jsx(IllustrationContext.Provider, { value: registry != null ? registry : {}, children });
87
+ var useIllustration = (name) => {
88
+ const registry = useContext(IllustrationContext);
89
+ if (!name) return void 0;
90
+ return registry[name];
91
+ };
92
+ var Glyph = ({ children }) => {
93
+ const t = useOnboardingTheme();
94
+ return /* @__PURE__ */ jsx(
95
+ View,
96
+ {
97
+ style: [
98
+ styles.disc,
99
+ { backgroundColor: t.colors.primarySoft, borderRadius: t.radius.full }
100
+ ],
101
+ children: /* @__PURE__ */ jsx(Text, { style: [styles.glyphText, { color: t.colors.primary }], allowFontScaling: false, children })
102
+ }
103
+ );
104
+ };
105
+ var MomentumGlyph = () => /* @__PURE__ */ jsx(Glyph, { children: "\u2197" });
106
+ var BeforeAfterGlyph = () => {
107
+ const t = useOnboardingTheme();
108
+ return /* @__PURE__ */ jsxs(View, { style: styles.row, children: [
109
+ /* @__PURE__ */ jsx(
110
+ View,
111
+ {
112
+ style: [
113
+ styles.chip,
114
+ { borderColor: t.colors.border, borderRadius: t.radius.md }
115
+ ]
116
+ }
117
+ ),
118
+ /* @__PURE__ */ jsx(Text, { style: [styles.arrow, { color: t.colors.primary }], allowFontScaling: false, children: "\u2192" }),
119
+ /* @__PURE__ */ jsx(
120
+ View,
121
+ {
122
+ style: [
123
+ styles.chip,
124
+ { backgroundColor: t.colors.primarySoft, borderColor: t.colors.primary, borderRadius: t.radius.md }
125
+ ]
126
+ }
127
+ )
128
+ ] });
129
+ };
130
+ var defaultIllustrations = {
131
+ momentum: /* @__PURE__ */ jsx(MomentumGlyph, {}),
132
+ "before-after": /* @__PURE__ */ jsx(BeforeAfterGlyph, {})
133
+ };
134
+ var styles = StyleSheet.create({
135
+ disc: {
136
+ width: 96,
137
+ height: 96,
138
+ alignItems: "center",
139
+ justifyContent: "center"
140
+ },
141
+ glyphText: {
142
+ fontSize: 44,
143
+ fontWeight: "700",
144
+ lineHeight: 52
145
+ },
146
+ row: {
147
+ flexDirection: "row",
148
+ alignItems: "center",
149
+ justifyContent: "center",
150
+ gap: 12
151
+ },
152
+ chip: {
153
+ width: 56,
154
+ height: 56,
155
+ borderWidth: 2
156
+ },
157
+ arrow: {
158
+ fontSize: 28,
159
+ fontWeight: "700"
160
+ }
161
+ });
162
+
163
+ // src/theme/typography.ts
164
+ var round = (n) => Math.round(n);
165
+ var bodyStyle = (f) => ({
166
+ fontFamily: f.regular,
167
+ fontSize: round(f.baseSize * f.scale),
168
+ lineHeight: round(f.baseSize * f.scale * 1.5)
169
+ });
170
+ var captionStyle = (f) => ({
171
+ fontFamily: f.regular,
172
+ fontSize: round(f.baseSize * 0.8125 * f.scale),
173
+ lineHeight: round(f.baseSize * 0.8125 * f.scale * 1.5)
174
+ });
175
+ var labelStyle = (f) => {
176
+ var _a;
177
+ return {
178
+ fontFamily: (_a = f.medium) != null ? _a : f.bold,
179
+ fontSize: round(f.baseSize * f.scale),
180
+ lineHeight: round(f.baseSize * f.scale * 1.4),
181
+ fontWeight: f.medium ? void 0 : "600"
182
+ };
183
+ };
184
+ var headingStyle = (f) => ({
185
+ fontFamily: f.bold,
186
+ fontSize: round(f.baseSize * 1.25 * f.scale),
187
+ lineHeight: round(f.baseSize * 1.25 * f.scale * 1.3),
188
+ fontWeight: f.bold ? void 0 : "700"
189
+ });
190
+ var buttonLabelStyle = (f) => {
191
+ var _a;
192
+ return {
193
+ fontFamily: (_a = f.bold) != null ? _a : f.medium,
194
+ fontSize: round(f.baseSize * f.scale),
195
+ fontWeight: f.bold ? void 0 : "600"
196
+ };
197
+ };
198
+
199
+ // src/motion/motionSpec.ts
200
+ var motionSpec_exports = {};
201
+ __export(motionSpec_exports, {
202
+ BUTTON_PRESS_SCALE: () => BUTTON_PRESS_SCALE,
203
+ BUTTON_PRESS_SPRING: () => BUTTON_PRESS_SPRING,
204
+ CARD_ENTER_MS: () => CARD_ENTER_MS,
205
+ CARD_ENTER_SCALE_FROM: () => CARD_ENTER_SCALE_FROM,
206
+ CARD_EXIT_LEAD_MS: () => CARD_EXIT_LEAD_MS,
207
+ CARD_EXIT_MS: () => CARD_EXIT_MS,
208
+ CARD_SLIDE_PX: () => CARD_SLIDE_PX,
209
+ CARD_SPRING: () => CARD_SPRING,
210
+ CHECK_SCALE_FROM: () => CHECK_SCALE_FROM,
211
+ CHECK_SPRING: () => CHECK_SPRING,
212
+ CHIP_FILL_MS: () => CHIP_FILL_MS,
213
+ CHIP_PRESS_SCALE: () => CHIP_PRESS_SCALE,
214
+ DURATION_BASE_MS: () => DURATION_BASE_MS,
215
+ DURATION_FAST_MS: () => DURATION_FAST_MS,
216
+ DURATION_SLOW_MS: () => DURATION_SLOW_MS,
217
+ INTERSTITIAL_CHECK_DELAY_MS: () => INTERSTITIAL_CHECK_DELAY_MS,
218
+ INTERSTITIAL_CHECK_SCALE_FROM: () => INTERSTITIAL_CHECK_SCALE_FROM,
219
+ INTERSTITIAL_HEAD_MS: () => INTERSTITIAL_HEAD_MS,
220
+ INTERSTITIAL_HEAD_STAGGER_MS: () => INTERSTITIAL_HEAD_STAGGER_MS,
221
+ INTERSTITIAL_ITEM_BASE_DELAY_MS: () => INTERSTITIAL_ITEM_BASE_DELAY_MS,
222
+ INTERSTITIAL_ITEM_MS: () => INTERSTITIAL_ITEM_MS,
223
+ INTERSTITIAL_ITEM_STAGGER_MS: () => INTERSTITIAL_ITEM_STAGGER_MS,
224
+ LOADER_HANDOFF_FADE_MS: () => LOADER_HANDOFF_FADE_MS,
225
+ LOADER_ORBIT_MS: () => LOADER_ORBIT_MS,
226
+ LOADER_PULSE_MIN_OPACITY: () => LOADER_PULSE_MIN_OPACITY,
227
+ LOADER_PULSE_MS: () => LOADER_PULSE_MS,
228
+ PRESS_SPRING: () => PRESS_SPRING,
229
+ PROGRESS_MS: () => PROGRESS_MS,
230
+ REDUCED_FADE_MS: () => REDUCED_FADE_MS,
231
+ REDUCED_PRESS_OPACITY: () => REDUCED_PRESS_OPACITY,
232
+ ROW_PRESS_SCALE: () => ROW_PRESS_SCALE,
233
+ SELECT_FILL_MS: () => SELECT_FILL_MS,
234
+ SKELETON_DELAY_MS: () => SKELETON_DELAY_MS,
235
+ SKELETON_MIN_OPACITY: () => SKELETON_MIN_OPACITY,
236
+ SKELETON_PULSE_MS: () => SKELETON_PULSE_MS,
237
+ STATUS_BURST_MS: () => STATUS_BURST_MS,
238
+ STATUS_BURST_OPACITY_FROM: () => STATUS_BURST_OPACITY_FROM,
239
+ STATUS_BURST_SCALE_FROM: () => STATUS_BURST_SCALE_FROM,
240
+ STATUS_BURST_SCALE_TO: () => STATUS_BURST_SCALE_TO,
241
+ STATUS_COPY_BASE_DELAY_MS: () => STATUS_COPY_BASE_DELAY_MS,
242
+ STATUS_COPY_MS: () => STATUS_COPY_MS,
243
+ STATUS_COPY_STAGGER_MS: () => STATUS_COPY_STAGGER_MS,
244
+ STATUS_POP_SCALE_FROM: () => STATUS_POP_SCALE_FROM,
245
+ STATUS_POP_SPRING: () => STATUS_POP_SPRING,
246
+ WIRE_BEZIER: () => WIRE_BEZIER,
247
+ entranceSpec: () => entranceSpec,
248
+ getMotionDurationScale: () => getMotionDurationScale,
249
+ interstitialCheckDelay: () => interstitialCheckDelay,
250
+ interstitialItemDelay: () => interstitialItemDelay,
251
+ loopEnabled: () => loopEnabled,
252
+ scaledMs: () => scaledMs,
253
+ setMotionDurationScale: () => setMotionDurationScale,
254
+ stateChangeDuration: () => stateChangeDuration,
255
+ statusCopyDelay: () => statusCopyDelay
256
+ });
257
+ var WIRE_BEZIER = [0.16, 1, 0.3, 1];
258
+ var DURATION_FAST_MS = 150;
259
+ var DURATION_BASE_MS = 300;
260
+ var DURATION_SLOW_MS = 500;
261
+ var REDUCED_FADE_MS = 120;
262
+ var ROW_PRESS_SCALE = 0.975;
263
+ var CHIP_PRESS_SCALE = 0.94;
264
+ var BUTTON_PRESS_SCALE = 0.975;
265
+ var REDUCED_PRESS_OPACITY = 0.85;
266
+ var PRESS_SPRING = { friction: 8, tension: 220 };
267
+ var BUTTON_PRESS_SPRING = { friction: 8, tension: 240 };
268
+ var SELECT_FILL_MS = 180;
269
+ var CHIP_FILL_MS = 160;
270
+ var CHECK_SPRING = { friction: 6, tension: 180 };
271
+ var CHECK_SCALE_FROM = 0.6;
272
+ var CARD_ENTER_MS = 220;
273
+ var CARD_EXIT_MS = 180;
274
+ var CARD_EXIT_LEAD_MS = 80;
275
+ var CARD_SLIDE_PX = 26;
276
+ var CARD_ENTER_SCALE_FROM = 0.96;
277
+ var CARD_SPRING = { damping: 14, stiffness: 180, mass: 1 };
278
+ var LOADER_HANDOFF_FADE_MS = 120;
279
+ var INTERSTITIAL_HEAD_MS = 500;
280
+ var INTERSTITIAL_HEAD_STAGGER_MS = 100;
281
+ var INTERSTITIAL_ITEM_MS = 300;
282
+ var INTERSTITIAL_ITEM_STAGGER_MS = 90;
283
+ var INTERSTITIAL_ITEM_BASE_DELAY_MS = 200;
284
+ var INTERSTITIAL_CHECK_DELAY_MS = 200;
285
+ var INTERSTITIAL_CHECK_SCALE_FROM = 0.4;
286
+ var interstitialItemDelay = (index) => INTERSTITIAL_ITEM_BASE_DELAY_MS + index * INTERSTITIAL_ITEM_STAGGER_MS;
287
+ var interstitialCheckDelay = (index) => interstitialItemDelay(index) + INTERSTITIAL_CHECK_DELAY_MS;
288
+ var STATUS_POP_SPRING = { friction: 6, tension: 200 };
289
+ var STATUS_POP_SCALE_FROM = 0.4;
290
+ var STATUS_BURST_MS = 500;
291
+ var STATUS_BURST_SCALE_FROM = 0.4;
292
+ var STATUS_BURST_SCALE_TO = 1.5;
293
+ var STATUS_BURST_OPACITY_FROM = 0.5;
294
+ var STATUS_COPY_MS = 300;
295
+ var STATUS_COPY_STAGGER_MS = 80;
296
+ var STATUS_COPY_BASE_DELAY_MS = 200;
297
+ var statusCopyDelay = (index) => STATUS_COPY_BASE_DELAY_MS + index * STATUS_COPY_STAGGER_MS;
298
+ var LOADER_PULSE_MS = 1600;
299
+ var LOADER_PULSE_MIN_OPACITY = 0.35;
300
+ var LOADER_ORBIT_MS = 2400;
301
+ var SKELETON_DELAY_MS = 300;
302
+ var SKELETON_PULSE_MS = 1500;
303
+ var SKELETON_MIN_OPACITY = 0.45;
304
+ var PROGRESS_MS = 400;
305
+ var durationScale = 1;
306
+ var setMotionDurationScale = (scale) => {
307
+ durationScale = Number.isFinite(scale) && scale > 0 ? Math.min(10, Math.max(0.1, scale)) : 1;
308
+ };
309
+ var getMotionDurationScale = () => durationScale;
310
+ var scaledMs = (ms) => Math.round(ms * durationScale);
311
+ var entranceSpec = (reduced, durationMs) => reduced ? { duration: REDUCED_FADE_MS, useTransform: false } : { duration: durationMs, useTransform: true };
312
+ var stateChangeDuration = (reduced, durationMs) => reduced ? 0 : durationMs;
313
+ var loopEnabled = (reduced) => !reduced;
314
+ var lastKnown = false;
315
+ var useReducedMotion = () => {
316
+ const [reduced, setReduced] = useState(lastKnown);
317
+ useEffect(() => {
318
+ let mounted = true;
319
+ AccessibilityInfo.isReduceMotionEnabled().then((value) => {
320
+ lastKnown = value;
321
+ if (mounted) setReduced(value);
322
+ }).catch(() => {
323
+ });
324
+ const sub = AccessibilityInfo.addEventListener("reduceMotionChanged", (value) => {
325
+ lastKnown = value;
326
+ setReduced(value);
327
+ });
328
+ return () => {
329
+ mounted = false;
330
+ sub == null ? void 0 : sub.remove();
331
+ };
332
+ }, []);
333
+ return reduced;
334
+ };
335
+ var ROTATION_MS = 1800;
336
+ var _AnimatedSparkle = ({ size = 30, color, variant = "spin" }) => {
337
+ const t = useOnboardingTheme();
338
+ const reduced = useReducedMotion();
339
+ const progress = useRef(new Animated.Value(0)).current;
340
+ useEffect(() => {
341
+ if (!loopEnabled(reduced)) {
342
+ progress.setValue(0);
343
+ return;
344
+ }
345
+ const loop = variant === "pulse" ? Animated.loop(
346
+ Animated.sequence([
347
+ Animated.timing(progress, {
348
+ toValue: 1,
349
+ duration: scaledMs(LOADER_PULSE_MS / 2),
350
+ easing: Easing.inOut(Easing.ease),
351
+ useNativeDriver: true
352
+ }),
353
+ Animated.timing(progress, {
354
+ toValue: 0,
355
+ duration: scaledMs(LOADER_PULSE_MS / 2),
356
+ easing: Easing.inOut(Easing.ease),
357
+ useNativeDriver: true
358
+ })
359
+ ])
360
+ ) : Animated.loop(
361
+ Animated.timing(progress, {
362
+ toValue: 1,
363
+ duration: scaledMs(ROTATION_MS),
364
+ easing: Easing.linear,
365
+ useNativeDriver: true
366
+ })
367
+ );
368
+ loop.start();
369
+ return () => loop.stop();
370
+ }, [progress, variant, reduced]);
371
+ const rotate = progress.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] });
372
+ const pulse = progress.interpolate({
373
+ inputRange: [0, 1],
374
+ outputRange: [1, LOADER_PULSE_MIN_OPACITY]
375
+ });
376
+ const animatedStyle = reduced ? void 0 : variant === "pulse" ? { opacity: pulse } : { transform: [{ rotate }] };
377
+ return /* @__PURE__ */ jsx(Animated.View, { style: animatedStyle, children: /* @__PURE__ */ jsx(
378
+ Text,
379
+ {
380
+ accessibilityElementsHidden: true,
381
+ importantForAccessibility: "no",
382
+ style: [styles2.glyph, { fontSize: size, lineHeight: size * 1.1, color: color != null ? color : t.colors.primary }],
383
+ children: "\u2726"
384
+ }
385
+ ) });
386
+ };
387
+ var AnimatedSparkle = React14.memo(_AnimatedSparkle);
388
+ var styles2 = StyleSheet.create({
389
+ glyph: { textAlign: "center" }
390
+ });
391
+ var LoaderChromeContext = createContext({ offsetY: 0 });
392
+ var LoaderChromeProvider = LoaderChromeContext.Provider;
393
+ var useLoaderChrome = () => useContext(LoaderChromeContext);
394
+ var STAGE_SIZE = 96;
395
+ var ORBIT_DOT = 9;
396
+ var OrbitDot = ({ color, reduced }) => {
397
+ const spin = useRef(new Animated.Value(0)).current;
398
+ useEffect(() => {
399
+ if (!loopEnabled(reduced)) return;
400
+ const loop = Animated.loop(
401
+ Animated.timing(spin, {
402
+ toValue: 1,
403
+ duration: scaledMs(LOADER_ORBIT_MS),
404
+ easing: Easing.linear,
405
+ useNativeDriver: true
406
+ })
407
+ );
408
+ loop.start();
409
+ return () => loop.stop();
410
+ }, [spin, reduced]);
411
+ if (reduced) return null;
412
+ const rotate = spin.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] });
413
+ return /* @__PURE__ */ jsx(Animated.View, { pointerEvents: "none", style: [StyleSheet.absoluteFill, { transform: [{ rotate }] }], children: /* @__PURE__ */ jsx(View, { style: [styles3.orbitDot, { backgroundColor: color }] }) });
414
+ };
415
+ var SkeletonLine = ({
416
+ width,
417
+ delay,
418
+ color,
419
+ reduced
420
+ }) => {
421
+ const pulse = useRef(new Animated.Value(1)).current;
422
+ useEffect(() => {
423
+ if (!loopEnabled(reduced)) return;
424
+ const loop = Animated.loop(
425
+ Animated.sequence([
426
+ Animated.delay(delay),
427
+ Animated.timing(pulse, {
428
+ toValue: SKELETON_MIN_OPACITY,
429
+ duration: scaledMs(SKELETON_PULSE_MS / 2),
430
+ easing: Easing.inOut(Easing.ease),
431
+ useNativeDriver: true
432
+ }),
433
+ Animated.timing(pulse, {
434
+ toValue: 1,
435
+ duration: scaledMs(SKELETON_PULSE_MS / 2),
436
+ easing: Easing.inOut(Easing.ease),
437
+ useNativeDriver: true
438
+ })
439
+ ])
440
+ );
441
+ loop.start();
442
+ return () => loop.stop();
443
+ }, [pulse, delay, reduced]);
444
+ return /* @__PURE__ */ jsx(
445
+ Animated.View,
446
+ {
447
+ style: [styles3.skeletonLine, { width, backgroundColor: color, opacity: reduced ? 1 : pulse }]
448
+ }
449
+ );
450
+ };
451
+ var _LoadingBlock = ({
452
+ title = "Thinking\u2026",
453
+ hint = "Personalizing your experience"
454
+ }) => {
455
+ const t = useOnboardingTheme();
456
+ const reduced = useReducedMotion();
457
+ const { offsetY } = useLoaderChrome();
458
+ const [showSkeleton, setShowSkeleton] = useState(false);
459
+ useEffect(() => {
460
+ const timer = setTimeout(() => setShowSkeleton(true), SKELETON_DELAY_MS);
461
+ return () => clearTimeout(timer);
462
+ }, []);
463
+ return /* @__PURE__ */ jsxs(
464
+ View,
465
+ {
466
+ style: [
467
+ styles3.center,
468
+ { gap: t.spacing.md, padding: t.spacing.lg },
469
+ offsetY ? { transform: [{ translateY: offsetY }] } : null
470
+ ],
471
+ children: [
472
+ /* @__PURE__ */ jsxs(View, { style: styles3.stage, children: [
473
+ /* @__PURE__ */ jsx(View, { style: [styles3.ring, { borderColor: t.colors.border, borderRadius: t.radius.full }] }),
474
+ /* @__PURE__ */ jsx(OrbitDot, { color: t.colors.primary, reduced }),
475
+ /* @__PURE__ */ jsx(AnimatedSparkle, { size: 40, variant: "pulse" })
476
+ ] }),
477
+ title ? /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text, textAlign: "center" }], children: title }) : null,
478
+ hint ? /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: hint }) : null,
479
+ showSkeleton ? /* @__PURE__ */ jsxs(View, { style: [styles3.skeleton, { gap: t.spacing.sm, marginTop: t.spacing.sm }], children: [
480
+ /* @__PURE__ */ jsx(SkeletonLine, { width: "80%", delay: 0, color: t.colors.surface, reduced }),
481
+ /* @__PURE__ */ jsx(SkeletonLine, { width: "60%", delay: 200, color: t.colors.surface, reduced })
482
+ ] }) : null
483
+ ]
484
+ }
485
+ );
486
+ };
487
+ var LoadingBlock = React14.memo(_LoadingBlock);
488
+ var styles3 = StyleSheet.create({
489
+ center: { flex: 1, alignItems: "center", justifyContent: "center" },
490
+ stage: {
491
+ width: STAGE_SIZE,
492
+ height: STAGE_SIZE,
493
+ alignItems: "center",
494
+ justifyContent: "center"
495
+ },
496
+ ring: {
497
+ position: "absolute",
498
+ top: 0,
499
+ left: 0,
500
+ right: 0,
501
+ bottom: 0,
502
+ borderWidth: 1.5,
503
+ borderStyle: "dashed"
504
+ },
505
+ orbitDot: {
506
+ position: "absolute",
507
+ top: -ORBIT_DOT / 2 + 1,
508
+ left: STAGE_SIZE / 2 - ORBIT_DOT / 2,
509
+ width: ORBIT_DOT,
510
+ height: ORBIT_DOT,
511
+ borderRadius: ORBIT_DOT / 2
512
+ },
513
+ skeleton: { width: "100%", alignItems: "center" },
514
+ skeletonLine: { height: 12, borderRadius: 6 }
515
+ });
516
+ var _LoadingScreen = ({ title, hint }) => {
517
+ const t = useOnboardingTheme();
518
+ return /* @__PURE__ */ jsx(
519
+ SafeAreaView,
520
+ {
521
+ edges: ["top", "bottom"],
522
+ style: [styles4.flex, { backgroundColor: t.colors.background }],
523
+ children: /* @__PURE__ */ jsx(LoadingBlock, { title, hint })
524
+ }
525
+ );
526
+ };
527
+ var LoadingScreen = React14.memo(_LoadingScreen);
528
+ var styles4 = StyleSheet.create({
529
+ flex: { flex: 1 }
530
+ });
531
+ var easeWire = Easing.bezier(...WIRE_BEZIER);
532
+ var DECAY = 0.62;
533
+ var MIN_FILL = 0.08;
534
+ var APPROX_CAP = 0.95;
535
+ var targetFor = (step, complete, approxScreens) => {
536
+ if (complete) return 1;
537
+ const s = Number.isFinite(step) && step > 0 ? Math.floor(step) : 1;
538
+ if (approxScreens && approxScreens > 0) {
539
+ return Math.max(MIN_FILL, Math.min(APPROX_CAP, s / approxScreens));
540
+ }
541
+ return Math.max(MIN_FILL, 1 - DECAY ** s);
542
+ };
543
+ var _StepProgress = ({ step, complete, approxScreens }) => {
544
+ const t = useOnboardingTheme();
545
+ const reduced = useReducedMotion();
546
+ const fill = useRef(new Animated.Value(targetFor(step, complete, approxScreens))).current;
547
+ useEffect(() => {
548
+ Animated.timing(fill, {
549
+ toValue: targetFor(step, complete, approxScreens),
550
+ duration: scaledMs(stateChangeDuration(reduced, PROGRESS_MS)),
551
+ easing: easeWire,
552
+ useNativeDriver: false
553
+ // animating width %
554
+ }).start();
555
+ }, [fill, step, complete, approxScreens, reduced]);
556
+ const width = fill.interpolate({ inputRange: [0, 1], outputRange: ["0%", "100%"] });
557
+ return /* @__PURE__ */ jsx(View, { style: [styles5.track, { backgroundColor: t.colors.border, borderRadius: t.radius.full }], children: /* @__PURE__ */ jsx(
558
+ Animated.View,
559
+ {
560
+ style: [
561
+ styles5.fill,
562
+ { width, backgroundColor: t.colors.primary, borderRadius: t.radius.full }
563
+ ]
564
+ }
565
+ ) });
566
+ };
567
+ var StepProgress = React14.memo(_StepProgress);
568
+ var styles5 = StyleSheet.create({
569
+ track: { height: 6, width: "100%", overflow: "hidden" },
570
+ fill: { height: 6 }
571
+ });
572
+ var PROGRESS_TRACK = 6;
573
+ var _OnboardingScaffold = ({
574
+ step,
575
+ complete,
576
+ approxScreens,
577
+ onBack,
578
+ onSkip,
579
+ skipLabel = "Skip",
580
+ backLabel = "Back",
581
+ children
582
+ }) => {
583
+ const t = useOnboardingTheme();
584
+ const hit = { top: 12, bottom: 12, left: 12, right: 12 };
585
+ const topChrome = t.spacing.md + PROGRESS_TRACK + t.spacing.md;
586
+ const loaderOffsetY = -Math.round(topChrome / 2);
587
+ return /* @__PURE__ */ jsxs(
588
+ SafeAreaView,
589
+ {
590
+ edges: ["top", "bottom"],
591
+ style: [styles6.flex, { backgroundColor: t.colors.background }],
592
+ children: [
593
+ /* @__PURE__ */ jsxs(View, { style: [styles6.header, { paddingHorizontal: t.spacing.md, paddingTop: t.spacing.md }], children: [
594
+ /* @__PURE__ */ jsx(View, { style: styles6.progressWrap, children: /* @__PURE__ */ jsx(StepProgress, { step, complete, approxScreens }) }),
595
+ onSkip ? /* @__PURE__ */ jsx(TouchableOpacity, { onPress: onSkip, hitSlop: hit, style: { marginLeft: t.spacing.md }, children: /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: skipLabel }) }) : null
596
+ ] }),
597
+ /* @__PURE__ */ jsx(View, { style: [styles6.body, { paddingHorizontal: t.spacing.md, paddingTop: t.spacing.md }], children: /* @__PURE__ */ jsx(LoaderChromeProvider, { value: { offsetY: loaderOffsetY }, children }) }),
598
+ onBack ? /* @__PURE__ */ jsx(View, { style: [styles6.footer, { paddingHorizontal: t.spacing.md, paddingBottom: t.spacing.sm }], children: /* @__PURE__ */ jsx(TouchableOpacity, { onPress: onBack, hitSlop: hit, children: /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: backLabel }) }) }) : null
599
+ ]
600
+ }
601
+ );
602
+ };
603
+ var OnboardingScaffold = React14.memo(_OnboardingScaffold);
604
+ var styles6 = StyleSheet.create({
605
+ flex: { flex: 1 },
606
+ header: { flexDirection: "row", alignItems: "center" },
607
+ progressWrap: { flex: 1 },
608
+ body: { flex: 1 },
609
+ footer: { flexDirection: "row", justifyContent: "flex-start" }
610
+ });
611
+ var _Button = ({
612
+ title,
613
+ onPress,
614
+ variant = "primary",
615
+ disabled = false,
616
+ fullWidth = false
617
+ }) => {
618
+ const t = useOnboardingTheme();
619
+ const reduced = useReducedMotion();
620
+ const isPrimary = variant === "primary";
621
+ const pressT = useRef(new Animated.Value(0)).current;
622
+ const pressTo = useCallback(
623
+ (to) => {
624
+ if (reduced) {
625
+ Animated.timing(pressT, {
626
+ toValue: to,
627
+ duration: scaledMs(REDUCED_FADE_MS),
628
+ useNativeDriver: true
629
+ }).start();
630
+ return;
631
+ }
632
+ Animated.spring(pressT, {
633
+ toValue: to,
634
+ friction: BUTTON_PRESS_SPRING.friction,
635
+ tension: BUTTON_PRESS_SPRING.tension,
636
+ useNativeDriver: true
637
+ }).start();
638
+ },
639
+ [pressT, reduced]
640
+ );
641
+ const handlePressIn = useCallback(() => pressTo(1), [pressTo]);
642
+ const handlePressOut = useCallback(() => pressTo(0), [pressTo]);
643
+ const scale = pressT.interpolate({
644
+ inputRange: [0, 1],
645
+ outputRange: [1, reduced ? 1 : BUTTON_PRESS_SCALE]
646
+ });
647
+ const opacity = pressT.interpolate({
648
+ inputRange: [0, 1],
649
+ outputRange: [1, reduced ? REDUCED_PRESS_OPACITY : 1]
650
+ });
651
+ return /* @__PURE__ */ jsx(
652
+ Pressable,
653
+ {
654
+ onPress: disabled ? void 0 : onPress,
655
+ onPressIn: disabled ? void 0 : handlePressIn,
656
+ onPressOut: disabled ? void 0 : handlePressOut,
657
+ disabled,
658
+ accessibilityRole: "button",
659
+ accessibilityState: { disabled },
660
+ style: fullWidth ? styles7.fill : void 0,
661
+ children: /* @__PURE__ */ jsx(
662
+ Animated.View,
663
+ {
664
+ style: [
665
+ styles7.base,
666
+ fullWidth && styles7.fill,
667
+ {
668
+ borderRadius: t.button.radius,
669
+ minHeight: t.button.height,
670
+ paddingHorizontal: t.button.paddingX,
671
+ opacity,
672
+ transform: [{ scale }]
673
+ },
674
+ isPrimary ? { backgroundColor: disabled ? t.colors.disabled : t.colors.primary } : { borderWidth: 1.5, borderColor: disabled ? t.colors.border : t.colors.primary }
675
+ ],
676
+ children: /* @__PURE__ */ jsx(
677
+ Text,
678
+ {
679
+ style: [
680
+ buttonLabelStyle(t.fonts),
681
+ {
682
+ color: disabled ? t.colors.textMuted : isPrimary ? t.colors.onPrimary : t.colors.primary
683
+ }
684
+ ],
685
+ children: title
686
+ }
687
+ )
688
+ }
689
+ )
690
+ }
691
+ );
692
+ };
693
+ var Button = React14.memo(_Button);
694
+ var styles7 = StyleSheet.create({
695
+ base: { alignItems: "center", justifyContent: "center" },
696
+ fill: { width: "100%" }
697
+ });
698
+ var _ErrorBlock = ({
699
+ title = "Something went wrong",
700
+ message = "We couldn't load this step. You can try again.",
701
+ retryLabel = "Try again",
702
+ onRetry
703
+ }) => {
704
+ const t = useOnboardingTheme();
705
+ return /* @__PURE__ */ jsxs(View, { style: [styles8.center, { gap: t.spacing.md, padding: t.spacing.lg }], children: [
706
+ /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text, textAlign: "center" }], children: title }),
707
+ /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: message }),
708
+ onRetry ? /* @__PURE__ */ jsx(Button, { title: retryLabel, onPress: onRetry, variant: "outline" }) : null
709
+ ] });
710
+ };
711
+ var ErrorBlock = React14.memo(_ErrorBlock);
712
+ var styles8 = StyleSheet.create({
713
+ center: { flex: 1, alignItems: "center", justifyContent: "center" }
714
+ });
715
+ var _CardLayout = ({
716
+ title,
717
+ subtitle,
718
+ children,
719
+ footer,
720
+ align = "top",
721
+ titleNode
722
+ }) => {
723
+ const t = useOnboardingTheme();
724
+ const hasHeader = !!title || !!subtitle || !!titleNode;
725
+ return /* @__PURE__ */ jsxs(KeyboardAvoidingView, { style: styles9.fill, behavior: "padding", children: [
726
+ hasHeader ? /* @__PURE__ */ jsxs(View, { style: { marginBottom: t.spacing.lg }, children: [
727
+ titleNode != null ? titleNode : title ? /* @__PURE__ */ jsx(Text, { style: [labelStyle(t.fonts), { color: t.colors.text }], children: title }) : null,
728
+ subtitle ? /* @__PURE__ */ jsx(
729
+ Text,
730
+ {
731
+ style: [
732
+ bodyStyle(t.fonts),
733
+ { color: t.colors.textMuted, marginTop: t.spacing.xs }
734
+ ],
735
+ children: subtitle
736
+ }
737
+ ) : null
738
+ ] }) : null,
739
+ /* @__PURE__ */ jsx(
740
+ ScrollView,
741
+ {
742
+ style: styles9.fill,
743
+ contentContainerStyle: [
744
+ styles9.scrollContent,
745
+ align === "center" && styles9.center
746
+ ],
747
+ keyboardShouldPersistTaps: "handled",
748
+ keyboardDismissMode: "on-drag",
749
+ showsVerticalScrollIndicator: false,
750
+ children
751
+ }
752
+ ),
753
+ footer ? /* @__PURE__ */ jsx(View, { style: { paddingTop: t.spacing.lg }, children: footer }) : null
754
+ ] });
755
+ };
756
+ var CardLayout = React14.memo(_CardLayout);
757
+ var styles9 = StyleSheet.create({
758
+ fill: { flex: 1 },
759
+ // flexGrow lets a short content block grow to fill (pinning the footer) while a
760
+ // tall one scrolls past the visible area.
761
+ scrollContent: { flexGrow: 1 },
762
+ center: { justifyContent: "center" }
763
+ });
764
+ var easeWire2 = Easing.bezier(...WIRE_BEZIER);
765
+ var BADGE_SIZE = 64;
766
+ var BURST_SIZE = 104;
767
+ var _CompletionView = ({
768
+ title,
769
+ message,
770
+ ctaLabel = "Let's go",
771
+ onContinue
772
+ }) => {
773
+ const t = useOnboardingTheme();
774
+ const reduced = useReducedMotion();
775
+ const [submitted, setSubmitted] = useState(false);
776
+ const pop = useRef(new Animated.Value(reduced ? 1 : 0)).current;
777
+ const burst = useRef(new Animated.Value(reduced ? 1 : 0)).current;
778
+ const titleT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
779
+ const messageT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
780
+ useEffect(() => {
781
+ if (reduced) {
782
+ pop.setValue(1);
783
+ burst.setValue(1);
784
+ titleT.setValue(1);
785
+ messageT.setValue(1);
786
+ return;
787
+ }
788
+ const rise = (value, delay) => Animated.timing(value, {
789
+ toValue: 1,
790
+ duration: scaledMs(STATUS_COPY_MS),
791
+ delay,
792
+ easing: easeWire2,
793
+ useNativeDriver: true
794
+ });
795
+ const anims = [
796
+ Animated.spring(pop, {
797
+ toValue: 1,
798
+ friction: STATUS_POP_SPRING.friction,
799
+ tension: STATUS_POP_SPRING.tension,
800
+ useNativeDriver: true
801
+ }),
802
+ // One-shot celebratory burst: fires once, never loops.
803
+ Animated.timing(burst, {
804
+ toValue: 1,
805
+ duration: scaledMs(STATUS_BURST_MS),
806
+ easing: Easing.out(Easing.ease),
807
+ useNativeDriver: true
808
+ }),
809
+ rise(titleT, scaledMs(statusCopyDelay(0))),
810
+ rise(messageT, scaledMs(statusCopyDelay(1)))
811
+ ];
812
+ anims.forEach((a) => a.start());
813
+ return () => anims.forEach((a) => a.stop());
814
+ }, [reduced, pop, burst, titleT, messageT]);
815
+ const handlePress = useCallback(() => {
816
+ if (submitted) return;
817
+ setSubmitted(true);
818
+ onContinue();
819
+ }, [submitted, onContinue]);
820
+ const scale = pop.interpolate({ inputRange: [0, 1], outputRange: [STATUS_POP_SCALE_FROM, 1] });
821
+ const burstScale = burst.interpolate({
822
+ inputRange: [0, 1],
823
+ outputRange: [STATUS_BURST_SCALE_FROM, STATUS_BURST_SCALE_TO]
824
+ });
825
+ const burstOpacity = burst.interpolate({
826
+ inputRange: [0, 1],
827
+ outputRange: [STATUS_BURST_OPACITY_FROM, 0]
828
+ });
829
+ const riseStyle = (value) => ({
830
+ opacity: value,
831
+ transform: [{ translateY: value.interpolate({ inputRange: [0, 1], outputRange: [10, 0] }) }]
832
+ });
833
+ return /* @__PURE__ */ jsx(
834
+ CardLayout,
835
+ {
836
+ align: "center",
837
+ footer: /* @__PURE__ */ jsx(Button, { title: ctaLabel, onPress: handlePress, variant: "primary", disabled: submitted }),
838
+ children: /* @__PURE__ */ jsxs(View, { style: [styles10.center, { gap: t.spacing.md }], children: [
839
+ /* @__PURE__ */ jsxs(View, { style: styles10.badgeStage, children: [
840
+ /* @__PURE__ */ jsx(
841
+ Animated.View,
842
+ {
843
+ pointerEvents: "none",
844
+ style: [
845
+ styles10.burst,
846
+ {
847
+ backgroundColor: t.colors.primarySoft,
848
+ borderRadius: t.radius.full,
849
+ opacity: burstOpacity,
850
+ transform: [{ scale: burstScale }]
851
+ }
852
+ ]
853
+ }
854
+ ),
855
+ /* @__PURE__ */ jsx(
856
+ Animated.View,
857
+ {
858
+ style: [
859
+ styles10.badge,
860
+ {
861
+ backgroundColor: t.colors.primarySoft,
862
+ borderRadius: t.radius.full,
863
+ transform: [{ scale }],
864
+ opacity: pop
865
+ }
866
+ ],
867
+ children: /* @__PURE__ */ jsx(Text, { style: [styles10.check, { color: t.colors.primary }], children: "\u2713" })
868
+ }
869
+ )
870
+ ] }),
871
+ /* @__PURE__ */ jsx(Animated.View, { style: riseStyle(titleT), children: /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text, textAlign: "center" }], children: title }) }),
872
+ message ? /* @__PURE__ */ jsx(Animated.View, { style: riseStyle(messageT), children: /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: message }) }) : null
873
+ ] })
874
+ }
875
+ );
876
+ };
877
+ var CompletionView = React14.memo(_CompletionView);
878
+ var styles10 = StyleSheet.create({
879
+ center: { width: "100%", alignItems: "center", justifyContent: "center" },
880
+ badgeStage: {
881
+ width: BURST_SIZE,
882
+ height: BURST_SIZE,
883
+ alignItems: "center",
884
+ justifyContent: "center"
885
+ },
886
+ burst: { position: "absolute", width: BURST_SIZE, height: BURST_SIZE },
887
+ badge: { width: BADGE_SIZE, height: BADGE_SIZE, alignItems: "center", justifyContent: "center" },
888
+ check: { fontSize: 34, fontWeight: "800" }
889
+ });
890
+ var easeWire3 = Easing.bezier(...WIRE_BEZIER);
891
+ var _CardHandoff = ({
892
+ transitionKey,
893
+ variant = "slide",
894
+ children
895
+ }) => {
896
+ const reduced = useReducedMotion();
897
+ const [leaving, setLeaving] = useState(null);
898
+ const enterT = useRef(new Animated.Value(0)).current;
899
+ const exitT = useRef(new Animated.Value(0)).current;
900
+ const prev = useRef({ key: transitionKey, node: children, variant });
901
+ const [enterVariant, setEnterVariant] = useState(variant);
902
+ const running = useRef({});
903
+ const mounted = useRef(false);
904
+ useEffect(() => {
905
+ var _a, _b;
906
+ const last = prev.current;
907
+ prev.current = { key: transitionKey, node: children, variant };
908
+ if (!mounted.current) {
909
+ mounted.current = true;
910
+ setEnterVariant(variant);
911
+ running.current.enter = startEnter(enterT, variant, reduced, 0);
912
+ return;
913
+ }
914
+ if (last.key === transitionKey) return;
915
+ (_a = running.current.enter) == null ? void 0 : _a.stop();
916
+ (_b = running.current.exit) == null ? void 0 : _b.stop();
917
+ setLeaving(last);
918
+ setEnterVariant(variant);
919
+ enterT.setValue(0);
920
+ exitT.setValue(0);
921
+ const exitDuration = reduced ? REDUCED_FADE_MS : last.variant === "fade" ? LOADER_HANDOFF_FADE_MS : CARD_EXIT_MS;
922
+ const exit = Animated.timing(exitT, {
923
+ toValue: 1,
924
+ duration: scaledMs(exitDuration),
925
+ easing: easeWire3,
926
+ useNativeDriver: true
927
+ });
928
+ running.current.exit = exit;
929
+ exit.start();
930
+ const enter = startEnter(
931
+ enterT,
932
+ variant,
933
+ reduced,
934
+ reduced ? 0 : CARD_EXIT_LEAD_MS,
935
+ () => setLeaving(null)
936
+ );
937
+ running.current.enter = enter;
938
+ });
939
+ useEffect(
940
+ () => () => {
941
+ var _a, _b;
942
+ (_a = running.current.enter) == null ? void 0 : _a.stop();
943
+ (_b = running.current.exit) == null ? void 0 : _b.stop();
944
+ },
945
+ []
946
+ );
947
+ const enterStyle = {
948
+ opacity: enterT,
949
+ transform: transformFor(enterVariant, enterT, reduced, "enter")
950
+ };
951
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
952
+ leaving ? /* @__PURE__ */ jsx(
953
+ Animated.View,
954
+ {
955
+ pointerEvents: "none",
956
+ style: [
957
+ StyleSheet.absoluteFill,
958
+ {
959
+ opacity: exitT.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }),
960
+ transform: transformFor(leaving.variant, exitT, reduced, "exit")
961
+ }
962
+ ],
963
+ children: leaving.node
964
+ }
965
+ ) : null,
966
+ /* @__PURE__ */ jsx(Animated.View, { style: [styles11.fill, enterStyle], children })
967
+ ] });
968
+ };
969
+ var startEnter = (value, variant, reduced, delay, onDone) => {
970
+ value.setValue(0);
971
+ const anim = !reduced && variant === "spring" ? Animated.spring(value, {
972
+ toValue: 1,
973
+ delay: scaledMs(delay),
974
+ damping: CARD_SPRING.damping,
975
+ stiffness: CARD_SPRING.stiffness,
976
+ mass: CARD_SPRING.mass,
977
+ useNativeDriver: true
978
+ }) : Animated.timing(value, {
979
+ toValue: 1,
980
+ delay: scaledMs(delay),
981
+ duration: scaledMs(
982
+ reduced ? REDUCED_FADE_MS : variant === "fade" ? LOADER_HANDOFF_FADE_MS : CARD_ENTER_MS
983
+ ),
984
+ easing: easeWire3,
985
+ useNativeDriver: true
986
+ });
987
+ anim.start(({ finished }) => {
988
+ if (finished) onDone == null ? void 0 : onDone();
989
+ });
990
+ return anim;
991
+ };
992
+ var transformFor = (variant, t, reduced, direction) => {
993
+ if (reduced || variant === "fade") return [];
994
+ if (variant === "spring" && direction === "enter") {
995
+ return [{ scale: t.interpolate({ inputRange: [0, 1], outputRange: [CARD_ENTER_SCALE_FROM, 1] }) }];
996
+ }
997
+ return [
998
+ {
999
+ translateX: t.interpolate({
1000
+ inputRange: [0, 1],
1001
+ outputRange: direction === "enter" ? [CARD_SLIDE_PX, 0] : [0, -CARD_SLIDE_PX]
1002
+ })
1003
+ }
1004
+ ];
1005
+ };
1006
+ var CardHandoff = React14.memo(_CardHandoff);
1007
+ var styles11 = StyleSheet.create({
1008
+ fill: { flex: 1 }
1009
+ });
1010
+
1011
+ // src/utils/deriveAnswers.ts
1012
+ var stripPrefix = (s) => s.replace(/^(I selected:|My answer is:|I tapped:)\s*/i, "").trim();
1013
+ var tryParse = (s) => {
1014
+ try {
1015
+ return JSON.parse(s);
1016
+ } catch {
1017
+ return s;
1018
+ }
1019
+ };
1020
+ var deriveAnswers = (messages) => {
1021
+ var _a, _b, _c;
1022
+ const out = {};
1023
+ for (let i = 0; i < messages.length; i++) {
1024
+ const m = messages[i];
1025
+ const r = m == null ? void 0 : m.response;
1026
+ if (!m || m.role !== "assistant" || !r || r.action !== "render") continue;
1027
+ const props = (_a = r.props) != null ? _a : {};
1028
+ const progress = props.progress;
1029
+ const questionText = (_c = (_b = props.title) != null ? _b : props.label) != null ? _c : props.question;
1030
+ const key = (progress == null ? void 0 : progress.key) || questionText;
1031
+ const reply = messages[i + 1];
1032
+ if (key && (reply == null ? void 0 : reply.role) === "user") {
1033
+ out[key] = tryParse(stripPrefix(reply.content));
1034
+ }
1035
+ }
1036
+ return out;
1037
+ };
1038
+
1039
+ // src/utils/readProgress.ts
1040
+ var readProgress = (response) => {
1041
+ var _a;
1042
+ if (!response || response.action !== "render") return {};
1043
+ const props = (_a = response.props) != null ? _a : {};
1044
+ const raw = props.progress;
1045
+ if (!raw || typeof raw !== "object") return {};
1046
+ const p = raw;
1047
+ return {
1048
+ step: typeof p.step === "number" ? p.step : void 0,
1049
+ total: typeof p.total === "number" ? p.total : void 0,
1050
+ key: typeof p.key === "string" ? p.key : void 0,
1051
+ skippable: typeof p.skippable === "boolean" ? p.skippable : void 0
1052
+ };
1053
+ };
1054
+
1055
+ // src/analytics/reportClientEvent.ts
1056
+ var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1057
+ var reportClientEvents = (target, events) => {
1058
+ if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return;
1059
+ try {
1060
+ const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
1061
+ const headers = { "Content-Type": "application/json" };
1062
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
1063
+ void fetch(url, {
1064
+ method: "POST",
1065
+ headers,
1066
+ body: JSON.stringify({ events })
1067
+ }).catch(() => {
1068
+ });
1069
+ } catch {
1070
+ }
1071
+ };
1072
+ var reportClientEvent = (target, event) => reportClientEvents(target, [event]);
1073
+
1074
+ // src/analytics/sendPreview.ts
1075
+ var sendPreview = (target, { sessionId, userMessage }) => {
1076
+ if (!(target == null ? void 0 : target.serverUrl)) return;
1077
+ try {
1078
+ const url = `${target.serverUrl.replace(/\/$/, "")}/a2a`;
1079
+ const headers = { "Content-Type": "application/json" };
1080
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
1081
+ void fetch(url, {
1082
+ method: "POST",
1083
+ headers,
1084
+ body: JSON.stringify({
1085
+ jsonrpc: "2.0",
1086
+ id: Math.floor(Math.random() * 1e9),
1087
+ method: "message/preview",
1088
+ params: {
1089
+ message: { role: "user", parts: [{ text: userMessage }] },
1090
+ contextId: sessionId,
1091
+ metadata: { sessionId }
1092
+ }
1093
+ })
1094
+ }).catch(() => {
1095
+ });
1096
+ } catch {
1097
+ }
1098
+ };
1099
+ var SKIP_ONE_SENTINEL = "__wireai_skip__";
1100
+ var ANSWER_CALLBACKS = ["onSubmit", "onSelect", "onConfirm", "onPress"];
1101
+ var DEFAULT_COPY = {
1102
+ startingTitle: "Getting started\u2026",
1103
+ startingHint: "Setting up your onboarding",
1104
+ restoringTitle: "Picking up where you left off\u2026",
1105
+ restoringHint: "Restoring your session",
1106
+ thinkingTitle: "Personalizing\u2026",
1107
+ thinkingHint: "Tailoring your next question",
1108
+ checkingTitle: "Checking\u2026",
1109
+ checkingHint: "One moment",
1110
+ completeTitle: "You're all set",
1111
+ completeCta: "Let's go"
1112
+ };
1113
+ var OnboardingFlow = ({
1114
+ validators,
1115
+ onComplete,
1116
+ onError,
1117
+ onEvent,
1118
+ copy,
1119
+ approxScreens,
1120
+ startMessage,
1121
+ startTimeoutMs,
1122
+ fallbackFlow,
1123
+ maxRetries,
1124
+ reportTarget,
1125
+ sessionId,
1126
+ resumed = false,
1127
+ clientContext
1128
+ }) => {
1129
+ var _a, _b, _c, _d, _e, _f;
1130
+ const { messages, error, isLoading, sendMessage, reset } = useWireAIThread();
1131
+ const makeActions = useWireAIAction(sendMessage);
1132
+ const c = useMemo(() => ({ ...DEFAULT_COPY, ...copy }), [copy]);
1133
+ const onEventRef = useRef(onEvent);
1134
+ onEventRef.current = onEvent;
1135
+ const onErrorRef = useRef(onError);
1136
+ onErrorRef.current = onError;
1137
+ const reportTargetRef = useRef(reportTarget);
1138
+ reportTargetRef.current = reportTarget;
1139
+ const sessionIdRef = useRef(sessionId);
1140
+ sessionIdRef.current = sessionId;
1141
+ const clientContextRef = useRef(clientContext);
1142
+ clientContextRef.current = clientContext;
1143
+ const lastScreenIndexRef = useRef(-1);
1144
+ const previewTimer = useRef(null);
1145
+ const started = useRef(false);
1146
+ const finished = useRef(false);
1147
+ const errored = useRef(false);
1148
+ const attempts = useRef(0);
1149
+ const degradedRef = useRef(false);
1150
+ const [degraded, setDegraded] = useState(false);
1151
+ const [timedOut, setTimedOut] = useState(false);
1152
+ const [validationError, setValidationError] = useState();
1153
+ const [validating, setValidating] = useState(false);
1154
+ const lastCard = useMemo(
1155
+ () => [...messages].reverse().find((m) => {
1156
+ var _a2;
1157
+ return m.role === "assistant" && ((_a2 = m.response) == null ? void 0 : _a2.action) === "render";
1158
+ }),
1159
+ [messages]
1160
+ );
1161
+ const renderedCount = useMemo(
1162
+ () => messages.filter((m) => {
1163
+ var _a2;
1164
+ return m.role === "assistant" && ((_a2 = m.response) == null ? void 0 : _a2.action) === "render";
1165
+ }).length,
1166
+ [messages]
1167
+ );
1168
+ const progress = useMemo(() => readProgress(lastCard == null ? void 0 : lastCard.response), [lastCard]);
1169
+ useEffect(() => {
1170
+ var _a2;
1171
+ if (started.current) return;
1172
+ started.current = true;
1173
+ sendMessage(resumed ? SKIP_ONE_SENTINEL : startMessage);
1174
+ (_a2 = onEventRef.current) == null ? void 0 : _a2.call(
1175
+ onEventRef,
1176
+ resumed ? { type: "resumed", contextId: sessionId } : { type: "started", contextId: sessionId }
1177
+ );
1178
+ }, [sendMessage, startMessage, resumed, sessionId]);
1179
+ useEffect(() => {
1180
+ if (lastCard) return;
1181
+ const timer = setTimeout(() => setTimedOut(true), startTimeoutMs);
1182
+ return () => clearTimeout(timer);
1183
+ }, [lastCard, startTimeoutMs]);
1184
+ useEffect(() => {
1185
+ var _a2, _b2, _c2, _d2;
1186
+ setValidationError(void 0);
1187
+ if (!(lastCard == null ? void 0 : lastCard.id)) return;
1188
+ const step2 = (_a2 = progress.step) != null ? _a2 : renderedCount;
1189
+ lastScreenIndexRef.current = Math.max(0, step2 - 1);
1190
+ (_c2 = onEventRef.current) == null ? void 0 : _c2.call(onEventRef, {
1191
+ type: "turn",
1192
+ step: step2,
1193
+ component: (_b2 = lastCard.response) == null ? void 0 : _b2.component
1194
+ });
1195
+ if (((_d2 = lastCard.response) == null ? void 0 : _d2.component) === "InterstitialCard") {
1196
+ sendPreview(reportTargetRef.current, {
1197
+ sessionId: sessionIdRef.current,
1198
+ userMessage: `My answer is: ${JSON.stringify("continue")}`
1199
+ });
1200
+ }
1201
+ }, [lastCard == null ? void 0 : lastCard.id]);
1202
+ useEffect(() => {
1203
+ return () => {
1204
+ var _a2, _b2, _c2;
1205
+ if (finished.current || degradedRef.current) return;
1206
+ reportClientEvent(reportTargetRef.current, {
1207
+ event_type: "dropped",
1208
+ session_id: sessionIdRef.current,
1209
+ screen_index: lastScreenIndexRef.current >= 0 ? lastScreenIndexRef.current : void 0,
1210
+ device: (_a2 = clientContextRef.current) == null ? void 0 : _a2.device,
1211
+ user_context: (_b2 = clientContextRef.current) == null ? void 0 : _b2.userContext,
1212
+ user_id: (_c2 = clientContextRef.current) == null ? void 0 : _c2.userId
1213
+ });
1214
+ };
1215
+ }, []);
1216
+ const restartThread = useCallback(() => {
1217
+ setTimedOut(false);
1218
+ started.current = false;
1219
+ errored.current = false;
1220
+ reset();
1221
+ }, [reset]);
1222
+ useEffect(() => {
1223
+ var _a2, _b2, _c2, _d2, _e2, _f2, _g;
1224
+ if (errored.current) return;
1225
+ if (!error && !timedOut) return;
1226
+ errored.current = true;
1227
+ const reason = error ? "backend" : "timeout";
1228
+ (_a2 = onEventRef.current) == null ? void 0 : _a2.call(onEventRef, { type: "error", reason });
1229
+ attempts.current += 1;
1230
+ if (attempts.current <= maxRetries) {
1231
+ (_b2 = onEventRef.current) == null ? void 0 : _b2.call(onEventRef, { type: "retry", reason, attempt: attempts.current });
1232
+ restartThread();
1233
+ } else {
1234
+ (_c2 = onEventRef.current) == null ? void 0 : _c2.call(onEventRef, { type: "fallback", reason });
1235
+ reportClientEvent(reportTargetRef.current, {
1236
+ event_type: "client_fallback",
1237
+ session_id: sessionIdRef.current,
1238
+ screen_index: lastScreenIndexRef.current >= 0 ? lastScreenIndexRef.current : void 0,
1239
+ meta: JSON.stringify({ reason }),
1240
+ device: (_d2 = clientContextRef.current) == null ? void 0 : _d2.device,
1241
+ user_context: (_e2 = clientContextRef.current) == null ? void 0 : _e2.userContext,
1242
+ user_id: (_f2 = clientContextRef.current) == null ? void 0 : _f2.userId
1243
+ });
1244
+ degradedRef.current = true;
1245
+ setDegraded(true);
1246
+ (_g = onErrorRef.current) == null ? void 0 : _g.call(onErrorRef, error != null ? error : new Error("Onboarding timed out"));
1247
+ }
1248
+ }, [error, timedOut, maxRetries]);
1249
+ const handleFinish = useCallback(() => {
1250
+ if (finished.current) return;
1251
+ finished.current = true;
1252
+ onComplete({ answers: deriveAnswers(messages), raw: messages });
1253
+ }, [messages, onComplete]);
1254
+ const handleRetry = useCallback(() => {
1255
+ attempts.current = 0;
1256
+ degradedRef.current = false;
1257
+ setDegraded(false);
1258
+ restartThread();
1259
+ }, [restartThread]);
1260
+ const skipQuestion = useCallback(() => {
1261
+ if (isLoading) return;
1262
+ sendMessage(SKIP_ONE_SENTINEL);
1263
+ }, [isLoading, sendMessage]);
1264
+ const previewOnSelect = useCallback((value) => {
1265
+ const target = reportTargetRef.current;
1266
+ if (!(target == null ? void 0 : target.serverUrl)) return;
1267
+ if (Array.isArray(value) ? value.length === 0 : !value) return;
1268
+ if (previewTimer.current) clearTimeout(previewTimer.current);
1269
+ previewTimer.current = setTimeout(() => {
1270
+ sendPreview(target, {
1271
+ sessionId: sessionIdRef.current,
1272
+ userMessage: `I selected: ${JSON.stringify(value)}`
1273
+ });
1274
+ }, 300);
1275
+ }, []);
1276
+ useEffect(() => () => {
1277
+ if (previewTimer.current) clearTimeout(previewTimer.current);
1278
+ }, []);
1279
+ const wrappedActions = useCallback(
1280
+ (messageId) => {
1281
+ const base = makeActions(messageId);
1282
+ const key = progress.key;
1283
+ const validator = key ? validators == null ? void 0 : validators[key] : void 0;
1284
+ if (!validator) return base;
1285
+ const out = { ...base };
1286
+ for (const name of ANSWER_CALLBACKS) {
1287
+ const original = base[name];
1288
+ if (!original) continue;
1289
+ out[name] = (...args) => {
1290
+ const value = args[0];
1291
+ if (typeof value !== "string") return original(...args);
1292
+ setValidating(true);
1293
+ validator(value).then((res) => {
1294
+ var _a2;
1295
+ if (res.ok) {
1296
+ setValidationError(void 0);
1297
+ original(...args);
1298
+ } else {
1299
+ setValidationError((_a2 = res.error) != null ? _a2 : "That value isn't available.");
1300
+ }
1301
+ }).catch(() => {
1302
+ original(...args);
1303
+ }).finally(() => setValidating(false));
1304
+ };
1305
+ }
1306
+ return out;
1307
+ },
1308
+ [makeActions, progress.key, validators]
1309
+ );
1310
+ if (degraded) {
1311
+ if (fallbackFlow !== void 0) return /* @__PURE__ */ jsx(Fragment, { children: fallbackFlow });
1312
+ if (onError) {
1313
+ return /* @__PURE__ */ jsx(LoadingScreen, { title: c.startingTitle, hint: c.startingHint });
1314
+ }
1315
+ return /* @__PURE__ */ jsx(
1316
+ ErrorBlock,
1317
+ {
1318
+ onRetry: handleRetry,
1319
+ message: timedOut ? "This is taking longer than expected. Try again?" : "We couldn't reach the onboarding service. Try again?"
1320
+ }
1321
+ );
1322
+ }
1323
+ if (error || timedOut) {
1324
+ return /* @__PURE__ */ jsx(LoadingScreen, { title: c.startingTitle, hint: c.startingHint });
1325
+ }
1326
+ const response = lastCard == null ? void 0 : lastCard.response;
1327
+ if (!lastCard || !response) {
1328
+ return /* @__PURE__ */ jsx(LoadingScreen, { title: c.startingTitle, hint: c.startingHint });
1329
+ }
1330
+ const step = (_a = progress.step) != null ? _a : renderedCount;
1331
+ const total = (_b = progress.total) != null ? _b : approxScreens;
1332
+ const isTerminal = response.action === "render" && response.component === "StatusCard";
1333
+ if (isTerminal) {
1334
+ const props = (_c = response.props) != null ? _c : {};
1335
+ return /* @__PURE__ */ jsx(OnboardingScaffold, { step, complete: true, approxScreens: total, children: /* @__PURE__ */ jsx(
1336
+ CompletionView,
1337
+ {
1338
+ title: (_d = props.title) != null ? _d : c.completeTitle,
1339
+ message: props.message,
1340
+ ctaLabel: (_e = props.ctaLabel) != null ? _e : c.completeCta,
1341
+ onContinue: handleFinish
1342
+ }
1343
+ ) });
1344
+ }
1345
+ const overrides = {
1346
+ ...wrappedActions(lastCard.id),
1347
+ // Inject the inline error onto whichever card supports it (TextInputCard).
1348
+ validationError,
1349
+ // Choice cards (Selection/ChipSelect) call this as the live selection changes, so the flow
1350
+ // can prefetch the next turn before Continue. Non-committing; ignored by cards that don't use it.
1351
+ onSelectionChange: previewOnSelect
1352
+ };
1353
+ return /* @__PURE__ */ jsx(
1354
+ OnboardingScaffold,
1355
+ {
1356
+ step,
1357
+ onSkip: progress.skippable ? skipQuestion : void 0,
1358
+ approxScreens: total,
1359
+ children: /* @__PURE__ */ jsx(
1360
+ CardHandoff,
1361
+ {
1362
+ transitionKey: validating ? "loader:checking" : isLoading ? "loader:thinking" : lastCard.id,
1363
+ variant: validating || isLoading ? "fade" : ((_f = lastCard.response) == null ? void 0 : _f.component) === "InterstitialCard" ? "spring" : "slide",
1364
+ children: validating ? /* @__PURE__ */ jsx(LoadingBlock, { title: c.checkingTitle, hint: c.checkingHint }) : isLoading ? (
1365
+ // Between turns: the answer was sent and we're waiting on the next card.
1366
+ // Show the "thinking" loader instead of leaving the old card on screen.
1367
+ /* @__PURE__ */ jsx(LoadingBlock, { title: c.thinkingTitle, hint: c.thinkingHint })
1368
+ ) : (
1369
+ // key={lastCard.id} is load-bearing: cards (SelectionCard/ChipSelectCard)
1370
+ // hold internal `selected`/`submitted` state, and React reuses a component
1371
+ // instance across turns when the rendered type is identical (e.g. two
1372
+ // SelectionCards in a row). Without a per-turn key the second card inherits
1373
+ // the first's `submitted=true` and silently rejects all taps. Keying on the
1374
+ // message id forces a fresh mount every turn. (CardHandoff's transitionKey
1375
+ // shares the id, but this renderer key stays the state-isolation guarantee.)
1376
+ /* @__PURE__ */ jsx(
1377
+ ComponentRenderer,
1378
+ {
1379
+ messageId: lastCard.id,
1380
+ response,
1381
+ callbackOverrides: overrides
1382
+ },
1383
+ lastCard.id
1384
+ )
1385
+ )
1386
+ }
1387
+ )
1388
+ }
1389
+ );
1390
+ };
1391
+ var easeWire4 = Easing.bezier(...WIRE_BEZIER);
1392
+ var schema = z.object({
1393
+ title: z.string().describe("Question or section header, e.g. 'Which season?'"),
1394
+ chips: z.array(z.string()).min(1).describe("List of option labels shown as chips, e.g. ['Spring','Summer','Autumn','Winter']"),
1395
+ multiSelect: z.boolean().optional().describe("Allow multiple chips selected at once, default true"),
1396
+ submitLabel: z.string().optional().describe("Confirm button label, default: Continue"),
1397
+ maxSelections: z.number().optional().describe("Maximum number of chips the user can select at once")
1398
+ });
1399
+ var _ChipItem = ({ chip, isSelected, multiSelect, onToggle }) => {
1400
+ const t = useOnboardingTheme();
1401
+ const reduced = useReducedMotion();
1402
+ const handlePress = useCallback(() => onToggle(chip), [chip, onToggle]);
1403
+ const pressScale = useRef(new Animated.Value(1)).current;
1404
+ const pressTo = useCallback(
1405
+ (to) => {
1406
+ if (reduced) return;
1407
+ Animated.spring(pressScale, {
1408
+ toValue: to,
1409
+ friction: PRESS_SPRING.friction,
1410
+ tension: PRESS_SPRING.tension,
1411
+ useNativeDriver: true
1412
+ }).start();
1413
+ },
1414
+ [pressScale, reduced]
1415
+ );
1416
+ const handlePressIn = useCallback(() => pressTo(CHIP_PRESS_SCALE), [pressTo]);
1417
+ const handlePressOut = useCallback(() => pressTo(1), [pressTo]);
1418
+ const fillT = useRef(new Animated.Value(isSelected ? 1 : 0)).current;
1419
+ useEffect(() => {
1420
+ const anim = Animated.timing(fillT, {
1421
+ toValue: isSelected ? 1 : 0,
1422
+ duration: scaledMs(stateChangeDuration(reduced, CHIP_FILL_MS)),
1423
+ easing: easeWire4,
1424
+ useNativeDriver: true
1425
+ });
1426
+ anim.start();
1427
+ return () => anim.stop();
1428
+ }, [isSelected, reduced, fillT]);
1429
+ const checkT = useRef(new Animated.Value(isSelected ? 1 : 0)).current;
1430
+ useEffect(() => {
1431
+ if (!isSelected) {
1432
+ checkT.setValue(0);
1433
+ return;
1434
+ }
1435
+ if (reduced) {
1436
+ checkT.setValue(1);
1437
+ return;
1438
+ }
1439
+ checkT.setValue(0);
1440
+ const anim = Animated.spring(checkT, {
1441
+ toValue: 1,
1442
+ friction: CHECK_SPRING.friction,
1443
+ tension: CHECK_SPRING.tension,
1444
+ useNativeDriver: true
1445
+ });
1446
+ anim.start();
1447
+ return () => anim.stop();
1448
+ }, [isSelected, reduced, checkT]);
1449
+ const checkScale = checkT.interpolate({ inputRange: [0, 1], outputRange: [CHECK_SCALE_FROM, 1] });
1450
+ return /* @__PURE__ */ jsx(
1451
+ Pressable,
1452
+ {
1453
+ onPress: handlePress,
1454
+ onPressIn: handlePressIn,
1455
+ onPressOut: handlePressOut,
1456
+ accessibilityRole: multiSelect ? "checkbox" : "radio",
1457
+ accessibilityState: multiSelect ? { checked: isSelected } : { selected: isSelected },
1458
+ accessibilityLabel: chip,
1459
+ children: /* @__PURE__ */ jsxs(
1460
+ Animated.View,
1461
+ {
1462
+ style: [
1463
+ styles12.chip,
1464
+ {
1465
+ borderRadius: t.radius.full,
1466
+ paddingHorizontal: t.spacing.md,
1467
+ paddingVertical: t.spacing.sm,
1468
+ borderColor: t.colors.border,
1469
+ backgroundColor: t.colors.background,
1470
+ transform: [{ scale: pressScale }]
1471
+ }
1472
+ ],
1473
+ children: [
1474
+ /* @__PURE__ */ jsx(
1475
+ Animated.View,
1476
+ {
1477
+ pointerEvents: "none",
1478
+ style: [
1479
+ StyleSheet.absoluteFill,
1480
+ styles12.fillOverlay,
1481
+ {
1482
+ borderRadius: t.radius.full,
1483
+ borderColor: t.colors.primary,
1484
+ backgroundColor: t.colors.primary,
1485
+ opacity: fillT
1486
+ }
1487
+ ]
1488
+ }
1489
+ ),
1490
+ isSelected ? /* @__PURE__ */ jsx(
1491
+ Animated.Text,
1492
+ {
1493
+ style: [
1494
+ styles12.check,
1495
+ { color: t.colors.onPrimary, opacity: checkT, transform: [{ scale: checkScale }] }
1496
+ ],
1497
+ children: "\u2713"
1498
+ }
1499
+ ) : null,
1500
+ /* @__PURE__ */ jsx(
1501
+ Text,
1502
+ {
1503
+ style: [
1504
+ captionStyle(t.fonts),
1505
+ styles12.chipLabel,
1506
+ { color: isSelected ? t.colors.onPrimary : t.colors.textMuted }
1507
+ ],
1508
+ children: chip
1509
+ }
1510
+ )
1511
+ ]
1512
+ }
1513
+ )
1514
+ }
1515
+ );
1516
+ };
1517
+ var ChipItem = React14.memo(_ChipItem);
1518
+ var _ChipSelectCard = ({
1519
+ title,
1520
+ chips,
1521
+ multiSelect = true,
1522
+ submitLabel = "Continue",
1523
+ maxSelections,
1524
+ onSelect,
1525
+ onSelectionChange
1526
+ }) => {
1527
+ const t = useOnboardingTheme();
1528
+ const [selected, setSelected] = useState([]);
1529
+ const [submitted, setSubmitted] = useState(false);
1530
+ useEffect(() => {
1531
+ if (!submitted && selected.length) onSelectionChange == null ? void 0 : onSelectionChange(multiSelect ? selected : selected[0]);
1532
+ }, [selected, submitted, multiSelect, onSelectionChange]);
1533
+ const toggle = useCallback(
1534
+ (chip) => {
1535
+ if (submitted) return;
1536
+ if (multiSelect) {
1537
+ setSelected((prev) => {
1538
+ if (prev.includes(chip)) return prev.filter((c) => c !== chip);
1539
+ if (maxSelections && prev.length >= maxSelections) return prev;
1540
+ return [...prev, chip];
1541
+ });
1542
+ } else {
1543
+ setSelected([chip]);
1544
+ }
1545
+ },
1546
+ [submitted, multiSelect, maxSelections]
1547
+ );
1548
+ const handleSubmit = useCallback(() => {
1549
+ if (!selected.length || !onSelect || submitted) return;
1550
+ setSubmitted(true);
1551
+ onSelect(multiSelect ? selected : selected[0]);
1552
+ }, [selected, onSelect, submitted, multiSelect]);
1553
+ return /* @__PURE__ */ jsx(
1554
+ CardLayout,
1555
+ {
1556
+ title,
1557
+ subtitle: maxSelections ? `Select up to ${maxSelections}` : void 0,
1558
+ footer: /* @__PURE__ */ jsx(
1559
+ Button,
1560
+ {
1561
+ title: submitLabel,
1562
+ onPress: handleSubmit,
1563
+ variant: "primary",
1564
+ disabled: selected.length === 0 || submitted
1565
+ }
1566
+ ),
1567
+ children: /* @__PURE__ */ jsx(View, { style: [styles12.chipsRow, { gap: t.spacing.sm + t.spacing.xs }], children: chips.map((chip) => /* @__PURE__ */ jsx(
1568
+ ChipItem,
1569
+ {
1570
+ chip,
1571
+ isSelected: selected.includes(chip),
1572
+ multiSelect,
1573
+ onToggle: toggle
1574
+ },
1575
+ chip
1576
+ )) })
1577
+ }
1578
+ );
1579
+ };
1580
+ var ChipSelectCard = {
1581
+ name: "ChipSelectCard",
1582
+ 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.",
1583
+ component: React14.memo(_ChipSelectCard),
1584
+ propsSchema: schema,
1585
+ defaultProps: { multiSelect: true, submitLabel: "Continue" }
1586
+ };
1587
+ var styles12 = StyleSheet.create({
1588
+ chipsRow: { flexDirection: "row", flexWrap: "wrap" },
1589
+ chip: { borderWidth: 1.5, flexDirection: "row", alignItems: "center", gap: 6, overflow: "hidden" },
1590
+ fillOverlay: { borderWidth: 1.5 },
1591
+ chipLabel: { fontWeight: "600" },
1592
+ check: { fontSize: 13, fontWeight: "800", lineHeight: 15 }
1593
+ });
1594
+ var schema2 = z.object({
1595
+ label: z.string().describe("Input field label / the question being asked"),
1596
+ hint: z.string().optional().describe(
1597
+ "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."
1598
+ ),
1599
+ placeholder: z.string().optional().describe(
1600
+ 'A concrete example answer shown inside the box, phrased like a real user wrote it and starting with "e.g. ". ALWAYS provide one for open-ended questions: it shows the expected answer shape and inspires the user.'
1601
+ ),
1602
+ submitLabel: z.string().optional().describe("Submit button label, default: Submit"),
1603
+ multiline: z.boolean().optional().describe(
1604
+ "Tall multi-line box for open-ended answers (goals, notes, feedback). Default true. Set false for a single-line field like a name or username."
1605
+ ),
1606
+ lines: z.number().optional().describe("Approx visible rows when multiline (default 6). Height is capped at 40% of the screen.")
1607
+ });
1608
+ var _TextInputCard = ({
1609
+ label,
1610
+ hint,
1611
+ placeholder,
1612
+ submitLabel = "Submit",
1613
+ onSubmit,
1614
+ validationError,
1615
+ multiline = true,
1616
+ lines = 6
1617
+ }) => {
1618
+ const t = useOnboardingTheme();
1619
+ const { height: screenHeight } = useWindowDimensions();
1620
+ const [value, setValue] = useState("");
1621
+ const [submitted, setSubmitted] = useState(false);
1622
+ const locked = submitted && !validationError;
1623
+ const APPROX_ROW_HEIGHT = 26;
1624
+ const minHeight = multiline ? Math.min(Math.max(lines, 4) * APPROX_ROW_HEIGHT + t.spacing.md * 2, screenHeight * 0.4) : 52;
1625
+ const handleSubmit = useCallback(() => {
1626
+ if (value.trim() && onSubmit && !locked) {
1627
+ setSubmitted(true);
1628
+ onSubmit(value.trim());
1629
+ }
1630
+ }, [value, onSubmit, locked]);
1631
+ return /* @__PURE__ */ jsx(
1632
+ CardLayout,
1633
+ {
1634
+ title: label,
1635
+ subtitle: hint,
1636
+ footer: /* @__PURE__ */ jsx(
1637
+ Button,
1638
+ {
1639
+ title: submitLabel,
1640
+ onPress: handleSubmit,
1641
+ variant: "primary",
1642
+ disabled: !value.trim() || locked
1643
+ }
1644
+ ),
1645
+ children: /* @__PURE__ */ jsxs(View, { style: { gap: t.spacing.sm }, children: [
1646
+ /* @__PURE__ */ jsx(
1647
+ TextInput,
1648
+ {
1649
+ value,
1650
+ onChangeText: setValue,
1651
+ placeholder: placeholder != null ? placeholder : "",
1652
+ placeholderTextColor: t.colors.textMuted,
1653
+ editable: !locked,
1654
+ multiline,
1655
+ numberOfLines: multiline ? lines : 1,
1656
+ textAlignVertical: multiline ? "top" : "center",
1657
+ style: [
1658
+ bodyStyle(t.fonts),
1659
+ styles13.input,
1660
+ {
1661
+ color: t.colors.text,
1662
+ backgroundColor: t.colors.surface,
1663
+ borderRadius: t.radius.md,
1664
+ borderColor: validationError ? t.colors.error : t.colors.border,
1665
+ padding: t.spacing.md,
1666
+ minHeight
1667
+ }
1668
+ ]
1669
+ }
1670
+ ),
1671
+ validationError ? /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.error }], children: validationError }) : null
1672
+ ] })
1673
+ }
1674
+ );
1675
+ };
1676
+ var TextInputCard = {
1677
+ name: "TextInputCard",
1678
+ 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.",
1679
+ component: React14.memo(_TextInputCard),
1680
+ propsSchema: schema2,
1681
+ defaultProps: { submitLabel: "Submit", multiline: true, lines: 6 }
1682
+ };
1683
+ var styles13 = StyleSheet.create({
1684
+ input: { borderWidth: 1 }
1685
+ });
1686
+ var easeWire5 = Easing.bezier(...WIRE_BEZIER);
1687
+ var optionObjectSchema = z.object({
1688
+ value: z.string().describe("Option value"),
1689
+ label: z.string().describe("Option display label")
1690
+ });
1691
+ var schema3 = z.object({
1692
+ title: z.string().describe("Question or prompt for the user"),
1693
+ options: z.preprocess(
1694
+ (arr) => Array.isArray(arr) ? arr.map((item) => typeof item === "string" ? { value: item, label: item } : item) : arr,
1695
+ z.array(optionObjectSchema).min(1)
1696
+ ).describe("List of choices"),
1697
+ submitLabel: z.string().optional().describe("Submit button label, default: Continue"),
1698
+ multiSelect: z.boolean().optional().describe("Allow multiple selections")
1699
+ });
1700
+ var _OptionRow = ({ opt, isSelected, multiSelect, onToggle }) => {
1701
+ const t = useOnboardingTheme();
1702
+ const reduced = useReducedMotion();
1703
+ const handlePress = useCallback(() => onToggle(opt.value), [opt.value, onToggle]);
1704
+ const pressScale = useRef(new Animated.Value(1)).current;
1705
+ const pressTo = useCallback(
1706
+ (to) => {
1707
+ if (reduced) return;
1708
+ Animated.spring(pressScale, {
1709
+ toValue: to,
1710
+ friction: PRESS_SPRING.friction,
1711
+ tension: PRESS_SPRING.tension,
1712
+ useNativeDriver: true
1713
+ }).start();
1714
+ },
1715
+ [pressScale, reduced]
1716
+ );
1717
+ const handlePressIn = useCallback(() => pressTo(ROW_PRESS_SCALE), [pressTo]);
1718
+ const handlePressOut = useCallback(() => pressTo(1), [pressTo]);
1719
+ const selectT = useRef(new Animated.Value(isSelected ? 1 : 0)).current;
1720
+ useEffect(() => {
1721
+ const anim = Animated.timing(selectT, {
1722
+ toValue: isSelected ? 1 : 0,
1723
+ duration: scaledMs(stateChangeDuration(reduced, SELECT_FILL_MS)),
1724
+ easing: easeWire5,
1725
+ useNativeDriver: true
1726
+ });
1727
+ anim.start();
1728
+ return () => anim.stop();
1729
+ }, [isSelected, reduced, selectT]);
1730
+ const checkT = useRef(new Animated.Value(isSelected ? 1 : 0)).current;
1731
+ useEffect(() => {
1732
+ if (!isSelected) {
1733
+ checkT.setValue(0);
1734
+ return;
1735
+ }
1736
+ if (reduced) {
1737
+ checkT.setValue(1);
1738
+ return;
1739
+ }
1740
+ checkT.setValue(0);
1741
+ const anim = Animated.spring(checkT, {
1742
+ toValue: 1,
1743
+ friction: CHECK_SPRING.friction,
1744
+ tension: CHECK_SPRING.tension,
1745
+ useNativeDriver: true
1746
+ });
1747
+ anim.start();
1748
+ return () => anim.stop();
1749
+ }, [isSelected, reduced, checkT]);
1750
+ const checkScale = checkT.interpolate({ inputRange: [0, 1], outputRange: [CHECK_SCALE_FROM, 1] });
1751
+ return /* @__PURE__ */ jsx(
1752
+ Pressable,
1753
+ {
1754
+ onPress: handlePress,
1755
+ onPressIn: handlePressIn,
1756
+ onPressOut: handlePressOut,
1757
+ accessibilityRole: multiSelect ? "checkbox" : "radio",
1758
+ accessibilityState: multiSelect ? { checked: isSelected } : { selected: isSelected },
1759
+ accessibilityLabel: opt.label,
1760
+ children: /* @__PURE__ */ jsxs(
1761
+ Animated.View,
1762
+ {
1763
+ style: [
1764
+ styles14.option,
1765
+ {
1766
+ padding: t.spacing.md,
1767
+ borderRadius: t.radius.md,
1768
+ gap: t.spacing.sm,
1769
+ borderColor: t.colors.border,
1770
+ backgroundColor: t.colors.background,
1771
+ transform: [{ scale: pressScale }]
1772
+ }
1773
+ ],
1774
+ children: [
1775
+ /* @__PURE__ */ jsx(
1776
+ Animated.View,
1777
+ {
1778
+ pointerEvents: "none",
1779
+ style: [
1780
+ StyleSheet.absoluteFill,
1781
+ styles14.selectedOverlay,
1782
+ {
1783
+ borderRadius: t.radius.md,
1784
+ borderColor: t.colors.primary,
1785
+ backgroundColor: t.colors.primarySoft,
1786
+ opacity: selectT
1787
+ }
1788
+ ]
1789
+ }
1790
+ ),
1791
+ /* @__PURE__ */ jsx(
1792
+ View,
1793
+ {
1794
+ style: [
1795
+ styles14.indicator,
1796
+ { borderRadius: t.radius.full },
1797
+ isSelected ? { borderColor: t.colors.primary, backgroundColor: t.colors.primary } : { borderColor: t.colors.borderStrong, backgroundColor: "transparent" }
1798
+ ],
1799
+ children: isSelected ? /* @__PURE__ */ jsx(
1800
+ Animated.Text,
1801
+ {
1802
+ style: [
1803
+ styles14.check,
1804
+ { color: t.colors.onPrimary, opacity: checkT, transform: [{ scale: checkScale }] }
1805
+ ],
1806
+ children: "\u2713"
1807
+ }
1808
+ ) : null
1809
+ }
1810
+ ),
1811
+ /* @__PURE__ */ jsx(Text, { style: [labelStyle(t.fonts), { color: t.colors.text }], children: opt.label })
1812
+ ]
1813
+ }
1814
+ )
1815
+ }
1816
+ );
1817
+ };
1818
+ var OptionRow = React14.memo(_OptionRow);
1819
+ var _SelectionCard = ({
1820
+ title,
1821
+ options,
1822
+ submitLabel = "Continue",
1823
+ multiSelect = false,
1824
+ onSelect,
1825
+ onSelectionChange
1826
+ }) => {
1827
+ const t = useOnboardingTheme();
1828
+ const [selected, setSelected] = useState([]);
1829
+ const [submitted, setSubmitted] = useState(false);
1830
+ useEffect(() => {
1831
+ if (!submitted && selected.length) onSelectionChange == null ? void 0 : onSelectionChange(multiSelect ? selected : selected[0]);
1832
+ }, [selected, submitted, multiSelect, onSelectionChange]);
1833
+ const toggle = useCallback(
1834
+ (value) => {
1835
+ if (submitted) return;
1836
+ if (multiSelect) {
1837
+ setSelected((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]);
1838
+ } else {
1839
+ setSelected([value]);
1840
+ }
1841
+ },
1842
+ [submitted, multiSelect]
1843
+ );
1844
+ const handleSubmit = useCallback(() => {
1845
+ if (!selected.length || !onSelect || submitted) return;
1846
+ setSubmitted(true);
1847
+ onSelect(multiSelect ? selected : selected[0]);
1848
+ }, [selected, onSelect, submitted, multiSelect]);
1849
+ return /* @__PURE__ */ jsx(
1850
+ CardLayout,
1851
+ {
1852
+ title,
1853
+ footer: /* @__PURE__ */ jsx(
1854
+ Button,
1855
+ {
1856
+ title: submitLabel,
1857
+ onPress: handleSubmit,
1858
+ variant: "primary",
1859
+ disabled: selected.length === 0 || submitted
1860
+ }
1861
+ ),
1862
+ children: /* @__PURE__ */ jsx(View, { style: { gap: t.spacing.sm + t.spacing.xs }, children: options.map((opt) => /* @__PURE__ */ jsx(
1863
+ OptionRow,
1864
+ {
1865
+ opt,
1866
+ isSelected: selected.includes(opt.value),
1867
+ multiSelect,
1868
+ onToggle: toggle
1869
+ },
1870
+ opt.value
1871
+ )) })
1872
+ }
1873
+ );
1874
+ };
1875
+ var SelectionCard = {
1876
+ name: "SelectionCard",
1877
+ description: "Use for mutually exclusive (radio) or multi-select (checkbox) choices from 2\u20138 options. Use ChipSelectCard instead when options are short single words.",
1878
+ component: React14.memo(_SelectionCard),
1879
+ propsSchema: schema3,
1880
+ defaultProps: { submitLabel: "Continue", multiSelect: false }
1881
+ };
1882
+ var styles14 = StyleSheet.create({
1883
+ option: { borderWidth: 1.5, flexDirection: "row", alignItems: "center", overflow: "hidden" },
1884
+ selectedOverlay: { borderWidth: 1.5 },
1885
+ indicator: { width: 24, height: 24, borderWidth: 2, alignItems: "center", justifyContent: "center" },
1886
+ check: { fontSize: 13, fontWeight: "800", lineHeight: 15 }
1887
+ });
1888
+ var easeWire6 = Easing.bezier(...WIRE_BEZIER);
1889
+ var schema4 = z.object({
1890
+ status: z.enum(["success", "warning", "error", "info"]).describe("Status type"),
1891
+ title: z.string().describe("Status title"),
1892
+ message: z.string().optional().describe("Supporting message"),
1893
+ ctaLabel: z.string().optional().describe(
1894
+ "Optional call-to-action button label. Always include this on success status to keep the conversation going."
1895
+ )
1896
+ });
1897
+ var STATUS_ICONS = {
1898
+ success: "\u2713",
1899
+ warning: "\u26A0",
1900
+ error: "\u2717",
1901
+ info: "\u2139"
1902
+ };
1903
+ var _StatusCard = ({ status, title, message, ctaLabel, onContinue, onPress, onSubmit }) => {
1904
+ var _a, _b;
1905
+ const t = useOnboardingTheme();
1906
+ const reduced = useReducedMotion();
1907
+ const [submitted, setSubmitted] = useState(false);
1908
+ const colorFor = {
1909
+ success: t.colors.success,
1910
+ warning: t.colors.warning,
1911
+ error: t.colors.error,
1912
+ info: t.colors.info
1913
+ };
1914
+ const color = (_a = colorFor[status]) != null ? _a : t.colors.info;
1915
+ const popT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
1916
+ const copyT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
1917
+ useEffect(() => {
1918
+ if (reduced) {
1919
+ popT.setValue(1);
1920
+ copyT.setValue(1);
1921
+ return;
1922
+ }
1923
+ const anims = [
1924
+ Animated.spring(popT, {
1925
+ toValue: 1,
1926
+ friction: STATUS_POP_SPRING.friction,
1927
+ tension: STATUS_POP_SPRING.tension,
1928
+ useNativeDriver: true
1929
+ }),
1930
+ Animated.timing(copyT, {
1931
+ toValue: 1,
1932
+ duration: scaledMs(STATUS_COPY_MS),
1933
+ delay: scaledMs(STATUS_COPY_STAGGER_MS),
1934
+ easing: easeWire6,
1935
+ useNativeDriver: true
1936
+ })
1937
+ ];
1938
+ anims.forEach((a) => a.start());
1939
+ return () => anims.forEach((a) => a.stop());
1940
+ }, [reduced, popT, copyT]);
1941
+ const popScale = popT.interpolate({ inputRange: [0, 1], outputRange: [STATUS_POP_SCALE_FROM, 1] });
1942
+ const copyRise = copyT.interpolate({ inputRange: [0, 1], outputRange: [10, 0] });
1943
+ const handleContinue = useCallback(() => {
1944
+ if (!submitted) {
1945
+ setSubmitted(true);
1946
+ if (onContinue) onContinue();
1947
+ else if (onPress && ctaLabel) onPress(ctaLabel);
1948
+ else if (onSubmit) onSubmit("continue");
1949
+ }
1950
+ }, [submitted, onContinue, onPress, onSubmit, ctaLabel]);
1951
+ return /* @__PURE__ */ jsx(
1952
+ CardLayout,
1953
+ {
1954
+ align: "center",
1955
+ footer: ctaLabel ? /* @__PURE__ */ jsx(Button, { title: ctaLabel, onPress: handleContinue, variant: "primary", disabled: submitted }) : void 0,
1956
+ children: /* @__PURE__ */ jsxs(View, { style: [styles15.center, { gap: t.spacing.sm }], children: [
1957
+ /* @__PURE__ */ jsxs(View, { style: [styles15.titleRow, { gap: t.spacing.sm }], children: [
1958
+ /* @__PURE__ */ jsx(
1959
+ Animated.Text,
1960
+ {
1961
+ style: [
1962
+ headingStyle(t.fonts),
1963
+ { color, opacity: popT, transform: [{ scale: popScale }] }
1964
+ ],
1965
+ children: (_b = STATUS_ICONS[status]) != null ? _b : "\xB7"
1966
+ }
1967
+ ),
1968
+ /* @__PURE__ */ jsx(
1969
+ Animated.Text,
1970
+ {
1971
+ style: [
1972
+ headingStyle(t.fonts),
1973
+ { color, textAlign: "center", opacity: copyT, transform: [{ translateY: copyRise }] }
1974
+ ],
1975
+ children: title
1976
+ }
1977
+ )
1978
+ ] }),
1979
+ message ? /* @__PURE__ */ jsx(
1980
+ Animated.Text,
1981
+ {
1982
+ style: [
1983
+ bodyStyle(t.fonts),
1984
+ {
1985
+ color: t.colors.textMuted,
1986
+ textAlign: "center",
1987
+ opacity: copyT,
1988
+ transform: [{ translateY: copyRise }]
1989
+ }
1990
+ ],
1991
+ children: message
1992
+ }
1993
+ ) : null
1994
+ ] })
1995
+ }
1996
+ );
1997
+ };
1998
+ var StatusCard = {
1999
+ name: "StatusCard",
2000
+ 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.",
2001
+ component: React14.memo(_StatusCard),
2002
+ propsSchema: schema4
2003
+ };
2004
+ var styles15 = StyleSheet.create({
2005
+ center: { width: "100%", alignItems: "center", justifyContent: "center" },
2006
+ titleRow: { flexDirection: "row", alignItems: "center", justifyContent: "center" }
2007
+ });
2008
+ var schema5 = z.object({
2009
+ label: z.string().describe("What is being counted, e.g. 'Number of days' or 'Travellers'"),
2010
+ min: z.number().optional().describe("Minimum value, default 1"),
2011
+ max: z.number().optional().describe("Maximum value, default 30"),
2012
+ defaultValue: z.number().optional().describe("Starting value"),
2013
+ step: z.number().optional().describe("Increment per tap, default 1"),
2014
+ unit: z.string().optional().describe("Unit label shown next to value, e.g. 'days' or 'people'"),
2015
+ submitLabel: z.string().optional().describe("Confirm button label, default: Confirm")
2016
+ });
2017
+ var safeNum = (n, fallback) => typeof n === "number" && Number.isFinite(n) ? n : fallback;
2018
+ var _NumberStepperCard = ({
2019
+ label,
2020
+ min,
2021
+ max,
2022
+ defaultValue,
2023
+ step,
2024
+ unit,
2025
+ submitLabel = "Confirm",
2026
+ onSubmit
2027
+ }) => {
2028
+ const t = useOnboardingTheme();
2029
+ const safeMin = safeNum(min, 1);
2030
+ const safeMaxRaw = safeNum(max, 30);
2031
+ const safeMax = safeMaxRaw < safeMin ? safeMin : safeMaxRaw;
2032
+ const safeStepRaw = safeNum(step, 1);
2033
+ const safeStep = safeStepRaw > 0 ? safeStepRaw : 1;
2034
+ const safeDefault = safeNum(defaultValue, safeMin);
2035
+ const [value, setValue] = useState(Math.min(Math.max(safeDefault, safeMin), safeMax));
2036
+ const [submitted, setSubmitted] = useState(false);
2037
+ const handleDecrement = useCallback(() => {
2038
+ if (!submitted) setValue((v) => Math.max(v - safeStep, safeMin));
2039
+ }, [submitted, safeStep, safeMin]);
2040
+ const handleIncrement = useCallback(() => {
2041
+ if (!submitted) setValue((v) => Math.min(v + safeStep, safeMax));
2042
+ }, [submitted, safeStep, safeMax]);
2043
+ const handleSubmit = useCallback(() => {
2044
+ if (!submitted) {
2045
+ setSubmitted(true);
2046
+ onSubmit == null ? void 0 : onSubmit(value);
2047
+ }
2048
+ }, [submitted, onSubmit, value]);
2049
+ const atMin = value <= safeMin;
2050
+ const atMax = value >= safeMax;
2051
+ const stepBtnStyle = (disabled) => [
2052
+ styles16.stepBtn,
2053
+ { borderRadius: t.radius.full, backgroundColor: t.colors.background, borderColor: disabled ? t.colors.border : t.colors.primary }
2054
+ ];
2055
+ return /* @__PURE__ */ jsx(
2056
+ CardLayout,
2057
+ {
2058
+ title: label,
2059
+ align: "center",
2060
+ footer: /* @__PURE__ */ jsx(Button, { title: submitLabel, onPress: handleSubmit, variant: "primary", disabled: submitted }),
2061
+ children: /* @__PURE__ */ jsxs(View, { style: { gap: t.spacing.md, alignItems: "center" }, children: [
2062
+ /* @__PURE__ */ jsxs(View, { style: [styles16.stepper, { gap: t.spacing.lg }], children: [
2063
+ /* @__PURE__ */ jsx(Pressable, { onPress: atMin || submitted ? void 0 : handleDecrement, disabled: atMin || submitted, style: stepBtnStyle(atMin), children: /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: atMin ? t.colors.textMuted : t.colors.primary }], children: "\u2212" }) }),
2064
+ /* @__PURE__ */ jsxs(View, { style: styles16.valueBox, children: [
2065
+ /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.primary }], children: String(value) }),
2066
+ unit ? /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: unit }) : null
2067
+ ] }),
2068
+ /* @__PURE__ */ jsx(Pressable, { onPress: atMax || submitted ? void 0 : handleIncrement, disabled: atMax || submitted, style: stepBtnStyle(atMax), children: /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: atMax ? t.colors.textMuted : t.colors.primary }], children: "+" }) })
2069
+ ] }),
2070
+ /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: `${safeMin} \u2013 ${safeMax}${unit ? ` ${unit}` : ""}` })
2071
+ ] })
2072
+ }
2073
+ );
2074
+ };
2075
+ var NumberStepperCard = {
2076
+ name: "NumberStepperCard",
2077
+ 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.",
2078
+ component: React14.memo(_NumberStepperCard),
2079
+ propsSchema: schema5,
2080
+ defaultProps: { min: 1, max: 30, defaultValue: 1, step: 1, submitLabel: "Confirm" }
2081
+ };
2082
+ var styles16 = StyleSheet.create({
2083
+ stepper: { flexDirection: "row", alignItems: "center", justifyContent: "center" },
2084
+ stepBtn: { width: 44, height: 44, borderWidth: 2, alignItems: "center", justifyContent: "center" },
2085
+ valueBox: { alignItems: "center", minWidth: 80 }
2086
+ });
2087
+ var easeWire7 = Easing.bezier(...WIRE_BEZIER);
2088
+ var RISE_PX = 10;
2089
+ var schema6 = z.object({
2090
+ title: z.string().describe("Short, warm headline for the value/momentum moment"),
2091
+ body: z.string().optional().describe("One or two supporting lines that reflect the user's answers back and build momentum"),
2092
+ items: z.array(z.string()).max(4).optional().describe(
2093
+ "2-3 very short 'work being done' lines that each reference something the user already answered (e.g. 'Sized for a small team'). Shown as ticked items appearing in sequence. Prefer these over `body` when you have concrete answers to reflect back."
2094
+ ),
2095
+ illustration: z.string().optional().describe(
2096
+ "Name of an app-provided illustration to show (the app's registry decides the artwork). Prefer this over imageUrl."
2097
+ ),
2098
+ imageUrl: z.string().url().refine((v) => v.startsWith("https://"), "imageUrl must be an https URL").optional().describe(
2099
+ "Fallback remote HTTPS image URL, used only when no named illustration is registered. A non-https or malformed value is rejected and degrades to the named-illustration fallback."
2100
+ ),
2101
+ illustrationFallback: z.string().optional().describe(
2102
+ "Named illustration to show when neither `illustration` nor `imageUrl` resolves (or the image fails to load). Defaults to the kit's always-present 'momentum' art so the screen is never blank."
2103
+ ),
2104
+ ctaLabel: z.string().optional().describe("Continue button label, default: Continue")
2105
+ });
2106
+ var MIN_DISPLAY_MS = 500;
2107
+ var _WorkItem = ({
2108
+ text,
2109
+ index,
2110
+ reduced
2111
+ }) => {
2112
+ const t = useOnboardingTheme();
2113
+ const rowT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
2114
+ const checkT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
2115
+ useEffect(() => {
2116
+ if (reduced) {
2117
+ rowT.setValue(1);
2118
+ checkT.setValue(1);
2119
+ return;
2120
+ }
2121
+ const anims = [
2122
+ Animated.timing(rowT, {
2123
+ toValue: 1,
2124
+ duration: scaledMs(INTERSTITIAL_ITEM_MS),
2125
+ delay: scaledMs(interstitialItemDelay(index)),
2126
+ easing: easeWire7,
2127
+ useNativeDriver: true
2128
+ }),
2129
+ Animated.spring(checkT, {
2130
+ toValue: 1,
2131
+ delay: scaledMs(interstitialCheckDelay(index)),
2132
+ friction: CHECK_SPRING.friction,
2133
+ tension: CHECK_SPRING.tension,
2134
+ useNativeDriver: true
2135
+ })
2136
+ ];
2137
+ anims.forEach((a) => a.start());
2138
+ return () => anims.forEach((a) => a.stop());
2139
+ }, [reduced, index, rowT, checkT]);
2140
+ const rise = rowT.interpolate({ inputRange: [0, 1], outputRange: [RISE_PX, 0] });
2141
+ const checkScale = checkT.interpolate({
2142
+ inputRange: [0, 1],
2143
+ outputRange: [INTERSTITIAL_CHECK_SCALE_FROM, 1]
2144
+ });
2145
+ return /* @__PURE__ */ jsxs(
2146
+ Animated.View,
2147
+ {
2148
+ style: [styles17.itemRow, { gap: t.spacing.sm + t.spacing.xs, opacity: rowT, transform: [{ translateY: rise }] }],
2149
+ children: [
2150
+ /* @__PURE__ */ jsx(
2151
+ View,
2152
+ {
2153
+ style: [
2154
+ styles17.itemBadge,
2155
+ { backgroundColor: t.colors.primarySoft, borderRadius: t.radius.full }
2156
+ ],
2157
+ children: /* @__PURE__ */ jsx(
2158
+ Animated.Text,
2159
+ {
2160
+ style: [
2161
+ styles17.itemCheck,
2162
+ { color: t.colors.primary, opacity: checkT, transform: [{ scale: checkScale }] }
2163
+ ],
2164
+ children: "\u2713"
2165
+ }
2166
+ )
2167
+ }
2168
+ ),
2169
+ /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), styles17.itemText, { color: t.colors.text }], children: text })
2170
+ ]
2171
+ }
2172
+ );
2173
+ };
2174
+ var WorkItem = React14.memo(_WorkItem);
2175
+ var _InterstitialCard = ({
2176
+ title,
2177
+ body,
2178
+ items,
2179
+ illustration,
2180
+ imageUrl,
2181
+ illustrationFallback,
2182
+ ctaLabel = "Continue",
2183
+ onSubmit,
2184
+ onPress
2185
+ }) => {
2186
+ const t = useOnboardingTheme();
2187
+ const reduced = useReducedMotion();
2188
+ const node = useIllustration(illustration);
2189
+ const fallbackNode = useIllustration(illustrationFallback);
2190
+ const [submitted, setSubmitted] = useState(false);
2191
+ const [imgFailed, setImgFailed] = useState(false);
2192
+ const [canAdvance, setCanAdvance] = useState(false);
2193
+ useEffect(() => {
2194
+ const timer = setTimeout(() => setCanAdvance(true), MIN_DISPLAY_MS);
2195
+ return () => clearTimeout(timer);
2196
+ }, []);
2197
+ const artT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
2198
+ const titleT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
2199
+ const bodyT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
2200
+ useEffect(() => {
2201
+ if (reduced) {
2202
+ artT.setValue(1);
2203
+ titleT.setValue(1);
2204
+ bodyT.setValue(1);
2205
+ return;
2206
+ }
2207
+ const rise = (value, delay) => Animated.timing(value, {
2208
+ toValue: 1,
2209
+ duration: scaledMs(INTERSTITIAL_HEAD_MS),
2210
+ delay,
2211
+ easing: easeWire7,
2212
+ useNativeDriver: true
2213
+ });
2214
+ const anims = [
2215
+ rise(artT, 0),
2216
+ rise(titleT, scaledMs(INTERSTITIAL_HEAD_STAGGER_MS)),
2217
+ rise(bodyT, scaledMs(INTERSTITIAL_HEAD_STAGGER_MS * 2))
2218
+ ];
2219
+ anims.forEach((a) => a.start());
2220
+ return () => anims.forEach((a) => a.stop());
2221
+ }, [reduced, artT, titleT, bodyT]);
2222
+ const riseStyle = (value) => ({
2223
+ opacity: value,
2224
+ transform: [{ translateY: value.interpolate({ inputRange: [0, 1], outputRange: [RISE_PX, 0] }) }]
2225
+ });
2226
+ const handleContinue = useCallback(() => {
2227
+ if (submitted || !canAdvance) return;
2228
+ setSubmitted(true);
2229
+ if (onSubmit) onSubmit("continue");
2230
+ else if (onPress) onPress(ctaLabel);
2231
+ }, [submitted, canAdvance, onSubmit, onPress, ctaLabel]);
2232
+ const showImage = !node && !!imageUrl && !imgFailed;
2233
+ const art = node != null ? node : showImage ? void 0 : fallbackNode;
2234
+ return /* @__PURE__ */ jsx(
2235
+ CardLayout,
2236
+ {
2237
+ align: "center",
2238
+ footer: /* @__PURE__ */ jsx(
2239
+ Button,
2240
+ {
2241
+ title: ctaLabel,
2242
+ onPress: handleContinue,
2243
+ variant: "primary",
2244
+ disabled: submitted || !canAdvance
2245
+ }
2246
+ ),
2247
+ children: /* @__PURE__ */ jsxs(View, { style: [styles17.center, { gap: t.spacing.md }], children: [
2248
+ /* @__PURE__ */ jsx(Animated.View, { style: [styles17.fullWidth, riseStyle(artT)], children: showImage ? /* @__PURE__ */ jsx(
2249
+ Image,
2250
+ {
2251
+ source: { uri: imageUrl },
2252
+ style: styles17.image,
2253
+ resizeMode: "contain",
2254
+ onError: () => setImgFailed(true)
2255
+ }
2256
+ ) : art ? /* @__PURE__ */ jsx(View, { style: styles17.art, children: art }) : null }),
2257
+ /* @__PURE__ */ jsx(Animated.View, { style: riseStyle(titleT), children: /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text, textAlign: "center" }], children: title }) }),
2258
+ (items == null ? void 0 : items.length) ? /* @__PURE__ */ jsx(View, { style: [styles17.items, { gap: t.spacing.md, marginTop: t.spacing.sm }], children: items.map((item, i) => /* @__PURE__ */ jsx(WorkItem, { text: item, index: i, reduced }, item)) }) : 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
2259
+ ] })
2260
+ }
2261
+ );
2262
+ };
2263
+ var InterstitialCard = {
2264
+ name: "InterstitialCard",
2265
+ 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.",
2266
+ component: React14.memo(_InterstitialCard),
2267
+ propsSchema: schema6,
2268
+ defaultProps: { ctaLabel: "Continue", illustrationFallback: "momentum" }
2269
+ };
2270
+ var styles17 = StyleSheet.create({
2271
+ center: { width: "100%", alignItems: "center", justifyContent: "center" },
2272
+ fullWidth: { width: "100%", alignItems: "center" },
2273
+ art: { alignItems: "center", justifyContent: "center" },
2274
+ image: { width: "100%", height: 200 },
2275
+ items: { width: "100%", alignSelf: "stretch" },
2276
+ itemRow: { flexDirection: "row", alignItems: "center" },
2277
+ itemBadge: { width: 24, height: 24, alignItems: "center", justifyContent: "center" },
2278
+ itemCheck: { fontSize: 13, fontWeight: "800", lineHeight: 15 },
2279
+ itemText: { flex: 1 }
2280
+ });
2281
+
2282
+ // src/cards/index.ts
2283
+ var onboardingComponents = [
2284
+ ChipSelectCard,
2285
+ TextInputCard,
2286
+ SelectionCard,
2287
+ StatusCard,
2288
+ NumberStepperCard,
2289
+ InterstitialCard
2290
+ ];
2291
+ var deriveFormFactor = (iosIdiom, width, height) => {
2292
+ if (iosIdiom === "pad") return "tablet";
2293
+ if (iosIdiom === "phone") return "phone";
2294
+ if (typeof width === "number" && typeof height === "number") {
2295
+ return Math.min(width, height) >= 600 ? "tablet" : "phone";
2296
+ }
2297
+ return void 0;
2298
+ };
2299
+ var collectDeviceContext = () => {
2300
+ var _a;
2301
+ const ctx = { platform: Platform.OS };
2302
+ try {
2303
+ const version = Platform.Version;
2304
+ if (version !== void 0 && version !== null && String(version)) {
2305
+ ctx.osVersion = String(version);
2306
+ }
2307
+ } catch {
2308
+ }
2309
+ let constants = {};
2310
+ try {
2311
+ constants = (_a = Platform.constants) != null ? _a : {};
2312
+ } catch {
2313
+ constants = {};
2314
+ }
2315
+ let iosIdiom;
2316
+ try {
2317
+ if (Platform.OS === "android") {
2318
+ const brand = constants.Brand;
2319
+ const model = constants.Model;
2320
+ const release = constants.Release;
2321
+ if (typeof brand === "string" && brand) ctx.brand = brand;
2322
+ if (typeof model === "string" && model) ctx.model = model;
2323
+ if (release !== void 0 && release !== null && String(release)) {
2324
+ ctx.osVersion = String(release);
2325
+ }
2326
+ } else if (Platform.OS === "ios") {
2327
+ const osVersion = constants.osVersion;
2328
+ const idiom = constants.interfaceIdiom;
2329
+ if (osVersion !== void 0 && osVersion !== null && String(osVersion)) {
2330
+ ctx.osVersion = String(osVersion);
2331
+ }
2332
+ if (typeof idiom === "string" && idiom) {
2333
+ ctx.interfaceIdiom = idiom;
2334
+ iosIdiom = idiom;
2335
+ }
2336
+ }
2337
+ } catch {
2338
+ }
2339
+ try {
2340
+ const screen = Dimensions.get("screen");
2341
+ if (screen) {
2342
+ if (typeof screen.width === "number") ctx.screenWidth = screen.width;
2343
+ if (typeof screen.height === "number") ctx.screenHeight = screen.height;
2344
+ if (typeof screen.scale === "number") ctx.screenScale = screen.scale;
2345
+ const formFactor = deriveFormFactor(iosIdiom, screen.width, screen.height);
2346
+ if (formFactor) ctx.formFactor = formFactor;
2347
+ }
2348
+ } catch {
2349
+ }
2350
+ try {
2351
+ ctx.isRTL = I18nManager.isRTL;
2352
+ } catch {
2353
+ }
2354
+ try {
2355
+ const resolved = Intl.DateTimeFormat().resolvedOptions();
2356
+ if (resolved.locale) ctx.locale = resolved.locale;
2357
+ if (resolved.timeZone) ctx.timeZone = resolved.timeZone;
2358
+ } catch {
2359
+ }
2360
+ return ctx;
2361
+ };
2362
+
2363
+ // src/session/persistedSession.ts
2364
+ var DEFAULT_SESSION_TTL_MS = 36e5;
2365
+ var READ_TIMEOUT_MS = 1500;
2366
+ var sessionStorageKey = (appId) => `wireai:session:${appId}`;
2367
+ var withTimeout = (p, ms) => {
2368
+ let timer;
2369
+ const timeout = new Promise((resolve) => {
2370
+ timer = setTimeout(() => resolve(void 0), ms);
2371
+ });
2372
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
2373
+ };
2374
+ var parsePersisted = (raw) => {
2375
+ if (!raw) return void 0;
2376
+ try {
2377
+ const parsed = JSON.parse(raw);
2378
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.id === "string" && typeof parsed.ts === "number") {
2379
+ return parsed;
2380
+ }
2381
+ } catch {
2382
+ }
2383
+ return void 0;
2384
+ };
2385
+ var loadPersistedSession = async (storage, key, ttlMs = DEFAULT_SESSION_TTL_MS) => {
2386
+ let stored;
2387
+ try {
2388
+ stored = parsePersisted(await withTimeout(storage.getItem(key), READ_TIMEOUT_MS));
2389
+ } catch {
2390
+ stored = void 0;
2391
+ }
2392
+ if (stored && Date.now() - stored.ts < ttlMs) {
2393
+ return { id: stored.id, resumed: true };
2394
+ }
2395
+ const id = makeSessionId();
2396
+ savePersistedSession(storage, key, id);
2397
+ return { id, resumed: false };
2398
+ };
2399
+ var peekPersistedSession = async (storage, key) => {
2400
+ try {
2401
+ return parsePersisted(await withTimeout(storage.getItem(key), READ_TIMEOUT_MS));
2402
+ } catch {
2403
+ return void 0;
2404
+ }
2405
+ };
2406
+ var savePersistedSession = (storage, key, id) => {
2407
+ try {
2408
+ void storage.setItem(key, JSON.stringify({ id, ts: Date.now() })).catch(() => {
2409
+ });
2410
+ } catch {
2411
+ }
2412
+ };
2413
+ var clearPersistedSession = (storage, key) => {
2414
+ try {
2415
+ void storage.removeItem(key).catch(() => {
2416
+ });
2417
+ } catch {
2418
+ }
2419
+ };
2420
+
2421
+ // src/identity/userIdentity.ts
2422
+ var USER_ID_MAX_LENGTH = 128;
2423
+ var sanitizeUserId = (raw) => {
2424
+ if (typeof raw !== "string") return void 0;
2425
+ const trimmed = raw.trim();
2426
+ if (!trimmed) return void 0;
2427
+ return trimmed.length > USER_ID_MAX_LENGTH ? trimmed.slice(0, USER_ID_MAX_LENGTH) : trimmed;
2428
+ };
2429
+ var identifyOnboarding = async (opts) => {
2430
+ var _a, _b, _c;
2431
+ const userId = sanitizeUserId(opts.userId);
2432
+ if (!userId || !((_a = opts.config) == null ? void 0 : _a.serverUrl)) return false;
2433
+ let contextId = ((_b = opts.contextId) == null ? void 0 : _b.trim()) || void 0;
2434
+ if (!contextId && opts.storage) {
2435
+ const key = (_c = opts.persistKey) != null ? _c : opts.appId ? sessionStorageKey(opts.appId) : void 0;
2436
+ if (key) {
2437
+ const stored = await peekPersistedSession(opts.storage, key);
2438
+ contextId = stored == null ? void 0 : stored.id;
2439
+ }
2440
+ }
2441
+ if (!contextId) return false;
2442
+ reportClientEvent(
2443
+ { serverUrl: opts.config.serverUrl, apiKey: opts.config.apiKey },
2444
+ { event_type: "identify", session_id: contextId, user_id: userId }
2445
+ );
2446
+ return true;
2447
+ };
2448
+ var WireOnboarding = ({
2449
+ config,
2450
+ theme,
2451
+ components,
2452
+ illustrations,
2453
+ validators,
2454
+ onComplete,
2455
+ onSkip,
2456
+ onError,
2457
+ onEvent,
2458
+ copy,
2459
+ approxScreens,
2460
+ startMessage = "start",
2461
+ startTimeoutMs = 15e3,
2462
+ fallbackFlow,
2463
+ maxRetries = 1,
2464
+ storage,
2465
+ sessionTtlMs = DEFAULT_SESSION_TTL_MS,
2466
+ persistKey,
2467
+ userContext,
2468
+ userId
2469
+ }) => {
2470
+ var _a, _b, _c;
2471
+ const boundUserId = useMemo(() => sanitizeUserId(userId), [userId]);
2472
+ const device = useMemo(() => {
2473
+ const collected = collectDeviceContext();
2474
+ return config.appVersion ? { ...collected, appVersion: config.appVersion } : collected;
2475
+ }, [config.appVersion]);
2476
+ const userContextKey = userContext ? JSON.stringify(userContext) : "";
2477
+ const userContextStable = useMemo(
2478
+ () => userContextKey ? JSON.parse(userContextKey) : void 0,
2479
+ [userContextKey]
2480
+ );
2481
+ const clientContext = useMemo(
2482
+ () => ({ device, userContext: userContextStable, userId: boundUserId }),
2483
+ [device, userContextStable, boundUserId]
2484
+ );
2485
+ const [session, setSession] = useState(
2486
+ () => storage ? null : { id: makeSessionId(), resumed: false }
2487
+ );
2488
+ const storageKey = persistKey != null ? persistKey : sessionStorageKey(config.appId);
2489
+ useEffect(() => {
2490
+ if (!storage || session) return;
2491
+ let cancelled = false;
2492
+ void loadPersistedSession(storage, storageKey, sessionTtlMs).then((loaded) => {
2493
+ if (!cancelled) setSession(loaded);
2494
+ });
2495
+ return () => {
2496
+ cancelled = true;
2497
+ };
2498
+ }, [storage, storageKey, sessionTtlMs, session]);
2499
+ const handleComplete = useCallback(
2500
+ (result) => {
2501
+ if (storage) clearPersistedSession(storage, storageKey);
2502
+ onComplete(result);
2503
+ },
2504
+ [storage, storageKey, onComplete]
2505
+ );
2506
+ const sessionId = (_a = session == null ? void 0 : session.id) != null ? _a : "";
2507
+ const startupUserIdRef = useRef(boundUserId);
2508
+ const llm = useMemo(() => {
2509
+ const baseUrl = config.serverUrl.replace(/\/$/, "");
2510
+ return {
2511
+ provider: "a2a",
2512
+ baseUrl: `${baseUrl}/a2a`,
2513
+ model: config.appId,
2514
+ apiKey: config.apiKey,
2515
+ // Carry the kit session id alongside any host attribution so it reaches the A2A
2516
+ // request's `params.metadata` (the correlation seed; see note above), plus the
2517
+ // names of the components this device can actually render — so the backend only
2518
+ // emits cards the kit knows how to draw.
2519
+ metadata: {
2520
+ ...config.metadata,
2521
+ sessionId,
2522
+ supportedComponents: (components != null ? components : onboardingComponents).map((c) => c.name),
2523
+ // Device snapshot + host-injected user context ride the session-start metadata so the
2524
+ // backend can segment the funnel. Old servers ignore these unknown keys (backward compat).
2525
+ device,
2526
+ ...userContextStable ? { userContext: userContextStable } : {},
2527
+ // The session-start user id (from the ref) rides the session-start metadata so the
2528
+ // server binds the session to a real user at creation. Reading the ref — not
2529
+ // `boundUserId` — keeps this memo off the userId dependency, so a mid-session change
2530
+ // does NOT recreate the adapter; it's bound by the `identify` effect below instead.
2531
+ ...startupUserIdRef.current ? { userId: startupUserIdRef.current } : {}
2532
+ },
2533
+ timeoutMs: 6e4
2534
+ };
2535
+ }, [config.serverUrl, config.appId, config.apiKey, config.metadata, sessionId, components, device, userContextStable]);
2536
+ const reportTarget = useMemo(
2537
+ () => ({ serverUrl: config.serverUrl, apiKey: config.apiKey }),
2538
+ [config.serverUrl, config.apiKey]
2539
+ );
2540
+ const identifiedRef = useRef(void 0);
2541
+ const identifySeededRef = useRef(false);
2542
+ useEffect(() => {
2543
+ if (!session || !sessionId) return;
2544
+ if (!identifySeededRef.current) {
2545
+ identifySeededRef.current = true;
2546
+ identifiedRef.current = session.resumed ? void 0 : boundUserId;
2547
+ }
2548
+ if (!boundUserId || boundUserId === identifiedRef.current) return;
2549
+ identifiedRef.current = boundUserId;
2550
+ reportClientEvent(reportTarget, {
2551
+ event_type: "identify",
2552
+ session_id: sessionId,
2553
+ user_id: boundUserId
2554
+ });
2555
+ }, [session, sessionId, boundUserId, reportTarget]);
2556
+ const cards = components != null ? components : onboardingComponents;
2557
+ if (!session) {
2558
+ return /* @__PURE__ */ jsx(OnboardingThemeProvider, { theme, children: /* @__PURE__ */ jsx(
2559
+ LoadingScreen,
2560
+ {
2561
+ title: (_b = copy == null ? void 0 : copy.restoringTitle) != null ? _b : DEFAULT_COPY.restoringTitle,
2562
+ hint: (_c = copy == null ? void 0 : copy.restoringHint) != null ? _c : DEFAULT_COPY.restoringHint
2563
+ }
2564
+ ) });
2565
+ }
2566
+ return /* @__PURE__ */ jsx(OnboardingThemeProvider, { theme, children: /* @__PURE__ */ jsx(IllustrationProvider, { registry: { ...defaultIllustrations, ...illustrations }, children: /* @__PURE__ */ jsx(WireAIProvider, { llm, components: cards, children: /* @__PURE__ */ jsx(
2567
+ OnboardingFlow,
2568
+ {
2569
+ validators,
2570
+ onComplete: handleComplete,
2571
+ onSkip,
2572
+ onError,
2573
+ onEvent,
2574
+ copy,
2575
+ approxScreens,
2576
+ startMessage,
2577
+ startTimeoutMs,
2578
+ fallbackFlow,
2579
+ maxRetries,
2580
+ reportTarget,
2581
+ sessionId,
2582
+ resumed: session.resumed,
2583
+ clientContext
2584
+ }
2585
+ ) }) }) });
2586
+ };
2587
+ function DemoOnboarding({
2588
+ config,
2589
+ theme,
2590
+ illustrations,
2591
+ validators,
2592
+ copy,
2593
+ label = "Demo onboarding",
2594
+ onComplete,
2595
+ renderTrigger
2596
+ }) {
2597
+ const [open, setOpen] = useState(false);
2598
+ const [runId, setRunId] = useState(0);
2599
+ const t = useMemo(() => mergeTheme(theme != null ? theme : {}), [theme]);
2600
+ const styles20 = useMemo(() => makeStyles(t), [t]);
2601
+ const openDemo = useCallback(() => {
2602
+ setRunId((n) => n + 1);
2603
+ setOpen(true);
2604
+ }, []);
2605
+ const close = useCallback(() => setOpen(false), []);
2606
+ const finish = useCallback(
2607
+ (result) => {
2608
+ onComplete == null ? void 0 : onComplete(result);
2609
+ setOpen(false);
2610
+ },
2611
+ [onComplete]
2612
+ );
2613
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2614
+ renderTrigger ? renderTrigger(openDemo) : /* @__PURE__ */ jsx(Pressable, { onPress: openDemo, style: styles20.trigger, accessibilityRole: "button", children: /* @__PURE__ */ jsx(Text, { style: styles20.triggerText, children: label }) }),
2615
+ /* @__PURE__ */ jsx(
2616
+ Modal,
2617
+ {
2618
+ visible: open,
2619
+ animationType: "slide",
2620
+ presentationStyle: "fullScreen",
2621
+ onRequestClose: close,
2622
+ children: /* @__PURE__ */ jsx(View, { style: styles20.modal, children: config ? /* @__PURE__ */ jsx(
2623
+ WireOnboarding,
2624
+ {
2625
+ config,
2626
+ theme,
2627
+ illustrations,
2628
+ validators,
2629
+ copy,
2630
+ onComplete: finish,
2631
+ onSkip: close,
2632
+ onError: close
2633
+ },
2634
+ runId
2635
+ ) : /* @__PURE__ */ jsxs(View, { style: styles20.notice, children: [
2636
+ /* @__PURE__ */ jsx(Text, { style: styles20.noticeTitle, children: "Wire AI not configured" }),
2637
+ /* @__PURE__ */ jsx(Text, { style: styles20.noticeBody, children: "Set the Wire AI key + server URL (e.g. via wireConfigFromEnv) to demo the AI onboarding here." }),
2638
+ /* @__PURE__ */ jsx(Pressable, { onPress: close, style: styles20.trigger, accessibilityRole: "button", children: /* @__PURE__ */ jsx(Text, { style: styles20.triggerText, children: "Close" }) })
2639
+ ] }) })
2640
+ }
2641
+ )
2642
+ ] });
2643
+ }
2644
+ function makeStyles(t) {
2645
+ return StyleSheet.create({
2646
+ trigger: {
2647
+ backgroundColor: t.colors.primary,
2648
+ borderRadius: t.button.radius,
2649
+ paddingVertical: 14,
2650
+ paddingHorizontal: 20,
2651
+ alignItems: "center"
2652
+ },
2653
+ triggerText: { color: t.colors.onPrimary, fontFamily: t.fonts.medium, fontSize: 16 },
2654
+ modal: { flex: 1, backgroundColor: t.colors.background },
2655
+ notice: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, gap: 12 },
2656
+ noticeTitle: { color: t.colors.text, fontFamily: t.fonts.bold, fontSize: 20 },
2657
+ noticeBody: {
2658
+ color: t.colors.textMuted,
2659
+ fontFamily: t.fonts.regular,
2660
+ fontSize: 15,
2661
+ textAlign: "center"
2662
+ }
2663
+ });
2664
+ }
2665
+
2666
+ // src/theme/themeFromBrand.ts
2667
+ var hexToRgb = (hex) => {
2668
+ const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
2669
+ if (!m) return null;
2670
+ let h = m[1];
2671
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
2672
+ const int = parseInt(h, 16);
2673
+ return [int >> 16 & 255, int >> 8 & 255, int & 255];
2674
+ };
2675
+ var softFromPrimary = (primary) => {
2676
+ const rgb = hexToRgb(primary);
2677
+ if (!rgb) return defaultOnboardingTheme.colors.primarySoft;
2678
+ const [r, g, b] = rgb;
2679
+ return `rgba(${r},${g},${b},0.10)`;
2680
+ };
2681
+ var themeFromBrand = (input) => {
2682
+ const { primary, background, surface, text, onPrimary, font, extra } = input;
2683
+ const derived = {
2684
+ colors: {
2685
+ ...defaultOnboardingTheme.colors,
2686
+ primary,
2687
+ primarySoft: softFromPrimary(primary),
2688
+ onPrimary: onPrimary != null ? onPrimary : defaultOnboardingTheme.colors.onPrimary,
2689
+ ...background ? { background } : {},
2690
+ ...surface ? { surface } : {},
2691
+ ...text ? { text } : {}
2692
+ },
2693
+ ...font ? {
2694
+ fonts: {
2695
+ ...defaultOnboardingTheme.fonts,
2696
+ regular: font,
2697
+ medium: font,
2698
+ bold: font
2699
+ }
2700
+ } : {}
2701
+ };
2702
+ const base = mergeTheme(derived);
2703
+ if (!extra) return base;
2704
+ return {
2705
+ colors: { ...base.colors, ...extra.colors },
2706
+ fonts: { ...base.fonts, ...extra.fonts },
2707
+ radius: { ...base.radius, ...extra.radius },
2708
+ spacing: { ...base.spacing, ...extra.spacing },
2709
+ button: { ...base.button, ...extra.button }
2710
+ };
2711
+ };
2712
+ var _DoneBlock = ({
2713
+ title = "You're all set",
2714
+ message = "Personalizing your experience\u2026"
2715
+ }) => {
2716
+ const t = useOnboardingTheme();
2717
+ return /* @__PURE__ */ jsxs(View, { style: [styles18.center, { gap: t.spacing.sm, padding: t.spacing.lg }], children: [
2718
+ /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.success, textAlign: "center" }], children: title }),
2719
+ /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted, textAlign: "center" }], children: message })
2720
+ ] });
2721
+ };
2722
+ var DoneBlock = React14.memo(_DoneBlock);
2723
+ var styles18 = StyleSheet.create({
2724
+ center: { flex: 1, alignItems: "center", justifyContent: "center" }
2725
+ });
2726
+
2727
+ // src/motion/centeredModalMotion.ts
2728
+ var CENTERED_MODAL_ANIM = {
2729
+ /** Spring config for the card scale/opacity entrance (mirrors CompletionView's pop). */
2730
+ enterSpring: { friction: 7, tension: 80 },
2731
+ /** Exit timing in ms (fade + shrink back out before unmount). */
2732
+ exitMs: 160,
2733
+ /** Card scale at rest (progress 0); grows to 1 at progress 1. */
2734
+ scaleFrom: 0.9,
2735
+ /** Peak dim of the backdrop at progress 1. */
2736
+ backdropOpacity: 0.55
2737
+ };
2738
+ var _CenteredModal = forwardRef(
2739
+ ({ theme, dismissible = true, onDismiss, children }, ref) => {
2740
+ const progress = useRef(new Animated.Value(0)).current;
2741
+ const [open, setOpen] = useState(true);
2742
+ const closingRef = useRef(false);
2743
+ const reduced = useReducedMotion();
2744
+ useEffect(() => {
2745
+ const anim = reduced ? Animated.timing(progress, {
2746
+ toValue: 1,
2747
+ duration: REDUCED_FADE_MS,
2748
+ useNativeDriver: true
2749
+ }) : Animated.spring(progress, {
2750
+ toValue: 1,
2751
+ useNativeDriver: true,
2752
+ friction: CENTERED_MODAL_ANIM.enterSpring.friction,
2753
+ tension: CENTERED_MODAL_ANIM.enterSpring.tension
2754
+ });
2755
+ anim.start();
2756
+ return () => anim.stop();
2757
+ }, [progress, reduced]);
2758
+ const close = useCallback(() => {
2759
+ if (closingRef.current) return;
2760
+ closingRef.current = true;
2761
+ Animated.timing(progress, {
2762
+ toValue: 0,
2763
+ duration: reduced ? REDUCED_FADE_MS : CENTERED_MODAL_ANIM.exitMs,
2764
+ easing: Easing.in(Easing.cubic),
2765
+ useNativeDriver: true
2766
+ }).start(() => {
2767
+ setOpen(false);
2768
+ onDismiss();
2769
+ });
2770
+ }, [progress, onDismiss, reduced]);
2771
+ useImperativeHandle(ref, () => ({ close }), [close]);
2772
+ useEffect(() => {
2773
+ const sub = BackHandler.addEventListener("hardwareBackPress", () => {
2774
+ if (dismissible) close();
2775
+ return true;
2776
+ });
2777
+ return () => sub.remove();
2778
+ }, [dismissible, close]);
2779
+ const onBackdrop = useCallback(() => {
2780
+ if (dismissible) close();
2781
+ }, [dismissible, close]);
2782
+ const backdropOpacity = progress.interpolate({
2783
+ inputRange: [0, 1],
2784
+ outputRange: [0, CENTERED_MODAL_ANIM.backdropOpacity]
2785
+ });
2786
+ const cardScale = reduced ? 1 : progress.interpolate({
2787
+ inputRange: [0, 1],
2788
+ outputRange: [CENTERED_MODAL_ANIM.scaleFrom, 1]
2789
+ });
2790
+ return /* @__PURE__ */ jsxs(
2791
+ Modal,
2792
+ {
2793
+ visible: open,
2794
+ transparent: true,
2795
+ animationType: "none",
2796
+ statusBarTranslucent: true,
2797
+ onRequestClose: dismissible ? close : void 0,
2798
+ children: [
2799
+ /* @__PURE__ */ jsx(
2800
+ Animated.View,
2801
+ {
2802
+ style: [styles19.backdrop, { opacity: backdropOpacity }],
2803
+ pointerEvents: "none"
2804
+ }
2805
+ ),
2806
+ /* @__PURE__ */ jsx(
2807
+ Pressable,
2808
+ {
2809
+ style: styles19.fill,
2810
+ onPress: onBackdrop,
2811
+ accessibilityRole: "button",
2812
+ accessibilityLabel: "Dismiss",
2813
+ children: /* @__PURE__ */ jsx(SafeAreaView, { style: styles19.center, pointerEvents: "box-none", children: /* @__PURE__ */ jsx(Pressable, { onPress: () => {
2814
+ }, style: styles19.cardWrap, children: /* @__PURE__ */ jsx(
2815
+ Animated.View,
2816
+ {
2817
+ accessibilityViewIsModal: true,
2818
+ accessibilityRole: "alert",
2819
+ style: [
2820
+ styles19.card,
2821
+ {
2822
+ backgroundColor: theme.colors.surface,
2823
+ borderRadius: theme.radius.lg,
2824
+ padding: theme.spacing.lg,
2825
+ gap: theme.spacing.md,
2826
+ opacity: progress,
2827
+ transform: [{ scale: cardScale }]
2828
+ }
2829
+ ],
2830
+ children
2831
+ }
2832
+ ) }) })
2833
+ }
2834
+ )
2835
+ ]
2836
+ }
2837
+ );
2838
+ }
2839
+ );
2840
+ _CenteredModal.displayName = "CenteredModal";
2841
+ var CenteredModal = React14.memo(_CenteredModal);
2842
+ var styles19 = StyleSheet.create({
2843
+ fill: { flex: 1 },
2844
+ backdrop: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#000" },
2845
+ center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
2846
+ cardWrap: { width: "100%", maxWidth: 420 },
2847
+ card: {
2848
+ width: "100%",
2849
+ // Soft elevation so the popup reads as lifted above the app in both themes.
2850
+ shadowColor: "#000",
2851
+ shadowOffset: { width: 0, height: 8 },
2852
+ shadowOpacity: 0.28,
2853
+ shadowRadius: 24,
2854
+ elevation: 12
2855
+ }
2856
+ });
2857
+
2858
+ // src/features/defaults.ts
2859
+ var defaultWireFeatures = Object.freeze({
2860
+ coachmarks: Object.freeze({ enabled: true }),
2861
+ showcase: Object.freeze({ enabled: true }),
2862
+ onboarding: Object.freeze({ mode: "ai" }),
2863
+ review: Object.freeze({ enabled: true }),
2864
+ questionnaire: Object.freeze({ enabled: true }),
2865
+ announcements: Object.freeze({ enabled: true })
2866
+ });
2867
+ var VALID_MODES = ["ai", "static", "off"];
2868
+ var readEnabled = (raw) => {
2869
+ if (raw && typeof raw === "object" && typeof raw.enabled === "boolean") {
2870
+ return { enabled: raw.enabled };
2871
+ }
2872
+ return { enabled: true };
2873
+ };
2874
+ var parseWireFeatures = (raw) => {
2875
+ var _a;
2876
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return defaultWireFeatures;
2877
+ const src = raw;
2878
+ const modeRaw = (_a = src.onboarding) == null ? void 0 : _a.mode;
2879
+ const mode = typeof modeRaw === "string" && VALID_MODES.includes(modeRaw) ? modeRaw : "ai";
2880
+ return {
2881
+ coachmarks: readEnabled(src.coachmarks),
2882
+ showcase: readEnabled(src.showcase),
2883
+ onboarding: { mode },
2884
+ review: readEnabled(src.review),
2885
+ questionnaire: readEnabled(src.questionnaire),
2886
+ announcements: readEnabled(src.announcements)
2887
+ };
2888
+ };
2889
+ var featuresEqual = (a, b) => a === b || a.coachmarks.enabled === b.coachmarks.enabled && a.showcase.enabled === b.showcase.enabled && a.onboarding.mode === b.onboarding.mode && a.review.enabled === b.review.enabled && a.questionnaire.enabled === b.questionnaire.enabled && a.announcements.enabled === b.announcements.enabled;
2890
+
2891
+ // src/features/cache.ts
2892
+ var DEFAULT_FEATURES_TTL_MS = 3e5;
2893
+ var READ_TIMEOUT_MS2 = 1500;
2894
+ var featuresCacheKey = (appId) => `wireai:features:${appId && appId.length > 0 ? appId : "default"}`;
2895
+ var withTimeout2 = (p, ms) => {
2896
+ let timer;
2897
+ const timeout = new Promise((resolve) => {
2898
+ timer = setTimeout(() => resolve(void 0), ms);
2899
+ });
2900
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
2901
+ };
2902
+ var isFeaturesFresh = (ageMs, ttlMs = DEFAULT_FEATURES_TTL_MS) => ageMs >= 0 && ageMs < ttlMs;
2903
+ var readCachedFeatures = async (storage, key) => {
2904
+ let raw;
2905
+ try {
2906
+ raw = await withTimeout2(storage.getItem(key), READ_TIMEOUT_MS2);
2907
+ } catch {
2908
+ return void 0;
2909
+ }
2910
+ if (!raw) return void 0;
2911
+ try {
2912
+ const parsed = JSON.parse(raw);
2913
+ if (parsed && typeof parsed === "object" && typeof parsed.ts === "number") {
2914
+ const ts = parsed.ts;
2915
+ return {
2916
+ features: parseWireFeatures(parsed.features),
2917
+ ageMs: Date.now() - ts
2918
+ };
2919
+ }
2920
+ } catch {
2921
+ }
2922
+ return void 0;
2923
+ };
2924
+ var writeCachedFeatures = (storage, key, features) => {
2925
+ try {
2926
+ void storage.setItem(key, JSON.stringify({ features, ts: Date.now() })).catch(() => {
2927
+ });
2928
+ } catch {
2929
+ }
2930
+ };
2931
+
2932
+ // src/features/fetchWireFeatures.ts
2933
+ var DEFAULT_TIMEOUT_MS = 4e3;
2934
+ var failOpen = async (storage, key) => {
2935
+ var _a;
2936
+ if (!storage) return defaultWireFeatures;
2937
+ const cached = await readCachedFeatures(storage, key);
2938
+ return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
2939
+ };
2940
+ var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
2941
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
2942
+ const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
2943
+ try {
2944
+ return await fetch(url, {
2945
+ method: "GET",
2946
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
2947
+ signal: controller == null ? void 0 : controller.signal
2948
+ });
2949
+ } finally {
2950
+ clearTimeout(timer);
2951
+ }
2952
+ };
2953
+ var fetchWireFeatures = async (config) => {
2954
+ const key = featuresCacheKey(config == null ? void 0 : config.appId);
2955
+ const storage = config == null ? void 0 : config.storage;
2956
+ if (!(config == null ? void 0 : config.serverUrl) || !(config == null ? void 0 : config.apiKey)) return failOpen(storage, key);
2957
+ const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
2958
+ const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
2959
+ try {
2960
+ const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
2961
+ if (!res || !res.ok) return failOpen(storage, key);
2962
+ const json = await res.json();
2963
+ const features = parseWireFeatures(json);
2964
+ if (storage) writeCachedFeatures(storage, key, features);
2965
+ return features;
2966
+ } catch {
2967
+ return failOpen(storage, key);
2968
+ }
2969
+ };
2970
+ var useStableValue = (value, isEqual) => {
2971
+ const ref = useRef(value);
2972
+ if (!isEqual(ref.current, value)) ref.current = value;
2973
+ return ref.current;
2974
+ };
2975
+ 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);
2976
+ var useWireFeatures = (config) => {
2977
+ const stableConfig = useStableValue(config, sameFetchKey);
2978
+ const [flags, setFlags] = useState(defaultWireFeatures);
2979
+ const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
2980
+ useEffect(() => {
2981
+ if (!canFetch) {
2982
+ setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
2983
+ return;
2984
+ }
2985
+ let alive = true;
2986
+ void fetchWireFeatures(stableConfig).then((next) => {
2987
+ if (!alive) return;
2988
+ setFlags((prev) => featuresEqual(prev, next) ? prev : next);
2989
+ });
2990
+ return () => {
2991
+ alive = false;
2992
+ };
2993
+ }, [canFetch, stableConfig]);
2994
+ return flags;
2995
+ };
2996
+ var WireFeaturesContext = createContext(null);
2997
+ var WireFeaturesProvider = ({
2998
+ config,
2999
+ flags,
3000
+ children
3001
+ }) => {
3002
+ const fetched = useWireFeatures(flags ? void 0 : config);
3003
+ const value = flags != null ? flags : fetched;
3004
+ const stable = useMemo(() => value, [value]);
3005
+ return /* @__PURE__ */ jsx(WireFeaturesContext.Provider, { value: stable, children });
3006
+ };
3007
+ WireFeaturesProvider.displayName = "WireFeaturesProvider";
3008
+ var useWireFeaturesContext = () => useContext(WireFeaturesContext);
3009
+ var useResolvedFeatures = (options) => {
3010
+ var _a, _b, _c;
3011
+ const ctx = useWireFeaturesContext();
3012
+ const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
3013
+ const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
3014
+ return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
3015
+ };
3016
+
3017
+ // src/config/wireConfigFromEnv.ts
3018
+ var wireConfigFromEnv = (overrides) => {
3019
+ var _a, _b, _c, _d;
3020
+ const apiKey = (_a = overrides == null ? void 0 : overrides.apiKey) != null ? _a : process.env.EXPO_PUBLIC_WIREAI_API_KEY;
3021
+ const serverUrl = (_b = overrides == null ? void 0 : overrides.serverUrl) != null ? _b : process.env.EXPO_PUBLIC_WIREAI_SERVER_URL;
3022
+ const appId = (_d = (_c = overrides == null ? void 0 : overrides.appId) != null ? _c : process.env.EXPO_PUBLIC_WIREAI_APP_ID) != null ? _d : "default";
3023
+ if (!apiKey || !serverUrl) return null;
3024
+ return {
3025
+ apiKey,
3026
+ serverUrl,
3027
+ appId,
3028
+ ...(overrides == null ? void 0 : overrides.metadata) ? { metadata: overrides.metadata } : {}
3029
+ };
3030
+ };
3031
+
3032
+ // src/config/onboardingFlag.ts
3033
+ var isOnboardingEnabled = (opts) => {
3034
+ var _a;
3035
+ const transportPresent = wireConfigFromEnv(opts == null ? void 0 : opts.config) !== null;
3036
+ if (!transportPresent) return false;
3037
+ return (_a = opts == null ? void 0 : opts.remote) != null ? _a : true;
3038
+ };
3039
+
3040
+ // src/analytics/analyticsEvent.ts
3041
+ var WIRE_ONBOARDING_EVENTS = {
3042
+ started: "wire_onboarding_started",
3043
+ /** A persisted session was restored after an app kill (fires instead of `started`). */
3044
+ resumed: "wire_onboarding_resumed",
3045
+ turn: "wire_onboarding_turn",
3046
+ error: "wire_onboarding_error",
3047
+ retry: "wire_onboarding_retry",
3048
+ fallback: "wire_onboarding_fallback",
3049
+ /** Logged by the host on `onComplete` (no matching kit `OnboardingEvent`). */
3050
+ completed: "wire_onboarding_completed"
3051
+ };
3052
+ var toAnalyticsEvent = (event) => {
3053
+ switch (event.type) {
3054
+ case "started":
3055
+ return { name: WIRE_ONBOARDING_EVENTS.started };
3056
+ case "resumed":
3057
+ return { name: WIRE_ONBOARDING_EVENTS.resumed };
3058
+ case "turn":
3059
+ return {
3060
+ name: WIRE_ONBOARDING_EVENTS.turn,
3061
+ params: { step: event.step, component: event.component }
3062
+ };
3063
+ case "error":
3064
+ return { name: WIRE_ONBOARDING_EVENTS.error, params: { reason: event.reason } };
3065
+ case "retry":
3066
+ return {
3067
+ name: WIRE_ONBOARDING_EVENTS.retry,
3068
+ params: { reason: event.reason, attempt: event.attempt }
3069
+ };
3070
+ case "fallback":
3071
+ return { name: WIRE_ONBOARDING_EVENTS.fallback, params: { reason: event.reason } };
3072
+ default: {
3073
+ const _exhaustive = event;
3074
+ return _exhaustive;
3075
+ }
3076
+ }
3077
+ };
3078
+
3079
+ // src/attribution/attribution.ts
3080
+ var attributionMetadata = (a) => {
3081
+ const attribution = {};
3082
+ for (const [key, value] of Object.entries(a)) {
3083
+ if (value !== void 0) attribution[key] = value;
3084
+ }
3085
+ return { attribution };
3086
+ };
3087
+
3088
+ // src/session-analytics/reportSessionStart.ts
3089
+ var SESSION_STARTED_EVENT = "app.session_started";
3090
+ var _emitted = /* @__PURE__ */ new Set();
3091
+ var _GUARD_MAX = 128;
3092
+ var resetSessionStartGuard = () => {
3093
+ _emitted.clear();
3094
+ };
3095
+ var reportSessionStart = (opts) => {
3096
+ var _a;
3097
+ const target = opts.target;
3098
+ if (!(target == null ? void 0 : target.serverUrl)) return;
3099
+ const sessionId = (_a = opts.sessionId) != null ? _a : makeSessionId();
3100
+ if (opts.once !== false) {
3101
+ if (_emitted.has(sessionId)) return;
3102
+ if (_emitted.size >= _GUARD_MAX) {
3103
+ const oldest = _emitted.values().next().value;
3104
+ if (oldest !== void 0) _emitted.delete(oldest);
3105
+ }
3106
+ _emitted.add(sessionId);
3107
+ }
3108
+ try {
3109
+ const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
3110
+ const headers = { "Content-Type": "application/json" };
3111
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
3112
+ const userContext = {};
3113
+ if (opts.deviceKey) userContext.device_key = opts.deviceKey;
3114
+ if (typeof opts.sessionCount === "number" && Number.isFinite(opts.sessionCount)) {
3115
+ userContext.session_count = opts.sessionCount;
3116
+ userContext.returning = opts.sessionCount > 1;
3117
+ }
3118
+ if (opts.appVersion) userContext.app_version = opts.appVersion;
3119
+ if (opts.platform) userContext.platform = opts.platform;
3120
+ const event = {
3121
+ event_type: "app_event",
3122
+ question_key: SESSION_STARTED_EVENT,
3123
+ session_id: sessionId
3124
+ };
3125
+ const userId = sanitizeUserId(opts.userId);
3126
+ if (userId) event.user_id = userId;
3127
+ if (Object.keys(userContext).length > 0) event.user_context = userContext;
3128
+ if (opts.device) event.device = opts.device;
3129
+ if (opts.meta && Object.keys(opts.meta).length > 0) event.meta = JSON.stringify(opts.meta);
3130
+ void fetch(url, {
3131
+ method: "POST",
3132
+ headers,
3133
+ body: JSON.stringify({ events: [event] })
3134
+ }).catch(() => {
3135
+ });
3136
+ } catch {
3137
+ }
3138
+ };
3139
+ var BACKGROUND_SESSION_MS = 30 * 60 * 1e3;
3140
+ var useSessionStart = (config, options = {}) => {
3141
+ const latest = useRef({ config, options });
3142
+ latest.current = { config, options };
3143
+ useEffect(() => {
3144
+ const fire = () => {
3145
+ var _a, _b;
3146
+ const { config: cfg, options: opts } = latest.current;
3147
+ if (!(cfg == null ? void 0 : cfg.serverUrl)) return;
3148
+ if (opts.enabled === false) return;
3149
+ const target = { serverUrl: cfg.serverUrl, apiKey: (_a = cfg.apiKey) != null ? _a : "" };
3150
+ const device = collectDeviceContext();
3151
+ if (cfg.appVersion && !device.appVersion) device.appVersion = cfg.appVersion;
3152
+ reportSessionStart({
3153
+ target,
3154
+ // A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
3155
+ userId: opts.userId,
3156
+ deviceKey: opts.deviceKey,
3157
+ sessionCount: opts.sessionCount,
3158
+ appVersion: (_b = cfg.appVersion) != null ? _b : device.appVersion,
3159
+ platform: Platform.OS,
3160
+ device,
3161
+ meta: opts.meta
3162
+ });
3163
+ };
3164
+ fire();
3165
+ let backgroundedAt = null;
3166
+ const onChange = (state) => {
3167
+ if (state === "background" || state === "inactive") {
3168
+ if (backgroundedAt == null) backgroundedAt = Date.now();
3169
+ return;
3170
+ }
3171
+ if (state === "active") {
3172
+ const since = backgroundedAt;
3173
+ backgroundedAt = null;
3174
+ if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fire();
3175
+ }
3176
+ };
3177
+ const sub = AppState.addEventListener("change", onChange);
3178
+ return () => {
3179
+ if (sub && typeof sub.remove === "function") sub.remove();
3180
+ };
3181
+ }, []);
3182
+ };
3183
+
3184
+ export { AnimatedSparkle, BACKGROUND_SESSION_MS, CardHandoff, CenteredModal, ChipSelectCard, CompletionView, DEFAULT_FEATURES_TTL_MS, DEFAULT_SESSION_TTL_MS, DemoOnboarding, DoneBlock, ErrorBlock, IllustrationProvider, InterstitialCard, LoadingBlock, LoadingScreen, NumberStepperCard, Button as OnboardingButton, OnboardingFlow, OnboardingScaffold, OnboardingThemeProvider, SESSION_STARTED_EVENT, SelectionCard, StatusCard, StepProgress, TextInputCard, USER_ID_MAX_LENGTH, WIRE_ONBOARDING_EVENTS, WireFeaturesProvider, WireOnboarding, attributionMetadata, clearPersistedSession, collectDeviceContext, defaultIllustrations, defaultOnboardingTheme, defaultWireFeatures, deriveAnswers, featuresCacheKey, featuresEqual, fetchWireFeatures, identifyOnboarding, isFeaturesFresh, isOnboardingEnabled, loadPersistedSession, makeSessionId, mergeTheme, motionSpec_exports as motionSpec, onboardingComponents, parseWireFeatures, peekPersistedSession, readCachedFeatures, readProgress, reportClientEvent, reportClientEvents, reportSessionStart, resetSessionStartGuard, sanitizeUserId, savePersistedSession, sessionStorageKey, themeFromBrand, toAnalyticsEvent, useIllustration, useOnboardingTheme, useReducedMotion, useResolvedFeatures, useSessionStart, useWireFeatures, useWireFeaturesContext, wireConfigFromEnv, writeCachedFeatures };
3185
+ //# sourceMappingURL=index.mjs.map
3186
+ //# sourceMappingURL=index.mjs.map