noboarding 0.1.0-beta → 1.0.2-beta

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.
@@ -237,7 +237,11 @@ export const SummaryScreen: React.FC<CustomScreenProps> = ({
237
237
 
238
238
  ```typescript
239
239
  <OnboardingFlow
240
- apiKey="sk_live_..."
240
+ // Dual API keys for automatic environment detection
241
+ testKey="nb_test_..."
242
+ productionKey="nb_live_..."
243
+ // SDK auto-detects __DEV__ and uses appropriate key
244
+
241
245
  customComponents={{
242
246
  NameScreen,
243
247
  AgeScreen,
@@ -543,7 +547,12 @@ import { AgeScreen } from './screens/AgeScreen';
543
547
  import { PreferencesScreen } from './screens/PreferencesScreen';
544
548
 
545
549
  <OnboardingFlow
546
- apiKey="sk_live_your_api_key"
550
+ // Use dual API keys for automatic environment detection
551
+ testKey="nb_test_your_test_key"
552
+ productionKey="nb_live_your_production_key"
553
+ // The SDK automatically uses testKey when __DEV__ is true
554
+ // and productionKey in production builds
555
+
547
556
  customComponents={{
548
557
  NameScreen: NameScreen, // Component name MUST match exactly
549
558
  AgeScreen: AgeScreen, // Case-sensitive!
@@ -593,10 +602,14 @@ npm start
593
602
 
594
603
  ### Step 5: Deploy & Publish
595
604
 
596
- 1. Build production app
597
- 2. Submit to App Store / Google Play
598
- 3. **WAIT for approval**
599
- 4. **After app is live:** Publish flow in dashboard
605
+ 1. Test locally with Test API Key (development mode)
606
+ 2. In dashboard: **Publish Publish for Testing**
607
+ 3. Build production app
608
+ 4. Submit to App Store / Google Play
609
+ 5. **WAIT for approval**
610
+ 6. **After app is live:** In dashboard, **Publish → Publish to Production**
611
+
612
+ **Note:** Test and Production environments are separate. You can safely test changes using the Test API Key before rolling them out to production users with the Production API Key.
600
613
 
601
614
  ---
602
615
 
@@ -831,7 +844,10 @@ const styles = StyleSheet.create({
831
844
 
832
845
  ```typescript
833
846
  <OnboardingFlow
834
- apiKey="sk_live_..."
847
+ // Dual API keys - SDK auto-detects environment
848
+ testKey="nb_test_..."
849
+ productionKey="nb_live_..."
850
+
835
851
  customComponents={{
836
852
  Step1EmailScreen,
837
853
  Step2GoalsScreen,
package/README.md CHANGED
@@ -513,6 +513,8 @@ import { API, AnalyticsManager } from 'noboarding';
513
513
 
514
514
  ## Development
515
515
 
516
+ ### Building the SDK
517
+
516
518
  The TestApp imports the SDK from the compiled `lib/` directory (`"main": "lib/index.js"`), not from `src/` directly. After making any changes to files in `sdk/src/`, you must rebuild before testing:
517
519
 
518
520
  ```bash
@@ -522,6 +524,63 @@ npm run build
522
524
 
523
525
  Then restart the TestApp. If you skip this step, the TestApp will still be running the old compiled code and your changes won't take effect.
524
526
 
527
+ ### Dashboard Preview Integration
528
+
529
+ The dashboard uses **local copies** of SDK source files for the preview feature. When you modify SDK source files, they need to be synced to the dashboard.
530
+
531
+ **Why copies?** Next.js/Turbopack doesn't support importing from external directories with the react-native-web setup, so the dashboard maintains local copies in `dashboard/lib/sdk/`.
532
+
533
+ #### Files That Need Syncing
534
+
535
+ When you modify these SDK files:
536
+ - `src/types.ts` → Auto-synced to `dashboard/lib/sdk/types.ts`
537
+ - `src/variableUtils.ts` → Auto-synced to `dashboard/lib/sdk/variableUtils.ts`
538
+ - `src/components/ElementRenderer.tsx` → ⚠️ **NOT auto-synced** (dashboard has web-specific modifications)
539
+
540
+ **ElementRenderer Special Case:**
541
+
542
+ The dashboard copy of `ElementRenderer.tsx` has web-specific modifications for icon support:
543
+ - Uses `react-icons` instead of `@expo/vector-icons`
544
+ - Renders real icons in preview (Feather, Material, Ionicons, FontAwesome)
545
+ - Gradients fall back to solid colors
546
+
547
+ **If you modify ElementRenderer.tsx significantly:**
548
+ 1. Run `npm run sync:full` from project root to copy it
549
+ 2. Manually re-add web icon imports and logic (check git diff to see what changed)
550
+
551
+ #### Syncing Methods
552
+
553
+ **Manual sync (run when needed):**
554
+ ```bash
555
+ # From project root
556
+ npm run sync
557
+ ```
558
+
559
+ **Auto-sync during development:**
560
+ ```bash
561
+ # From project root
562
+ npm run sync:watch
563
+ ```
564
+
565
+ This watches SDK files and automatically copies changes to the dashboard when you save.
566
+
567
+ **Full development mode:**
568
+ ```bash
569
+ # From project root - starts dashboard + auto-sync
570
+ npm run dev
571
+ ```
572
+
573
+ This command:
574
+ 1. Syncs SDK files to dashboard
575
+ 2. Starts file watcher for auto-sync
576
+ 3. Starts dashboard dev server
577
+
578
+ #### Important Notes
579
+
580
+ - ⚠️ **Dashboard preview uses copies** - Changes to SDK files won't appear in dashboard preview until synced
581
+ - ✅ **Mobile app uses npm package** - TestApp uses the built SDK from `lib/`, requires `npm run build`
582
+ - 🔄 **Keep in sync** - Run `npm run sync:watch` while developing SDK to keep dashboard preview accurate
583
+
525
584
  ## Requirements
526
585
 
527
586
  - React Native >= 0.60.0
@@ -110,7 +110,10 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
110
110
  // Fetch configuration
111
111
  const configResponse = await api.getConfig();
112
112
  // Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
113
- const normalizedScreens = configResponse.config.screens.map(s => (Object.assign(Object.assign({}, s), { type: (s.type === 'custom_screen' && s.elements) ? 'noboard_screen' : s.type })));
113
+ const normalizedScreens = configResponse.config.screens
114
+ .map(s => (Object.assign(Object.assign({}, s), { type: (s.type === 'custom_screen' && s.elements) ? 'noboard_screen' : s.type })))
115
+ // Filter out hidden screens (dashboard show/hide feature)
116
+ .filter(s => !s.hidden);
114
117
  setScreens(normalizedScreens);
115
118
  setLoading(false);
116
119
  }
@@ -191,8 +194,19 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
191
194
  const currentScreen = screens[currentIndex];
192
195
  // Handle noboard_screen type — render with ElementRenderer
193
196
  if (currentScreen.type === 'noboard_screen' && currentScreen.elements) {
197
+ // Merge collectedData (from custom screens) + variables (from noboard screens)
198
+ // This allows noboard screens to reference custom screen data in templates like {height_cm}
199
+ const allVariables = Object.assign(Object.assign({}, collectedData), variables);
200
+ // Warn about conflicts (same key in both sources)
201
+ if (__DEV__) {
202
+ Object.keys(collectedData).forEach(key => {
203
+ if (variables[key] !== undefined) {
204
+ console.warn(`[Noboarding] Variable conflict: "${key}" exists in both custom screen data and noboard variables. Using noboard value.`);
205
+ }
206
+ });
207
+ }
194
208
  const handleElementNavigate = (destination) => {
195
- const resolved = (0, variableUtils_1.resolveDestination)(destination, variables);
209
+ const resolved = (0, variableUtils_1.resolveDestination)(destination, allVariables);
196
210
  if (!resolved)
197
211
  return;
198
212
  if (resolved === 'next') {
@@ -213,7 +227,7 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
213
227
  }
214
228
  };
215
229
  return (<react_native_1.View style={styles.container}>
216
- <ElementRenderer_1.ElementRenderer elements={currentScreen.elements} analytics={analyticsRef.current} screenId={currentScreen.id} onNavigate={handleElementNavigate} onDismiss={onSkip ? handleSkipAll : handleNext} variables={variables} onSetVariable={handleSetVariable}/>
230
+ <ElementRenderer_1.ElementRenderer elements={currentScreen.elements} analytics={analyticsRef.current} screenId={currentScreen.id} onNavigate={handleElementNavigate} onDismiss={onSkip ? handleSkipAll : handleNext} variables={allVariables} onSetVariable={handleSetVariable}/>
217
231
  </react_native_1.View>);
218
232
  }
219
233
  // Handle custom_screen type — developer-registered React Native components
@@ -48,36 +48,17 @@ exports.ElementRenderer = void 0;
48
48
  const react_1 = __importStar(require("react"));
49
49
  const react_native_1 = require("react-native");
50
50
  const variableUtils_1 = require("../variableUtils");
51
- // Try to import LinearGradient — optional peer dependency
52
- let LinearGradient = null;
53
- try {
54
- LinearGradient = require('expo-linear-gradient').LinearGradient;
55
- }
56
- catch (_a) {
57
- try {
58
- LinearGradient = require('react-native-linear-gradient').default;
59
- }
60
- catch (_b) {
61
- // Neither available — gradients will fall back to first color
62
- }
63
- }
64
- // Try to import vector icons — optional peer dependency
65
- let IconSets = {};
66
- try {
67
- const icons = require('@expo/vector-icons');
68
- IconSets = {
69
- lucide: icons.Feather, // Closest match to Lucide
70
- feather: icons.Feather,
71
- material: icons.MaterialIcons,
72
- 'material-community': icons.MaterialCommunityIcons,
73
- ionicons: icons.Ionicons,
74
- fontawesome: icons.FontAwesome,
75
- 'sf-symbols': icons.Ionicons, // Closest match to SF Symbols
76
- };
77
- }
78
- catch (_c) {
79
- // Not available — icons will fall back to text placeholder
80
- }
51
+ const expo_linear_gradient_1 = require("expo-linear-gradient");
52
+ const vector_icons_1 = require("@expo/vector-icons");
53
+ const IconSets = {
54
+ lucide: vector_icons_1.Feather,
55
+ feather: vector_icons_1.Feather,
56
+ material: vector_icons_1.MaterialIcons,
57
+ 'material-community': vector_icons_1.MaterialCommunityIcons,
58
+ ionicons: vector_icons_1.Ionicons,
59
+ fontawesome: vector_icons_1.FontAwesome,
60
+ 'sf-symbols': vector_icons_1.Ionicons,
61
+ };
81
62
  const ElementRenderer = ({ elements, analytics, screenId, onNavigate, onDismiss, variables = {}, onSetVariable, }) => {
82
63
  // Track toggled element IDs for toggle actions
83
64
  const [toggledIds, setToggledIds] = (0, react_1.useState)(new Set());
@@ -169,8 +150,8 @@ const ElementRenderer = ({ elements, analytics, screenId, onNavigate, onDismiss,
169
150
  </>);
170
151
  };
171
152
  exports.ElementRenderer = ElementRenderer;
172
- const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables }) => {
173
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
153
+ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables, onSetVariable }) => {
154
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6;
174
155
  // Variable-based conditions — hide element if condition is not met
175
156
  if ((_a = element.conditions) === null || _a === void 0 ? void 0 : _a.show_if) {
176
157
  const shouldShow = (0, variableUtils_1.evaluateCondition)(element.conditions.show_if, variables);
@@ -204,11 +185,18 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables
204
185
  const wrapWithAction = (content) => {
205
186
  if (!hasAction)
206
187
  return content;
207
- return (<react_native_1.TouchableOpacity key={element.id} activeOpacity={0.7} onPress={() => onAction(element)}>
188
+ // Extract width/alignment styles that should apply to TouchableOpacity wrapper
189
+ // This ensures buttons with width: "100%" don't shrink to content
190
+ const wrapperStyle = {};
191
+ if (style.width)
192
+ wrapperStyle.width = style.width;
193
+ if (style.alignSelf)
194
+ wrapperStyle.alignSelf = style.alignSelf;
195
+ return (<react_native_1.TouchableOpacity key={element.id} activeOpacity={0.7} onPress={() => onAction(element)} style={wrapperStyle}>
208
196
  {content}
209
197
  </react_native_1.TouchableOpacity>);
210
198
  };
211
- const childProps = { toggledIds, groupSelections, onAction, variables };
199
+ const childProps = { toggledIds, groupSelections, onAction, variables, onSetVariable };
212
200
  switch (element.type) {
213
201
  // ─── Containers ───
214
202
  case 'vstack': {
@@ -339,7 +327,7 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables
339
327
  {element.props.animationDescription}
340
328
  </react_native_1.Text>)}
341
329
  </react_native_1.View>);
342
- case 'input':
330
+ case 'input': {
343
331
  // Only apply default border if borderWidth is not explicitly defined (including 0)
344
332
  const inputStyle = style;
345
333
  const defaultInputStyle = {};
@@ -347,7 +335,15 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables
347
335
  defaultInputStyle.borderWidth = 1;
348
336
  defaultInputStyle.borderColor = '#E5E5E5';
349
337
  }
350
- return (<react_native_1.TextInput style={[defaultInputStyle, inputStyle]} placeholder={((_2 = element.props) === null || _2 === void 0 ? void 0 : _2.placeholder) || 'Enter text...'} keyboardType={getKeyboardType((_3 = element.props) === null || _3 === void 0 ? void 0 : _3.type)} secureTextEntry={((_4 = element.props) === null || _4 === void 0 ? void 0 : _4.type) === 'password'} autoCapitalize={((_5 = element.props) === null || _5 === void 0 ? void 0 : _5.type) === 'email' ? 'none' : 'sentences'}/>);
338
+ // Get the variable name - use props.variable if specified, otherwise use element.id
339
+ const variableName = ((_2 = element.props) === null || _2 === void 0 ? void 0 : _2.variable) || element.id;
340
+ const currentValue = variables[variableName] || '';
341
+ return (<react_native_1.TextInput style={[defaultInputStyle, inputStyle]} placeholder={((_3 = element.props) === null || _3 === void 0 ? void 0 : _3.placeholder) || 'Enter text...'} keyboardType={getKeyboardType((_4 = element.props) === null || _4 === void 0 ? void 0 : _4.type)} secureTextEntry={((_5 = element.props) === null || _5 === void 0 ? void 0 : _5.type) === 'password'} autoCapitalize={((_6 = element.props) === null || _6 === void 0 ? void 0 : _6.type) === 'email' ? 'none' : 'sentences'} value={currentValue} onChangeText={(text) => {
342
+ if (onSetVariable) {
343
+ onSetVariable(variableName, text);
344
+ }
345
+ }}/>);
346
+ }
351
347
  case 'spacer':
352
348
  return <react_native_1.View style={style || { flex: 1 }}/>;
353
349
  case 'divider':
@@ -462,7 +458,7 @@ function convertStyle(style) {
462
458
  rnStyle.textDecorationLine = style.textDecorationLine;
463
459
  // backgroundGradient is handled by wrapWithGradient at the component level.
464
460
  // If LinearGradient is not available, fall back to the first gradient color.
465
- if (style.backgroundGradient && !LinearGradient && ((_a = style.backgroundGradient.colors) === null || _a === void 0 ? void 0 : _a.length)) {
461
+ if (style.backgroundGradient && !expo_linear_gradient_1.LinearGradient && ((_a = style.backgroundGradient.colors) === null || _a === void 0 ? void 0 : _a.length)) {
466
462
  const firstColor = style.backgroundGradient.colors[0];
467
463
  rnStyle.backgroundColor = typeof firstColor === 'string' ? firstColor : firstColor.color;
468
464
  }
@@ -480,7 +476,7 @@ function angleToCoords(angle) {
480
476
  function wrapWithGradient(content, elementStyle, viewStyle) {
481
477
  var _a, _b;
482
478
  const gradient = elementStyle === null || elementStyle === void 0 ? void 0 : elementStyle.backgroundGradient;
483
- if (!gradient || !LinearGradient || !((_a = gradient.colors) === null || _a === void 0 ? void 0 : _a.length))
479
+ if (!gradient || !expo_linear_gradient_1.LinearGradient || !((_a = gradient.colors) === null || _a === void 0 ? void 0 : _a.length))
484
480
  return content;
485
481
  // Handle both { color, position } objects and plain color strings
486
482
  const colors = gradient.colors.map((c) => typeof c === 'string' ? c : c.color);
@@ -510,9 +506,9 @@ function wrapWithGradient(content, elementStyle, viewStyle) {
510
506
  else {
511
507
  coords = angleToCoords((_b = gradient.angle) !== null && _b !== void 0 ? _b : 180);
512
508
  }
513
- return (<LinearGradient colors={colors} locations={locations} start={coords.start} end={coords.end} style={gradientStyle}>
509
+ return (<expo_linear_gradient_1.LinearGradient colors={colors} locations={locations} start={coords.start} end={coords.end} style={gradientStyle}>
514
510
  {content.props.children}
515
- </LinearGradient>);
511
+ </expo_linear_gradient_1.LinearGradient>);
516
512
  }
517
513
  // ─── Helpers ───
518
514
  function getKeyboardType(type) {
package/lib/types.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface ScreenConfig {
6
6
  props: Record<string, any>;
7
7
  elements?: ElementNode[];
8
8
  custom_component_name?: string;
9
+ hidden?: boolean;
9
10
  }
10
11
  export type ElementType = 'vstack' | 'hstack' | 'zstack' | 'scrollview' | 'text' | 'image' | 'video' | 'lottie' | 'icon' | 'input' | 'spacer' | 'divider';
11
12
  export interface ElementNode {
package/package.json CHANGED
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "name": "noboarding",
3
- "version": "0.1.0-beta",
4
- "description": "React Native SDK for remote onboarding flow management",
3
+ "version": "1.0.2-beta",
4
+ "description": "Expo SDK for remote onboarding flow management",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/index.d.ts",
10
+ "default": "./lib/index.js"
11
+ },
12
+ "./src/*": "./src/*"
13
+ },
7
14
  "scripts": {
8
15
  "build": "tsc",
9
- "watch": "tsc --watch",
10
- "prepare": "npm run build"
16
+ "watch": "tsc --watch"
11
17
  },
12
18
  "keywords": [
13
19
  "react-native",
@@ -20,20 +26,12 @@
20
26
  "license": "MIT",
21
27
  "peerDependencies": {
22
28
  "react": ">=16.8.0",
23
- "react-native": ">=0.60.0",
24
- "expo-linear-gradient": ">=12.0.0",
25
- "@expo/vector-icons": ">=14.0.0"
26
- },
27
- "peerDependenciesMeta": {
28
- "expo-linear-gradient": {
29
- "optional": true
30
- },
31
- "@expo/vector-icons": {
32
- "optional": true
33
- }
29
+ "react-native": ">=0.60.0"
34
30
  },
35
31
  "dependencies": {
36
- "@react-native-async-storage/async-storage": "^1.19.0"
32
+ "@react-native-async-storage/async-storage": "^1.19.0",
33
+ "expo-linear-gradient": ">=12.0.0",
34
+ "@expo/vector-icons": ">=14.0.0"
37
35
  },
38
36
  "devDependencies": {
39
37
  "@types/node": "^25.2.3",
@@ -102,10 +102,13 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
102
102
  // Fetch configuration
103
103
  const configResponse = await api.getConfig();
104
104
  // Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
105
- const normalizedScreens = configResponse.config.screens.map(s => ({
106
- ...s,
107
- type: (s.type === ('custom_screen' as any) && s.elements) ? 'noboard_screen' as ScreenConfig['type'] : s.type,
108
- }));
105
+ const normalizedScreens = configResponse.config.screens
106
+ .map(s => ({
107
+ ...s,
108
+ type: (s.type === ('custom_screen' as any) && s.elements) ? 'noboard_screen' as ScreenConfig['type'] : s.type,
109
+ }))
110
+ // Filter out hidden screens (dashboard show/hide feature)
111
+ .filter(s => !s.hidden);
109
112
  setScreens(normalizedScreens);
110
113
 
111
114
  setLoading(false);
@@ -209,8 +212,23 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
209
212
 
210
213
  // Handle noboard_screen type — render with ElementRenderer
211
214
  if (currentScreen.type === 'noboard_screen' && currentScreen.elements) {
215
+ // Merge collectedData (from custom screens) + variables (from noboard screens)
216
+ // This allows noboard screens to reference custom screen data in templates like {height_cm}
217
+ const allVariables = { ...collectedData, ...variables };
218
+
219
+ // Warn about conflicts (same key in both sources)
220
+ if (__DEV__) {
221
+ Object.keys(collectedData).forEach(key => {
222
+ if (variables[key] !== undefined) {
223
+ console.warn(
224
+ `[Noboarding] Variable conflict: "${key}" exists in both custom screen data and noboard variables. Using noboard value.`
225
+ );
226
+ }
227
+ });
228
+ }
229
+
212
230
  const handleElementNavigate = (destination: string | ConditionalDestination | ConditionalRoutes) => {
213
- const resolved = resolveDestination(destination, variables);
231
+ const resolved = resolveDestination(destination, allVariables);
214
232
  if (!resolved) return;
215
233
 
216
234
  if (resolved === 'next') {
@@ -236,7 +254,7 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
236
254
  screenId={currentScreen.id}
237
255
  onNavigate={handleElementNavigate}
238
256
  onDismiss={onSkip ? handleSkipAll : handleNext}
239
- variables={variables}
257
+ variables={allVariables}
240
258
  onSetVariable={handleSetVariable}
241
259
  />
242
260
  </View>
@@ -14,35 +14,18 @@ import {
14
14
  } from 'react-native';
15
15
  import { ElementNode, ElementStyle, ElementAction, Analytics, ConditionalDestination, ConditionalRoutes } from '../types';
16
16
  import { resolveTemplate, evaluateCondition } from '../variableUtils';
17
-
18
- // Try to import LinearGradient optional peer dependency
19
- let LinearGradient: any = null;
20
- try {
21
- LinearGradient = require('expo-linear-gradient').LinearGradient;
22
- } catch {
23
- try {
24
- LinearGradient = require('react-native-linear-gradient').default;
25
- } catch {
26
- // Neither available — gradients will fall back to first color
27
- }
28
- }
29
-
30
- // Try to import vector icons — optional peer dependency
31
- let IconSets: Record<string, any> = {};
32
- try {
33
- const icons = require('@expo/vector-icons');
34
- IconSets = {
35
- lucide: icons.Feather, // Closest match to Lucide
36
- feather: icons.Feather,
37
- material: icons.MaterialIcons,
38
- 'material-community': icons.MaterialCommunityIcons,
39
- ionicons: icons.Ionicons,
40
- fontawesome: icons.FontAwesome,
41
- 'sf-symbols': icons.Ionicons, // Closest match to SF Symbols
42
- };
43
- } catch {
44
- // Not available — icons will fall back to text placeholder
45
- }
17
+ import { LinearGradient } from 'expo-linear-gradient';
18
+ import { Feather, MaterialIcons, MaterialCommunityIcons, Ionicons, FontAwesome } from '@expo/vector-icons';
19
+
20
+ const IconSets: Record<string, any> = {
21
+ lucide: Feather,
22
+ feather: Feather,
23
+ material: MaterialIcons,
24
+ 'material-community': MaterialCommunityIcons,
25
+ ionicons: Ionicons,
26
+ fontawesome: FontAwesome,
27
+ 'sf-symbols': Ionicons,
28
+ };
46
29
 
47
30
  interface ElementRendererProps {
48
31
  elements: ElementNode[];
@@ -179,9 +162,10 @@ interface RenderNodeProps {
179
162
  groupSelections: Record<string, string>;
180
163
  onAction: (element: ElementNode) => void;
181
164
  variables: Record<string, any>;
165
+ onSetVariable?: (name: string, value: any) => void;
182
166
  }
183
167
 
184
- const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelections, onAction, variables }) => {
168
+ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelections, onAction, variables, onSetVariable }) => {
185
169
  // Variable-based conditions — hide element if condition is not met
186
170
  if (element.conditions?.show_if) {
187
171
  const shouldShow = evaluateCondition(element.conditions.show_if, variables);
@@ -216,18 +200,26 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
216
200
  const hasAction = !!element.action || (element.actions && element.actions.length > 0);
217
201
  const wrapWithAction = (content: React.ReactElement): React.ReactElement => {
218
202
  if (!hasAction) return content;
203
+
204
+ // Extract width/alignment styles that should apply to TouchableOpacity wrapper
205
+ // This ensures buttons with width: "100%" don't shrink to content
206
+ const wrapperStyle: any = {};
207
+ if (style.width) wrapperStyle.width = style.width;
208
+ if (style.alignSelf) wrapperStyle.alignSelf = style.alignSelf;
209
+
219
210
  return (
220
211
  <TouchableOpacity
221
212
  key={element.id}
222
213
  activeOpacity={0.7}
223
214
  onPress={() => onAction(element)}
215
+ style={wrapperStyle}
224
216
  >
225
217
  {content}
226
218
  </TouchableOpacity>
227
219
  );
228
220
  };
229
221
 
230
- const childProps = { toggledIds, groupSelections, onAction, variables };
222
+ const childProps = { toggledIds, groupSelections, onAction, variables, onSetVariable };
231
223
 
232
224
  switch (element.type) {
233
225
  // ─── Containers ───
@@ -438,7 +430,7 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
438
430
  </View>
439
431
  );
440
432
 
441
- case 'input':
433
+ case 'input': {
442
434
  // Only apply default border if borderWidth is not explicitly defined (including 0)
443
435
  const inputStyle = style as TextStyle;
444
436
  const defaultInputStyle: TextStyle = {};
@@ -447,6 +439,10 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
447
439
  defaultInputStyle.borderColor = '#E5E5E5';
448
440
  }
449
441
 
442
+ // Get the variable name - use props.variable if specified, otherwise use element.id
443
+ const variableName = element.props?.variable || element.id;
444
+ const currentValue = variables[variableName] || '';
445
+
450
446
  return (
451
447
  <TextInput
452
448
  style={[defaultInputStyle, inputStyle]}
@@ -454,8 +450,15 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
454
450
  keyboardType={getKeyboardType(element.props?.type)}
455
451
  secureTextEntry={element.props?.type === 'password'}
456
452
  autoCapitalize={element.props?.type === 'email' ? 'none' : 'sentences'}
453
+ value={currentValue}
454
+ onChangeText={(text) => {
455
+ if (onSetVariable) {
456
+ onSetVariable(variableName, text);
457
+ }
458
+ }}
457
459
  />
458
460
  );
461
+ }
459
462
 
460
463
  case 'spacer':
461
464
  return <View style={style || { flex: 1 }} />;
package/src/types.ts CHANGED
@@ -12,6 +12,8 @@ export interface ScreenConfig {
12
12
  elements?: ElementNode[];
13
13
  // For custom_screen type — name of the developer-registered component
14
14
  custom_component_name?: string;
15
+ // Dashboard visibility control — if true, screen is hidden from onboarding flow
16
+ hidden?: boolean;
15
17
  }
16
18
 
17
19
  // ─── Element Tree Types (matches dashboard primitives) ───