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.
- package/AI_CUSTOM_SCREEN_GUIDE.md +23 -7
- package/README.md +59 -0
- package/lib/OnboardingFlow.js +17 -3
- package/lib/components/ElementRenderer.js +36 -40
- package/lib/types.d.ts +1 -0
- package/package.json +14 -16
- package/src/OnboardingFlow.tsx +24 -6
- package/src/components/ElementRenderer.tsx +35 -32
- package/src/types.ts +2 -0
|
@@ -237,7 +237,11 @@ export const SummaryScreen: React.FC<CustomScreenProps> = ({
|
|
|
237
237
|
|
|
238
238
|
```typescript
|
|
239
239
|
<OnboardingFlow
|
|
240
|
-
|
|
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
|
-
|
|
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.
|
|
597
|
-
2.
|
|
598
|
-
3.
|
|
599
|
-
4.
|
|
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
|
-
|
|
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
|
package/lib/OnboardingFlow.js
CHANGED
|
@@ -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
|
|
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,
|
|
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={
|
|
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
|
-
|
|
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);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
+
"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",
|
package/src/OnboardingFlow.tsx
CHANGED
|
@@ -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
|
|
106
|
-
|
|
107
|
-
|
|
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,
|
|
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={
|
|
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
|
-
|
|
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);
|
|
@@ -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) ───
|