noboarding 1.0.1-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.
@@ -194,8 +194,19 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
194
194
  const currentScreen = screens[currentIndex];
195
195
  // Handle noboard_screen type — render with ElementRenderer
196
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
+ }
197
208
  const handleElementNavigate = (destination) => {
198
- const resolved = (0, variableUtils_1.resolveDestination)(destination, variables);
209
+ const resolved = (0, variableUtils_1.resolveDestination)(destination, allVariables);
199
210
  if (!resolved)
200
211
  return;
201
212
  if (resolved === 'next') {
@@ -216,7 +227,7 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
216
227
  }
217
228
  };
218
229
  return (<react_native_1.View style={styles.container}>
219
- <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}/>
220
231
  </react_native_1.View>);
221
232
  }
222
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);
@@ -215,7 +196,7 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables
215
196
  {content}
216
197
  </react_native_1.TouchableOpacity>);
217
198
  };
218
- const childProps = { toggledIds, groupSelections, onAction, variables };
199
+ const childProps = { toggledIds, groupSelections, onAction, variables, onSetVariable };
219
200
  switch (element.type) {
220
201
  // ─── Containers ───
221
202
  case 'vstack': {
@@ -346,7 +327,7 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables
346
327
  {element.props.animationDescription}
347
328
  </react_native_1.Text>)}
348
329
  </react_native_1.View>);
349
- case 'input':
330
+ case 'input': {
350
331
  // Only apply default border if borderWidth is not explicitly defined (including 0)
351
332
  const inputStyle = style;
352
333
  const defaultInputStyle = {};
@@ -354,7 +335,15 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables
354
335
  defaultInputStyle.borderWidth = 1;
355
336
  defaultInputStyle.borderColor = '#E5E5E5';
356
337
  }
357
- 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
+ }
358
347
  case 'spacer':
359
348
  return <react_native_1.View style={style || { flex: 1 }}/>;
360
349
  case 'divider':
@@ -469,7 +458,7 @@ function convertStyle(style) {
469
458
  rnStyle.textDecorationLine = style.textDecorationLine;
470
459
  // backgroundGradient is handled by wrapWithGradient at the component level.
471
460
  // If LinearGradient is not available, fall back to the first gradient color.
472
- 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)) {
473
462
  const firstColor = style.backgroundGradient.colors[0];
474
463
  rnStyle.backgroundColor = typeof firstColor === 'string' ? firstColor : firstColor.color;
475
464
  }
@@ -487,7 +476,7 @@ function angleToCoords(angle) {
487
476
  function wrapWithGradient(content, elementStyle, viewStyle) {
488
477
  var _a, _b;
489
478
  const gradient = elementStyle === null || elementStyle === void 0 ? void 0 : elementStyle.backgroundGradient;
490
- 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))
491
480
  return content;
492
481
  // Handle both { color, position } objects and plain color strings
493
482
  const colors = gradient.colors.map((c) => typeof c === 'string' ? c : c.color);
@@ -517,9 +506,9 @@ function wrapWithGradient(content, elementStyle, viewStyle) {
517
506
  else {
518
507
  coords = angleToCoords((_b = gradient.angle) !== null && _b !== void 0 ? _b : 180);
519
508
  }
520
- 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}>
521
510
  {content.props.children}
522
- </LinearGradient>);
511
+ </expo_linear_gradient_1.LinearGradient>);
523
512
  }
524
513
  // ─── Helpers ───
525
514
  function getKeyboardType(type) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "noboarding",
3
- "version": "1.0.1-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
7
  "exports": {
@@ -26,20 +26,12 @@
26
26
  "license": "MIT",
27
27
  "peerDependencies": {
28
28
  "react": ">=16.8.0",
29
- "react-native": ">=0.60.0",
30
- "expo-linear-gradient": ">=12.0.0",
31
- "@expo/vector-icons": ">=14.0.0"
32
- },
33
- "peerDependenciesMeta": {
34
- "expo-linear-gradient": {
35
- "optional": true
36
- },
37
- "@expo/vector-icons": {
38
- "optional": true
39
- }
29
+ "react-native": ">=0.60.0"
40
30
  },
41
31
  "dependencies": {
42
- "@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"
43
35
  },
44
36
  "devDependencies": {
45
37
  "@types/node": "^25.2.3",
@@ -212,8 +212,23 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
212
212
 
213
213
  // Handle noboard_screen type — render with ElementRenderer
214
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
+
215
230
  const handleElementNavigate = (destination: string | ConditionalDestination | ConditionalRoutes) => {
216
- const resolved = resolveDestination(destination, variables);
231
+ const resolved = resolveDestination(destination, allVariables);
217
232
  if (!resolved) return;
218
233
 
219
234
  if (resolved === 'next') {
@@ -239,7 +254,7 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
239
254
  screenId={currentScreen.id}
240
255
  onNavigate={handleElementNavigate}
241
256
  onDismiss={onSkip ? handleSkipAll : handleNext}
242
- variables={variables}
257
+ variables={allVariables}
243
258
  onSetVariable={handleSetVariable}
244
259
  />
245
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);
@@ -235,7 +219,7 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
235
219
  );
236
220
  };
237
221
 
238
- const childProps = { toggledIds, groupSelections, onAction, variables };
222
+ const childProps = { toggledIds, groupSelections, onAction, variables, onSetVariable };
239
223
 
240
224
  switch (element.type) {
241
225
  // ─── Containers ───
@@ -446,7 +430,7 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
446
430
  </View>
447
431
  );
448
432
 
449
- case 'input':
433
+ case 'input': {
450
434
  // Only apply default border if borderWidth is not explicitly defined (including 0)
451
435
  const inputStyle = style as TextStyle;
452
436
  const defaultInputStyle: TextStyle = {};
@@ -455,6 +439,10 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
455
439
  defaultInputStyle.borderColor = '#E5E5E5';
456
440
  }
457
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
+
458
446
  return (
459
447
  <TextInput
460
448
  style={[defaultInputStyle, inputStyle]}
@@ -462,8 +450,15 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
462
450
  keyboardType={getKeyboardType(element.props?.type)}
463
451
  secureTextEntry={element.props?.type === 'password'}
464
452
  autoCapitalize={element.props?.type === 'email' ? 'none' : 'sentences'}
453
+ value={currentValue}
454
+ onChangeText={(text) => {
455
+ if (onSetVariable) {
456
+ onSetVariable(variableName, text);
457
+ }
458
+ }}
465
459
  />
466
460
  );
461
+ }
467
462
 
468
463
  case 'spacer':
469
464
  return <View style={style || { flex: 1 }} />;