@rocapine/react-native-onboarding-ui 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/UI/OnboardingPage.js +1 -1
  2. package/dist/UI/OnboardingPage.js.map +1 -1
  3. package/dist/UI/Pages/ComposableScreen/Renderer.d.ts +0 -2
  4. package/dist/UI/Pages/ComposableScreen/Renderer.d.ts.map +1 -1
  5. package/dist/UI/Pages/ComposableScreen/Renderer.js +13 -274
  6. package/dist/UI/Pages/ComposableScreen/Renderer.js.map +1 -1
  7. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.d.ts +39 -0
  8. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.d.ts.map +1 -0
  9. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.js +20 -0
  10. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.js.map +1 -0
  11. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts +67 -0
  12. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts.map +1 -0
  13. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js +65 -0
  14. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js.map +1 -0
  15. package/dist/UI/Pages/ComposableScreen/elements/IconElement.d.ts +49 -0
  16. package/dist/UI/Pages/ComposableScreen/elements/IconElement.d.ts.map +1 -0
  17. package/dist/UI/Pages/ComposableScreen/elements/IconElement.js +37 -0
  18. package/dist/UI/Pages/ComposableScreen/elements/IconElement.js.map +1 -0
  19. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts +50 -0
  20. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts.map +1 -0
  21. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js +34 -0
  22. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js.map +1 -0
  23. package/dist/UI/Pages/ComposableScreen/elements/InputElement.d.ts +110 -0
  24. package/dist/UI/Pages/ComposableScreen/elements/InputElement.d.ts.map +1 -0
  25. package/dist/UI/Pages/ComposableScreen/elements/InputElement.js +66 -0
  26. package/dist/UI/Pages/ComposableScreen/elements/InputElement.js.map +1 -0
  27. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.d.ts +47 -0
  28. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.d.ts.map +1 -0
  29. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.js +60 -0
  30. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.js.map +1 -0
  31. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts +86 -0
  32. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts.map +1 -0
  33. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js +120 -0
  34. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js.map +1 -0
  35. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.d.ts +70 -0
  36. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.d.ts.map +1 -0
  37. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.js +68 -0
  38. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.js.map +1 -0
  39. package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts +94 -0
  40. package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts.map +1 -0
  41. package/dist/UI/Pages/ComposableScreen/elements/StackElement.js +66 -0
  42. package/dist/UI/Pages/ComposableScreen/elements/StackElement.js.map +1 -0
  43. package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts +66 -0
  44. package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts.map +1 -0
  45. package/dist/UI/Pages/ComposableScreen/elements/TextElement.js +59 -0
  46. package/dist/UI/Pages/ComposableScreen/elements/TextElement.js.map +1 -0
  47. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.d.ts +49 -0
  48. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.d.ts.map +1 -0
  49. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.js +84 -0
  50. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.js.map +1 -0
  51. package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts +5 -0
  52. package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts.map +1 -0
  53. package/dist/UI/Pages/ComposableScreen/elements/renderElement.js +49 -0
  54. package/dist/UI/Pages/ComposableScreen/elements/renderElement.js.map +1 -0
  55. package/dist/UI/Pages/ComposableScreen/elements/shared.d.ts +13 -0
  56. package/dist/UI/Pages/ComposableScreen/elements/shared.d.ts.map +1 -0
  57. package/dist/UI/Pages/ComposableScreen/elements/shared.js +6 -0
  58. package/dist/UI/Pages/ComposableScreen/elements/shared.js.map +1 -0
  59. package/dist/UI/Pages/ComposableScreen/types.d.ts +40 -114
  60. package/dist/UI/Pages/ComposableScreen/types.d.ts.map +1 -1
  61. package/dist/UI/Pages/ComposableScreen/types.js +33 -122
  62. package/dist/UI/Pages/ComposableScreen/types.js.map +1 -1
  63. package/dist/UI/Provider/OnboardingProgressProvider.d.ts +6 -2
  64. package/dist/UI/Provider/OnboardingProgressProvider.d.ts.map +1 -1
  65. package/dist/UI/Provider/OnboardingProgressProvider.js +4 -3
  66. package/dist/UI/Provider/OnboardingProgressProvider.js.map +1 -1
  67. package/package.json +2 -2
  68. package/src/UI/OnboardingPage.tsx +1 -1
  69. package/src/UI/Pages/ComposableScreen/Renderer.tsx +22 -431
  70. package/src/UI/Pages/ComposableScreen/elements/BaseBoxProps.ts +33 -0
  71. package/src/UI/Pages/ComposableScreen/elements/ButtonElement.tsx +96 -0
  72. package/src/UI/Pages/ComposableScreen/elements/IconElement.tsx +67 -0
  73. package/src/UI/Pages/ComposableScreen/elements/ImageElement.tsx +52 -0
  74. package/src/UI/Pages/ComposableScreen/elements/InputElement.tsx +109 -0
  75. package/src/UI/Pages/ComposableScreen/elements/LottieElement.tsx +97 -0
  76. package/src/UI/Pages/ComposableScreen/elements/RadioGroupElement.tsx +182 -0
  77. package/src/UI/Pages/ComposableScreen/elements/RiveElement.tsx +105 -0
  78. package/src/UI/Pages/ComposableScreen/elements/StackElement.tsx +106 -0
  79. package/src/UI/Pages/ComposableScreen/elements/TextElement.tsx +95 -0
  80. package/src/UI/Pages/ComposableScreen/elements/VideoElement.tsx +113 -0
  81. package/src/UI/Pages/ComposableScreen/elements/renderElement.tsx +61 -0
  82. package/src/UI/Pages/ComposableScreen/elements/shared.ts +15 -0
  83. package/src/UI/Pages/ComposableScreen/types.ts +56 -235
  84. package/src/UI/Provider/OnboardingProgressProvider.tsx +8 -5
@@ -1,405 +1,38 @@
1
- import React, { useState, useEffect, useContext, useMemo } from "react";
2
- import { View, Text, Image, ScrollView, StyleSheet, TouchableOpacity, TextInput } from "react-native";
1
+ import { useContext, useMemo } from "react";
2
+ import { ScrollView, StyleSheet } from "react-native";
3
3
  import { ComposableScreenStepType, ComposableScreenStepTypeSchema, UIElement } from "./types";
4
- import { Theme } from "../../Theme/types";
5
- import { defaultTheme } from "../../Theme/defaultTheme";
6
- import { getTextStyle } from "../../Theme/helpers";
7
4
  import { withErrorBoundary } from "../../ErrorBoundary";
8
5
  import { OnboardingTemplate } from "../../Templates/OnboardingTemplate";
9
6
  import { OnboardingProgressContext } from "../../Provider/OnboardingProgressProvider";
10
-
11
- let LottieView: React.ComponentType<{
12
- source: string | object;
13
- autoPlay?: boolean;
14
- loop?: boolean;
15
- speed?: number;
16
- style?: object;
17
- }> | null = null;
18
- try {
19
- LottieView = require("lottie-react-native").default;
20
- } catch {
21
- // lottie-react-native not installed
22
- }
23
-
24
- // Metro cannot tree-shake optional peer deps at runtime; the try/catch pattern
25
- // below is the standard React Native approach for optional native modules.
26
- type VideoUIElement = Extract<UIElement, { type: "Video" }>;
27
- let VideoElementComponent: React.ComponentType<{ element: VideoUIElement; style: object }> | null = null;
28
- try {
29
- const { VideoView, useVideoPlayer } = require("expo-video");
30
- VideoElementComponent = ({ element, style }: { element: VideoUIElement; style: object }) => {
31
- const player = useVideoPlayer(element.props.url, (p: any) => {
32
- p.loop = element.props.loop ?? false;
33
- p.muted = element.props.muted ?? true;
34
- if (element.props.autoPlay) p.play();
35
- });
36
- return (
37
- <VideoView
38
- player={player}
39
- style={style}
40
- allowsFullscreen={false}
41
- nativeControls={element.props.controls ?? false}
42
- />
43
- );
44
- };
45
- } catch {
46
- // expo-video not installed
47
- }
48
-
49
- type InputUIElement = Extract<UIElement, { type: "Input" }>;
50
- const InputElementComponent = ({ element, theme, variables, onVariableChange }: { element: InputUIElement; theme: Theme; variables: Record<string, string>; onVariableChange: (key: string, value: string) => void }) => {
51
- const persistedValue = element.props.variableName ? variables[element.props.variableName] : undefined;
52
- const [value, setValue] = useState(persistedValue ?? element.props.defaultValue ?? "");
53
-
54
- useEffect(() => {
55
- if (element.props.variableName && element.props.defaultValue && persistedValue === undefined) {
56
- onVariableChange(element.props.variableName, element.props.defaultValue);
57
- }
58
- }, []);
59
-
60
- const handleChange = (text: string) => {
61
- setValue(text);
62
- if (element.props.variableName) {
63
- onVariableChange(element.props.variableName, text);
64
- }
65
- };
66
-
67
- return (
68
- <View
69
- style={{
70
- backgroundColor: element.props.backgroundColor ?? theme.colors.neutral.lowest,
71
- borderWidth: element.props.borderWidth ?? 1,
72
- borderRadius: element.props.borderRadius ?? 8,
73
- borderColor: element.props.borderColor ?? theme.colors.neutral.low,
74
- width: element.props.width,
75
- height: element.props.height,
76
- opacity: element.props.opacity,
77
- margin: element.props.margin,
78
- marginHorizontal: element.props.marginHorizontal,
79
- marginVertical: element.props.marginVertical,
80
- overflow: "hidden",
81
- }}
82
- >
83
- <TextInput
84
- value={value}
85
- onChangeText={handleChange}
86
- placeholder={element.props.placeholder}
87
- placeholderTextColor={element.props.placeholderColor ?? theme.colors.text.tertiary}
88
- keyboardType={element.props.keyboardType ?? "default"}
89
- returnKeyType={element.props.returnKeyType ?? "done"}
90
- autoCapitalize={element.props.autoCapitalize ?? "sentences"}
91
- secureTextEntry={element.props.secureTextEntry ?? false}
92
- maxLength={element.props.maxLength}
93
- multiline={element.props.multiline ?? false}
94
- numberOfLines={element.props.numberOfLines}
95
- editable={element.props.editable ?? true}
96
- style={{
97
- flex: 1,
98
- color: element.props.color ?? theme.colors.text.primary,
99
- fontSize: element.props.fontSize ?? theme.typography.textStyles.body.fontSize,
100
- fontWeight: element.props.fontWeight as any,
101
- textAlign: element.props.textAlign,
102
- padding: element.props.padding ?? 12,
103
- paddingHorizontal: element.props.paddingHorizontal,
104
- paddingVertical: element.props.paddingVertical,
105
- }}
106
- />
107
- </View>
108
- );
109
- };
110
-
111
- type RiveUIElement = Extract<UIElement, { type: "Rive" }>;
112
- let RiveElementComponent: React.ComponentType<{ element: RiveUIElement; riveStyle: object }> | null = null;
113
- try {
114
- const riveModule = require("rive-react-native");
115
- const Rive = riveModule.default;
116
- const { Fit, Alignment } = riveModule;
117
- RiveElementComponent = ({ element, riveStyle }: { element: RiveUIElement; riveStyle: object }) => {
118
- return (
119
- <Rive
120
- url={element.props.url}
121
- autoplay={element.props.autoplay ?? true}
122
- fit={element.props.fit ? Fit[element.props.fit] : Fit.Contain}
123
- alignment={element.props.alignment ? Alignment[element.props.alignment] : Alignment.Center}
124
- artboardName={element.props.artboardName}
125
- stateMachineName={element.props.stateMachineName}
126
- style={riveStyle}
127
- onError={console.error}
128
- />
129
- );
130
- };
131
- } catch {
132
- // rive-react-native not installed - will show fallback if Rive is used
133
- }
134
-
135
- const interpolate = (template: string, variables: Record<string, string>): string =>
136
- template.replace(/\{\{([^}]+?)\}\}/g, (_, key) => variables[key] ?? "");
7
+ import { useTheme } from "../../Theme/useTheme";
8
+ import { RenderContext } from "./elements/shared";
9
+ import { renderElement } from "./elements/renderElement";
137
10
 
138
11
  type ContentProps = {
139
12
  step: ComposableScreenStepType;
140
13
  onContinue: () => void;
141
- theme?: Theme;
142
14
  };
143
15
 
144
- const renderElement = (element: UIElement, theme: Theme, variables: Record<string, string>, setVariable: (key: string, value: string) => void, parentType?: "XStack" | "YStack"): React.ReactNode => {
145
- if (element.type === "YStack" || element.type === "XStack") {
146
- return (
147
- <View
148
- key={element.id}
149
- style={{
150
- flexDirection: element.type === "XStack" ? "row" : "column",
151
- gap: element.props.gap,
152
- padding: element.props.padding,
153
- paddingHorizontal: element.props.paddingHorizontal,
154
- paddingVertical: element.props.paddingVertical,
155
- margin: element.props.margin,
156
- marginHorizontal: element.props.marginHorizontal,
157
- marginVertical: element.props.marginVertical,
158
- flex: element.props.flex,
159
- width: element.props.width,
160
- height: element.props.height,
161
- minWidth: element.props.minWidth,
162
- maxWidth: element.props.maxWidth,
163
- minHeight: element.props.minHeight,
164
- maxHeight: element.props.maxHeight,
165
- flexShrink: element.props.flexShrink ?? (parentType === "XStack" ? 1 : undefined),
166
- flexWrap: element.props.flexWrap,
167
- alignItems: element.props.alignItems,
168
- justifyContent: element.props.justifyContent,
169
- backgroundColor: element.props.backgroundColor,
170
- borderWidth: element.props.borderWidth,
171
- borderRadius: element.props.borderRadius,
172
- borderColor: element.props.borderColor,
173
- overflow: element.props.overflow,
174
- opacity: element.props.opacity,
175
- }}
176
- >
177
- {element.children.map((child) => renderElement(child, theme, variables, setVariable, element.type))}
178
- </View>
179
- );
180
- }
181
-
182
- if (element.type === "Text") {
183
- const content = element.props.mode === "expression"
184
- ? interpolate(element.props.content, variables)
185
- : element.props.content;
186
- return (
187
- <Text
188
- key={element.id}
189
- style={{
190
- fontSize: element.props.fontSize,
191
- fontWeight: element.props.fontWeight as any,
192
- fontFamily: element.props.fontFamily,
193
- color: element.props.color ?? theme.colors.text.primary,
194
- textAlign: element.props.textAlign,
195
- letterSpacing: element.props.letterSpacing,
196
- lineHeight: element.props.lineHeight,
197
- backgroundColor: element.props.backgroundColor,
198
- padding: element.props.padding,
199
- paddingHorizontal: element.props.paddingHorizontal,
200
- paddingVertical: element.props.paddingVertical,
201
- margin: element.props.margin,
202
- marginHorizontal: element.props.marginHorizontal,
203
- marginVertical: element.props.marginVertical,
204
- borderWidth: element.props.borderWidth,
205
- borderRadius: element.props.borderRadius,
206
- borderColor: element.props.borderColor,
207
- opacity: element.props.opacity,
208
- flexShrink: parentType === "XStack" ? 1 : undefined,
209
- }}
210
- >
211
- {content}
212
- </Text>
213
- );
214
- }
215
-
216
- if (element.type === "Image") {
217
- const hasExplicitHeight = element.props.height !== undefined;
218
- const aspectRatio = hasExplicitHeight
219
- ? undefined
220
- : (element.props.aspectRatio ?? 16 / 9);
221
- return (
222
- <Image
223
- key={element.id}
224
- source={{ uri: element.props.url }}
225
- resizeMode={element.props.resizeMode ?? "cover"}
226
- style={{
227
- width: element.props.width ?? "100%",
228
- height: element.props.height,
229
- aspectRatio,
230
- borderRadius: element.props.borderRadius,
231
- borderWidth: element.props.borderWidth,
232
- borderColor: element.props.borderColor,
233
- opacity: element.props.opacity,
234
- margin: element.props.margin,
235
- marginHorizontal: element.props.marginHorizontal,
236
- marginVertical: element.props.marginVertical,
237
- padding: element.props.padding,
238
- paddingHorizontal: element.props.paddingHorizontal,
239
- paddingVertical: element.props.paddingVertical,
240
- }}
241
- />
242
- );
243
- }
244
-
245
- if (element.type === "Lottie") {
246
- const wrapperStyle = {
247
- width: element.props.width ?? ("100%" as `${number}%`),
248
- height: element.props.height ?? 200,
249
- opacity: element.props.opacity,
250
- margin: element.props.margin,
251
- marginHorizontal: element.props.marginHorizontal,
252
- marginVertical: element.props.marginVertical,
253
- padding: element.props.padding,
254
- paddingHorizontal: element.props.paddingHorizontal,
255
- paddingVertical: element.props.paddingVertical,
256
- borderWidth: element.props.borderWidth,
257
- borderRadius: element.props.borderRadius,
258
- borderColor: element.props.borderColor,
259
- overflow: "hidden" as const,
260
- };
261
-
262
- if (!LottieView) {
263
- return (
264
- <View key={element.id} style={[wrapperStyle, styles.mediaFallback, { backgroundColor: theme.colors.neutral.lowest }]}>
265
- <Text style={[styles.mediaFallbackText, getTextStyle(theme, "caption"), { color: theme.colors.text.tertiary }]}>
266
- Install lottie-react-native to render Lottie animations.
267
- </Text>
268
- </View>
269
- );
270
- }
271
-
272
- return (
273
- <View key={element.id} style={wrapperStyle}>
274
- <LottieView
275
- source={{ uri: element.props.source }}
276
- autoPlay={element.props.autoPlay ?? true}
277
- loop={element.props.loop ?? true}
278
- speed={element.props.speed}
279
- style={styles.fill}
280
- />
281
- </View>
282
- );
283
- }
284
-
285
- if (element.type === "Rive") {
286
- const wrapperStyle = {
287
- width: element.props.width ?? ("100%" as `${number}%`),
288
- height: element.props.height ?? 200,
289
- opacity: element.props.opacity,
290
- margin: element.props.margin,
291
- marginHorizontal: element.props.marginHorizontal,
292
- marginVertical: element.props.marginVertical,
293
- padding: element.props.padding,
294
- paddingHorizontal: element.props.paddingHorizontal,
295
- paddingVertical: element.props.paddingVertical,
296
- borderWidth: element.props.borderWidth,
297
- borderRadius: element.props.borderRadius,
298
- borderColor: element.props.borderColor,
299
- overflow: "hidden" as const,
300
- };
301
-
302
- if (!RiveElementComponent) {
303
- return (
304
- <View key={element.id} style={[wrapperStyle, styles.mediaFallback, { backgroundColor: theme.colors.neutral.lowest }]}>
305
- <Text style={[styles.mediaFallbackText, getTextStyle(theme, "caption"), { color: theme.colors.text.tertiary }]}>
306
- Install rive-react-native to render Rive animations.
307
- </Text>
308
- </View>
309
- );
310
- }
311
-
312
- return (
313
- <View key={element.id} style={wrapperStyle}>
314
- <RiveElementComponent element={element} riveStyle={styles.fill} />
315
- </View>
316
- );
317
- }
318
-
319
- if (element.type === "Icon") {
320
- const icons = require("lucide-react-native");
321
- const IconComp = icons[element.props.name] as React.ComponentType<{
322
- size?: number;
323
- color?: string;
324
- strokeWidth?: number;
325
- }> | undefined;
326
- return (
327
- <View
328
- key={element.id}
329
- style={{
330
- width: element.props.width,
331
- height: element.props.height,
332
- margin: element.props.margin,
333
- marginHorizontal: element.props.marginHorizontal,
334
- marginVertical: element.props.marginVertical,
335
- padding: element.props.padding,
336
- paddingHorizontal: element.props.paddingHorizontal,
337
- paddingVertical: element.props.paddingVertical,
338
- borderWidth: element.props.borderWidth,
339
- borderRadius: element.props.borderRadius,
340
- borderColor: element.props.borderColor,
341
- opacity: element.props.opacity,
342
- }}
343
- >
344
- {IconComp ? (
345
- <IconComp
346
- size={element.props.size ?? 24}
347
- color={element.props.color ?? theme.colors.text.primary}
348
- strokeWidth={element.props.strokeWidth ?? 2}
349
- />
350
- ) : null}
351
- </View>
352
- );
353
- }
354
-
355
- if (element.type === "Video") {
356
- const wrapperStyle = {
357
- width: element.props.width ?? ("100%" as `${number}%`),
358
- height: element.props.height ?? 200,
359
- opacity: element.props.opacity,
360
- margin: element.props.margin,
361
- marginHorizontal: element.props.marginHorizontal,
362
- marginVertical: element.props.marginVertical,
363
- padding: element.props.padding,
364
- paddingHorizontal: element.props.paddingHorizontal,
365
- paddingVertical: element.props.paddingVertical,
366
- borderWidth: element.props.borderWidth,
367
- borderRadius: element.props.borderRadius,
368
- borderColor: element.props.borderColor,
369
- overflow: "hidden" as const,
370
- };
371
-
372
- if (!VideoElementComponent) {
373
- return (
374
- <View key={element.id} style={[wrapperStyle, styles.mediaFallback, { backgroundColor: theme.colors.neutral.lowest }]}>
375
- <Text style={[styles.mediaFallbackText, getTextStyle(theme, "caption"), { color: theme.colors.text.tertiary }]}>
376
- Install expo-video to render videos.
377
- </Text>
378
- </View>
379
- );
380
- }
381
-
382
- return (
383
- <View key={element.id} style={wrapperStyle}>
384
- <VideoElementComponent element={element} style={styles.fill} />
385
- </View>
386
- );
387
- }
388
-
389
- if (element.type === "Input") {
390
- return <InputElementComponent key={element.id} element={element} theme={theme} variables={variables} onVariableChange={setVariable} />;
391
- }
392
-
393
- return null;
394
- };
395
-
396
- const ComposableScreenRendererBase = ({ step, onContinue, theme = defaultTheme }: ContentProps) => {
16
+ const ComposableScreenRendererBase = ({ step, onContinue }: ContentProps) => {
17
+ const { theme } = useTheme();
397
18
  const validatedData = useMemo(() => ComposableScreenStepTypeSchema.parse(step), [step]);
398
19
  const { elements } = validatedData.payload;
399
20
  const { composableVariables, setComposableVariable } = useContext(OnboardingProgressContext);
21
+
22
+ const renderChildren = (children: UIElement[], parentType: "XStack" | "YStack") =>
23
+ children.map((child) => renderElement(child, ctx, parentType));
24
+
25
+ const ctx: RenderContext = {
26
+ theme,
27
+ variables: composableVariables,
28
+ setVariable: setComposableVariable,
29
+ onContinue,
30
+ renderChildren,
31
+ };
32
+
400
33
  return (
401
34
  <OnboardingTemplate
402
- step={step}
35
+ step={validatedData}
403
36
  onContinue={onContinue}
404
37
  theme={theme}
405
38
  >
@@ -409,58 +42,16 @@ const ComposableScreenRendererBase = ({ step, onContinue, theme = defaultTheme }
409
42
  alwaysBounceVertical={false}
410
43
  keyboardShouldPersistTaps="handled"
411
44
  >
412
- {elements.map((element) => renderElement(element, theme, composableVariables, setComposableVariable))}
413
- <View style={styles.bottomSection}>
414
- <TouchableOpacity
415
- style={[styles.ctaButton, { backgroundColor: theme.colors.primary }]}
416
- onPress={onContinue}
417
- activeOpacity={0.8}
418
- >
419
- <Text
420
- style={[
421
- getTextStyle(theme, "button"),
422
- { color: theme.colors.text.opposite },
423
- ]}
424
- >
425
- {validatedData.continueButtonLabel}
426
- </Text>
427
- </TouchableOpacity>
428
- </View>
45
+ {elements.map((element) => renderElement(element, ctx))}
429
46
  </ScrollView>
430
47
  </OnboardingTemplate>
431
- )
48
+ );
432
49
  };
433
50
 
434
51
  const styles = StyleSheet.create({
435
- container: {
436
- flex: 1,
437
- },
438
- fill: {
439
- width: "100%",
440
- height: "100%",
441
- },
442
52
  scrollContent: {
443
53
  flexGrow: 1,
444
54
  },
445
- bottomSection: {
446
- paddingHorizontal: 32,
447
- alignItems: "center",
448
- },
449
- ctaButton: {
450
- borderRadius: 90,
451
- paddingVertical: 18,
452
- paddingHorizontal: 24,
453
- minWidth: 234,
454
- alignItems: "center",
455
- },
456
- mediaFallback: {
457
- alignItems: "center",
458
- justifyContent: "center",
459
- },
460
- mediaFallbackText: {
461
- textAlign: "center",
462
- paddingHorizontal: 16,
463
- },
464
55
  });
465
56
 
466
57
  export const ComposableScreenRenderer = withErrorBoundary(ComposableScreenRendererBase, "ComposableScreen");
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+
3
+ export type BaseBoxProps = {
4
+ width?: number;
5
+ height?: number;
6
+ alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline";
7
+ opacity?: number;
8
+ margin?: number;
9
+ marginHorizontal?: number;
10
+ marginVertical?: number;
11
+ padding?: number;
12
+ paddingHorizontal?: number;
13
+ paddingVertical?: number;
14
+ borderWidth?: number;
15
+ borderRadius?: number;
16
+ borderColor?: string;
17
+ };
18
+
19
+ export const BaseBoxPropsSchema = z.object({
20
+ width: z.number().min(0).optional(),
21
+ height: z.number().min(0).optional(),
22
+ alignSelf: z.enum(["auto", "flex-start", "flex-end", "center", "stretch", "baseline"]).optional(),
23
+ opacity: z.number().min(0).max(1).optional(),
24
+ margin: z.number().optional(),
25
+ marginHorizontal: z.number().optional(),
26
+ marginVertical: z.number().optional(),
27
+ padding: z.number().min(0).optional(),
28
+ paddingHorizontal: z.number().min(0).optional(),
29
+ paddingVertical: z.number().min(0).optional(),
30
+ borderWidth: z.number().min(0).optional(),
31
+ borderRadius: z.number().min(0).optional(),
32
+ borderColor: z.string().optional(),
33
+ });
@@ -0,0 +1,96 @@
1
+ import React from "react";
2
+ import { z } from "zod";
3
+ import { Text, TouchableOpacity } from "react-native";
4
+ import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
5
+ import { UIElement } from "../types";
6
+ import { RenderContext } from "./shared";
7
+
8
+ export type ButtonElementProps = BaseBoxProps & {
9
+ label: string;
10
+ action?: "continue";
11
+ variant?: "filled" | "outlined" | "ghost";
12
+ backgroundColor?: string;
13
+ color?: string;
14
+ fontSize?: number;
15
+ fontWeight?: string;
16
+ fontFamily?: string;
17
+ textAlign?: "left" | "center" | "right";
18
+ alignSelf?: "auto" | "flex-start" | "center" | "flex-end" | "stretch";
19
+ };
20
+
21
+ export const ButtonElementPropsSchema = BaseBoxPropsSchema.extend({
22
+ label: z.string().min(1, "label must not be empty"),
23
+ action: z.enum(["continue"]).optional(),
24
+ variant: z.enum(["filled", "outlined", "ghost"]).optional(),
25
+ backgroundColor: z.string().optional(),
26
+ color: z.string().optional(),
27
+ fontSize: z.number().optional(),
28
+ fontWeight: z.string().optional(),
29
+ fontFamily: z.string().optional(),
30
+ textAlign: z.enum(["left", "center", "right"]).optional(),
31
+ alignSelf: z.enum(["auto", "flex-start", "center", "flex-end", "stretch"]).optional(),
32
+ });
33
+
34
+ type ButtonUIElement = Extract<UIElement, { type: "Button" }>;
35
+
36
+ type Props = {
37
+ element: ButtonUIElement;
38
+ ctx: RenderContext;
39
+ };
40
+
41
+ export const ButtonElementComponent = ({ element, ctx }: Props): React.ReactElement => {
42
+ const { theme, onContinue } = ctx;
43
+ const action = element.props.action;
44
+ const handlePress = () => {
45
+ if (action === undefined || action === "continue") {
46
+ onContinue();
47
+ }
48
+ // other action values are no-ops
49
+ };
50
+ const variant = element.props.variant ?? "filled";
51
+ const isFilled = variant === "filled";
52
+ const isOutlined = variant === "outlined";
53
+ const bgColor = isFilled
54
+ ? (element.props.backgroundColor ?? theme.colors.primary)
55
+ : "transparent";
56
+ const textColor = isFilled
57
+ ? (element.props.color ?? theme.colors.text.opposite)
58
+ : (element.props.color ?? theme.colors.primary);
59
+
60
+ return (
61
+ <TouchableOpacity
62
+ activeOpacity={0.8}
63
+ onPress={handlePress}
64
+ style={{
65
+ backgroundColor: bgColor,
66
+ borderRadius: element.props.borderRadius ?? 90,
67
+ borderWidth: isOutlined ? (element.props.borderWidth ?? 1) : (element.props.borderWidth ?? 0),
68
+ borderColor: isOutlined ? (element.props.borderColor ?? theme.colors.primary) : element.props.borderColor,
69
+ padding: element.props.padding,
70
+ paddingVertical: element.props.paddingVertical ?? 14,
71
+ paddingHorizontal: element.props.paddingHorizontal ?? 24,
72
+ width: element.props.width,
73
+ height: element.props.height,
74
+ margin: element.props.margin,
75
+ marginHorizontal: element.props.marginHorizontal,
76
+ marginVertical: element.props.marginVertical,
77
+ opacity: element.props.opacity,
78
+ alignSelf: element.props.alignSelf ?? (element.props.width ? undefined : "stretch"),
79
+ alignItems: "center",
80
+ justifyContent: "center",
81
+ }}
82
+ >
83
+ <Text
84
+ style={{
85
+ color: textColor,
86
+ fontSize: element.props.fontSize ?? theme.typography.textStyles.button.fontSize,
87
+ fontWeight: (element.props.fontWeight as any) ?? theme.typography.textStyles.button.fontWeight,
88
+ fontFamily: element.props.fontFamily,
89
+ textAlign: element.props.textAlign ?? "center",
90
+ }}
91
+ >
92
+ {element.props.label}
93
+ </Text>
94
+ </TouchableOpacity>
95
+ );
96
+ };
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import { z } from "zod";
3
+ import { View } from "react-native";
4
+ import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
5
+ import { UIElement } from "../types";
6
+ import { RenderContext } from "./shared";
7
+
8
+ export type IconElementProps = BaseBoxProps & {
9
+ name: string;
10
+ size?: number;
11
+ color?: string;
12
+ strokeWidth?: number;
13
+ backgroundColor?: string;
14
+ };
15
+
16
+ export const IconElementPropsSchema = BaseBoxPropsSchema.extend({
17
+ name: z.string().min(1, "icon name must not be empty"),
18
+ size: z.number().nonnegative().optional(),
19
+ color: z.string().optional(),
20
+ strokeWidth: z.number().nonnegative().optional(),
21
+ backgroundColor: z.string().optional(),
22
+ });
23
+
24
+ type IconUIElement = Extract<UIElement, { type: "Icon" }>;
25
+
26
+ type Props = {
27
+ element: IconUIElement;
28
+ ctx: RenderContext;
29
+ };
30
+
31
+ export const IconElementComponent = ({ element, ctx }: Props): React.ReactElement => {
32
+ const { theme } = ctx;
33
+ const icons = require("lucide-react-native");
34
+ const IconComp = icons[element.props.name] as React.ComponentType<{
35
+ size?: number;
36
+ color?: string;
37
+ strokeWidth?: number;
38
+ }> | undefined;
39
+
40
+ return (
41
+ <View
42
+ style={{
43
+ width: element.props.width,
44
+ height: element.props.height,
45
+ margin: element.props.margin,
46
+ marginHorizontal: element.props.marginHorizontal,
47
+ marginVertical: element.props.marginVertical,
48
+ padding: element.props.padding,
49
+ paddingHorizontal: element.props.paddingHorizontal,
50
+ paddingVertical: element.props.paddingVertical,
51
+ borderWidth: element.props.borderWidth,
52
+ borderRadius: element.props.borderRadius,
53
+ borderColor: element.props.borderColor,
54
+ backgroundColor: element.props.backgroundColor,
55
+ opacity: element.props.opacity,
56
+ }}
57
+ >
58
+ {IconComp ? (
59
+ <IconComp
60
+ size={element.props.size ?? 24}
61
+ color={element.props.color ?? theme.colors.text.primary}
62
+ strokeWidth={element.props.strokeWidth ?? 2}
63
+ />
64
+ ) : null}
65
+ </View>
66
+ );
67
+ };