@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
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Themed SelectionCard — drop-in for the OSS SDK's SelectionCard. Same `name`,
3
+ * `propsSchema` (including the string|object option preprocess), `defaultProps`,
4
+ * and `onSelect(string | string[])` contract. Radio/checkbox styling is themed.
5
+ *
6
+ * Motion (design-refs/EXTRACTED-SPEC.md rows 1-3): rows tick on press (scale
7
+ * 0.975 spring), the selected fill/border eases in over 180ms ease-wire (a
8
+ * native-driver opacity crossfade of a selected-style overlay), and the check
9
+ * dot pops with a spring. All gated by useReducedMotion (snap, no spring).
10
+ */
11
+ import React, { useCallback, useEffect, useRef, useState } from "react";
12
+ import { Animated, Easing, Pressable, StyleSheet, Text, View } from "react-native";
13
+ import { z } from "zod";
14
+ import type { InjectedProps, WireAIComponent } from "wireai-rn";
15
+ import { useOnboardingTheme } from "../theme/ThemeContext";
16
+ import { labelStyle } from "../theme/typography";
17
+ import { Button } from "../components/Button";
18
+ import { CardLayout } from "../components/CardLayout";
19
+ import {
20
+ CHECK_SCALE_FROM,
21
+ CHECK_SPRING,
22
+ PRESS_SPRING,
23
+ ROW_PRESS_SCALE,
24
+ SELECT_FILL_MS,
25
+ WIRE_BEZIER,
26
+ scaledMs,
27
+ stateChangeDuration,
28
+ } from "../motion/motionSpec";
29
+ import { useReducedMotion } from "../motion/useReducedMotion";
30
+
31
+ const easeWire = Easing.bezier(...WIRE_BEZIER);
32
+
33
+ const optionObjectSchema = z.object({
34
+ value: z.string().describe("Option value"),
35
+ label: z.string().describe("Option display label"),
36
+ });
37
+
38
+ const schema = z.object({
39
+ title: z.string().describe("Question or prompt for the user"),
40
+ options: z
41
+ .preprocess(
42
+ (arr) =>
43
+ Array.isArray(arr)
44
+ ? arr.map((item) => (typeof item === "string" ? { value: item, label: item } : item))
45
+ : arr,
46
+ z.array(optionObjectSchema).min(1),
47
+ )
48
+ .describe("List of choices"),
49
+ submitLabel: z.string().optional().describe("Submit button label, default: Continue"),
50
+ multiSelect: z.boolean().optional().describe("Allow multiple selections"),
51
+ });
52
+
53
+ type Props = z.infer<typeof schema> &
54
+ InjectedProps & {
55
+ onSelect?: (selected: string | string[]) => void;
56
+ /** Fires as the live selection changes (before Continue), so the flow can prefetch the
57
+ * next turn. Same value shape as onSelect. Optional — injected by OnboardingFlow. */
58
+ onSelectionChange?: (selected: string | string[]) => void;
59
+ };
60
+
61
+ type OptionRowProps = {
62
+ opt: z.infer<typeof optionObjectSchema>;
63
+ isSelected: boolean;
64
+ multiSelect: boolean;
65
+ onToggle: (value: string) => void;
66
+ };
67
+
68
+ const _OptionRow: React.FC<OptionRowProps> = ({ opt, isSelected, multiSelect, onToggle }) => {
69
+ const t = useOnboardingTheme();
70
+ const reduced = useReducedMotion();
71
+ const handlePress = useCallback(() => onToggle(opt.value), [opt.value, onToggle]);
72
+
73
+ // Press tick: scale 1→0.975, spring, native driver. Skipped under reduce motion.
74
+ const pressScale = useRef(new Animated.Value(1)).current;
75
+ const pressTo = useCallback(
76
+ (to: number) => {
77
+ if (reduced) return;
78
+ Animated.spring(pressScale, {
79
+ toValue: to,
80
+ friction: PRESS_SPRING.friction,
81
+ tension: PRESS_SPRING.tension,
82
+ useNativeDriver: true,
83
+ }).start();
84
+ },
85
+ [pressScale, reduced],
86
+ );
87
+ const handlePressIn = useCallback(() => pressTo(ROW_PRESS_SCALE), [pressTo]);
88
+ const handlePressOut = useCallback(() => pressTo(1), [pressTo]);
89
+
90
+ // Selected fill: the selected border+bg live on an absolute overlay whose opacity
91
+ // crossfades in 180ms ease-wire — same visual as a color interpolation, but it
92
+ // stays on the native driver (see EXTRACTED-SPEC.md D7). Reduce motion: snap.
93
+ const selectT = useRef(new Animated.Value(isSelected ? 1 : 0)).current;
94
+ useEffect(() => {
95
+ const anim = Animated.timing(selectT, {
96
+ toValue: isSelected ? 1 : 0,
97
+ duration: scaledMs(stateChangeDuration(reduced, SELECT_FILL_MS)),
98
+ easing: easeWire,
99
+ useNativeDriver: true,
100
+ });
101
+ anim.start();
102
+ return () => anim.stop();
103
+ }, [isSelected, reduced, selectT]);
104
+
105
+ // Check pop: scale 0.6→1 + fade, springy. Reduce motion: shown at scale 1 instantly.
106
+ const checkT = useRef(new Animated.Value(isSelected ? 1 : 0)).current;
107
+ useEffect(() => {
108
+ if (!isSelected) {
109
+ checkT.setValue(0);
110
+ return;
111
+ }
112
+ if (reduced) {
113
+ checkT.setValue(1);
114
+ return;
115
+ }
116
+ checkT.setValue(0);
117
+ const anim = Animated.spring(checkT, {
118
+ toValue: 1,
119
+ friction: CHECK_SPRING.friction,
120
+ tension: CHECK_SPRING.tension,
121
+ useNativeDriver: true,
122
+ });
123
+ anim.start();
124
+ return () => anim.stop();
125
+ }, [isSelected, reduced, checkT]);
126
+
127
+ const checkScale = checkT.interpolate({ inputRange: [0, 1], outputRange: [CHECK_SCALE_FROM, 1] });
128
+
129
+ return (
130
+ <Pressable
131
+ onPress={handlePress}
132
+ onPressIn={handlePressIn}
133
+ onPressOut={handlePressOut}
134
+ accessibilityRole={multiSelect ? "checkbox" : "radio"}
135
+ accessibilityState={multiSelect ? { checked: isSelected } : { selected: isSelected }}
136
+ accessibilityLabel={opt.label}
137
+ >
138
+ <Animated.View
139
+ style={[
140
+ styles.option,
141
+ {
142
+ padding: t.spacing.md,
143
+ borderRadius: t.radius.md,
144
+ gap: t.spacing.sm,
145
+ borderColor: t.colors.border,
146
+ backgroundColor: t.colors.background,
147
+ transform: [{ scale: pressScale }],
148
+ },
149
+ ]}
150
+ >
151
+ <Animated.View
152
+ pointerEvents="none"
153
+ style={[
154
+ StyleSheet.absoluteFill,
155
+ styles.selectedOverlay,
156
+ {
157
+ borderRadius: t.radius.md,
158
+ borderColor: t.colors.primary,
159
+ backgroundColor: t.colors.primarySoft,
160
+ opacity: selectT,
161
+ },
162
+ ]}
163
+ />
164
+ <View
165
+ style={[
166
+ styles.indicator,
167
+ { borderRadius: t.radius.full },
168
+ isSelected
169
+ ? { borderColor: t.colors.primary, backgroundColor: t.colors.primary }
170
+ : { borderColor: t.colors.borderStrong, backgroundColor: "transparent" },
171
+ ]}
172
+ >
173
+ {isSelected ? (
174
+ <Animated.Text
175
+ style={[
176
+ styles.check,
177
+ { color: t.colors.onPrimary, opacity: checkT, transform: [{ scale: checkScale }] },
178
+ ]}
179
+ >
180
+
181
+ </Animated.Text>
182
+ ) : null}
183
+ </View>
184
+ {/* Accent discipline: the label keeps the text color in both states; the
185
+ accent lives on the border, fill, and dot only. */}
186
+ <Text style={[labelStyle(t.fonts), { color: t.colors.text }]}>{opt.label}</Text>
187
+ </Animated.View>
188
+ </Pressable>
189
+ );
190
+ };
191
+ const OptionRow = React.memo(_OptionRow);
192
+
193
+ const _SelectionCard: React.FC<Props> = ({
194
+ title,
195
+ options,
196
+ submitLabel = "Continue",
197
+ multiSelect = false,
198
+ onSelect,
199
+ onSelectionChange,
200
+ }) => {
201
+ const t = useOnboardingTheme();
202
+ const [selected, setSelected] = useState<string[]>([]);
203
+ const [submitted, setSubmitted] = useState(false);
204
+
205
+ // Report the settled selection upward (before Continue) so the flow can prefetch the next
206
+ // turn. Debouncing lives in the flow; here we just emit on every change once something's picked.
207
+ useEffect(() => {
208
+ if (!submitted && selected.length) onSelectionChange?.(multiSelect ? selected : selected[0]!);
209
+ }, [selected, submitted, multiSelect, onSelectionChange]);
210
+
211
+ const toggle = useCallback(
212
+ (value: string) => {
213
+ if (submitted) return;
214
+ if (multiSelect) {
215
+ setSelected((prev) => (prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]));
216
+ } else {
217
+ setSelected([value]);
218
+ }
219
+ },
220
+ [submitted, multiSelect],
221
+ );
222
+
223
+ const handleSubmit = useCallback(() => {
224
+ if (!selected.length || !onSelect || submitted) return;
225
+ setSubmitted(true);
226
+ onSelect(multiSelect ? selected : selected[0]!);
227
+ }, [selected, onSelect, submitted, multiSelect]);
228
+
229
+ return (
230
+ <CardLayout
231
+ title={title}
232
+ footer={
233
+ <Button
234
+ title={submitLabel}
235
+ onPress={handleSubmit}
236
+ variant="primary"
237
+ disabled={selected.length === 0 || submitted}
238
+ />
239
+ }
240
+ >
241
+ <View style={{ gap: t.spacing.sm + t.spacing.xs }}>
242
+ {options.map((opt) => (
243
+ <OptionRow
244
+ key={opt.value}
245
+ opt={opt}
246
+ isSelected={selected.includes(opt.value)}
247
+ multiSelect={multiSelect}
248
+ onToggle={toggle}
249
+ />
250
+ ))}
251
+ </View>
252
+ </CardLayout>
253
+ );
254
+ };
255
+
256
+ export const SelectionCard: WireAIComponent = {
257
+ name: "SelectionCard",
258
+ description:
259
+ "Use for mutually exclusive (radio) or multi-select (checkbox) choices from 2–8 options. Use ChipSelectCard instead when options are short single words.",
260
+ component: React.memo(_SelectionCard) as WireAIComponent["component"],
261
+ propsSchema: schema,
262
+ defaultProps: { submitLabel: "Continue", multiSelect: false },
263
+ };
264
+
265
+ const styles = StyleSheet.create({
266
+ option: { borderWidth: 1.5, flexDirection: "row", alignItems: "center", overflow: "hidden" },
267
+ selectedOverlay: { borderWidth: 1.5 },
268
+ indicator: { width: 24, height: 24, borderWidth: 2, alignItems: "center", justifyContent: "center" },
269
+ check: { fontSize: 13, fontWeight: "800", lineHeight: 15 },
270
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Themed StatusCard — drop-in for the OSS SDK's StatusCard. Same `name`,
3
+ * `propsSchema`, and the onContinue/onPress/onSubmit callback fallback chain.
4
+ * The terminal StatusCard is what OnboardingFlow treats as "flow complete".
5
+ * Status colors read from the theme's semantic palette.
6
+ *
7
+ * Motion (design-refs/EXTRACTED-SPEC.md rows 14, 16): the status glyph pops in
8
+ * with a spring (scale 0.4→1, overshoot) and the copy fades up 80ms behind it.
9
+ * Reduce motion: final frame instantly. (The terminal payoff with the burst is
10
+ * CompletionView; this card is the inline status treatment.)
11
+ */
12
+ import React, { useCallback, useEffect, useRef, useState } from "react";
13
+ import { Animated, Easing, StyleSheet, Text, View } from "react-native";
14
+ import { z } from "zod";
15
+ import type { InjectedProps, WireAIComponent } from "wireai-rn";
16
+ import { useOnboardingTheme } from "../theme/ThemeContext";
17
+ import { bodyStyle, headingStyle } from "../theme/typography";
18
+ import { Button } from "../components/Button";
19
+ import { CardLayout } from "../components/CardLayout";
20
+ import {
21
+ STATUS_COPY_MS,
22
+ STATUS_COPY_STAGGER_MS,
23
+ STATUS_POP_SCALE_FROM,
24
+ STATUS_POP_SPRING,
25
+ WIRE_BEZIER,
26
+ scaledMs,
27
+ } from "../motion/motionSpec";
28
+ import { useReducedMotion } from "../motion/useReducedMotion";
29
+
30
+ const easeWire = Easing.bezier(...WIRE_BEZIER);
31
+
32
+ const schema = z.object({
33
+ status: z.enum(["success", "warning", "error", "info"]).describe("Status type"),
34
+ title: z.string().describe("Status title"),
35
+ message: z.string().optional().describe("Supporting message"),
36
+ ctaLabel: z
37
+ .string()
38
+ .optional()
39
+ .describe(
40
+ "Optional call-to-action button label. Always include this on success status to keep the conversation going.",
41
+ ),
42
+ });
43
+
44
+ type Props = z.infer<typeof schema> &
45
+ InjectedProps & {
46
+ onContinue?: () => void;
47
+ onPress?: (label: string) => void;
48
+ onSubmit?: (value: string) => void;
49
+ };
50
+
51
+ const STATUS_ICONS: Record<string, string> = {
52
+ success: "✓",
53
+ warning: "⚠",
54
+ error: "✗",
55
+ info: "ℹ",
56
+ };
57
+
58
+ const _StatusCard: React.FC<Props> = ({ status, title, message, ctaLabel, onContinue, onPress, onSubmit }) => {
59
+ const t = useOnboardingTheme();
60
+ const reduced = useReducedMotion();
61
+ const [submitted, setSubmitted] = useState(false);
62
+
63
+ const colorFor: Record<string, string> = {
64
+ success: t.colors.success,
65
+ warning: t.colors.warning,
66
+ error: t.colors.error,
67
+ info: t.colors.info,
68
+ };
69
+ const color = colorFor[status] ?? t.colors.info;
70
+
71
+ // Glyph pop + copy fade-up (final frame under reduce motion).
72
+ const popT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
73
+ const copyT = useRef(new Animated.Value(reduced ? 1 : 0)).current;
74
+ useEffect(() => {
75
+ if (reduced) {
76
+ popT.setValue(1);
77
+ copyT.setValue(1);
78
+ return;
79
+ }
80
+ const anims = [
81
+ Animated.spring(popT, {
82
+ toValue: 1,
83
+ friction: STATUS_POP_SPRING.friction,
84
+ tension: STATUS_POP_SPRING.tension,
85
+ useNativeDriver: true,
86
+ }),
87
+ Animated.timing(copyT, {
88
+ toValue: 1,
89
+ duration: scaledMs(STATUS_COPY_MS),
90
+ delay: scaledMs(STATUS_COPY_STAGGER_MS),
91
+ easing: easeWire,
92
+ useNativeDriver: true,
93
+ }),
94
+ ];
95
+ anims.forEach((a) => a.start());
96
+ return () => anims.forEach((a) => a.stop());
97
+ }, [reduced, popT, copyT]);
98
+
99
+ const popScale = popT.interpolate({ inputRange: [0, 1], outputRange: [STATUS_POP_SCALE_FROM, 1] });
100
+ const copyRise = copyT.interpolate({ inputRange: [0, 1], outputRange: [10, 0] });
101
+
102
+ const handleContinue = useCallback(() => {
103
+ if (!submitted) {
104
+ setSubmitted(true);
105
+ if (onContinue) onContinue();
106
+ else if (onPress && ctaLabel) onPress(ctaLabel);
107
+ else if (onSubmit) onSubmit("continue");
108
+ }
109
+ }, [submitted, onContinue, onPress, onSubmit, ctaLabel]);
110
+
111
+ return (
112
+ <CardLayout
113
+ align="center"
114
+ footer={
115
+ ctaLabel ? (
116
+ <Button title={ctaLabel} onPress={handleContinue} variant="primary" disabled={submitted} />
117
+ ) : undefined
118
+ }
119
+ >
120
+ <View style={[styles.center, { gap: t.spacing.sm }]}>
121
+ <View style={[styles.titleRow, { gap: t.spacing.sm }]}>
122
+ <Animated.Text
123
+ style={[
124
+ headingStyle(t.fonts),
125
+ { color, opacity: popT, transform: [{ scale: popScale }] },
126
+ ]}
127
+ >
128
+ {STATUS_ICONS[status] ?? "·"}
129
+ </Animated.Text>
130
+ <Animated.Text
131
+ style={[
132
+ headingStyle(t.fonts),
133
+ { color, textAlign: "center", opacity: copyT, transform: [{ translateY: copyRise }] },
134
+ ]}
135
+ >
136
+ {title}
137
+ </Animated.Text>
138
+ </View>
139
+ {message ? (
140
+ <Animated.Text
141
+ style={[
142
+ bodyStyle(t.fonts),
143
+ {
144
+ color: t.colors.textMuted,
145
+ textAlign: "center",
146
+ opacity: copyT,
147
+ transform: [{ translateY: copyRise }],
148
+ },
149
+ ]}
150
+ >
151
+ {message}
152
+ </Animated.Text>
153
+ ) : null}
154
+ </View>
155
+ </CardLayout>
156
+ );
157
+ };
158
+
159
+ export const StatusCard: WireAIComponent = {
160
+ name: "StatusCard",
161
+ description:
162
+ "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.",
163
+ component: React.memo(_StatusCard) as WireAIComponent["component"],
164
+ propsSchema: schema,
165
+ };
166
+
167
+ const styles = StyleSheet.create({
168
+ center: { width: "100%", alignItems: "center", justifyContent: "center" },
169
+ titleRow: { flexDirection: "row", alignItems: "center", justifyContent: "center" },
170
+ });
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Themed TextInputCard — drop-in for the OSS SDK's TextInputCard. Same `name`,
3
+ * `propsSchema`, `defaultProps`, and `onSubmit(value: string)` contract. The input
4
+ * is themed and supports an optional inline error string (used by the kit's
5
+ * per-step validators — e.g. "username taken"), set via the `validationError` prop
6
+ * which the OnboardingFlow injects through callbackOverrides.
7
+ *
8
+ * Open-ended answers (goals, notes, "what would make this better") render a tall
9
+ * multi-line box by default so the user has room to actually write. Short fields
10
+ * (a name, a username) set `multiline: false` for the compact single-line height.
11
+ */
12
+ import React, { useCallback, useState } from "react";
13
+ import { StyleSheet, Text, TextInput, View, useWindowDimensions } from "react-native";
14
+ import { z } from "zod";
15
+ import type { InjectedProps, WireAIComponent } from "wireai-rn";
16
+ import { useOnboardingTheme } from "../theme/ThemeContext";
17
+ import { bodyStyle, captionStyle } from "../theme/typography";
18
+ import { Button } from "../components/Button";
19
+ import { CardLayout } from "../components/CardLayout";
20
+
21
+ const schema = z.object({
22
+ label: z.string().describe("Input field label / the question being asked"),
23
+ hint: z
24
+ .string()
25
+ .optional()
26
+ .describe(
27
+ "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."
28
+ ),
29
+ placeholder: z
30
+ .string()
31
+ .optional()
32
+ .describe(
33
+ '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.'
34
+ ),
35
+ submitLabel: z.string().optional().describe("Submit button label, default: Submit"),
36
+ multiline: z
37
+ .boolean()
38
+ .optional()
39
+ .describe(
40
+ "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."
41
+ ),
42
+ lines: z
43
+ .number()
44
+ .optional()
45
+ .describe("Approx visible rows when multiline (default 6). Height is capped at 40% of the screen."),
46
+ });
47
+
48
+ type Props = z.infer<typeof schema> &
49
+ InjectedProps & {
50
+ onSubmit?: (value: string) => void;
51
+ /**
52
+ * Inline error injected by OnboardingFlow when a validator rejects the value
53
+ * (e.g. username already taken). Re-enables the input so the user can edit.
54
+ */
55
+ validationError?: string;
56
+ };
57
+
58
+ const _TextInputCard: React.FC<Props> = ({
59
+ label,
60
+ hint,
61
+ placeholder,
62
+ submitLabel = "Submit",
63
+ onSubmit,
64
+ validationError,
65
+ multiline = true,
66
+ lines = 6,
67
+ }) => {
68
+ const t = useOnboardingTheme();
69
+ const { height: screenHeight } = useWindowDimensions();
70
+ const [value, setValue] = useState("");
71
+ const [submitted, setSubmitted] = useState(false);
72
+
73
+ // A validator rejection arrives as `validationError`; reopen the field so the
74
+ // user can fix their answer instead of being stuck on a submitted state.
75
+ const locked = submitted && !validationError;
76
+
77
+ // Multi-line free-text gets a generous box (~`lines` rows), capped at 40% of
78
+ // screen height so it stays comfortable on small devices. Single-line fields
79
+ // keep the compact 52pt height.
80
+ const APPROX_ROW_HEIGHT = 26;
81
+ const minHeight = multiline
82
+ ? Math.min(Math.max(lines, 4) * APPROX_ROW_HEIGHT + t.spacing.md * 2, screenHeight * 0.4)
83
+ : 52;
84
+
85
+ const handleSubmit = useCallback(() => {
86
+ if (value.trim() && onSubmit && !locked) {
87
+ setSubmitted(true);
88
+ onSubmit(value.trim());
89
+ }
90
+ }, [value, onSubmit, locked]);
91
+
92
+ return (
93
+ <CardLayout
94
+ title={label}
95
+ subtitle={hint}
96
+ footer={
97
+ <Button
98
+ title={submitLabel}
99
+ onPress={handleSubmit}
100
+ variant="primary"
101
+ disabled={!value.trim() || locked}
102
+ />
103
+ }
104
+ >
105
+ <View style={{ gap: t.spacing.sm }}>
106
+ <TextInput
107
+ value={value}
108
+ onChangeText={setValue}
109
+ placeholder={placeholder ?? ""}
110
+ placeholderTextColor={t.colors.textMuted}
111
+ editable={!locked}
112
+ multiline={multiline}
113
+ numberOfLines={multiline ? lines : 1}
114
+ textAlignVertical={multiline ? "top" : "center"}
115
+ style={[
116
+ bodyStyle(t.fonts),
117
+ styles.input,
118
+ {
119
+ color: t.colors.text,
120
+ backgroundColor: t.colors.surface,
121
+ borderRadius: t.radius.md,
122
+ borderColor: validationError ? t.colors.error : t.colors.border,
123
+ padding: t.spacing.md,
124
+ minHeight,
125
+ },
126
+ ]}
127
+ />
128
+ {validationError ? (
129
+ <Text style={[captionStyle(t.fonts), { color: t.colors.error }]}>{validationError}</Text>
130
+ ) : null}
131
+ </View>
132
+ </CardLayout>
133
+ );
134
+ };
135
+
136
+ export const TextInputCard: WireAIComponent = {
137
+ name: "TextInputCard",
138
+ description:
139
+ "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.",
140
+ component: React.memo(_TextInputCard) as WireAIComponent["component"],
141
+ propsSchema: schema,
142
+ defaultProps: { submitLabel: "Submit", multiline: true, lines: 6 },
143
+ };
144
+
145
+ const styles = StyleSheet.create({
146
+ input: { borderWidth: 1 },
147
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * onboardingComponents — the constrained set of themed cards the kit registers
3
+ * with the SDK. The backend's `allowed_components` must be a subset of these
4
+ * names so every card the agent asks for has a themed renderer.
5
+ */
6
+ import type { WireAIComponent } from "wireai-rn";
7
+ import { ChipSelectCard } from "./ChipSelectCard";
8
+ import { TextInputCard } from "./TextInputCard";
9
+ import { SelectionCard } from "./SelectionCard";
10
+ import { StatusCard } from "./StatusCard";
11
+ import { NumberStepperCard } from "./NumberStepperCard";
12
+ import { InterstitialCard } from "./InterstitialCard";
13
+
14
+ export { ChipSelectCard, TextInputCard, SelectionCard, StatusCard, NumberStepperCard, InterstitialCard };
15
+
16
+ export const onboardingComponents: WireAIComponent[] = [
17
+ ChipSelectCard,
18
+ TextInputCard,
19
+ SelectionCard,
20
+ StatusCard,
21
+ NumberStepperCard,
22
+ InterstitialCard,
23
+ ];
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+
3
+ import { useCoachmarkOverlay } from "./coachmarkOverlayStore";
4
+ import { SpotlightOverlay } from "./SpotlightOverlay";
5
+
6
+ export interface CoachmarkOverlayHostProps {
7
+ /** Ring/tooltip accent; defaults to the onboarding theme's `primary`. */
8
+ accentColor?: string;
9
+ /** Text/icon color on the accent surface; defaults to the theme's `onPrimary`. */
10
+ accentTextColor?: string;
11
+ }
12
+
13
+ /**
14
+ * Mounts once at the app root (rendered by CoachmarkProvider as a sibling of the
15
+ * app tree) so the blur + spotlight paint OVER the bottom tab bar — which
16
+ * react-navigation renders above screen components, out of reach of any
17
+ * screen-level overlay. Renders nothing until a step is active.
18
+ */
19
+ const CoachmarkOverlayHostComponent: React.FC<CoachmarkOverlayHostProps> = ({
20
+ accentColor,
21
+ accentTextColor,
22
+ }) => {
23
+ const active = useCoachmarkOverlay();
24
+
25
+ if (!active) return null;
26
+
27
+ return (
28
+ <SpotlightOverlay
29
+ message={active.message}
30
+ targetRect={active.targetRect}
31
+ gesture={active.gesture}
32
+ placement={active.placement}
33
+ onEngage={active.onEngage}
34
+ onDismiss={active.onDismiss}
35
+ accentColor={accentColor}
36
+ accentTextColor={accentTextColor}
37
+ />
38
+ );
39
+ };
40
+
41
+ export const CoachmarkOverlayHost = React.memo(CoachmarkOverlayHostComponent);
42
+ CoachmarkOverlayHost.displayName = "CoachmarkOverlayHost";
43
+
44
+ export default CoachmarkOverlayHost;