noboarding 0.1.0-alpha → 1.0.1-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.
@@ -0,0 +1,888 @@
1
+ # AI Assistant Guide: Building Custom Screens for Noboarding SDK
2
+
3
+ > **Purpose:** Copy this entire guide and paste it to your AI coding assistant (Claude Code, Cursor, GitHub Copilot, etc.) when you need to build a custom screen for the Noboarding SDK.
4
+
5
+ ---
6
+
7
+ ## Instructions for AI Assistant
8
+
9
+ I need you to help me build a custom screen component for the Noboarding SDK. This guide explains how to structure the code, handle data flow between screens, and set up the screen in the dashboard.
10
+
11
+ ---
12
+
13
+ ## Context: How Custom Screens Work
14
+
15
+ Custom screens are React Native components that integrate into remotely-managed onboarding flows. They:
16
+ - Live in the app code (not on servers)
17
+ - Collect user data during onboarding
18
+ - Pass data to subsequent screens
19
+ - Can access data from previous screens
20
+ - Are positioned in flows via the dashboard
21
+
22
+ **Data Flow:**
23
+ ```
24
+ Screen 1 (SDK or Custom) → collects data → passes to Screen 2
25
+ Screen 2 (Custom) → receives data from Screen 1 → adds more data → passes to Screen 3
26
+ Screen 3 (SDK or Custom) → receives all previous data → continues...
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Part 1: Custom Screen Component Structure
32
+
33
+ ### Required Imports
34
+
35
+ ```typescript
36
+ import React, { useState, useEffect } from 'react';
37
+ import { View, Text, Button, StyleSheet } from 'react-native';
38
+ import type { CustomScreenProps } from 'noboarding';
39
+ ```
40
+
41
+ ### Component Props Interface
42
+
43
+ Every custom screen receives these props from the SDK:
44
+
45
+ ```typescript
46
+ interface CustomScreenProps {
47
+ // Analytics tracking
48
+ analytics: {
49
+ track: (event: string, properties?: Record<string, any>) => void;
50
+ };
51
+
52
+ // Navigation
53
+ onNext: () => void; // Move to next screen
54
+ onBack?: () => void; // Move to previous screen (undefined on first screen)
55
+ onSkip?: () => void; // Skip this screen (optional)
56
+
57
+ // Data flow
58
+ data?: Record<string, any>; // Data from PREVIOUS screens (read-only)
59
+ onDataUpdate?: (newData: Record<string, any>) => void; // Add YOUR data
60
+
61
+ // Preview mode
62
+ preview?: boolean; // True when rendering in dashboard
63
+ }
64
+ ```
65
+
66
+ ### Basic Component Template
67
+
68
+ ```typescript
69
+ export const MyCustomScreen: React.FC<CustomScreenProps> = ({
70
+ analytics,
71
+ onNext,
72
+ onSkip,
73
+ preview,
74
+ data, // Data from previous screens
75
+ onDataUpdate, // Function to add your data
76
+ }) => {
77
+ // 1. Local state for THIS screen's data
78
+ const [myData, setMyData] = useState({
79
+ // Initialize with default values
80
+ });
81
+
82
+ // 2. Track screen view on mount
83
+ useEffect(() => {
84
+ analytics.track('screen_viewed', {
85
+ screen_id: 'my_custom_screen',
86
+ screen_type: 'custom'
87
+ });
88
+ }, []);
89
+
90
+ // 3. Handle continue/next
91
+ const handleContinue = () => {
92
+ // Update collected data with THIS screen's data
93
+ onDataUpdate?.({
94
+ // Add your screen's data here
95
+ ...myData,
96
+ });
97
+
98
+ // Track completion
99
+ analytics.track('screen_completed', {
100
+ screen_id: 'my_custom_screen',
101
+ });
102
+
103
+ // Navigate to next screen
104
+ onNext();
105
+ };
106
+
107
+ // 4. Preview mode (for dashboard)
108
+ if (preview) {
109
+ return (
110
+ <View style={styles.previewContainer}>
111
+ <Text style={styles.previewText}>Preview: My Custom Screen</Text>
112
+ <Button title="Continue" onPress={onNext} />
113
+ </View>
114
+ );
115
+ }
116
+
117
+ // 5. Real implementation
118
+ return (
119
+ <View style={styles.container}>
120
+ {/* Access data from PREVIOUS screens */}
121
+ {data?.userName && (
122
+ <Text>Welcome back, {data.userName}!</Text>
123
+ )}
124
+
125
+ {/* Your screen UI here */}
126
+
127
+ <Button title="Continue" onPress={handleContinue} />
128
+ {onSkip && (
129
+ <Button title="Skip" onPress={onSkip} color="#666" />
130
+ )}
131
+ </View>
132
+ );
133
+ };
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Part 2: Data Flow Between Screens
139
+
140
+ ### Example: Multi-Screen Data Collection
141
+
142
+ **Screen 1: Name Collection (Custom)**
143
+
144
+ ```typescript
145
+ export const NameScreen: React.FC<CustomScreenProps> = ({
146
+ analytics,
147
+ onNext,
148
+ data,
149
+ onDataUpdate,
150
+ }) => {
151
+ const [name, setName] = useState('');
152
+
153
+ const handleContinue = () => {
154
+ // Add THIS screen's data
155
+ onDataUpdate?.({
156
+ userName: name,
157
+ nameCollectedAt: new Date().toISOString(),
158
+ });
159
+
160
+ analytics.track('name_collected', { name });
161
+ onNext();
162
+ };
163
+
164
+ return (
165
+ <View>
166
+ <TextInput
167
+ placeholder="Enter your name"
168
+ value={name}
169
+ onChangeText={setName}
170
+ />
171
+ <Button title="Continue" onPress={handleContinue} />
172
+ </View>
173
+ );
174
+ };
175
+ ```
176
+
177
+ **Screen 2: Age Collection (Custom)**
178
+
179
+ ```typescript
180
+ export const AgeScreen: React.FC<CustomScreenProps> = ({
181
+ analytics,
182
+ onNext,
183
+ data, // Contains: { userName, nameCollectedAt }
184
+ onDataUpdate,
185
+ }) => {
186
+ const [age, setAge] = useState('');
187
+
188
+ const handleContinue = () => {
189
+ // Add THIS screen's data
190
+ onDataUpdate?.({
191
+ userAge: parseInt(age),
192
+ ageCollectedAt: new Date().toISOString(),
193
+ });
194
+
195
+ analytics.track('age_collected', { age });
196
+ onNext();
197
+ };
198
+
199
+ return (
200
+ <View>
201
+ {/* Access data from previous screen */}
202
+ <Text>Hi {data?.userName}! What's your age?</Text>
203
+
204
+ <TextInput
205
+ placeholder="Enter your age"
206
+ value={age}
207
+ onChangeText={setAge}
208
+ keyboardType="numeric"
209
+ />
210
+ <Button title="Continue" onPress={handleContinue} />
211
+ </View>
212
+ );
213
+ };
214
+ ```
215
+
216
+ **Screen 3: Summary (Custom)**
217
+
218
+ ```typescript
219
+ export const SummaryScreen: React.FC<CustomScreenProps> = ({
220
+ analytics,
221
+ onNext,
222
+ data, // Contains: { userName, nameCollectedAt, userAge, ageCollectedAt }
223
+ }) => {
224
+ return (
225
+ <View>
226
+ <Text>Summary:</Text>
227
+ <Text>Name: {data?.userName}</Text>
228
+ <Text>Age: {data?.userAge}</Text>
229
+
230
+ <Button title="Confirm" onPress={onNext} />
231
+ </View>
232
+ );
233
+ };
234
+ ```
235
+
236
+ **Final onComplete Handler (in App.tsx)**
237
+
238
+ ```typescript
239
+ <OnboardingFlow
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
+
245
+ customComponents={{
246
+ NameScreen,
247
+ AgeScreen,
248
+ SummaryScreen,
249
+ }}
250
+ onComplete={(userData) => {
251
+ // userData contains ALL collected data:
252
+ console.log(userData);
253
+ // {
254
+ // userName: "John",
255
+ // nameCollectedAt: "2025-02-20...",
256
+ // userAge: 25,
257
+ // ageCollectedAt: "2025-02-20...",
258
+ // _variables: { ... }
259
+ // }
260
+ }}
261
+ />
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Part 3: Common Patterns
267
+
268
+ ### Pattern 1: Form with Validation
269
+
270
+ ```typescript
271
+ export const FormScreen: React.FC<CustomScreenProps> = ({
272
+ analytics,
273
+ onNext,
274
+ onDataUpdate,
275
+ }) => {
276
+ const [formData, setFormData] = useState({
277
+ email: '',
278
+ phone: '',
279
+ });
280
+ const [errors, setErrors] = useState<Record<string, string>>({});
281
+
282
+ const validate = () => {
283
+ const newErrors: Record<string, string> = {};
284
+
285
+ if (!formData.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
286
+ newErrors.email = 'Invalid email';
287
+ }
288
+
289
+ if (!formData.phone.match(/^\d{10}$/)) {
290
+ newErrors.phone = 'Phone must be 10 digits';
291
+ }
292
+
293
+ setErrors(newErrors);
294
+ return Object.keys(newErrors).length === 0;
295
+ };
296
+
297
+ const handleSubmit = () => {
298
+ if (validate()) {
299
+ onDataUpdate?.(formData);
300
+ analytics.track('form_submitted', { valid: true });
301
+ onNext();
302
+ } else {
303
+ analytics.track('form_validation_failed', { errors: Object.keys(errors) });
304
+ }
305
+ };
306
+
307
+ return (
308
+ <View>
309
+ <TextInput
310
+ placeholder="Email"
311
+ value={formData.email}
312
+ onChangeText={(email) => setFormData({ ...formData, email })}
313
+ />
314
+ {errors.email && <Text style={styles.error}>{errors.email}</Text>}
315
+
316
+ <TextInput
317
+ placeholder="Phone"
318
+ value={formData.phone}
319
+ onChangeText={(phone) => setFormData({ ...formData, phone })}
320
+ />
321
+ {errors.phone && <Text style={styles.error}>{errors.phone}</Text>}
322
+
323
+ <Button title="Continue" onPress={handleSubmit} />
324
+ </View>
325
+ );
326
+ };
327
+ ```
328
+
329
+ ### Pattern 2: Multi-Select Options
330
+
331
+ ```typescript
332
+ export const PreferencesScreen: React.FC<CustomScreenProps> = ({
333
+ analytics,
334
+ onNext,
335
+ data,
336
+ onDataUpdate,
337
+ }) => {
338
+ const [selectedPreferences, setSelectedPreferences] = useState<string[]>([]);
339
+
340
+ const options = ['Option A', 'Option B', 'Option C', 'Option D'];
341
+
342
+ const togglePreference = (option: string) => {
343
+ if (selectedPreferences.includes(option)) {
344
+ setSelectedPreferences(selectedPreferences.filter(p => p !== option));
345
+ } else {
346
+ setSelectedPreferences([...selectedPreferences, option]);
347
+ }
348
+ };
349
+
350
+ const handleContinue = () => {
351
+ onDataUpdate?.({
352
+ preferences: selectedPreferences,
353
+ preferencesCount: selectedPreferences.length,
354
+ });
355
+
356
+ analytics.track('preferences_selected', {
357
+ count: selectedPreferences.length,
358
+ selections: selectedPreferences,
359
+ });
360
+
361
+ onNext();
362
+ };
363
+
364
+ return (
365
+ <View>
366
+ <Text>Select your preferences:</Text>
367
+
368
+ {options.map(option => (
369
+ <TouchableOpacity
370
+ key={option}
371
+ onPress={() => togglePreference(option)}
372
+ style={[
373
+ styles.option,
374
+ selectedPreferences.includes(option) && styles.optionSelected
375
+ ]}
376
+ >
377
+ <Text>{option}</Text>
378
+ {selectedPreferences.includes(option) && <Text>✓</Text>}
379
+ </TouchableOpacity>
380
+ ))}
381
+
382
+ <Button
383
+ title="Continue"
384
+ onPress={handleContinue}
385
+ disabled={selectedPreferences.length === 0}
386
+ />
387
+ </View>
388
+ );
389
+ };
390
+ ```
391
+
392
+ ### Pattern 3: API Call with Data from Previous Screen
393
+
394
+ ```typescript
395
+ export const ProfileSetupScreen: React.FC<CustomScreenProps> = ({
396
+ analytics,
397
+ onNext,
398
+ data, // Data from previous screens
399
+ onDataUpdate,
400
+ }) => {
401
+ const [loading, setLoading] = useState(false);
402
+
403
+ const createProfile = async () => {
404
+ setLoading(true);
405
+ analytics.track('profile_creation_started');
406
+
407
+ try {
408
+ // Use data from previous screens
409
+ const response = await fetch('https://your-api.com/profile', {
410
+ method: 'POST',
411
+ headers: { 'Content-Type': 'application/json' },
412
+ body: JSON.stringify({
413
+ name: data?.userName,
414
+ age: data?.userAge,
415
+ email: data?.email,
416
+ preferences: data?.preferences,
417
+ }),
418
+ });
419
+
420
+ const result = await response.json();
421
+
422
+ // Add API response to collected data
423
+ onDataUpdate?.({
424
+ profileId: result.id,
425
+ profileCreated: true,
426
+ profileCreatedAt: new Date().toISOString(),
427
+ });
428
+
429
+ analytics.track('profile_created', { profileId: result.id });
430
+ onNext();
431
+ } catch (error: any) {
432
+ analytics.track('profile_creation_failed', { error: error.message });
433
+ Alert.alert('Error', 'Failed to create profile. Please try again.');
434
+ } finally {
435
+ setLoading(false);
436
+ }
437
+ };
438
+
439
+ return (
440
+ <View>
441
+ <Text>Creating profile for {data?.userName}...</Text>
442
+ <Button
443
+ title="Create Profile"
444
+ onPress={createProfile}
445
+ disabled={loading}
446
+ />
447
+ {loading && <ActivityIndicator />}
448
+ </View>
449
+ );
450
+ };
451
+ ```
452
+
453
+ ### Pattern 4: Conditional Logic Based on Previous Data
454
+
455
+ ```typescript
456
+ export const ConditionalScreen: React.FC<CustomScreenProps> = ({
457
+ analytics,
458
+ onNext,
459
+ data,
460
+ onDataUpdate,
461
+ }) => {
462
+ // Show different UI based on previous data
463
+ const isPremiumUser = data?.userAge && data.userAge > 25;
464
+
465
+ if (isPremiumUser) {
466
+ return (
467
+ <View>
468
+ <Text>Premium Experience</Text>
469
+ <Text>Welcome {data?.userName}! You qualify for premium features.</Text>
470
+ <Button
471
+ title="Continue"
472
+ onPress={() => {
473
+ onDataUpdate?.({ userTier: 'premium' });
474
+ onNext();
475
+ }}
476
+ />
477
+ </View>
478
+ );
479
+ }
480
+
481
+ return (
482
+ <View>
483
+ <Text>Standard Experience</Text>
484
+ <Text>Welcome {data?.userName}!</Text>
485
+ <Button
486
+ title="Continue"
487
+ onPress={() => {
488
+ onDataUpdate?.({ userTier: 'standard' });
489
+ onNext();
490
+ }}
491
+ />
492
+ </View>
493
+ );
494
+ };
495
+ ```
496
+
497
+ ---
498
+
499
+ ## Part 4: Analytics Best Practices
500
+
501
+ ### Track All Key Events
502
+
503
+ ```typescript
504
+ // Screen lifecycle
505
+ useEffect(() => {
506
+ analytics.track('screen_viewed', {
507
+ screen_id: 'my_screen',
508
+ screen_type: 'custom',
509
+ });
510
+
511
+ return () => {
512
+ analytics.track('screen_exited', {
513
+ screen_id: 'my_screen',
514
+ });
515
+ };
516
+ }, []);
517
+
518
+ // User interactions
519
+ analytics.track('button_clicked', { button: 'submit' });
520
+ analytics.track('input_focused', { field: 'email' });
521
+ analytics.track('option_selected', { option: 'premium' });
522
+
523
+ // Completion
524
+ analytics.track('screen_completed', {
525
+ screen_id: 'my_screen',
526
+ data_collected: true,
527
+ });
528
+
529
+ // Errors
530
+ analytics.track('error_occurred', {
531
+ error_type: 'validation',
532
+ error_message: 'Invalid email',
533
+ });
534
+ ```
535
+
536
+ ---
537
+
538
+ ## Part 5: Setting Up Custom Screens in Dashboard
539
+
540
+ ### Step 1: Register Component in App
541
+
542
+ ```typescript
543
+ // App.tsx
544
+ import { OnboardingFlow } from 'noboarding';
545
+ import { NameScreen } from './screens/NameScreen';
546
+ import { AgeScreen } from './screens/AgeScreen';
547
+ import { PreferencesScreen } from './screens/PreferencesScreen';
548
+
549
+ <OnboardingFlow
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
+
556
+ customComponents={{
557
+ NameScreen: NameScreen, // Component name MUST match exactly
558
+ AgeScreen: AgeScreen, // Case-sensitive!
559
+ PreferencesScreen: PreferencesScreen,
560
+ }}
561
+ onComplete={(userData) => {
562
+ console.log('All collected data:', userData);
563
+ }}
564
+ />
565
+ ```
566
+
567
+ ### Step 2: Add to Dashboard Flow
568
+
569
+ 1. **Log in to Noboarding Dashboard**
570
+ 2. **Go to Flows** and select or create a flow
571
+ 3. **Click "Add Custom Screen"**
572
+ 4. **Enter Component Name:** (must match EXACTLY what you registered)
573
+ - Example: `NameScreen`
574
+ - ❌ Wrong: `nameScreen`, `name_screen`, `NameScreenComponent`
575
+ - ✅ Correct: `NameScreen`
576
+ 5. **Add Description:** "Collects user's name"
577
+ 6. **Position in Flow:** Drag to desired position
578
+ 7. **Save Draft**
579
+
580
+ ### Step 3: Flow Order Example
581
+
582
+ ```
583
+ Dashboard Flow Builder:
584
+ ┌─────────────────────────────────────┐
585
+ │ 1. Welcome Screen (SDK) │
586
+ │ 2. NameScreen (Custom) │ ← Your custom screen
587
+ │ 3. AgeScreen (Custom) │ ← Your custom screen
588
+ │ 4. Feature Tour (SDK) │
589
+ │ 5. PreferencesScreen (Custom) │ ← Your custom screen
590
+ │ 6. Complete (SDK) │
591
+ └─────────────────────────────────────┘
592
+ ```
593
+
594
+ ### Step 4: Test Locally
595
+
596
+ ```bash
597
+ npm start
598
+ # Test in development mode
599
+ # Navigate through onboarding
600
+ # Check console for collected data
601
+ ```
602
+
603
+ ### Step 5: Deploy & Publish
604
+
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.
613
+
614
+ ---
615
+
616
+ ## Part 6: Complete Example - Multi-Step Form
617
+
618
+ Here's a complete example showing data flow across 3 custom screens:
619
+
620
+ ```typescript
621
+ // screens/Step1EmailScreen.tsx
622
+ export const Step1EmailScreen: React.FC<CustomScreenProps> = ({
623
+ analytics,
624
+ onNext,
625
+ preview,
626
+ onDataUpdate,
627
+ }) => {
628
+ const [email, setEmail] = useState('');
629
+
630
+ useEffect(() => {
631
+ analytics.track('screen_viewed', { screen_id: 'step1_email' });
632
+ }, []);
633
+
634
+ const handleContinue = () => {
635
+ onDataUpdate?.({
636
+ email: email,
637
+ emailCollectedAt: new Date().toISOString(),
638
+ });
639
+ analytics.track('email_collected');
640
+ onNext();
641
+ };
642
+
643
+ if (preview) {
644
+ return (
645
+ <View style={styles.preview}>
646
+ <Text>📧 Email Collection Screen</Text>
647
+ <Button title="Continue" onPress={onNext} />
648
+ </View>
649
+ );
650
+ }
651
+
652
+ return (
653
+ <View style={styles.container}>
654
+ <Text style={styles.title}>What's your email?</Text>
655
+ <TextInput
656
+ style={styles.input}
657
+ placeholder="email@example.com"
658
+ value={email}
659
+ onChangeText={setEmail}
660
+ keyboardType="email-address"
661
+ autoCapitalize="none"
662
+ />
663
+ <Button
664
+ title="Continue"
665
+ onPress={handleContinue}
666
+ disabled={!email}
667
+ />
668
+ </View>
669
+ );
670
+ };
671
+
672
+ // screens/Step2GoalsScreen.tsx
673
+ export const Step2GoalsScreen: React.FC<CustomScreenProps> = ({
674
+ analytics,
675
+ onNext,
676
+ data, // Contains: { email, emailCollectedAt }
677
+ preview,
678
+ onDataUpdate,
679
+ }) => {
680
+ const [selectedGoals, setSelectedGoals] = useState<string[]>([]);
681
+
682
+ useEffect(() => {
683
+ analytics.track('screen_viewed', {
684
+ screen_id: 'step2_goals',
685
+ user_email: data?.email
686
+ });
687
+ }, []);
688
+
689
+ const goals = ['Fitness', 'Nutrition', 'Sleep', 'Mindfulness'];
690
+
691
+ const toggleGoal = (goal: string) => {
692
+ if (selectedGoals.includes(goal)) {
693
+ setSelectedGoals(selectedGoals.filter(g => g !== goal));
694
+ } else {
695
+ setSelectedGoals([...selectedGoals, goal]);
696
+ }
697
+ };
698
+
699
+ const handleContinue = () => {
700
+ onDataUpdate?.({
701
+ goals: selectedGoals,
702
+ goalsCount: selectedGoals.length,
703
+ });
704
+ analytics.track('goals_selected', { count: selectedGoals.length });
705
+ onNext();
706
+ };
707
+
708
+ if (preview) {
709
+ return (
710
+ <View style={styles.preview}>
711
+ <Text>🎯 Goals Selection Screen</Text>
712
+ <Button title="Continue" onPress={onNext} />
713
+ </View>
714
+ );
715
+ }
716
+
717
+ return (
718
+ <View style={styles.container}>
719
+ <Text style={styles.title}>
720
+ Hi {data?.email?.split('@')[0]}! What are your goals?
721
+ </Text>
722
+
723
+ {goals.map(goal => (
724
+ <TouchableOpacity
725
+ key={goal}
726
+ onPress={() => toggleGoal(goal)}
727
+ style={[
728
+ styles.goalOption,
729
+ selectedGoals.includes(goal) && styles.goalSelected
730
+ ]}
731
+ >
732
+ <Text>{goal}</Text>
733
+ {selectedGoals.includes(goal) && <Text>✓</Text>}
734
+ </TouchableOpacity>
735
+ ))}
736
+
737
+ <Button
738
+ title="Continue"
739
+ onPress={handleContinue}
740
+ disabled={selectedGoals.length === 0}
741
+ />
742
+ </View>
743
+ );
744
+ };
745
+
746
+ // screens/Step3SummaryScreen.tsx
747
+ export const Step3SummaryScreen: React.FC<CustomScreenProps> = ({
748
+ analytics,
749
+ onNext,
750
+ data, // Contains: { email, emailCollectedAt, goals, goalsCount }
751
+ preview,
752
+ }) => {
753
+ useEffect(() => {
754
+ analytics.track('screen_viewed', { screen_id: 'step3_summary' });
755
+ }, []);
756
+
757
+ if (preview) {
758
+ return (
759
+ <View style={styles.preview}>
760
+ <Text>📋 Summary Screen</Text>
761
+ <Button title="Continue" onPress={onNext} />
762
+ </View>
763
+ );
764
+ }
765
+
766
+ return (
767
+ <View style={styles.container}>
768
+ <Text style={styles.title}>Summary</Text>
769
+
770
+ <View style={styles.summaryCard}>
771
+ <Text style={styles.label}>Email:</Text>
772
+ <Text style={styles.value}>{data?.email}</Text>
773
+ </View>
774
+
775
+ <View style={styles.summaryCard}>
776
+ <Text style={styles.label}>Goals ({data?.goalsCount}):</Text>
777
+ {data?.goals?.map((goal: string) => (
778
+ <Text key={goal} style={styles.value}>• {goal}</Text>
779
+ ))}
780
+ </View>
781
+
782
+ <Button title="Complete Setup" onPress={onNext} />
783
+ </View>
784
+ );
785
+ };
786
+
787
+ const styles = StyleSheet.create({
788
+ container: {
789
+ flex: 1,
790
+ padding: 20,
791
+ backgroundColor: '#fff',
792
+ },
793
+ preview: {
794
+ flex: 1,
795
+ justifyContent: 'center',
796
+ alignItems: 'center',
797
+ padding: 20,
798
+ },
799
+ title: {
800
+ fontSize: 24,
801
+ fontWeight: 'bold',
802
+ marginBottom: 20,
803
+ },
804
+ input: {
805
+ borderWidth: 1,
806
+ borderColor: '#ccc',
807
+ borderRadius: 8,
808
+ padding: 12,
809
+ fontSize: 16,
810
+ marginBottom: 20,
811
+ },
812
+ goalOption: {
813
+ flexDirection: 'row',
814
+ justifyContent: 'space-between',
815
+ padding: 16,
816
+ borderWidth: 1,
817
+ borderColor: '#ccc',
818
+ borderRadius: 8,
819
+ marginBottom: 12,
820
+ },
821
+ goalSelected: {
822
+ borderColor: '#007AFF',
823
+ backgroundColor: '#E3F2FD',
824
+ },
825
+ summaryCard: {
826
+ padding: 16,
827
+ backgroundColor: '#f5f5f5',
828
+ borderRadius: 8,
829
+ marginBottom: 16,
830
+ },
831
+ label: {
832
+ fontSize: 14,
833
+ color: '#666',
834
+ marginBottom: 4,
835
+ },
836
+ value: {
837
+ fontSize: 16,
838
+ color: '#000',
839
+ },
840
+ });
841
+ ```
842
+
843
+ **Register in App.tsx:**
844
+
845
+ ```typescript
846
+ <OnboardingFlow
847
+ // Dual API keys - SDK auto-detects environment
848
+ testKey="nb_test_..."
849
+ productionKey="nb_live_..."
850
+
851
+ customComponents={{
852
+ Step1EmailScreen,
853
+ Step2GoalsScreen,
854
+ Step3SummaryScreen,
855
+ }}
856
+ onComplete={(userData) => {
857
+ console.log(userData);
858
+ // {
859
+ // email: "john@example.com",
860
+ // emailCollectedAt: "2025-02-20...",
861
+ // goals: ["Fitness", "Nutrition"],
862
+ // goalsCount: 2,
863
+ // _variables: { ... }
864
+ // }
865
+ }}
866
+ />
867
+ ```
868
+
869
+ ---
870
+
871
+ ## Summary Checklist
872
+
873
+ When building a custom screen, ensure you:
874
+
875
+ - [ ] Import `CustomScreenProps` from 'noboarding'
876
+ - [ ] Use all required props: `analytics`, `onNext`, `data`, `onDataUpdate`
877
+ - [ ] Track `screen_viewed` on mount
878
+ - [ ] Implement `preview` mode for dashboard
879
+ - [ ] Call `onDataUpdate()` to add your data before `onNext()`
880
+ - [ ] Track `screen_completed` before navigating
881
+ - [ ] Access previous data via `data` prop
882
+ - [ ] Register component in `customComponents` with EXACT name
883
+ - [ ] Add to dashboard with matching component name
884
+ - [ ] Test data flow across multiple screens
885
+
886
+ ---
887
+
888
+ **Now you're ready to build custom screens!** Ask me any questions if you need clarification on any part of this guide.
package/README.md CHANGED
@@ -14,6 +14,7 @@ yarn add noboarding
14
14
  - **[AI Setup](./SETUP_GUIDE.md#ai-setup)** - Copy/paste instructions for your AI coding assistant (Claude Code, Cursor, etc.)
15
15
  - **[Manual Setup](./SETUP_GUIDE.md#normal-setup)** - Step-by-step instructions
16
16
  - **[RevenueCat Integration](./REVENUECAT_SETUP.md)** - Detailed RevenueCat paywall guide
17
+ - **[AI Custom Screen Guide](./AI_CUSTOM_SCREEN_GUIDE.md)** - Complete guide for building custom screens with data flow (for AI assistants)
17
18
 
18
19
  ## Quick Start
19
20
 
@@ -26,7 +27,14 @@ function App() {
26
27
  if (showOnboarding) {
27
28
  return (
28
29
  <OnboardingFlow
29
- apiKey="sk_live_your_api_key_here"
30
+ // Recommended: Use dual keys for automatic environment detection
31
+ testKey="nb_test_your_test_key_here"
32
+ productionKey="nb_live_your_production_key_here"
33
+ // The SDK automatically uses testKey in __DEV__ and productionKey in production
34
+
35
+ // Alternative: Legacy single key (still supported)
36
+ // apiKey="nb_test_your_api_key_here"
37
+
30
38
  onComplete={(userData) => {
31
39
  console.log('Collected data:', userData);
32
40
  setShowOnboarding(false);
@@ -47,6 +55,14 @@ function App() {
47
55
  }
48
56
  ```
49
57
 
58
+ ### API Keys
59
+
60
+ You'll find two API keys in your dashboard:
61
+ - **Test Key** (`nb_test_...`) - Used for development and testing
62
+ - **Production Key** (`nb_live_...`) - Used for production builds
63
+
64
+ The SDK automatically detects your environment using React Native's `__DEV__` flag and uses the appropriate key.
65
+
50
66
  ## How It Works
51
67
 
52
68
  1. The SDK fetches your onboarding configuration from Supabase at runtime
@@ -240,6 +256,7 @@ interface CustomScreenProps {
240
256
  track: (event: string, properties?: Record<string, any>) => void;
241
257
  };
242
258
  onNext: () => void;
259
+ onBack?: () => void; // Navigate to previous screen (undefined on first screen)
243
260
  onSkip?: () => void;
244
261
  preview?: boolean; // True when rendering in dashboard preview
245
262
  data?: Record<string, any>; // Previously collected user data
@@ -426,7 +443,7 @@ useEffect(() => {
426
443
  - ✅ Handle errors gracefully with user-friendly messages
427
444
  - ✅ Call `onNext()` when screen is complete
428
445
 
429
- For more details, see [Custom Screens Guide](./cusomte_screens.md).
446
+ For more details, see [AI Custom Screen Guide](./AI_CUSTOM_SCREEN_GUIDE.md).
430
447
 
431
448
  ## API
432
449
 
@@ -496,6 +513,8 @@ import { API, AnalyticsManager } from 'noboarding';
496
513
 
497
514
  ## Development
498
515
 
516
+ ### Building the SDK
517
+
499
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:
500
519
 
501
520
  ```bash
@@ -505,6 +524,63 @@ npm run build
505
524
 
506
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.
507
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
+
508
584
  ## Requirements
509
585
 
510
586
  - React Native >= 0.60.0
@@ -47,7 +47,31 @@ const generateUserId = () => {
47
47
  const generateSessionId = () => {
48
48
  return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
49
49
  };
50
- const OnboardingFlow = ({ apiKey, onComplete, onSkip, baseUrl, initialVariables, customComponents, onUserIdGenerated, }) => {
50
+ // Detect environment: true in dev/Expo, false in production builds
51
+ const detectEnvironment = () => {
52
+ if (__DEV__)
53
+ return 'test';
54
+ return 'production';
55
+ };
56
+ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, baseUrl, initialVariables, customComponents, onUserIdGenerated, }) => {
57
+ // Determine which API key to use
58
+ const getApiKey = () => {
59
+ // If dual keys provided, use environment detection
60
+ if (testKey && productionKey) {
61
+ const env = detectEnvironment();
62
+ return env === 'test' ? testKey : productionKey;
63
+ }
64
+ // If only one dual key provided, use it
65
+ if (testKey)
66
+ return testKey;
67
+ if (productionKey)
68
+ return productionKey;
69
+ // Fallback to legacy single key
70
+ if (apiKey)
71
+ return apiKey;
72
+ throw new Error('Noboarding SDK: No API key provided. Please provide either apiKey, or both testKey and productionKey.');
73
+ };
74
+ const activeApiKey = getApiKey();
51
75
  const [loading, setLoading] = (0, react_1.useState)(true);
52
76
  const [error, setError] = (0, react_1.useState)(null);
53
77
  const [screens, setScreens] = (0, react_1.useState)([]);
@@ -75,8 +99,8 @@ const OnboardingFlow = ({ apiKey, onComplete, onSkip, baseUrl, initialVariables,
75
99
  try {
76
100
  setLoading(true);
77
101
  setError(null);
78
- // Initialize API client
79
- const api = new api_1.API(apiKey, baseUrl);
102
+ // Initialize API client with detected API key
103
+ const api = new api_1.API(activeApiKey, baseUrl);
80
104
  apiRef.current = api;
81
105
  // Initialize analytics
82
106
  const analytics = new analytics_1.AnalyticsManager(api, userIdRef.current, sessionIdRef.current);
@@ -86,7 +110,10 @@ const OnboardingFlow = ({ apiKey, onComplete, onSkip, baseUrl, initialVariables,
86
110
  // Fetch configuration
87
111
  const configResponse = await api.getConfig();
88
112
  // Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
89
- 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);
90
117
  setScreens(normalizedScreens);
91
118
  setLoading(false);
92
119
  }
@@ -109,6 +136,12 @@ const OnboardingFlow = ({ apiKey, onComplete, onSkip, baseUrl, initialVariables,
109
136
  setCurrentIndex((prev) => prev + 1);
110
137
  }
111
138
  };
139
+ const handleBack = () => {
140
+ // Navigate to previous screen (only if not on first screen)
141
+ if (currentIndex > 0) {
142
+ setCurrentIndex((prev) => prev - 1);
143
+ }
144
+ };
112
145
  const handleSkipScreen = () => {
113
146
  // Move to next screen or complete
114
147
  if (currentIndex >= screens.length - 1) {
@@ -201,7 +234,7 @@ const OnboardingFlow = ({ apiKey, onComplete, onSkip, baseUrl, initialVariables,
201
234
  </react_native_1.View>);
202
235
  }
203
236
  return (<react_native_1.View style={styles.container}>
204
- <CustomComponent analytics={analyticsRef.current} onNext={() => handleNext()} onSkip={onSkip ? handleSkipAll : undefined} data={collectedData} onDataUpdate={(newData) => setCollectedData(prev => (Object.assign(Object.assign({}, prev), newData)))}/>
237
+ <CustomComponent analytics={analyticsRef.current} onNext={() => handleNext()} onBack={currentIndex > 0 ? handleBack : undefined} onSkip={onSkip ? handleSkipAll : undefined} data={collectedData} onDataUpdate={(newData) => setCollectedData(prev => (Object.assign(Object.assign({}, prev), newData)))}/>
205
238
  </react_native_1.View>);
206
239
  }
207
240
  // Unknown screen type fallback
@@ -170,7 +170,7 @@ const ElementRenderer = ({ elements, analytics, screenId, onNavigate, onDismiss,
170
170
  };
171
171
  exports.ElementRenderer = ElementRenderer;
172
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;
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;
174
174
  // Variable-based conditions — hide element if condition is not met
175
175
  if ((_a = element.conditions) === null || _a === void 0 ? void 0 : _a.show_if) {
176
176
  const shouldShow = (0, variableUtils_1.evaluateCondition)(element.conditions.show_if, variables);
@@ -204,7 +204,14 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables
204
204
  const wrapWithAction = (content) => {
205
205
  if (!hasAction)
206
206
  return content;
207
- return (<react_native_1.TouchableOpacity key={element.id} activeOpacity={0.7} onPress={() => onAction(element)}>
207
+ // Extract width/alignment styles that should apply to TouchableOpacity wrapper
208
+ // This ensures buttons with width: "100%" don't shrink to content
209
+ const wrapperStyle = {};
210
+ if (style.width)
211
+ wrapperStyle.width = style.width;
212
+ if (style.alignSelf)
213
+ wrapperStyle.alignSelf = style.alignSelf;
214
+ return (<react_native_1.TouchableOpacity key={element.id} activeOpacity={0.7} onPress={() => onAction(element)} style={wrapperStyle}>
208
215
  {content}
209
216
  </react_native_1.TouchableOpacity>);
210
217
  };
@@ -340,7 +347,14 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables
340
347
  </react_native_1.Text>)}
341
348
  </react_native_1.View>);
342
349
  case 'input':
343
- return (<react_native_1.TextInput style={[style, { borderWidth: 1, borderColor: '#E5E5E5' }]} placeholder={((_0 = element.props) === null || _0 === void 0 ? void 0 : _0.placeholder) || 'Enter text...'} keyboardType={getKeyboardType((_1 = element.props) === null || _1 === void 0 ? void 0 : _1.type)} secureTextEntry={((_2 = element.props) === null || _2 === void 0 ? void 0 : _2.type) === 'password'} autoCapitalize={((_3 = element.props) === null || _3 === void 0 ? void 0 : _3.type) === 'email' ? 'none' : 'sentences'}/>);
350
+ // Only apply default border if borderWidth is not explicitly defined (including 0)
351
+ const inputStyle = style;
352
+ const defaultInputStyle = {};
353
+ if (((_0 = element.style) === null || _0 === void 0 ? void 0 : _0.borderWidth) === undefined && ((_1 = element.style) === null || _1 === void 0 ? void 0 : _1.borderColor) === undefined) {
354
+ defaultInputStyle.borderWidth = 1;
355
+ defaultInputStyle.borderColor = '#E5E5E5';
356
+ }
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'}/>);
344
358
  case 'spacer':
345
359
  return <react_native_1.View style={style || { flex: 1 }}/>;
346
360
  case 'divider':
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 {
@@ -169,13 +170,16 @@ export interface CustomScreenProps {
169
170
  track: (event: string, properties?: Record<string, any>) => void;
170
171
  };
171
172
  onNext: () => void;
173
+ onBack?: () => void;
172
174
  onSkip?: () => void;
173
175
  preview?: boolean;
174
176
  data?: Record<string, any>;
175
177
  onDataUpdate?: (data: Record<string, any>) => void;
176
178
  }
177
179
  export interface OnboardingFlowProps {
178
- apiKey: string;
180
+ testKey?: string;
181
+ productionKey?: string;
182
+ apiKey?: string;
179
183
  onComplete: (data?: Record<string, any>) => void;
180
184
  onSkip?: () => void;
181
185
  baseUrl?: string;
package/package.json CHANGED
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "name": "noboarding",
3
- "version": "0.1.0-alpha",
3
+ "version": "1.0.1-beta",
4
4
  "description": "React Native 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",
@@ -15,8 +15,16 @@ const generateSessionId = (): string => {
15
15
  return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
16
16
  };
17
17
 
18
+ // Detect environment: true in dev/Expo, false in production builds
19
+ const detectEnvironment = (): 'test' | 'production' => {
20
+ if (__DEV__) return 'test';
21
+ return 'production';
22
+ };
23
+
18
24
  export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
19
25
  apiKey,
26
+ testKey,
27
+ productionKey,
20
28
  onComplete,
21
29
  onSkip,
22
30
  baseUrl,
@@ -24,6 +32,25 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
24
32
  customComponents,
25
33
  onUserIdGenerated,
26
34
  }) => {
35
+ // Determine which API key to use
36
+ const getApiKey = (): string => {
37
+ // If dual keys provided, use environment detection
38
+ if (testKey && productionKey) {
39
+ const env = detectEnvironment();
40
+ return env === 'test' ? testKey : productionKey;
41
+ }
42
+
43
+ // If only one dual key provided, use it
44
+ if (testKey) return testKey;
45
+ if (productionKey) return productionKey;
46
+
47
+ // Fallback to legacy single key
48
+ if (apiKey) return apiKey;
49
+
50
+ throw new Error('Noboarding SDK: No API key provided. Please provide either apiKey, or both testKey and productionKey.');
51
+ };
52
+
53
+ const activeApiKey = getApiKey();
27
54
  const [loading, setLoading] = useState(true);
28
55
  const [error, setError] = useState<string | null>(null);
29
56
  const [screens, setScreens] = useState<ScreenConfig[]>([]);
@@ -57,8 +84,8 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
57
84
  setLoading(true);
58
85
  setError(null);
59
86
 
60
- // Initialize API client
61
- const api = new API(apiKey, baseUrl);
87
+ // Initialize API client with detected API key
88
+ const api = new API(activeApiKey, baseUrl);
62
89
  apiRef.current = api;
63
90
 
64
91
  // Initialize analytics
@@ -75,10 +102,13 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
75
102
  // Fetch configuration
76
103
  const configResponse = await api.getConfig();
77
104
  // Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
78
- const normalizedScreens = configResponse.config.screens.map(s => ({
79
- ...s,
80
- type: (s.type === ('custom_screen' as any) && s.elements) ? 'noboard_screen' as ScreenConfig['type'] : s.type,
81
- }));
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);
82
112
  setScreens(normalizedScreens);
83
113
 
84
114
  setLoading(false);
@@ -103,6 +133,13 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
103
133
  }
104
134
  };
105
135
 
136
+ const handleBack = () => {
137
+ // Navigate to previous screen (only if not on first screen)
138
+ if (currentIndex > 0) {
139
+ setCurrentIndex((prev) => prev - 1);
140
+ }
141
+ };
142
+
106
143
  const handleSkipScreen = () => {
107
144
  // Move to next screen or complete
108
145
  if (currentIndex >= screens.length - 1) {
@@ -235,6 +272,7 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
235
272
  <CustomComponent
236
273
  analytics={analyticsRef.current!}
237
274
  onNext={() => handleNext()}
275
+ onBack={currentIndex > 0 ? handleBack : undefined}
238
276
  onSkip={onSkip ? handleSkipAll : undefined}
239
277
  data={collectedData}
240
278
  onDataUpdate={(newData) => setCollectedData(prev => ({ ...prev, ...newData }))}
@@ -216,11 +216,19 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
216
216
  const hasAction = !!element.action || (element.actions && element.actions.length > 0);
217
217
  const wrapWithAction = (content: React.ReactElement): React.ReactElement => {
218
218
  if (!hasAction) return content;
219
+
220
+ // Extract width/alignment styles that should apply to TouchableOpacity wrapper
221
+ // This ensures buttons with width: "100%" don't shrink to content
222
+ const wrapperStyle: any = {};
223
+ if (style.width) wrapperStyle.width = style.width;
224
+ if (style.alignSelf) wrapperStyle.alignSelf = style.alignSelf;
225
+
219
226
  return (
220
227
  <TouchableOpacity
221
228
  key={element.id}
222
229
  activeOpacity={0.7}
223
230
  onPress={() => onAction(element)}
231
+ style={wrapperStyle}
224
232
  >
225
233
  {content}
226
234
  </TouchableOpacity>
@@ -439,9 +447,17 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
439
447
  );
440
448
 
441
449
  case 'input':
450
+ // Only apply default border if borderWidth is not explicitly defined (including 0)
451
+ const inputStyle = style as TextStyle;
452
+ const defaultInputStyle: TextStyle = {};
453
+ if (element.style?.borderWidth === undefined && element.style?.borderColor === undefined) {
454
+ defaultInputStyle.borderWidth = 1;
455
+ defaultInputStyle.borderColor = '#E5E5E5';
456
+ }
457
+
442
458
  return (
443
459
  <TextInput
444
- style={[style as TextStyle, { borderWidth: 1, borderColor: '#E5E5E5' }]}
460
+ style={[defaultInputStyle, inputStyle]}
445
461
  placeholder={element.props?.placeholder || 'Enter text...'}
446
462
  keyboardType={getKeyboardType(element.props?.type)}
447
463
  secureTextEntry={element.props?.type === 'password'}
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) ───
@@ -224,6 +226,7 @@ export interface CustomScreenProps {
224
226
  track: (event: string, properties?: Record<string, any>) => void;
225
227
  };
226
228
  onNext: () => void;
229
+ onBack?: () => void;
227
230
  onSkip?: () => void;
228
231
  preview?: boolean;
229
232
  data?: Record<string, any>;
@@ -232,7 +235,13 @@ export interface CustomScreenProps {
232
235
 
233
236
  // Main SDK props
234
237
  export interface OnboardingFlowProps {
235
- apiKey: string;
238
+ // Option 1: Auto-detection with dual keys (recommended)
239
+ testKey?: string; // nb_test_... key for development/testing
240
+ productionKey?: string; // nb_live_... key for production
241
+
242
+ // Option 2: Legacy single key (backwards compatible)
243
+ apiKey?: string;
244
+
236
245
  onComplete: (data?: Record<string, any>) => void;
237
246
  onSkip?: () => void;
238
247
  baseUrl?: string;