Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 160 additions & 3 deletions app/onboarding/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,173 @@
// app/onboarding/_layout.tsx
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { useRouter } from 'expo-router';

interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}

// Define the fallback component BEFORE the error boundary class that uses it
const OnboardingErrorFallback = React.memo(({ error, onRetry }: { error: Error | null; onRetry: () => void }) => {
const router = useRouter();

React.useEffect(() => {
console.error('[OnboardingErrorFallback] Rendering error fallback for:', error?.message);
}, [error]);

const handleGoHome = React.useCallback(() => {
console.log('[OnboardingErrorFallback] Navigating to home');
try {
router.replace('/');
} catch (err) {
console.error('[OnboardingErrorFallback] Failed to navigate home:', err);
}
}, [router]);

const handleRetry = React.useCallback(() => {
console.log('[OnboardingErrorFallback] Retrying onboarding');
onRetry();
}, [onRetry]);

return (
<View style={styles.errorContainer}>
<Text style={styles.errorTitle}>Oops! Something went wrong</Text>
<Text style={styles.errorMessage}>
We encountered an issue while loading the onboarding screen.
</Text>
{error && (
<Text style={styles.errorDetails}>
Error: {error.message}
</Text>
)}

<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.retryButton} onPress={handleRetry}>
<Text style={styles.retryButtonText}>Try Again</Text>
</TouchableOpacity>

<TouchableOpacity style={styles.homeButton} onPress={handleGoHome}>
<Text style={styles.homeButtonText}>Go to Home</Text>
</TouchableOpacity>
</View>

<Text style={styles.debugInfo}>
If this issue persists, please report it with this error message.
</Text>
</View>
);
});

class OnboardingErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('[OnboardingErrorBoundary] Error caught:', error);
console.error('[OnboardingErrorBoundary] Error info:', errorInfo);
}

render() {
if (this.state.hasError) {
return <OnboardingErrorFallback error={this.state.error} onRetry={() => this.setState({ hasError: false, error: null })} />;
}

return this.props.children;
}
}

export default function OnboardingLayout() {
return (
<>
<OnboardingErrorBoundary>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="age" options={{ headerShown: false }} />
<Stack.Screen name="child-safety" options={{ headerShown: false }} />
<Stack.Screen name="demo" options={{ headerShown: false }} />
<Stack.Screen name="features" options={{ headerShown: false }} />
<Stack.Screen name="userType" options={{ headerShown: false }} />
<Stack.Screen name="protocol" options={{ headerShown: false }} />
</Stack>
<StatusBar style="auto" />
</>
</OnboardingErrorBoundary>
);
}
}

const styles = StyleSheet.create({
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
backgroundColor: '#FFFFFF',
},
errorTitle: {
fontSize: 24,
fontWeight: '700',
color: '#D32F2F',
marginBottom: 16,
textAlign: 'center',
},
errorMessage: {
fontSize: 16,
color: '#424242',
textAlign: 'center',
marginBottom: 12,
lineHeight: 22,
},
errorDetails: {
fontSize: 14,
color: '#757575',
textAlign: 'center',
marginBottom: 32,
fontFamily: 'monospace',
padding: 12,
backgroundColor: '#F5F5F5',
borderRadius: 8,
},
buttonContainer: {
flexDirection: 'row',
gap: 16,
marginBottom: 32,
},
retryButton: {
backgroundColor: '#007AFF',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
retryButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
homeButton: {
backgroundColor: '#34C759',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
homeButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
debugInfo: {
fontSize: 12,
color: '#9E9E9E',
textAlign: 'center',
fontStyle: 'italic',
},
});
118 changes: 82 additions & 36 deletions app/onboarding/age.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert, Modal, ScrollView } from 'react-native';
import Animated, { FadeIn, FadeInDown } from 'react-native-reanimated';
import { useRouter } from 'expo-router';
Expand All @@ -22,6 +22,26 @@ export default function BirthDateCollection() {
const [selectedYear, setSelectedYear] = useState('');
const [validationError, setValidationError] = useState('');
const [showSafetyModal, setShowSafetyModal] = useState(false);
const [isNavigating, setIsNavigating] = useState(false);
const [debugInfo, setDebugInfo] = useState<string[]>([]);

useEffect(() => {
console.log('[BirthDateCollection] Component mounted');
setDebugInfo(['Birth date collection screen loaded']);
}, []);

const addDebugInfo = useCallback((info: string) => {
console.log(`[BirthDateCollection] ${info}`);
setDebugInfo(prev => [...prev, `${new Date().toISOString()}: ${info}`]);
}, []);

const showDebugInfo = useCallback(() => {
Alert.alert(
'Debug Information',
debugInfo.slice(-10).join('\n'),
[{ text: 'OK' }]
);
}, [debugInfo]);

// Calculate if current selection is valid
const isComplete = selectedMonth && selectedDay && selectedYear;
Expand Down Expand Up @@ -62,7 +82,7 @@ export default function BirthDateCollection() {
}
}, [selectedDay, selectedMonth]);

const handleContinue = useCallback(() => {
const handleContinue = useCallback(async () => {
if (!isComplete) {
setValidationError('Please select your complete birth date');
return;
Expand All @@ -73,37 +93,63 @@ export default function BirthDateCollection() {
return;
}

const age = calculateAge(birthDate);

// Log analytics
logAnalyticsEvent(ANALYTICS_EVENTS.BIRTH_DATE_COLLECTION_COMPLETED, {
age,
birth_year: selectedYear,
birth_month: selectedMonth,
age_range: age < 18 ? 'minor' : age < 65 ? 'adult' : 'senior'
});

// Route based on age (same logic as before)
if (age < 18) {
// Route to child safety screen for minors
router.push({
pathname: '/onboarding/child-safety',
params: {
age: age.toString(),
birthDate: birthDate
}
});
} else {
// Route to demo for adults
router.push({
pathname: '/onboarding/demo',
params: {
age: age.toString(),
birthDate: birthDate
}
try {
setIsNavigating(true);
setValidationError('');

const age = calculateAge(birthDate);
addDebugInfo(`User age calculated: ${age}`);

// Log analytics
logAnalyticsEvent(ANALYTICS_EVENTS.BIRTH_DATE_COLLECTION_COMPLETED, {
age,
birth_year: selectedYear,
birth_month: selectedMonth,
age_range: age < 18 ? 'minor' : age < 65 ? 'adult' : 'senior'
});

addDebugInfo(`Analytics logged for age: ${age}`);

// Route based on age with error handling
if (age < 18) {
addDebugInfo('Routing to child safety screen (minor)');
router.push({
pathname: '/onboarding/child-safety',
params: {
age: age.toString(),
birthDate: birthDate
}
});
} else {
addDebugInfo('Routing to demo screen (adult)');
router.push({
pathname: '/onboarding/demo',
params: {
age: age.toString(),
birthDate: birthDate
}
});
}

addDebugInfo('Navigation call completed successfully');

} catch (error) {
setIsNavigating(false);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
addDebugInfo(`Navigation error: ${errorMessage}`);
console.error('[BirthDateCollection] Navigation error:', error);

Alert.alert(
'Navigation Error',
`Failed to continue: ${errorMessage}\n\nWould you like to try again or report this issue?`,
[
{ text: 'Retry', onPress: () => handleContinue() },
{ text: 'Report Issue', onPress: () => showDebugInfo() },
{ text: 'Cancel', style: 'cancel' }
]
);
}
}, [isComplete, isValid, validation, birthDate, selectedYear, selectedMonth, router]);
}, [isComplete, isValid, validation, birthDate, selectedYear, selectedMonth, router, addDebugInfo, isNavigating]);

const handleSkip = useCallback(() => {
// Show safety explanation modal instead of directly skipping
Expand Down Expand Up @@ -233,22 +279,22 @@ export default function BirthDateCollection() {
style={[
styles.continueButton,
isMobileWeb && styles.continueButtonMobile,
!isValid && styles.continueButtonDisabled
(!isValid || isNavigating) && styles.continueButtonDisabled
]}
onPress={handleContinue}
disabled={!isValid}
disabled={!isValid || isNavigating}
accessibilityRole="button"
accessibilityLabel="Continue with birth date"
accessibilityHint="Proceed to next step"
>
<Text style={[
styles.continueButtonText,
isMobileWeb && styles.continueButtonTextMobile,
!isValid && styles.continueButtonTextDisabled
(!isValid || isNavigating) && styles.continueButtonTextDisabled
]}>
Continue
{isNavigating ? 'Loading...' : 'Continue'}
</Text>
<ArrowRight size={isMobileWeb ? 18 : 20} color={isValid ? "#FFFFFF" : "#8E8E93"} />
{!isNavigating && <ArrowRight size={isMobileWeb ? 18 : 20} color={isValid ? "#FFFFFF" : "#8E8E93"} />}
</TouchableOpacity>

<TouchableOpacity
Expand Down
Loading