noboarding 0.1.0-alpha → 0.1.0-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,872 @@
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
+ apiKey="sk_live_..."
241
+ customComponents={{
242
+ NameScreen,
243
+ AgeScreen,
244
+ SummaryScreen,
245
+ }}
246
+ onComplete={(userData) => {
247
+ // userData contains ALL collected data:
248
+ console.log(userData);
249
+ // {
250
+ // userName: "John",
251
+ // nameCollectedAt: "2025-02-20...",
252
+ // userAge: 25,
253
+ // ageCollectedAt: "2025-02-20...",
254
+ // _variables: { ... }
255
+ // }
256
+ }}
257
+ />
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Part 3: Common Patterns
263
+
264
+ ### Pattern 1: Form with Validation
265
+
266
+ ```typescript
267
+ export const FormScreen: React.FC<CustomScreenProps> = ({
268
+ analytics,
269
+ onNext,
270
+ onDataUpdate,
271
+ }) => {
272
+ const [formData, setFormData] = useState({
273
+ email: '',
274
+ phone: '',
275
+ });
276
+ const [errors, setErrors] = useState<Record<string, string>>({});
277
+
278
+ const validate = () => {
279
+ const newErrors: Record<string, string> = {};
280
+
281
+ if (!formData.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
282
+ newErrors.email = 'Invalid email';
283
+ }
284
+
285
+ if (!formData.phone.match(/^\d{10}$/)) {
286
+ newErrors.phone = 'Phone must be 10 digits';
287
+ }
288
+
289
+ setErrors(newErrors);
290
+ return Object.keys(newErrors).length === 0;
291
+ };
292
+
293
+ const handleSubmit = () => {
294
+ if (validate()) {
295
+ onDataUpdate?.(formData);
296
+ analytics.track('form_submitted', { valid: true });
297
+ onNext();
298
+ } else {
299
+ analytics.track('form_validation_failed', { errors: Object.keys(errors) });
300
+ }
301
+ };
302
+
303
+ return (
304
+ <View>
305
+ <TextInput
306
+ placeholder="Email"
307
+ value={formData.email}
308
+ onChangeText={(email) => setFormData({ ...formData, email })}
309
+ />
310
+ {errors.email && <Text style={styles.error}>{errors.email}</Text>}
311
+
312
+ <TextInput
313
+ placeholder="Phone"
314
+ value={formData.phone}
315
+ onChangeText={(phone) => setFormData({ ...formData, phone })}
316
+ />
317
+ {errors.phone && <Text style={styles.error}>{errors.phone}</Text>}
318
+
319
+ <Button title="Continue" onPress={handleSubmit} />
320
+ </View>
321
+ );
322
+ };
323
+ ```
324
+
325
+ ### Pattern 2: Multi-Select Options
326
+
327
+ ```typescript
328
+ export const PreferencesScreen: React.FC<CustomScreenProps> = ({
329
+ analytics,
330
+ onNext,
331
+ data,
332
+ onDataUpdate,
333
+ }) => {
334
+ const [selectedPreferences, setSelectedPreferences] = useState<string[]>([]);
335
+
336
+ const options = ['Option A', 'Option B', 'Option C', 'Option D'];
337
+
338
+ const togglePreference = (option: string) => {
339
+ if (selectedPreferences.includes(option)) {
340
+ setSelectedPreferences(selectedPreferences.filter(p => p !== option));
341
+ } else {
342
+ setSelectedPreferences([...selectedPreferences, option]);
343
+ }
344
+ };
345
+
346
+ const handleContinue = () => {
347
+ onDataUpdate?.({
348
+ preferences: selectedPreferences,
349
+ preferencesCount: selectedPreferences.length,
350
+ });
351
+
352
+ analytics.track('preferences_selected', {
353
+ count: selectedPreferences.length,
354
+ selections: selectedPreferences,
355
+ });
356
+
357
+ onNext();
358
+ };
359
+
360
+ return (
361
+ <View>
362
+ <Text>Select your preferences:</Text>
363
+
364
+ {options.map(option => (
365
+ <TouchableOpacity
366
+ key={option}
367
+ onPress={() => togglePreference(option)}
368
+ style={[
369
+ styles.option,
370
+ selectedPreferences.includes(option) && styles.optionSelected
371
+ ]}
372
+ >
373
+ <Text>{option}</Text>
374
+ {selectedPreferences.includes(option) && <Text>✓</Text>}
375
+ </TouchableOpacity>
376
+ ))}
377
+
378
+ <Button
379
+ title="Continue"
380
+ onPress={handleContinue}
381
+ disabled={selectedPreferences.length === 0}
382
+ />
383
+ </View>
384
+ );
385
+ };
386
+ ```
387
+
388
+ ### Pattern 3: API Call with Data from Previous Screen
389
+
390
+ ```typescript
391
+ export const ProfileSetupScreen: React.FC<CustomScreenProps> = ({
392
+ analytics,
393
+ onNext,
394
+ data, // Data from previous screens
395
+ onDataUpdate,
396
+ }) => {
397
+ const [loading, setLoading] = useState(false);
398
+
399
+ const createProfile = async () => {
400
+ setLoading(true);
401
+ analytics.track('profile_creation_started');
402
+
403
+ try {
404
+ // Use data from previous screens
405
+ const response = await fetch('https://your-api.com/profile', {
406
+ method: 'POST',
407
+ headers: { 'Content-Type': 'application/json' },
408
+ body: JSON.stringify({
409
+ name: data?.userName,
410
+ age: data?.userAge,
411
+ email: data?.email,
412
+ preferences: data?.preferences,
413
+ }),
414
+ });
415
+
416
+ const result = await response.json();
417
+
418
+ // Add API response to collected data
419
+ onDataUpdate?.({
420
+ profileId: result.id,
421
+ profileCreated: true,
422
+ profileCreatedAt: new Date().toISOString(),
423
+ });
424
+
425
+ analytics.track('profile_created', { profileId: result.id });
426
+ onNext();
427
+ } catch (error: any) {
428
+ analytics.track('profile_creation_failed', { error: error.message });
429
+ Alert.alert('Error', 'Failed to create profile. Please try again.');
430
+ } finally {
431
+ setLoading(false);
432
+ }
433
+ };
434
+
435
+ return (
436
+ <View>
437
+ <Text>Creating profile for {data?.userName}...</Text>
438
+ <Button
439
+ title="Create Profile"
440
+ onPress={createProfile}
441
+ disabled={loading}
442
+ />
443
+ {loading && <ActivityIndicator />}
444
+ </View>
445
+ );
446
+ };
447
+ ```
448
+
449
+ ### Pattern 4: Conditional Logic Based on Previous Data
450
+
451
+ ```typescript
452
+ export const ConditionalScreen: React.FC<CustomScreenProps> = ({
453
+ analytics,
454
+ onNext,
455
+ data,
456
+ onDataUpdate,
457
+ }) => {
458
+ // Show different UI based on previous data
459
+ const isPremiumUser = data?.userAge && data.userAge > 25;
460
+
461
+ if (isPremiumUser) {
462
+ return (
463
+ <View>
464
+ <Text>Premium Experience</Text>
465
+ <Text>Welcome {data?.userName}! You qualify for premium features.</Text>
466
+ <Button
467
+ title="Continue"
468
+ onPress={() => {
469
+ onDataUpdate?.({ userTier: 'premium' });
470
+ onNext();
471
+ }}
472
+ />
473
+ </View>
474
+ );
475
+ }
476
+
477
+ return (
478
+ <View>
479
+ <Text>Standard Experience</Text>
480
+ <Text>Welcome {data?.userName}!</Text>
481
+ <Button
482
+ title="Continue"
483
+ onPress={() => {
484
+ onDataUpdate?.({ userTier: 'standard' });
485
+ onNext();
486
+ }}
487
+ />
488
+ </View>
489
+ );
490
+ };
491
+ ```
492
+
493
+ ---
494
+
495
+ ## Part 4: Analytics Best Practices
496
+
497
+ ### Track All Key Events
498
+
499
+ ```typescript
500
+ // Screen lifecycle
501
+ useEffect(() => {
502
+ analytics.track('screen_viewed', {
503
+ screen_id: 'my_screen',
504
+ screen_type: 'custom',
505
+ });
506
+
507
+ return () => {
508
+ analytics.track('screen_exited', {
509
+ screen_id: 'my_screen',
510
+ });
511
+ };
512
+ }, []);
513
+
514
+ // User interactions
515
+ analytics.track('button_clicked', { button: 'submit' });
516
+ analytics.track('input_focused', { field: 'email' });
517
+ analytics.track('option_selected', { option: 'premium' });
518
+
519
+ // Completion
520
+ analytics.track('screen_completed', {
521
+ screen_id: 'my_screen',
522
+ data_collected: true,
523
+ });
524
+
525
+ // Errors
526
+ analytics.track('error_occurred', {
527
+ error_type: 'validation',
528
+ error_message: 'Invalid email',
529
+ });
530
+ ```
531
+
532
+ ---
533
+
534
+ ## Part 5: Setting Up Custom Screens in Dashboard
535
+
536
+ ### Step 1: Register Component in App
537
+
538
+ ```typescript
539
+ // App.tsx
540
+ import { OnboardingFlow } from 'noboarding';
541
+ import { NameScreen } from './screens/NameScreen';
542
+ import { AgeScreen } from './screens/AgeScreen';
543
+ import { PreferencesScreen } from './screens/PreferencesScreen';
544
+
545
+ <OnboardingFlow
546
+ apiKey="sk_live_your_api_key"
547
+ customComponents={{
548
+ NameScreen: NameScreen, // Component name MUST match exactly
549
+ AgeScreen: AgeScreen, // Case-sensitive!
550
+ PreferencesScreen: PreferencesScreen,
551
+ }}
552
+ onComplete={(userData) => {
553
+ console.log('All collected data:', userData);
554
+ }}
555
+ />
556
+ ```
557
+
558
+ ### Step 2: Add to Dashboard Flow
559
+
560
+ 1. **Log in to Noboarding Dashboard**
561
+ 2. **Go to Flows** and select or create a flow
562
+ 3. **Click "Add Custom Screen"**
563
+ 4. **Enter Component Name:** (must match EXACTLY what you registered)
564
+ - Example: `NameScreen`
565
+ - ❌ Wrong: `nameScreen`, `name_screen`, `NameScreenComponent`
566
+ - ✅ Correct: `NameScreen`
567
+ 5. **Add Description:** "Collects user's name"
568
+ 6. **Position in Flow:** Drag to desired position
569
+ 7. **Save Draft**
570
+
571
+ ### Step 3: Flow Order Example
572
+
573
+ ```
574
+ Dashboard Flow Builder:
575
+ ┌─────────────────────────────────────┐
576
+ │ 1. Welcome Screen (SDK) │
577
+ │ 2. NameScreen (Custom) │ ← Your custom screen
578
+ │ 3. AgeScreen (Custom) │ ← Your custom screen
579
+ │ 4. Feature Tour (SDK) │
580
+ │ 5. PreferencesScreen (Custom) │ ← Your custom screen
581
+ │ 6. Complete (SDK) │
582
+ └─────────────────────────────────────┘
583
+ ```
584
+
585
+ ### Step 4: Test Locally
586
+
587
+ ```bash
588
+ npm start
589
+ # Test in development mode
590
+ # Navigate through onboarding
591
+ # Check console for collected data
592
+ ```
593
+
594
+ ### Step 5: Deploy & Publish
595
+
596
+ 1. Build production app
597
+ 2. Submit to App Store / Google Play
598
+ 3. **WAIT for approval**
599
+ 4. **After app is live:** Publish flow in dashboard
600
+
601
+ ---
602
+
603
+ ## Part 6: Complete Example - Multi-Step Form
604
+
605
+ Here's a complete example showing data flow across 3 custom screens:
606
+
607
+ ```typescript
608
+ // screens/Step1EmailScreen.tsx
609
+ export const Step1EmailScreen: React.FC<CustomScreenProps> = ({
610
+ analytics,
611
+ onNext,
612
+ preview,
613
+ onDataUpdate,
614
+ }) => {
615
+ const [email, setEmail] = useState('');
616
+
617
+ useEffect(() => {
618
+ analytics.track('screen_viewed', { screen_id: 'step1_email' });
619
+ }, []);
620
+
621
+ const handleContinue = () => {
622
+ onDataUpdate?.({
623
+ email: email,
624
+ emailCollectedAt: new Date().toISOString(),
625
+ });
626
+ analytics.track('email_collected');
627
+ onNext();
628
+ };
629
+
630
+ if (preview) {
631
+ return (
632
+ <View style={styles.preview}>
633
+ <Text>📧 Email Collection Screen</Text>
634
+ <Button title="Continue" onPress={onNext} />
635
+ </View>
636
+ );
637
+ }
638
+
639
+ return (
640
+ <View style={styles.container}>
641
+ <Text style={styles.title}>What's your email?</Text>
642
+ <TextInput
643
+ style={styles.input}
644
+ placeholder="email@example.com"
645
+ value={email}
646
+ onChangeText={setEmail}
647
+ keyboardType="email-address"
648
+ autoCapitalize="none"
649
+ />
650
+ <Button
651
+ title="Continue"
652
+ onPress={handleContinue}
653
+ disabled={!email}
654
+ />
655
+ </View>
656
+ );
657
+ };
658
+
659
+ // screens/Step2GoalsScreen.tsx
660
+ export const Step2GoalsScreen: React.FC<CustomScreenProps> = ({
661
+ analytics,
662
+ onNext,
663
+ data, // Contains: { email, emailCollectedAt }
664
+ preview,
665
+ onDataUpdate,
666
+ }) => {
667
+ const [selectedGoals, setSelectedGoals] = useState<string[]>([]);
668
+
669
+ useEffect(() => {
670
+ analytics.track('screen_viewed', {
671
+ screen_id: 'step2_goals',
672
+ user_email: data?.email
673
+ });
674
+ }, []);
675
+
676
+ const goals = ['Fitness', 'Nutrition', 'Sleep', 'Mindfulness'];
677
+
678
+ const toggleGoal = (goal: string) => {
679
+ if (selectedGoals.includes(goal)) {
680
+ setSelectedGoals(selectedGoals.filter(g => g !== goal));
681
+ } else {
682
+ setSelectedGoals([...selectedGoals, goal]);
683
+ }
684
+ };
685
+
686
+ const handleContinue = () => {
687
+ onDataUpdate?.({
688
+ goals: selectedGoals,
689
+ goalsCount: selectedGoals.length,
690
+ });
691
+ analytics.track('goals_selected', { count: selectedGoals.length });
692
+ onNext();
693
+ };
694
+
695
+ if (preview) {
696
+ return (
697
+ <View style={styles.preview}>
698
+ <Text>🎯 Goals Selection Screen</Text>
699
+ <Button title="Continue" onPress={onNext} />
700
+ </View>
701
+ );
702
+ }
703
+
704
+ return (
705
+ <View style={styles.container}>
706
+ <Text style={styles.title}>
707
+ Hi {data?.email?.split('@')[0]}! What are your goals?
708
+ </Text>
709
+
710
+ {goals.map(goal => (
711
+ <TouchableOpacity
712
+ key={goal}
713
+ onPress={() => toggleGoal(goal)}
714
+ style={[
715
+ styles.goalOption,
716
+ selectedGoals.includes(goal) && styles.goalSelected
717
+ ]}
718
+ >
719
+ <Text>{goal}</Text>
720
+ {selectedGoals.includes(goal) && <Text>✓</Text>}
721
+ </TouchableOpacity>
722
+ ))}
723
+
724
+ <Button
725
+ title="Continue"
726
+ onPress={handleContinue}
727
+ disabled={selectedGoals.length === 0}
728
+ />
729
+ </View>
730
+ );
731
+ };
732
+
733
+ // screens/Step3SummaryScreen.tsx
734
+ export const Step3SummaryScreen: React.FC<CustomScreenProps> = ({
735
+ analytics,
736
+ onNext,
737
+ data, // Contains: { email, emailCollectedAt, goals, goalsCount }
738
+ preview,
739
+ }) => {
740
+ useEffect(() => {
741
+ analytics.track('screen_viewed', { screen_id: 'step3_summary' });
742
+ }, []);
743
+
744
+ if (preview) {
745
+ return (
746
+ <View style={styles.preview}>
747
+ <Text>📋 Summary Screen</Text>
748
+ <Button title="Continue" onPress={onNext} />
749
+ </View>
750
+ );
751
+ }
752
+
753
+ return (
754
+ <View style={styles.container}>
755
+ <Text style={styles.title}>Summary</Text>
756
+
757
+ <View style={styles.summaryCard}>
758
+ <Text style={styles.label}>Email:</Text>
759
+ <Text style={styles.value}>{data?.email}</Text>
760
+ </View>
761
+
762
+ <View style={styles.summaryCard}>
763
+ <Text style={styles.label}>Goals ({data?.goalsCount}):</Text>
764
+ {data?.goals?.map((goal: string) => (
765
+ <Text key={goal} style={styles.value}>• {goal}</Text>
766
+ ))}
767
+ </View>
768
+
769
+ <Button title="Complete Setup" onPress={onNext} />
770
+ </View>
771
+ );
772
+ };
773
+
774
+ const styles = StyleSheet.create({
775
+ container: {
776
+ flex: 1,
777
+ padding: 20,
778
+ backgroundColor: '#fff',
779
+ },
780
+ preview: {
781
+ flex: 1,
782
+ justifyContent: 'center',
783
+ alignItems: 'center',
784
+ padding: 20,
785
+ },
786
+ title: {
787
+ fontSize: 24,
788
+ fontWeight: 'bold',
789
+ marginBottom: 20,
790
+ },
791
+ input: {
792
+ borderWidth: 1,
793
+ borderColor: '#ccc',
794
+ borderRadius: 8,
795
+ padding: 12,
796
+ fontSize: 16,
797
+ marginBottom: 20,
798
+ },
799
+ goalOption: {
800
+ flexDirection: 'row',
801
+ justifyContent: 'space-between',
802
+ padding: 16,
803
+ borderWidth: 1,
804
+ borderColor: '#ccc',
805
+ borderRadius: 8,
806
+ marginBottom: 12,
807
+ },
808
+ goalSelected: {
809
+ borderColor: '#007AFF',
810
+ backgroundColor: '#E3F2FD',
811
+ },
812
+ summaryCard: {
813
+ padding: 16,
814
+ backgroundColor: '#f5f5f5',
815
+ borderRadius: 8,
816
+ marginBottom: 16,
817
+ },
818
+ label: {
819
+ fontSize: 14,
820
+ color: '#666',
821
+ marginBottom: 4,
822
+ },
823
+ value: {
824
+ fontSize: 16,
825
+ color: '#000',
826
+ },
827
+ });
828
+ ```
829
+
830
+ **Register in App.tsx:**
831
+
832
+ ```typescript
833
+ <OnboardingFlow
834
+ apiKey="sk_live_..."
835
+ customComponents={{
836
+ Step1EmailScreen,
837
+ Step2GoalsScreen,
838
+ Step3SummaryScreen,
839
+ }}
840
+ onComplete={(userData) => {
841
+ console.log(userData);
842
+ // {
843
+ // email: "john@example.com",
844
+ // emailCollectedAt: "2025-02-20...",
845
+ // goals: ["Fitness", "Nutrition"],
846
+ // goalsCount: 2,
847
+ // _variables: { ... }
848
+ // }
849
+ }}
850
+ />
851
+ ```
852
+
853
+ ---
854
+
855
+ ## Summary Checklist
856
+
857
+ When building a custom screen, ensure you:
858
+
859
+ - [ ] Import `CustomScreenProps` from 'noboarding'
860
+ - [ ] Use all required props: `analytics`, `onNext`, `data`, `onDataUpdate`
861
+ - [ ] Track `screen_viewed` on mount
862
+ - [ ] Implement `preview` mode for dashboard
863
+ - [ ] Call `onDataUpdate()` to add your data before `onNext()`
864
+ - [ ] Track `screen_completed` before navigating
865
+ - [ ] Access previous data via `data` prop
866
+ - [ ] Register component in `customComponents` with EXACT name
867
+ - [ ] Add to dashboard with matching component name
868
+ - [ ] Test data flow across multiple screens
869
+
870
+ ---
871
+
872
+ **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
 
@@ -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);
@@ -109,6 +133,12 @@ const OnboardingFlow = ({ apiKey, onComplete, onSkip, baseUrl, initialVariables,
109
133
  setCurrentIndex((prev) => prev + 1);
110
134
  }
111
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
+ };
112
142
  const handleSkipScreen = () => {
113
143
  // Move to next screen or complete
114
144
  if (currentIndex >= screens.length - 1) {
@@ -201,7 +231,7 @@ const OnboardingFlow = ({ apiKey, onComplete, onSkip, baseUrl, initialVariables,
201
231
  </react_native_1.View>);
202
232
  }
203
233
  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)))}/>
234
+ <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
235
  </react_native_1.View>);
206
236
  }
207
237
  // 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);
@@ -340,7 +340,14 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables
340
340
  </react_native_1.Text>)}
341
341
  </react_native_1.View>);
342
342
  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'}/>);
343
+ // Only apply default border if borderWidth is not explicitly defined (including 0)
344
+ const inputStyle = style;
345
+ const defaultInputStyle = {};
346
+ 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) {
347
+ defaultInputStyle.borderWidth = 1;
348
+ defaultInputStyle.borderColor = '#E5E5E5';
349
+ }
350
+ return (<react_native_1.TextInput style={[defaultInputStyle, inputStyle]} placeholder={((_2 = element.props) === null || _2 === void 0 ? void 0 : _2.placeholder) || 'Enter text...'} keyboardType={getKeyboardType((_3 = element.props) === null || _3 === void 0 ? void 0 : _3.type)} secureTextEntry={((_4 = element.props) === null || _4 === void 0 ? void 0 : _4.type) === 'password'} autoCapitalize={((_5 = element.props) === null || _5 === void 0 ? void 0 : _5.type) === 'email' ? 'none' : 'sentences'}/>);
344
351
  case 'spacer':
345
352
  return <react_native_1.View style={style || { flex: 1 }}/>;
346
353
  case 'divider':
package/lib/types.d.ts CHANGED
@@ -169,13 +169,16 @@ export interface CustomScreenProps {
169
169
  track: (event: string, properties?: Record<string, any>) => void;
170
170
  };
171
171
  onNext: () => void;
172
+ onBack?: () => void;
172
173
  onSkip?: () => void;
173
174
  preview?: boolean;
174
175
  data?: Record<string, any>;
175
176
  onDataUpdate?: (data: Record<string, any>) => void;
176
177
  }
177
178
  export interface OnboardingFlowProps {
178
- apiKey: string;
179
+ testKey?: string;
180
+ productionKey?: string;
181
+ apiKey?: string;
179
182
  onComplete: (data?: Record<string, any>) => void;
180
183
  onSkip?: () => void;
181
184
  baseUrl?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noboarding",
3
- "version": "0.1.0-alpha",
3
+ "version": "0.1.0-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",
@@ -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
@@ -103,6 +130,13 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
103
130
  }
104
131
  };
105
132
 
133
+ const handleBack = () => {
134
+ // Navigate to previous screen (only if not on first screen)
135
+ if (currentIndex > 0) {
136
+ setCurrentIndex((prev) => prev - 1);
137
+ }
138
+ };
139
+
106
140
  const handleSkipScreen = () => {
107
141
  // Move to next screen or complete
108
142
  if (currentIndex >= screens.length - 1) {
@@ -235,6 +269,7 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
235
269
  <CustomComponent
236
270
  analytics={analyticsRef.current!}
237
271
  onNext={() => handleNext()}
272
+ onBack={currentIndex > 0 ? handleBack : undefined}
238
273
  onSkip={onSkip ? handleSkipAll : undefined}
239
274
  data={collectedData}
240
275
  onDataUpdate={(newData) => setCollectedData(prev => ({ ...prev, ...newData }))}
@@ -439,9 +439,17 @@ const RenderNode: React.FC<RenderNodeProps> = ({ element, toggledIds, groupSelec
439
439
  );
440
440
 
441
441
  case 'input':
442
+ // Only apply default border if borderWidth is not explicitly defined (including 0)
443
+ const inputStyle = style as TextStyle;
444
+ const defaultInputStyle: TextStyle = {};
445
+ if (element.style?.borderWidth === undefined && element.style?.borderColor === undefined) {
446
+ defaultInputStyle.borderWidth = 1;
447
+ defaultInputStyle.borderColor = '#E5E5E5';
448
+ }
449
+
442
450
  return (
443
451
  <TextInput
444
- style={[style as TextStyle, { borderWidth: 1, borderColor: '#E5E5E5' }]}
452
+ style={[defaultInputStyle, inputStyle]}
445
453
  placeholder={element.props?.placeholder || 'Enter text...'}
446
454
  keyboardType={getKeyboardType(element.props?.type)}
447
455
  secureTextEntry={element.props?.type === 'password'}
package/src/types.ts CHANGED
@@ -224,6 +224,7 @@ export interface CustomScreenProps {
224
224
  track: (event: string, properties?: Record<string, any>) => void;
225
225
  };
226
226
  onNext: () => void;
227
+ onBack?: () => void;
227
228
  onSkip?: () => void;
228
229
  preview?: boolean;
229
230
  data?: Record<string, any>;
@@ -232,7 +233,13 @@ export interface CustomScreenProps {
232
233
 
233
234
  // Main SDK props
234
235
  export interface OnboardingFlowProps {
235
- apiKey: string;
236
+ // Option 1: Auto-detection with dual keys (recommended)
237
+ testKey?: string; // nb_test_... key for development/testing
238
+ productionKey?: string; // nb_live_... key for production
239
+
240
+ // Option 2: Legacy single key (backwards compatible)
241
+ apiKey?: string;
242
+
236
243
  onComplete: (data?: Record<string, any>) => void;
237
244
  onSkip?: () => void;
238
245
  baseUrl?: string;