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.
- package/lib/OnboardingFlow.js +13 -2
- package/lib/components/ElementRenderer.js +28 -39
- package/package.json +6 -14
- package/src/OnboardingFlow.tsx +17 -2
- package/src/components/ElementRenderer.tsx +27 -32
package/lib/OnboardingFlow.js
CHANGED
|
@@ -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,
|
|
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={
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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",
|
package/src/OnboardingFlow.tsx
CHANGED
|
@@ -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,
|
|
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={
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 }} />;
|