noboarding 0.1.0-alpha

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,1964 @@
1
+ # Custom Screens - Complete Implementation Guide
2
+
3
+ ## Table of Contents
4
+
5
+ 1. [Overview](#overview)
6
+ 2. [How Custom Screens Work](#how-custom-screens-work)
7
+ 3. [Quick Start](#quick-start)
8
+ 4. [Component API Reference](#component-api-reference)
9
+ 5. [Preview Mode](#preview-mode)
10
+ 6. [Best Practices](#best-practices)
11
+ 7. [Common Patterns](#common-patterns)
12
+ 8. [Version Management](#version-management)
13
+ 9. [Troubleshooting](#troubleshooting)
14
+ 10. [Advanced Topics](#advanced-topics)
15
+
16
+ ---
17
+
18
+ ## Overview
19
+
20
+ Custom screens let you integrate your own React Native components into onboarding flows managed by our platform. You get complete control over complex features while still benefiting from our analytics, A/B testing, and flow management.
21
+
22
+ ### When to Use Custom Screens
23
+
24
+ **✅ Perfect for:**
25
+ - Native device features (camera, biometrics, location, Face ID)
26
+ - Third-party SDK integrations (Stripe, Plaid, Auth0)
27
+ - Complex business logic unique to your app
28
+ - Existing screens you want to keep (gradual migration)
29
+ - Features requiring real-time API calls
30
+ - Advanced animations or custom interactions
31
+
32
+ **❌ Not recommended for:**
33
+ - Simple text and images (use SDK Text/Image components instead)
34
+ - Basic forms (use SDK TextInput component)
35
+ - Standard authentication (use SDK SocialLogin component)
36
+ - Features that can be built with SDK components
37
+
38
+ ### Key Benefits
39
+
40
+ - ✅ **Full control** - Write any React Native code you want
41
+ - ✅ **Native access** - Use camera, location, biometrics, etc.
42
+ - ✅ **Your APIs** - Call your own backend services
43
+ - ✅ **Analytics included** - Automatic tracking of views, completions, drop-offs
44
+ - ✅ **A/B testable** - Test different positions in flow
45
+ - ✅ **No MAU charges** - Custom screens don't count toward your usage limits
46
+
47
+ ---
48
+
49
+ ## How Custom Screens Work
50
+
51
+ ### Architecture
52
+
53
+ Custom screens are React Native components that **live in your app code**, not on our servers. The platform simply tells the SDK when to render them.
54
+ ```
55
+ ┌─────────────────────────┐ ┌──────────────────────┐
56
+ │ Your App (Local) │ │ Platform (Remote) │
57
+ ├─────────────────────────┤ ├──────────────────────┤
58
+ │ │ │ │
59
+ │ MealTrackerScreen.tsx │ ←───→ │ Dashboard Config: │
60
+ │ - Component code │ │ { │
61
+ │ - UI rendering │ │ "type": "custom", │
62
+ │ - API calls │ │ "name": "Meal..." │
63
+ │ - Business logic │ │ } │
64
+ │ │ │ │
65
+ └─────────────────────────┘ └──────────────────────┘
66
+ ```
67
+
68
+ **At runtime:**
69
+ ```
70
+ User starts onboarding
71
+
72
+ SDK fetches config from platform
73
+
74
+ Config says: "Screen 3 = custom: MealTrackerScreen"
75
+
76
+ SDK looks for MealTrackerScreen in customComponents
77
+
78
+ SDK renders YOUR component
79
+
80
+ Your component executes (calls your APIs, handles logic)
81
+
82
+ User completes screen
83
+
84
+ SDK continues to next screen
85
+ ```
86
+
87
+ ---
88
+
89
+ ### What's Remotely Editable vs. What Requires App Updates
90
+
91
+ | Action | Requires App Update? | Update Time |
92
+ |--------|---------------------|-------------|
93
+ | **Reorder custom screen in flow** | ❌ No | Instant |
94
+ | **Remove custom screen from flow** | ❌ No | Instant |
95
+ | **Show/hide custom screen conditionally** | ❌ No | Instant |
96
+ | **Change SDK component properties** | ❌ No | Instant |
97
+ | **Edit custom screen code/UI** | ✅ Yes | 1-3 days (App Store review) |
98
+ | **Add new custom screen** | ✅ Yes | 1-3 days (App Store review) |
99
+ | **Fix bugs in custom screen** | ✅ Yes | 1-3 days (App Store review) |
100
+
101
+ **Example:**
102
+ ```json
103
+ // ✅ This can change instantly (no app update):
104
+ {
105
+ "screens": [
106
+ {"type": "welcome_screen"},
107
+ {"type": "custom", "name": "MealTrackerScreen"}, // ← Can reorder
108
+ {"type": "goal_selector"}
109
+ ],
110
+ "conditions": {
111
+ "show_meal_tracker": {
112
+ "if": {"variable": "wants_nutrition", "equals": true} // ← Can change
113
+ }
114
+ }
115
+ }
116
+ ```
117
+ ```typescript
118
+ // ❌ This requires App Store submission:
119
+ export const MealTrackerScreen = () => {
120
+ // ANY changes to this code require app update
121
+ const [calories, setCalories] = useState(null);
122
+
123
+ return (
124
+ <View>
125
+ <Camera /> // ← Can't change this remotely
126
+ <Text>Calories: {calories}</Text> // ← Can't change this remotely
127
+ </View>
128
+ );
129
+ };
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Quick Start
135
+
136
+ ### Step 1: Create the Component
137
+
138
+ Create your custom screen file:
139
+ ```typescript
140
+ // screens/MealTrackerScreen.tsx
141
+
142
+ import React, { useState, useRef } from 'react';
143
+ import { View, Text, Button, Image, ActivityIndicator } from 'react-native';
144
+ import { Camera } from 'expo-camera';
145
+
146
+ export const MealTrackerScreen = ({
147
+ analytics, // Track events
148
+ onNext, // Go to next screen
149
+ onSkip, // Skip this screen (optional)
150
+ preview, // True when in dashboard preview
151
+ data, // Previously collected user data (optional)
152
+ onDataUpdate // Update collected data (optional)
153
+ }) => {
154
+ const [photo, setPhoto] = useState(null);
155
+ const [calories, setCalories] = useState(null);
156
+ const [loading, setLoading] = useState(false);
157
+ const cameraRef = useRef(null);
158
+
159
+ // Track screen view on mount
160
+ React.useEffect(() => {
161
+ analytics.track('screen_viewed', {
162
+ screen_id: 'meal_tracker',
163
+ screen_type: 'custom'
164
+ });
165
+ }, []);
166
+
167
+ // Handle photo capture
168
+ const takePicture = async () => {
169
+ if (!cameraRef.current) return;
170
+
171
+ analytics.track('photo_taken');
172
+
173
+ const photo = await cameraRef.current.takePictureAsync({
174
+ quality: 0.7,
175
+ base64: true
176
+ });
177
+
178
+ setPhoto(photo);
179
+ await analyzePhoto(photo);
180
+ };
181
+
182
+ // Analyze photo with YOUR API
183
+ const analyzePhoto = async (photo) => {
184
+ setLoading(true);
185
+ analytics.track('analysis_started');
186
+
187
+ try {
188
+ const response = await fetch('https://your-backend.com/api/analyze-meal', {
189
+ method: 'POST',
190
+ headers: {
191
+ 'Content-Type': 'application/json',
192
+ 'Authorization': `Bearer ${YOUR_API_KEY}`
193
+ },
194
+ body: JSON.stringify({
195
+ image: photo.base64,
196
+ userId: data?.userId // Use collected data if needed
197
+ })
198
+ });
199
+
200
+ if (!response.ok) {
201
+ throw new Error('Analysis failed');
202
+ }
203
+
204
+ const result = await response.json();
205
+ setCalories(result.calories);
206
+
207
+ // Update collected data
208
+ onDataUpdate?.({
209
+ meal_calories: result.calories,
210
+ meal_timestamp: new Date().toISOString()
211
+ });
212
+
213
+ analytics.track('analysis_completed', {
214
+ calories: result.calories
215
+ });
216
+ } catch (error) {
217
+ analytics.track('analysis_failed', {
218
+ error: error.message
219
+ });
220
+ alert('Failed to analyze meal. Please try again.');
221
+ } finally {
222
+ setLoading(false);
223
+ }
224
+ };
225
+
226
+ // Handle continue
227
+ const handleContinue = () => {
228
+ analytics.track('screen_completed', {
229
+ calories: calories
230
+ });
231
+ onNext();
232
+ };
233
+
234
+ // PREVIEW MODE (for dashboard)
235
+ if (preview) {
236
+ return (
237
+ <View style={{ padding: 20, alignItems: 'center' }}>
238
+ <View style={{
239
+ width: 300,
240
+ height: 300,
241
+ backgroundColor: '#f5f5f5',
242
+ borderRadius: 16,
243
+ justifyContent: 'center',
244
+ alignItems: 'center',
245
+ marginBottom: 24
246
+ }}>
247
+ <Text style={{ fontSize: 64 }}>📸</Text>
248
+ <Text style={{ marginTop: 12, fontSize: 16, color: '#666' }}>
249
+ Camera Preview
250
+ </Text>
251
+ <Text style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
252
+ (Only works in real app)
253
+ </Text>
254
+ </View>
255
+
256
+ <View style={{
257
+ padding: 16,
258
+ backgroundColor: '#fff',
259
+ borderRadius: 12,
260
+ width: '100%',
261
+ marginBottom: 20,
262
+ borderWidth: 1,
263
+ borderColor: '#e0e0e0'
264
+ }}>
265
+ <Text style={{ fontSize: 20, fontWeight: '600' }}>
266
+ Estimated Calories: 450
267
+ </Text>
268
+ <Text style={{ fontSize: 14, color: '#666', marginTop: 4 }}>
269
+ Mock data for preview
270
+ </Text>
271
+ </View>
272
+
273
+ <Button title="Continue" onPress={onNext} />
274
+ </View>
275
+ );
276
+ }
277
+
278
+ // REAL IMPLEMENTATION
279
+
280
+ // Before taking photo
281
+ if (!photo) {
282
+ return (
283
+ <View style={{ flex: 1 }}>
284
+ <Camera
285
+ ref={cameraRef}
286
+ style={{ flex: 1 }}
287
+ type={Camera.Constants.Type.back}
288
+ >
289
+ <View style={{
290
+ flex: 1,
291
+ backgroundColor: 'transparent',
292
+ justifyContent: 'flex-end',
293
+ padding: 20
294
+ }}>
295
+ <Button title="Take Photo" onPress={takePicture} />
296
+ {onSkip && (
297
+ <Button title="Skip" onPress={onSkip} color="#666" />
298
+ )}
299
+ </View>
300
+ </Camera>
301
+ </View>
302
+ );
303
+ }
304
+
305
+ // After taking photo
306
+ return (
307
+ <View style={{ flex: 1, padding: 20, alignItems: 'center' }}>
308
+ <Image
309
+ source={{ uri: photo.uri }}
310
+ style={{ width: 300, height: 300, borderRadius: 16 }}
311
+ />
312
+
313
+ {loading ? (
314
+ <View style={{ marginTop: 32, alignItems: 'center' }}>
315
+ <ActivityIndicator size="large" color="#FF6B6B" />
316
+ <Text style={{ marginTop: 12, fontSize: 16 }}>
317
+ Analyzing your meal...
318
+ </Text>
319
+ </View>
320
+ ) : calories ? (
321
+ <View style={{ marginTop: 32, alignItems: 'center', width: '100%' }}>
322
+ <Text style={{ fontSize: 28, fontWeight: 'bold' }}>
323
+ Estimated Calories: {calories}
324
+ </Text>
325
+ <View style={{ marginTop: 24, width: '100%', gap: 12 }}>
326
+ <Button title="Continue" onPress={handleContinue} />
327
+ <Button
328
+ title="Retake Photo"
329
+ onPress={() => setPhoto(null)}
330
+ color="#666"
331
+ />
332
+ </View>
333
+ </View>
334
+ ) : null}
335
+ </View>
336
+ );
337
+ };
338
+ ```
339
+
340
+ ---
341
+
342
+ ### Step 2: Register the Component
343
+
344
+ Register your custom screen with the SDK:
345
+ ```typescript
346
+ // App.tsx
347
+
348
+ import React, { useState } from 'react';
349
+ import { OnboardingFlow } from '@yourplatform/sdk';
350
+ import { MealTrackerScreen } from './screens/MealTrackerScreen';
351
+ import { WorkoutLogScreen } from './screens/WorkoutLogScreen';
352
+
353
+ export default function App() {
354
+ const [showOnboarding, setShowOnboarding] = useState(true);
355
+
356
+ if (showOnboarding) {
357
+ return (
358
+ <OnboardingFlow
359
+ apiKey="sk_live_abc123..."
360
+
361
+ // Register custom screens here
362
+ customComponents={{
363
+ MealTrackerScreen: MealTrackerScreen,
364
+ WorkoutLogScreen: WorkoutLogScreen,
365
+ // Add more custom screens as needed
366
+ }}
367
+
368
+ onComplete={(userData) => {
369
+ // userData contains both SDK and custom screen data
370
+ console.log('Onboarding completed:', userData);
371
+
372
+ // Save to your database
373
+ await saveUserProfile(userData);
374
+
375
+ // Hide onboarding
376
+ setShowOnboarding(false);
377
+ }}
378
+
379
+ onSkip={() => {
380
+ setShowOnboarding(false);
381
+ }}
382
+ />
383
+ );
384
+ }
385
+
386
+ return <MainApp />;
387
+ }
388
+ ```
389
+
390
+ ---
391
+
392
+ ### Step 3: Add to Dashboard Flow
393
+
394
+ 1. **Go to Flow Builder** in the dashboard
395
+ 2. **Click "🛠️ Add Custom Screen"**
396
+ 3. **Enter details:**
397
+ - **Component Name:** `MealTrackerScreen` (must match exactly)
398
+ - **Description:** "Takes photo of meal and estimates calories"
399
+ - **Minimum App Version (optional):** `1.1.0`
400
+ 4. **Position the screen** in your flow by dragging
401
+ 5. **Click "Save Draft"**
402
+
403
+ **Dashboard will show:**
404
+ ```
405
+ ┌────────────────────────────────────────────────┐
406
+ │ Onboarding Flow │
407
+ ├────────────────────────────────────────────────┤
408
+ │ 1. Welcome Screen (SDK) ✓ │
409
+ │ [Edit] [Delete] [↑] [↓] │
410
+ │ │
411
+ │ 2. Meal Tracker 🔒 (Custom) │
412
+ │ Component: MealTrackerScreen │
413
+ │ [View Only] [Delete] [↑] [↓] │
414
+ │ │
415
+ │ ⚠️ This screen requires app v1.1.0+ │
416
+ │ Custom screens require App Store approval │
417
+ │ to modify. You can reorder or remove. │
418
+ │ │
419
+ │ 3. Goal Selector (SDK) ✓ │
420
+ │ [Edit] [Delete] [↑] [↓] │
421
+ └────────────────────────────────────────────────┘
422
+ ```
423
+
424
+ ---
425
+
426
+ ### Step 4: Deploy Your App
427
+
428
+ 1. **Test locally** first with Expo Go or development build
429
+ 2. **Build production app** with the custom screen included
430
+ 3. **Submit to App Store / Google Play**
431
+ 4. **Wait for approval** (typically 1-3 days)
432
+
433
+ ---
434
+
435
+ ### Step 5: Publish the Flow
436
+
437
+ **After your app is approved and live:**
438
+
439
+ 1. Go back to dashboard
440
+ 2. Click **"Publish"**
441
+ 3. Users will now see the custom screen in their onboarding
442
+
443
+ **⚠️ Important:** Don't publish the flow until your app with the custom screen is live in the stores. Otherwise users will encounter errors.
444
+
445
+ ---
446
+
447
+ ## Component API Reference
448
+
449
+ ### Props Interface
450
+ ```typescript
451
+ interface CustomScreenProps {
452
+ /**
453
+ * Analytics tracking object
454
+ * Use to track events, errors, and user behavior
455
+ */
456
+ analytics: {
457
+ track: (eventName: string, properties?: Record<string, any>) => void;
458
+ };
459
+
460
+ /**
461
+ * Navigate to the next screen in the flow
462
+ * REQUIRED: Call this when user completes your screen
463
+ */
464
+ onNext: () => void;
465
+
466
+ /**
467
+ * Skip this screen (optional)
468
+ * Only provided if screen is configured as skippable
469
+ */
470
+ onSkip?: () => void;
471
+
472
+ /**
473
+ * Preview mode flag
474
+ * True when rendering in dashboard preview
475
+ * Use to show placeholder UI instead of real functionality
476
+ */
477
+ preview?: boolean;
478
+
479
+ /**
480
+ * Previously collected user data (optional)
481
+ * Contains data from SDK components and other custom screens
482
+ */
483
+ data?: Record<string, any>;
484
+
485
+ /**
486
+ * Update the collected user data (optional)
487
+ * Merge new data that will be passed to onComplete
488
+ */
489
+ onDataUpdate?: (newData: Record<string, any>) => void;
490
+ }
491
+ ```
492
+
493
+ ---
494
+
495
+ ### Analytics Tracking
496
+
497
+ Track events to understand user behavior and debug issues:
498
+ ```typescript
499
+ // ✅ Always track screen view on mount
500
+ useEffect(() => {
501
+ analytics.track('screen_viewed', {
502
+ screen_id: 'meal_tracker',
503
+ screen_type: 'custom'
504
+ });
505
+ }, []);
506
+
507
+ // ✅ Track user actions
508
+ analytics.track('button_clicked', {
509
+ button_name: 'take_photo'
510
+ });
511
+
512
+ analytics.track('photo_captured', {
513
+ quality: 0.7,
514
+ timestamp: Date.now()
515
+ });
516
+
517
+ // ✅ Track completion
518
+ analytics.track('screen_completed', {
519
+ screen_id: 'meal_tracker',
520
+ calories_analyzed: 450,
521
+ time_spent_seconds: 45
522
+ });
523
+
524
+ // ✅ Track skip
525
+ analytics.track('screen_skipped', {
526
+ screen_id: 'meal_tracker',
527
+ reason: 'user_declined'
528
+ });
529
+
530
+ // ✅ Track errors
531
+ analytics.track('error_occurred', {
532
+ error_type: 'api_failure',
533
+ error_message: 'Network timeout',
534
+ endpoint: '/api/analyze-meal'
535
+ });
536
+
537
+ // ✅ Track API calls
538
+ analytics.track('api_request_started', {
539
+ endpoint: '/analyze-meal'
540
+ });
541
+
542
+ analytics.track('api_request_completed', {
543
+ endpoint: '/analyze-meal',
544
+ duration_ms: 2340,
545
+ status_code: 200
546
+ });
547
+ ```
548
+
549
+ **These events appear in your dashboard analytics:**
550
+
551
+ - Filter by screen_id
552
+ - See conversion funnels
553
+ - Compare custom vs SDK screen performance
554
+ - A/B test custom screen positioning
555
+
556
+ ---
557
+
558
+ ### Navigation
559
+
560
+ **Always call `onNext()` when complete:**
561
+ ```typescript
562
+ const handleContinue = () => {
563
+ // Track completion first
564
+ analytics.track('screen_completed', {
565
+ screen_id: 'meal_tracker',
566
+ data_collected: true
567
+ });
568
+
569
+ // Then navigate
570
+ onNext();
571
+ };
572
+ ```
573
+
574
+ **Call `onSkip()` if user skips (if provided):**
575
+ ```typescript
576
+ const handleSkip = () => {
577
+ analytics.track('screen_skipped', {
578
+ screen_id: 'meal_tracker'
579
+ });
580
+
581
+ // Safe call - onSkip might be undefined
582
+ onSkip?.();
583
+ };
584
+
585
+ // Or with button
586
+ {onSkip && (
587
+ <Button title="Skip" onPress={handleSkip} />
588
+ )}
589
+ ```
590
+
591
+ ---
592
+
593
+ ### Data Collection
594
+
595
+ Custom screens can contribute to the final `userData` object:
596
+ ```typescript
597
+ export const GoalSelectionScreen = ({
598
+ analytics,
599
+ onNext,
600
+ data,
601
+ onDataUpdate
602
+ }) => {
603
+ const [selectedGoals, setSelectedGoals] = useState([]);
604
+
605
+ const handleContinue = () => {
606
+ // Add to collected data
607
+ onDataUpdate?.({
608
+ fitness_goals: selectedGoals,
609
+ goals_count: selectedGoals.length,
610
+ goals_timestamp: new Date().toISOString()
611
+ });
612
+
613
+ analytics.track('goals_selected', {
614
+ count: selectedGoals.length,
615
+ goals: selectedGoals
616
+ });
617
+
618
+ onNext();
619
+ };
620
+
621
+ return (
622
+ <View>
623
+ <CheckboxGroup
624
+ options={['Lose Weight', 'Build Muscle', 'Improve Endurance']}
625
+ selected={selectedGoals}
626
+ onChange={setSelectedGoals}
627
+ />
628
+ <Button title="Continue" onPress={handleContinue} />
629
+ </View>
630
+ );
631
+ };
632
+ ```
633
+
634
+ **The final `userData` merges SDK and custom screen data:**
635
+ ```typescript
636
+ <OnboardingFlow
637
+ customComponents={{ GoalSelectionScreen }}
638
+ onComplete={(userData) => {
639
+ // Contains data from both SDK and custom screens:
640
+ console.log(userData);
641
+ // {
642
+ // name: "John", // From SDK TextInput
643
+ // age: 25, // From SDK TextInput
644
+ // email: "john@example.com", // From SDK TextInput
645
+ // fitness_goals: ["Build Muscle"], // From custom screen
646
+ // goals_count: 1, // From custom screen
647
+ // goals_timestamp: "2025-02-17..." // From custom screen
648
+ // }
649
+ }}
650
+ />
651
+ ```
652
+
653
+ ---
654
+
655
+ ### Accessing Previous Data
656
+
657
+ Use the `data` prop to access previously collected information:
658
+ ```typescript
659
+ export const SummaryScreen = ({ analytics, onNext, data }) => {
660
+ return (
661
+ <View>
662
+ <Text>Welcome, {data?.name}!</Text>
663
+ <Text>Age: {data?.age}</Text>
664
+ <Text>Goals: {data?.fitness_goals?.join(', ')}</Text>
665
+
666
+ <Button title="Confirm" onPress={onNext} />
667
+ </View>
668
+ );
669
+ };
670
+ ```
671
+
672
+ ---
673
+
674
+ ## Preview Mode
675
+
676
+ ### Why Preview Mode is Critical
677
+
678
+ Custom screens often use native features that **don't work in the browser**:
679
+
680
+ - ❌ Camera
681
+ - ❌ Location/GPS
682
+ - ❌ Biometrics (Face ID, Touch ID)
683
+ - ❌ Bluetooth
684
+ - ❌ Push notifications
685
+ - ❌ Native modules
686
+
687
+ **Preview mode lets you show a placeholder in the dashboard** while providing full functionality in the real app.
688
+
689
+ ---
690
+
691
+ ### Implementing Preview Mode
692
+
693
+ <!-- **Always check the `preview` prop:**
694
+ ```typescript
695
+ export const CameraScreen = ({ analytics, onNext, preview }) => {
696
+ // PREVIEW MODE (dashboard - no camera access)
697
+ if (preview) {
698
+ return (
699
+ <View style={styles.previewContainer}>
700
+ <View style={styles.mockCamera}>
701
+ <Text style={styles.cameraIcon}>📸</Text>
702
+ <Text style={styles.previewLabel}>Camera Preview</Text>
703
+ <Text style={styles.previewNote}>
704
+ (Real camera only works in app)
705
+ </Text>
706
+ </View>
707
+
708
+ <View style={styles.mockResult}>
709
+ <Text style={styles.resultText}>
710
+ Result: Success (mock data)
711
+ </Text>
712
+ </View>
713
+
714
+ <Button title="Continue" onPress={onNext} />
715
+ </View>
716
+ );
717
+ } -->
718
+
719
+ // REAL IMPLEMENTATION (mobile app - full camera access)
720
+ return (
721
+ <Camera>
722
+ {/* Real camera implementation */}
723
+ </Camera>
724
+ );
725
+ };
726
+
727
+ const styles = StyleSheet.create({
728
+ previewContainer: {
729
+ flex: 1,
730
+ padding: 20,
731
+ alignItems: 'center',
732
+ justifyContent: 'center'
733
+ },
734
+ mockCamera: {
735
+ width: 300,
736
+ height: 400,
737
+ backgroundColor: '#f5f5f5',
738
+ borderRadius: 16,
739
+ justifyContent: 'center',
740
+ alignItems: 'center',
741
+ marginBottom: 24
742
+ },
743
+ cameraIcon: {
744
+ fontSize: 64
745
+ },
746
+ previewLabel: {
747
+ marginTop: 16,
748
+ fontSize: 18,
749
+ fontWeight: '600',
750
+ color: '#333'
751
+ },
752
+ previewNote: {
753
+ marginTop: 8,
754
+ fontSize: 14,
755
+ color: '#999'
756
+ },
757
+ mockResult: {
758
+ padding: 16,
759
+ backgroundColor: '#fff',
760
+ borderRadius: 12,
761
+ borderWidth: 1,
762
+ borderColor: '#e0e0e0',
763
+ marginBottom: 24
764
+ },
765
+ resultText: {
766
+ fontSize: 16,
767
+ fontWeight: '500'
768
+ }
769
+ });
770
+ ```
771
+
772
+ ---
773
+
774
+ ### Preview Best Practices
775
+
776
+ **✅ DO:**
777
+
778
+ - Show a visual placeholder that represents the screen's purpose
779
+ - Use icons/emojis to indicate functionality (📸 for camera, 📍 for location)
780
+ - Display mock data to demonstrate the UI flow
781
+ - Keep the same layout structure as the real screen
782
+ - Include Continue/Skip buttons so preview flow continues
783
+ - Show helpful text like "(Only works in real app)"
784
+
785
+ **❌ DON'T:**
786
+
787
+ - Return `null` or empty view (breaks preview flow)
788
+ - Show error messages or warnings
789
+ - Try to access native APIs in preview mode
790
+ - Make preview UI completely different from real UI
791
+ - Forget to handle the preview prop
792
+
793
+ ---
794
+
795
+ ### Preview Examples
796
+
797
+ **Location Request:**
798
+ ```typescript
799
+ if (preview) {
800
+ return (
801
+ <View style={{ padding: 20, alignItems: 'center' }}>
802
+ <Text style={{ fontSize: 64, marginBottom: 16 }}>📍</Text>
803
+ <Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 8 }}>
804
+ Location Access
805
+ </Text>
806
+ <Text style={{ fontSize: 16, color: '#666', textAlign: 'center' }}>
807
+ This screen requests location permission
808
+ </Text>
809
+ <Text style={{ fontSize: 14, color: '#999', marginTop: 8 }}>
810
+ (Preview mode - permission not actually requested)
811
+ </Text>
812
+ <View style={{ marginTop: 32, width: '100%' }}>
813
+ <Button title="Grant Permission (Mock)" onPress={onNext} />
814
+ <Button title="Skip" onPress={onSkip} color="#666" />
815
+ </View>
816
+ </View>
817
+ );
818
+ }
819
+ ```
820
+
821
+ **Payment/Checkout:**
822
+ ```typescript
823
+ if (preview) {
824
+ return (
825
+ <View style={{ padding: 20 }}>
826
+ <Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 16 }}>
827
+ Complete Purchase
828
+ </Text>
829
+
830
+ <View style={{
831
+ padding: 16,
832
+ backgroundColor: '#f5f5f5',
833
+ borderRadius: 12,
834
+ marginBottom: 24
835
+ }}>
836
+ <Text style={{ fontSize: 16, marginBottom: 8 }}>
837
+ 💳 Payment Form (Mock)
838
+ </Text>
839
+ <Text style={{ color: '#666' }}>
840
+ Card Number: •••• •••• •••• 1234
841
+ </Text>
842
+ <Text style={{ color: '#666' }}>
843
+ Expiry: 12/25
844
+ </Text>
845
+ </View>
846
+
847
+ <View style={{
848
+ padding: 16,
849
+ backgroundColor: '#e8f5e9',
850
+ borderRadius: 12,
851
+ marginBottom: 24
852
+ }}>
853
+ <Text style={{ color: '#2e7d32', fontWeight: '600' }}>
854
+ ✓ Payment Successful (Mock)
855
+ </Text>
856
+ </View>
857
+
858
+ <Button title="Continue" onPress={onNext} />
859
+ </View>
860
+ );
861
+ }
862
+ ```
863
+
864
+ ---
865
+
866
+ ## Best Practices
867
+
868
+ ### 1. File Organization
869
+
870
+ **Simple screens (one file):**
871
+ ```
872
+ /src
873
+ /screens
874
+ /custom
875
+ MealTrackerScreen.tsx
876
+ BiometricAuthScreen.tsx
877
+ LocationPermissionScreen.tsx
878
+ ```
879
+
880
+ **Complex screens (folder structure):**
881
+ ```
882
+ /src
883
+ /screens
884
+ /custom
885
+ /meal-tracker
886
+ index.tsx ← Export main component
887
+ MealTrackerScreen.tsx ← Main component
888
+ CameraView.tsx ← Sub-component
889
+ ResultView.tsx ← Sub-component
890
+ api.ts ← API calls
891
+ hooks.ts ← Custom hooks
892
+ utils.ts ← Helper functions
893
+ ```
894
+
895
+ **Main export (index.tsx):**
896
+ ```typescript
897
+ export { MealTrackerScreen } from './MealTrackerScreen';
898
+ ```
899
+
900
+ ---
901
+
902
+ ### 2. Error Handling
903
+
904
+ **Always handle errors gracefully:**
905
+ ```typescript
906
+ const analyzePhoto = async (photo) => {
907
+ setLoading(true);
908
+
909
+ try {
910
+ const response = await fetch('https://your-api.com/analyze', {
911
+ method: 'POST',
912
+ headers: {
913
+ 'Content-Type': 'application/json',
914
+ 'Authorization': `Bearer ${API_KEY}`
915
+ },
916
+ body: JSON.stringify({ image: photo.base64 }),
917
+ timeout: 30000 // 30 second timeout
918
+ });
919
+
920
+ if (!response.ok) {
921
+ const errorData = await response.json();
922
+ throw new Error(errorData.message || `HTTP ${response.status}`);
923
+ }
924
+
925
+ const data = await response.json();
926
+
927
+ if (!data.calories) {
928
+ throw new Error('Invalid response from server');
929
+ }
930
+
931
+ setCalories(data.calories);
932
+
933
+ analytics.track('analysis_completed', {
934
+ calories: data.calories,
935
+ duration_ms: Date.now() - startTime
936
+ });
937
+
938
+ } catch (error) {
939
+ // Track error
940
+ analytics.track('analysis_failed', {
941
+ error_type: error.name,
942
+ error_message: error.message,
943
+ stack_trace: error.stack
944
+ });
945
+
946
+ // Show user-friendly message
947
+ Alert.alert(
948
+ 'Analysis Failed',
949
+ 'We couldn\'t analyze your meal. Please try again or skip this step.',
950
+ [
951
+ { text: 'Retry', onPress: () => analyzePhoto(photo) },
952
+ { text: 'Skip', onPress: onSkip, style: 'cancel' }
953
+ ]
954
+ );
955
+ } finally {
956
+ setLoading(false);
957
+ }
958
+ };
959
+ ```
960
+
961
+ ---
962
+
963
+ ### 3. Loading States
964
+
965
+ **Show clear, informative loading states:**
966
+ ```typescript
967
+ const [loading, setLoading] = useState(false);
968
+ const [loadingMessage, setLoadingMessage] = useState('');
969
+
970
+ if (loading) {
971
+ return (
972
+ <View style={styles.loadingContainer}>
973
+ <ActivityIndicator size="large" color="#FF6B6B" />
974
+
975
+ <Text style={styles.loadingTitle}>
976
+ {loadingMessage || 'Please wait...'}
977
+ </Text>
978
+
979
+ <Text style={styles.loadingSubtitle}>
980
+ This may take a few seconds
981
+ </Text>
982
+
983
+ {/* Optional: Progress indicator */}
984
+ <View style={styles.progressBar}>
985
+ <View style={[styles.progress, { width: `${progress}%` }]} />
986
+ </View>
987
+ </View>
988
+ );
989
+ }
990
+
991
+ // When calling API
992
+ setLoading(true);
993
+ setLoadingMessage('Analyzing your meal...');
994
+ await analyzePhoto(photo);
995
+ setLoading(false);
996
+ ```
997
+
998
+ ---
999
+
1000
+ ### 4. Accessibility
1001
+
1002
+ **Make custom screens accessible:**
1003
+ ```typescript
1004
+ import { AccessibilityInfo } from 'react-native';
1005
+
1006
+ export const MealTrackerScreen = ({ analytics, onNext }) => {
1007
+ useEffect(() => {
1008
+ // Announce screen to screen readers
1009
+ AccessibilityInfo.announceForAccessibility(
1010
+ 'Meal tracker screen. Take a photo of your meal to estimate calories.'
1011
+ );
1012
+ }, []);
1013
+
1014
+ return (
1015
+ <View>
1016
+ <TouchableOpacity
1017
+ accessible={true}
1018
+ accessibilityRole="button"
1019
+ accessibilityLabel="Take photo of meal"
1020
+ accessibilityHint="Opens camera to capture an image of your meal"
1021
+ onPress={takePicture}
1022
+ >
1023
+ <Text>Take Photo</Text>
1024
+ </TouchableOpacity>
1025
+
1026
+ <TouchableOpacity
1027
+ accessible={true}
1028
+ accessibilityRole="button"
1029
+ accessibilityLabel="Skip meal tracking"
1030
+ accessibilityHint="Continues to next screen without taking photo"
1031
+ onPress={onSkip}
1032
+ >
1033
+ <Text>Skip</Text>
1034
+ </TouchableOpacity>
1035
+ </View>
1036
+ );
1037
+ };
1038
+ ```
1039
+
1040
+ ---
1041
+
1042
+ ### 5. Performance
1043
+
1044
+ **Optimize for performance:**
1045
+ ```typescript
1046
+ // ✅ Memoize expensive computations
1047
+ const processedData = useMemo(() => {
1048
+ return expensiveCalculation(rawData);
1049
+ }, [rawData]);
1050
+
1051
+ // ✅ Debounce API calls
1052
+ const debouncedSearch = useMemo(
1053
+ () => debounce((query) => searchAPI(query), 500),
1054
+ []
1055
+ );
1056
+
1057
+ // ✅ Cancel pending requests on unmount
1058
+ useEffect(() => {
1059
+ const controller = new AbortController();
1060
+
1061
+ fetch('https://api.example.com/data', {
1062
+ signal: controller.signal
1063
+ });
1064
+
1065
+ return () => controller.abort();
1066
+ }, []);
1067
+
1068
+ // ✅ Lazy load heavy components
1069
+ const HeavyComponent = lazy(() => import('./HeavyComponent'));
1070
+
1071
+ // ✅ Use React.memo for expensive renders
1072
+ export const ExpensiveComponent = React.memo(({ data }) => {
1073
+ // Complex rendering logic
1074
+ });
1075
+ ```
1076
+
1077
+ ---
1078
+
1079
+ ### 6. Track Everything
1080
+
1081
+ **Comprehensive analytics tracking:**
1082
+ ```typescript
1083
+ // Screen lifecycle
1084
+ useEffect(() => {
1085
+ const startTime = Date.now();
1086
+
1087
+ analytics.track('screen_viewed', {
1088
+ screen_id: 'meal_tracker',
1089
+ screen_type: 'custom',
1090
+ timestamp: startTime
1091
+ });
1092
+
1093
+ return () => {
1094
+ analytics.track('screen_exited', {
1095
+ screen_id: 'meal_tracker',
1096
+ time_spent_ms: Date.now() - startTime
1097
+ });
1098
+ };
1099
+ }, []);
1100
+
1101
+ // User interactions
1102
+ analytics.track('camera_opened');
1103
+ analytics.track('photo_captured', { quality: 0.7 });
1104
+ analytics.track('photo_retaken');
1105
+ analytics.track('analysis_requested');
1106
+ analytics.track('analysis_completed', { calories: 450, confidence: 0.92 });
1107
+ analytics.track('result_viewed');
1108
+ analytics.track('screen_completed');
1109
+ analytics.track('screen_skipped', { reason: 'user_declined' });
1110
+
1111
+ // Errors
1112
+ analytics.track('camera_permission_denied');
1113
+ analytics.track('camera_error', { error: error.message });
1114
+ analytics.track('api_error', { endpoint: '/analyze', status: 500 });
1115
+ analytics.track('network_timeout');
1116
+
1117
+ // Performance
1118
+ analytics.track('api_latency', {
1119
+ endpoint: '/analyze',
1120
+ duration_ms: 2341,
1121
+ success: true
1122
+ });
1123
+ ```
1124
+
1125
+ ---
1126
+
1127
+ ## Common Patterns
1128
+
1129
+ ### Pattern 1: Permission Request
1130
+ ```typescript
1131
+ export const PermissionScreen = ({
1132
+ analytics,
1133
+ onNext,
1134
+ onSkip,
1135
+ preview
1136
+ }) => {
1137
+ const [permissionStatus, setPermissionStatus] = useState(null);
1138
+
1139
+ useEffect(() => {
1140
+ analytics.track('screen_viewed', {
1141
+ screen_id: 'location_permission'
1142
+ });
1143
+ }, []);
1144
+
1145
+ // Preview mode
1146
+ if (preview) {
1147
+ return (
1148
+ <View style={{ padding: 20, alignItems: 'center' }}>
1149
+ <Text style={{ fontSize: 64, marginBottom: 20 }}>📍</Text>
1150
+ <Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 12 }}>
1151
+ Location Access
1152
+ </Text>
1153
+ <Text style={{ fontSize: 16, color: '#666', textAlign: 'center' }}>
1154
+ We use your location to find nearby gyms and track outdoor workouts
1155
+ </Text>
1156
+ <Text style={{ fontSize: 14, color: '#999', marginTop: 12 }}>
1157
+ (Preview mode - no actual permission request)
1158
+ </Text>
1159
+ <View style={{ marginTop: 32, width: '100%', gap: 12 }}>
1160
+ <Button title="Allow (Mock)" onPress={onNext} />
1161
+ <Button title="Don't Allow (Mock)" onPress={onSkip} color="#666" />
1162
+ </View>
1163
+ </View>
1164
+ );
1165
+ }
1166
+
1167
+ // Real implementation
1168
+ const requestPermission = async () => {
1169
+ analytics.track('permission_requested', { type: 'location' });
1170
+
1171
+ try {
1172
+ const { status } = await Location.requestForegroundPermissionsAsync();
1173
+
1174
+ setPermissionStatus(status);
1175
+
1176
+ if (status === 'granted') {
1177
+ analytics.track('permission_granted', { type: 'location' });
1178
+ onNext();
1179
+ } else {
1180
+ analytics.track('permission_denied', { type: 'location' });
1181
+ // Show explanation or allow skip
1182
+ }
1183
+ } catch (error) {
1184
+ analytics.track('permission_error', {
1185
+ type: 'location',
1186
+ error: error.message
1187
+ });
1188
+ }
1189
+ };
1190
+
1191
+ if (permissionStatus === 'denied') {
1192
+ return (
1193
+ <View style={{ padding: 20 }}>
1194
+ <Text style={{ fontSize: 20, fontWeight: 'bold', marginBottom: 12 }}>
1195
+ Location Access Denied
1196
+ </Text>
1197
+ <Text style={{ marginBottom: 20, color: '#666' }}>
1198
+ You can enable location access later in Settings to use location features.
1199
+ </Text>
1200
+ <Button title="Continue Anyway" onPress={onNext} />
1201
+ </View>
1202
+ );
1203
+ }
1204
+
1205
+ return (
1206
+ <View style={{ padding: 20, alignItems: 'center' }}>
1207
+ <Text style={{ fontSize: 64, marginBottom: 20 }}>📍</Text>
1208
+ <Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 12 }}>
1209
+ Enable Location
1210
+ </Text>
1211
+ <Text style={{ fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 32 }}>
1212
+ We use your location to find nearby gyms and track outdoor workouts
1213
+ </Text>
1214
+ <Button title="Enable Location" onPress={requestPermission} />
1215
+ {onSkip && (
1216
+ <Button title="Skip for Now" onPress={onSkip} color="#666" />
1217
+ )}
1218
+ </View>
1219
+ );
1220
+ };
1221
+ ```
1222
+
1223
+ ---
1224
+
1225
+ ### Pattern 2: Multi-Step Process
1226
+ ```typescript
1227
+ export const MultiStepScreen = ({ analytics, onNext, preview }) => {
1228
+ const [step, setStep] = useState(1);
1229
+ const [data, setData] = useState({});
1230
+
1231
+ useEffect(() => {
1232
+ analytics.track('screen_viewed', {
1233
+ screen_id: 'multi_step',
1234
+ initial_step: 1
1235
+ });
1236
+ }, []);
1237
+
1238
+ useEffect(() => {
1239
+ analytics.track('step_viewed', {
1240
+ screen_id: 'multi_step',
1241
+ step: step
1242
+ });
1243
+ }, [step]);
1244
+
1245
+ const handleStep1Complete = (stepData) => {
1246
+ setData({ ...data, ...stepData });
1247
+ analytics.track('step_completed', { step: 1 });
1248
+ setStep(2);
1249
+ };
1250
+
1251
+ const handleStep2Complete = (stepData) => {
1252
+ setData({ ...data, ...stepData });
1253
+ analytics.track('step_completed', { step: 2 });
1254
+ setStep(3);
1255
+ };
1256
+
1257
+ const handleFinalComplete = (stepData) => {
1258
+ const finalData = { ...data, ...stepData };
1259
+ analytics.track('screen_completed', {
1260
+ screen_id: 'multi_step',
1261
+ total_steps: 3
1262
+ });
1263
+ onNext();
1264
+ };
1265
+
1266
+ return (
1267
+ <View style={{ flex: 1 }}>
1268
+ {/* Progress indicator */}
1269
+ <View style={styles.progressBar}>
1270
+ <View style={[styles.progress, { width: `${(step / 3) * 100}%` }]} />
1271
+ </View>
1272
+
1273
+ {/* Step content */}
1274
+ {step === 1 && <Step1 onComplete={handleStep1Complete} />}
1275
+ {step === 2 && <Step2 onComplete={handleStep2Complete} onBack={() => setStep(1)} />}
1276
+ {step === 3 && <Step3 onComplete={handleFinalComplete} onBack={() => setStep(2)} />}
1277
+ </View>
1278
+ );
1279
+ };
1280
+ ```
1281
+
1282
+ ---
1283
+
1284
+ ### Pattern 3: API Integration with Retry
1285
+ ```typescript
1286
+ export const APIIntegrationScreen = ({ analytics, onNext, preview }) => {
1287
+ const [loading, setLoading] = useState(false);
1288
+ const [error, setError] = useState(null);
1289
+ const [retryCount, setRetryCount] = useState(0);
1290
+ const maxRetries = 3;
1291
+
1292
+ const fetchData = async () => {
1293
+ setLoading(true);
1294
+ setError(null);
1295
+
1296
+ analytics.track('api_request_started', {
1297
+ attempt: retryCount + 1,
1298
+ max_attempts: maxRetries
1299
+ });
1300
+
1301
+ try {
1302
+ const response = await fetch('https://your-api.com/endpoint', {
1303
+ method: 'POST',
1304
+ headers: { 'Content-Type': 'application/json' },
1305
+ body: JSON.stringify({ /* data */ }),
1306
+ timeout: 10000
1307
+ });
1308
+
1309
+ if (!response.ok) {
1310
+ throw new Error(`HTTP ${response.status}`);
1311
+ }
1312
+
1313
+ const data = await response.json();
1314
+
1315
+ analytics.track('api_request_completed', {
1316
+ attempt: retryCount + 1,
1317
+ success: true
1318
+ });
1319
+
1320
+ // Success - move to next screen
1321
+ onNext();
1322
+
1323
+ } catch (error) {
1324
+ analytics.track('api_request_failed', {
1325
+ attempt: retryCount + 1,
1326
+ error: error.message,
1327
+ will_retry: retryCount < maxRetries
1328
+ });
1329
+
1330
+ if (retryCount < maxRetries) {
1331
+ // Retry with exponential backoff
1332
+ const delay = Math.pow(2, retryCount) * 1000;
1333
+ setTimeout(() => {
1334
+ setRetryCount(retryCount + 1);
1335
+ fetchData();
1336
+ }, delay);
1337
+ } else {
1338
+ // Max retries reached
1339
+ setError(error.message);
1340
+ }
1341
+ } finally {
1342
+ setLoading(false);
1343
+ }
1344
+ };
1345
+
1346
+ if (error) {
1347
+ return (
1348
+ <View style={{ padding: 20, alignItems: 'center' }}>
1349
+ <Text style={{ fontSize: 64, marginBottom: 20 }}>⚠️</Text>
1350
+ <Text style={{ fontSize: 20, fontWeight: 'bold', marginBottom: 12 }}>
1351
+ Connection Failed
1352
+ </Text>
1353
+ <Text style={{ color: '#666', marginBottom: 32, textAlign: 'center' }}>
1354
+ We couldn't connect to the server. Please check your internet connection and try again.
1355
+ </Text>
1356
+ <Button
1357
+ title="Try Again"
1358
+ onPress={() => {
1359
+ setRetryCount(0);
1360
+ fetchData();
1361
+ }}
1362
+ />
1363
+ <Button title="Skip for Now" onPress={onSkip} color="#666" />
1364
+ </View>
1365
+ );
1366
+ }
1367
+
1368
+ if (loading) {
1369
+ return (
1370
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
1371
+ <ActivityIndicator size="large" />
1372
+ <Text style={{ marginTop: 20 }}>
1373
+ {retryCount > 0 ? `Retrying... (${retryCount}/${maxRetries})` : 'Connecting...'}
1374
+ </Text>
1375
+ </View>
1376
+ );
1377
+ }
1378
+
1379
+ return (
1380
+ <View style={{ padding: 20 }}>
1381
+ <Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 20 }}>
1382
+ Sync Your Data
1383
+ </Text>
1384
+ <Button title="Connect" onPress={fetchData} />
1385
+ </View>
1386
+ );
1387
+ };
1388
+ ```
1389
+
1390
+ ---
1391
+
1392
+ ### Pattern 4: Form with Validation
1393
+ ```typescript
1394
+ export const FormScreen = ({ analytics, onNext, onDataUpdate }) => {
1395
+ const [formData, setFormData] = useState({
1396
+ name: '',
1397
+ email: '',
1398
+ phone: ''
1399
+ });
1400
+ const [errors, setErrors] = useState({});
1401
+
1402
+ const validate = () => {
1403
+ const newErrors = {};
1404
+
1405
+ if (!formData.name.trim()) {
1406
+ newErrors.name = 'Name is required';
1407
+ }
1408
+
1409
+ if (!formData.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
1410
+ newErrors.email = 'Invalid email address';
1411
+ }
1412
+
1413
+ if (!formData.phone.match(/^\d{10}$/)) {
1414
+ newErrors.phone = 'Phone must be 10 digits';
1415
+ }
1416
+
1417
+ setErrors(newErrors);
1418
+ return Object.keys(newErrors).length === 0;
1419
+ };
1420
+
1421
+ const handleSubmit = () => {
1422
+ analytics.track('form_submitted', {
1423
+ screen_id: 'contact_form'
1424
+ });
1425
+
1426
+ if (validate()) {
1427
+ analytics.track('form_valid', {
1428
+ fields_filled: Object.keys(formData).length
1429
+ });
1430
+
1431
+ onDataUpdate?.(formData);
1432
+ onNext();
1433
+ } else {
1434
+ analytics.track('form_invalid', {
1435
+ errors: Object.keys(errors)
1436
+ });
1437
+ }
1438
+ };
1439
+
1440
+ return (
1441
+ <View style={{ padding: 20 }}>
1442
+ <TextInput
1443
+ placeholder="Name"
1444
+ value={formData.name}
1445
+ onChangeText={(name) => setFormData({ ...formData, name })}
1446
+ />
1447
+ {errors.name && <Text style={styles.error}>{errors.name}</Text>}
1448
+
1449
+ <TextInput
1450
+ placeholder="Email"
1451
+ value={formData.email}
1452
+ keyboardType="email-address"
1453
+ onChangeText={(email) => setFormData({ ...formData, email })}
1454
+ />
1455
+ {errors.email && <Text style={styles.error}>{errors.email}</Text>}
1456
+
1457
+ <TextInput
1458
+ placeholder="Phone"
1459
+ value={formData.phone}
1460
+ keyboardType="phone-pad"
1461
+ onChangeText={(phone) => setFormData({ ...formData, phone })}
1462
+ />
1463
+ {errors.phone && <Text style={styles.error}>{errors.phone}</Text>}
1464
+
1465
+ <Button title="Continue" onPress={handleSubmit} />
1466
+ </View>
1467
+ );
1468
+ };
1469
+ ```
1470
+
1471
+ ---
1472
+
1473
+ ## Version Management
1474
+
1475
+ ### Understanding Version Requirements
1476
+
1477
+ Custom screens exist **in your app code**. If you add a new custom screen or update an existing one, you need an App Store update.
1478
+
1479
+ **Timeline:**
1480
+ ```
1481
+ Day 1: Add MealTrackerScreen to app v1.1.0, submit to App Store
1482
+ Day 3: App Store approves v1.1.0
1483
+ Day 5: 30% of users updated to v1.1.0
1484
+ Day 7: 60% of users updated to v1.1.0
1485
+ Day 14: 90% of users updated to v1.1.0
1486
+ ```
1487
+
1488
+ **Problem:** What happens if you publish the config with MealTrackerScreen on Day 4, when only 30% of users have the new app?
1489
+
1490
+ **Answer:** 70% of users will encounter an error (component not found).
1491
+
1492
+ ---
1493
+
1494
+ ### Solution: Minimum Version Requirements
1495
+
1496
+ Set a minimum app version in the dashboard:
1497
+ ```json
1498
+ {
1499
+ "id": "meal_tracker",
1500
+ "type": "custom",
1501
+ "custom_component_name": "MealTrackerScreen",
1502
+ "min_app_version": "1.1.0"
1503
+ }
1504
+ ```
1505
+
1506
+ **SDK behavior:**
1507
+ ```typescript
1508
+ // SDK checks version before rendering
1509
+ import { getVersion } from 'react-native-device-info';
1510
+
1511
+ const currentVersion = getVersion(); // e.g., "1.0.5"
1512
+
1513
+ if (screenConfig.min_app_version) {
1514
+ if (!meetsMinVersion(currentVersion, screenConfig.min_app_version)) {
1515
+ // Skip this screen for users on old app version
1516
+ analytics.track('screen_skipped_version_mismatch', {
1517
+ screen_id: screenConfig.id,
1518
+ current_version: currentVersion,
1519
+ required_version: screenConfig.min_app_version
1520
+ });
1521
+
1522
+ // Move to next screen
1523
+ return null;
1524
+ }
1525
+ }
1526
+
1527
+ // Render screen for users on new version
1528
+ return <CustomComponent />;
1529
+ ```
1530
+
1531
+ ---
1532
+
1533
+ ### Dashboard Version Check
1534
+
1535
+ The dashboard shows version distribution:
1536
+ ```
1537
+ ┌────────────────────────────────────────────────┐
1538
+ │ App Version Distribution │
1539
+ ├────────────────────────────────────────────────┤
1540
+ │ │
1541
+ │ v1.1.0: ████████████████████░░ 90% (9,000) │
1542
+ │ v1.0.5: ████░░░░░░░░░░░░░░░░░░ 8% (800) │
1543
+ │ v1.0.0: ░░░░░░░░░░░░░░░░░░░░░░ 2% (200) │
1544
+ │ │
1545
+ │ Custom Screen: MealTrackerScreen │
1546
+ │ Requires: v1.1.0+ │
1547
+ │ Coverage: 90% of users can see this screen │
1548
+ │ │
1549
+ │ ✅ Safe to publish (high coverage) │
1550
+ │ │
1551
+ │ [Publish Now] [Wait for 95% Coverage] │
1552
+ └────────────────────────────────────────────────┘
1553
+ ```
1554
+
1555
+ ---
1556
+
1557
+ ### Gradual Rollout Strategy
1558
+
1559
+ **Recommended workflow:**
1560
+
1561
+ 1. **Add custom screen to app** (v1.1.0)
1562
+ 2. **Submit to App Store**
1563
+ 3. **Wait for approval**
1564
+ 4. **Monitor adoption** (check dashboard version analytics)
1565
+ 5. **Wait until 90%+ users on v1.1.0**
1566
+ 6. **Then publish config** with min_app_version set
1567
+
1568
+ **This ensures:**
1569
+ - Minimal user disruption
1570
+ - High success rate
1571
+ - Good user experience
1572
+
1573
+ ---
1574
+
1575
+ ### Handling Missing Components
1576
+
1577
+ **If user is on old app without the component:**
1578
+ ```typescript
1579
+ // SDK gracefully handles missing components
1580
+ const CustomComponent = customComponents[screenConfig.custom_component_name];
1581
+
1582
+ if (!CustomComponent) {
1583
+ analytics.track('custom_component_missing', {
1584
+ component_name: screenConfig.custom_component_name,
1585
+ app_version: getVersion()
1586
+ });
1587
+
1588
+ // Show update prompt
1589
+ return (
1590
+ <View style={styles.updatePrompt}>
1591
+ <Text style={styles.updateTitle}>Update Required</Text>
1592
+ <Text style={styles.updateMessage}>
1593
+ This feature requires the latest version of the app.
1594
+ Please update to continue.
1595
+ </Text>
1596
+ <Button
1597
+ title="Update Now"
1598
+ onPress={() => Linking.openURL('app-store-link')}
1599
+ />
1600
+ <Button
1601
+ title="Skip for Now"
1602
+ onPress={onSkip}
1603
+ color="#666"
1604
+ />
1605
+ </View>
1606
+ );
1607
+ }
1608
+
1609
+ return <CustomComponent {...props} />;
1610
+ ```
1611
+
1612
+ ---
1613
+
1614
+ ## Troubleshooting
1615
+
1616
+ ### Component Not Rendering
1617
+
1618
+ **Problem:** Custom screen doesn't appear in the app.
1619
+
1620
+ **Checklist:**
1621
+
1622
+ 1. **Is the component registered?**
1623
+ ```typescript
1624
+ <OnboardingFlow
1625
+ customComponents={{
1626
+ MealTrackerScreen: MealTrackerScreen // ← Must be here
1627
+ }}
1628
+ />
1629
+ ```
1630
+
1631
+ 2. **Does the name match exactly?**
1632
+ ```typescript
1633
+ // Dashboard config
1634
+ "custom_component_name": "MealTrackerScreen"
1635
+
1636
+ // Component registration
1637
+ customComponents={{
1638
+ MealTrackerScreen: MealTrackerScreen // ← Must match exactly (case-sensitive)
1639
+ }}
1640
+ ```
1641
+
1642
+ 3. **Is the component exported?**
1643
+ ```typescript
1644
+ // ✅ Correct
1645
+ export const MealTrackerScreen = ({ ... }) => { ... };
1646
+
1647
+ // ❌ Wrong (default export)
1648
+ export default MealTrackerScreen;
1649
+
1650
+ // ❌ Wrong (not exported)
1651
+ const MealTrackerScreen = ({ ... }) => { ... };
1652
+ ```
1653
+
1654
+ 4. **Is the app version sufficient?**
1655
+ - Check if `min_app_version` is set
1656
+ - Verify user's app meets minimum version
1657
+
1658
+ 5. **Check console for errors:**
1659
+ ```
1660
+ - "CustomComponent not found: MealTrackerScreen"
1661
+ - Import errors
1662
+ - Syntax errors in component
1663
+ ```
1664
+
1665
+ ---
1666
+
1667
+ ### Preview Not Showing
1668
+
1669
+ **Problem:** Preview shows blank/error in dashboard.
1670
+
1671
+ **Solutions:**
1672
+
1673
+ 1. **Implement preview mode:**
1674
+ ```typescript
1675
+ if (preview) {
1676
+ return <PreviewPlaceholder />;
1677
+ }
1678
+ ```
1679
+
1680
+ 2. **Check for native API calls:**
1681
+ - Camera, Location, Biometrics won't work in browser
1682
+ - Always check `preview` prop before using native features
1683
+
1684
+ 3. **Test in browser console:**
1685
+ - Open browser dev tools
1686
+ - Look for errors
1687
+ - Check network requests
1688
+
1689
+ ---
1690
+
1691
+ ### Analytics Not Tracking
1692
+
1693
+ **Problem:** Events from custom screen not appearing in dashboard.
1694
+
1695
+ **Solutions:**
1696
+
1697
+ 1. **Verify analytics calls:**
1698
+ ```typescript
1699
+ // ✅ Correct
1700
+ analytics.track('screen_viewed', { ... });
1701
+
1702
+ // ❌ Wrong
1703
+ Analytics.track('screen_viewed', { ... }); // Wrong import
1704
+ this.analytics.track('screen_viewed', { ... }); // Wrong usage
1705
+ ```
1706
+
1707
+ 2. **Check prop is passed:**
1708
+ ```typescript
1709
+ export const MyScreen = ({ analytics }) => {
1710
+ console.log('Analytics:', analytics); // Should not be undefined
1711
+ ```
1712
+
1713
+ 3. **Track on mount:**
1714
+ ```typescript
1715
+ useEffect(() => {
1716
+ analytics.track('screen_viewed', { ... });
1717
+ }, []); // Empty dependency array
1718
+ ```
1719
+
1720
+ ---
1721
+
1722
+ ### onNext Not Working
1723
+
1724
+ **Problem:** Clicking continue button doesn't navigate.
1725
+
1726
+ **Solutions:**
1727
+
1728
+ 1. **Verify onNext is called:**
1729
+ ```typescript
1730
+ const handleContinue = () => {
1731
+ console.log('Continue clicked'); // Debug log
1732
+ analytics.track('screen_completed');
1733
+ onNext(); // ← Make sure this is called
1734
+ };
1735
+
1736
+ <Button title="Continue" onPress={handleContinue} />
1737
+ ```
1738
+
1739
+ 2. **Check for async issues:**
1740
+ ```typescript
1741
+ // ❌ Wrong (onNext not awaited)
1742
+ const handleContinue = async () => {
1743
+ await saveData();
1744
+ onNext(); // Might not execute if error above
1745
+ };
1746
+
1747
+ // ✅ Correct (use try/finally)
1748
+ const handleContinue = async () => {
1749
+ try {
1750
+ await saveData();
1751
+ } finally {
1752
+ onNext(); // Always executes
1753
+ }
1754
+ };
1755
+ ```
1756
+
1757
+ ---
1758
+
1759
+ ## Advanced Topics
1760
+
1761
+ ### Conditional Display
1762
+
1763
+ Use platform config to show/hide custom screens based on user data:
1764
+ ```json
1765
+ {
1766
+ "screens": [
1767
+ {
1768
+ "id": "welcome",
1769
+ "type": "welcome_screen"
1770
+ },
1771
+ {
1772
+ "id": "meal_tracker",
1773
+ "type": "custom",
1774
+ "custom_component_name": "MealTrackerScreen",
1775
+ "conditions": {
1776
+ "show_if": {
1777
+ "all": [
1778
+ {"variable": "interested_in_nutrition", "equals": true},
1779
+ {"variable": "has_camera_permission", "equals": true}
1780
+ ]
1781
+ }
1782
+ }
1783
+ }
1784
+ ]
1785
+ }
1786
+ ```
1787
+
1788
+ ---
1789
+
1790
+ ### Data Persistence
1791
+
1792
+ Custom screens can use AsyncStorage for local persistence:
1793
+ ```typescript
1794
+ import AsyncStorage from '@react-native-async-storage/async-storage';
1795
+
1796
+ export const DataCollectionScreen = ({ analytics, onNext, onDataUpdate }) => {
1797
+ const [data, setData] = useState({});
1798
+
1799
+ // Load persisted data on mount
1800
+ useEffect(() => {
1801
+ const loadData = async () => {
1802
+ const saved = await AsyncStorage.getItem('custom_screen_data');
1803
+ if (saved) {
1804
+ setData(JSON.parse(saved));
1805
+ }
1806
+ };
1807
+ loadData();
1808
+ }, []);
1809
+
1810
+ // Save data on change
1811
+ const updateData = async (newData) => {
1812
+ setData(newData);
1813
+ await AsyncStorage.setItem('custom_screen_data', JSON.stringify(newData));
1814
+ onDataUpdate?.(newData);
1815
+ };
1816
+
1817
+ // Clear on complete
1818
+ const handleComplete = async () => {
1819
+ await AsyncStorage.removeItem('custom_screen_data');
1820
+ onNext();
1821
+ };
1822
+
1823
+ return (
1824
+ <View>
1825
+ {/* Form fields */}
1826
+ <Button title="Continue" onPress={handleComplete} />
1827
+ </View>
1828
+ );
1829
+ };
1830
+ ```
1831
+
1832
+ ---
1833
+
1834
+ ### Deep Linking
1835
+
1836
+ Handle deep links within custom screens:
1837
+ ```typescript
1838
+ import { Linking } from 'react-native';
1839
+
1840
+ export const AuthScreen = ({ analytics, onNext }) => {
1841
+ useEffect(() => {
1842
+ const handleDeepLink = (event) => {
1843
+ const { url } = event;
1844
+
1845
+ // Parse auth callback
1846
+ if (url.includes('/auth/callback')) {
1847
+ const token = extractTokenFromUrl(url);
1848
+
1849
+ analytics.track('auth_completed', {
1850
+ method: 'oauth',
1851
+ provider: 'google'
1852
+ });
1853
+
1854
+ // Save token and continue
1855
+ saveAuthToken(token);
1856
+ onNext();
1857
+ }
1858
+ };
1859
+
1860
+ Linking.addEventListener('url', handleDeepLink);
1861
+
1862
+ return () => {
1863
+ Linking.removeEventListener('url', handleDeepLink);
1864
+ };
1865
+ }, []);
1866
+
1867
+ const initiateOAuth = async () => {
1868
+ const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth?...';
1869
+ await Linking.openURL(authUrl);
1870
+ };
1871
+
1872
+ return (
1873
+ <View>
1874
+ <Button title="Sign in with Google" onPress={initiateOAuth} />
1875
+ </View>
1876
+ );
1877
+ };
1878
+ ```
1879
+
1880
+ ---
1881
+
1882
+ ### Using Context
1883
+
1884
+ Share data across multiple custom screens:
1885
+ ```typescript
1886
+ // OnboardingContext.tsx
1887
+ import React, { createContext, useContext, useState } from 'react';
1888
+
1889
+ const OnboardingContext = createContext(null);
1890
+
1891
+ export const OnboardingProvider = ({ children }) => {
1892
+ const [sharedData, setSharedData] = useState({});
1893
+
1894
+ return (
1895
+ <OnboardingContext.Provider value={{ sharedData, setSharedData }}>
1896
+ {children}
1897
+ </OnboardingContext.Provider>
1898
+ );
1899
+ };
1900
+
1901
+ export const useOnboarding = () => useContext(OnboardingContext);
1902
+
1903
+ // App.tsx
1904
+ <OnboardingProvider>
1905
+ <OnboardingFlow
1906
+ customComponents={{ Screen1, Screen2 }}
1907
+ />
1908
+ </OnboardingProvider>
1909
+
1910
+ // Screen1.tsx
1911
+ export const Screen1 = ({ onNext }) => {
1912
+ const { sharedData, setSharedData } = useOnboarding();
1913
+
1914
+ const handleContinue = () => {
1915
+ setSharedData({ ...sharedData, step1Complete: true });
1916
+ onNext();
1917
+ };
1918
+
1919
+ return <View>...</View>;
1920
+ };
1921
+
1922
+ // Screen2.tsx
1923
+ export const Screen2 = ({ onNext }) => {
1924
+ const { sharedData } = useOnboarding();
1925
+
1926
+ // Access data from Screen1
1927
+ console.log(sharedData.step1Complete); // true
1928
+
1929
+ return <View>...</View>;
1930
+ };
1931
+ ```
1932
+
1933
+ ---
1934
+
1935
+ ## Summary
1936
+
1937
+ Custom screens give you the flexibility to build complex, native-integrated onboarding experiences while still benefiting from our platform's analytics, A/B testing, and flow management.
1938
+
1939
+ **Key takeaways:**
1940
+
1941
+ 1. ✅ Custom screens live in YOUR app code
1942
+ 2. ✅ Changes require App Store approval
1943
+ 3. ✅ Always implement preview mode for dashboard
1944
+ 4. ✅ Track events comprehensively with analytics
1945
+ 5. ✅ Handle errors gracefully
1946
+ 6. ✅ Use min_app_version for gradual rollout
1947
+ 7. ✅ Custom screens don't count toward MAU limits
1948
+
1949
+ **Next steps:**
1950
+
1951
+ - Create your first custom screen using the Quick Start guide
1952
+ - Test locally with preview mode
1953
+ - Deploy to App Store
1954
+ - Monitor analytics in dashboard
1955
+ - Iterate based on user behavior
1956
+
1957
+ **Need help?**
1958
+ - 📖 Full documentation: https://docs.yourplatform.com
1959
+ - 💬 Discord community: https://discord.gg/yourplatform
1960
+ - ✉️ Support: support@yourplatform.com
1961
+
1962
+ ---
1963
+
1964
+ **Happy building! 🚀**