diff --git a/app/onboarding/navigation-flow.test.ts b/__tests__/onboarding/navigation-flow.test
similarity index 100%
rename from app/onboarding/navigation-flow.test.ts
rename to __tests__/onboarding/navigation-flow.test
diff --git a/app/onboarding/userType.integration.test.ts b/__tests__/onboarding/userType.integration.test.ts
similarity index 100%
rename from app/onboarding/userType.integration.test.ts
rename to __tests__/onboarding/userType.integration.test.ts
diff --git a/app/onboarding/userType.performance.test.ts b/__tests__/onboarding/userType.performance.test.ts
similarity index 100%
rename from app/onboarding/userType.performance.test.ts
rename to __tests__/onboarding/userType.performance.test.ts
diff --git a/app/onboarding/userType.recovery.test.ts b/__tests__/onboarding/userType.recovery.test.ts
similarity index 100%
rename from app/onboarding/userType.recovery.test.ts
rename to __tests__/onboarding/userType.recovery.test.ts
diff --git a/app/onboarding/_layout.tsx b/app/onboarding/_layout.tsx
index e66ad4f6..e67df79d 100644
--- a/app/onboarding/_layout.tsx
+++ b/app/onboarding/_layout.tsx
@@ -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 (
+
+ Oops! Something went wrong
+
+ We encountered an issue while loading the onboarding screen.
+
+ {error && (
+
+ Error: {error.message}
+
+ )}
+
+
+
+ Try Again
+
+
+
+ Go to Home
+
+
+
+
+ If this issue persists, please report it with this error message.
+
+
+ );
+});
+
+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 this.setState({ hasError: false, error: null })} />;
+ }
+
+ return this.props.children;
+ }
+}
export default function OnboardingLayout() {
return (
- <>
+
+
+
+
+
- >
+
);
-}
\ No newline at end of file
+}
+
+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',
+ },
+});
\ No newline at end of file
diff --git a/app/onboarding/age.tsx b/app/onboarding/age.tsx
index b0982abb..528cbd15 100644
--- a/app/onboarding/age.tsx
+++ b/app/onboarding/age.tsx
@@ -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';
@@ -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([]);
+
+ 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;
@@ -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;
@@ -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
@@ -233,10 +279,10 @@ 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"
@@ -244,11 +290,11 @@ export default function BirthDateCollection() {
- Continue
+ {isNavigating ? 'Loading...' : 'Continue'}
-
+ {!isNavigating && }
([]);
- const handleStart = useCallback(() => {
- router.push('/onboarding/age');
- }, [router]);
+ useEffect(() => {
+ console.log('[Welcome] Component mounted');
+ setDebugInfo(prev => [...prev, `Welcome screen loaded at ${new Date().toISOString()}`]);
+ }, []);
+
+ const addDebugInfo = useCallback((info: string) => {
+ console.log(`[Welcome] ${info}`);
+ setDebugInfo(prev => [...prev, `${new Date().toISOString()}: ${info}`]);
+ }, []);
+
+ const handleStart = useCallback(async () => {
+ try {
+ setIsNavigating(true);
+ addDebugInfo('User clicked Try Now button');
+
+ // Add a small delay to show loading state
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ addDebugInfo('Attempting navigation to /onboarding/age');
+ router.push('/onboarding/age');
+ addDebugInfo('Navigation call completed');
+
+ } catch (error) {
+ setIsNavigating(false);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ addDebugInfo(`Navigation error: ${errorMessage}`);
+ console.error('[Welcome] Navigation error:', error);
+
+ Alert.alert(
+ 'Navigation Error',
+ `Failed to start onboarding: ${errorMessage}\n\nDebug info:\n${debugInfo.slice(-3).join('\n')}`,
+ [
+ { text: 'Retry', onPress: () => handleStart() },
+ { text: 'Report Issue', onPress: () => showDebugInfo() }
+ ]
+ );
+ }
+ }, [router, addDebugInfo, debugInfo]);
const handleImageError = useCallback(() => {
+ addDebugInfo('Main image failed to load, using fallback');
setImageError(true);
- }, []);
+ }, [addDebugInfo]);
+
+ const showDebugInfo = useCallback(() => {
+ Alert.alert(
+ 'Debug Information',
+ debugInfo.slice(-10).join('\n'),
+ [{ text: 'OK' }]
+ );
+ }, [debugInfo]);
return (
@@ -46,15 +92,33 @@ export default function Welcome() {
- Try Now
-
+
+ {isNavigating ? 'Starting...' : 'Try Now'}
+
+ {!isNavigating && }
No account needed
+
+ {/* Debug button for development */}
+ {__DEV__ && (
+
+ Debug Info ({debugInfo.length})
+
+ )}
@@ -130,6 +194,21 @@ const styles = StyleSheet.create({
color: 'rgba(255, 255, 255, 0.8)',
textAlign: 'center',
},
+ buttonDisabled: {
+ opacity: 0.7,
+ },
+ debugButton: {
+ marginTop: 16,
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderRadius: 4,
+ },
+ debugButtonText: {
+ color: 'rgba(255, 255, 255, 0.9)',
+ fontSize: 12,
+ textAlign: 'center',
+ },
// Mobile-specific styles
contentMobile: {
paddingHorizontal: 16,
diff --git a/app/onboarding/protocol.tsx b/app/onboarding/protocol.tsx
new file mode 100644
index 00000000..54ee06fa
--- /dev/null
+++ b/app/onboarding/protocol.tsx
@@ -0,0 +1,862 @@
+import React, { useState, useCallback } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, ScrollView, TextInput, Alert } from 'react-native';
+import Animated, { FadeIn, FadeInDown, FadeInRight } from 'react-native-reanimated';
+import { useRouter } from 'expo-router';
+import { Check, ArrowRight, ArrowLeft, Syringe, Pill, Activity, Plus } from 'lucide-react-native';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { useUserProfile } from '@/contexts/UserProfileContext';
+import { useAuth } from '@/contexts/AuthContext';
+import { Protocol, ProtocolType, PROTOCOL_TEMPLATES, ProtocolTemplate } from '@/types/protocol';
+import { logAnalyticsEvent, ANALYTICS_EVENTS } from '@/lib/analytics';
+import { isMobileWeb } from '@/lib/utils';
+
+interface ProtocolSetupData {
+ type: ProtocolType | null;
+ medication: string;
+ dosage: string;
+ unit: 'mg' | 'mL' | 'IU' | 'mcg';
+ frequency: 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'custom';
+ customFrequencyDays?: number;
+}
+
+export default function ProtocolSetup() {
+ const router = useRouter();
+ const { user } = useAuth();
+ const { profile, saveProfile } = useUserProfile();
+ const [currentStep, setCurrentStep] = useState(0);
+ const [isCompleting, setIsCompleting] = useState(false);
+ const [errorMessage, setErrorMessage] = useState(null);
+ const [fieldErrors, setFieldErrors] = useState<{[key: string]: string}>({});
+ const [setupData, setSetupData] = useState({
+ type: null,
+ medication: '',
+ dosage: '',
+ unit: 'mg',
+ frequency: 'weekly',
+ });
+
+ // Log analytics when step starts
+ React.useEffect(() => {
+ logAnalyticsEvent(ANALYTICS_EVENTS.ONBOARDING_STEP_START, {
+ step: currentStep + 1,
+ step_name: getStepName(currentStep),
+ flow: 'protocol_setup'
+ });
+ }, [currentStep]);
+
+ const getStepName = (step: number): string => {
+ switch (step) {
+ case 0: return 'protocol_type_selection';
+ case 1: return 'medication_details';
+ default: return 'unknown';
+ }
+ };
+
+ const handleProtocolTypeSelect = useCallback((type: ProtocolType) => {
+ setSetupData(prev => ({ ...prev, type }));
+
+ // Log the selection
+ logAnalyticsEvent(ANALYTICS_EVENTS.ONBOARDING_STEP_COMPLETE, {
+ step: currentStep + 1,
+ step_name: getStepName(currentStep),
+ question: 'protocol_type',
+ answer: type,
+ flow: 'protocol_setup'
+ });
+ }, [currentStep]);
+
+ const handleNext = useCallback(() => {
+ if (currentStep < 1) {
+ setCurrentStep(currentStep + 1);
+ } else {
+ handleComplete();
+ }
+ }, [currentStep]);
+
+ const handleBack = useCallback(() => {
+ if (currentStep > 0) {
+ setCurrentStep(currentStep - 1);
+ } else {
+ // Go back to user type screen
+ router.back();
+ }
+ }, [currentStep, router]);
+
+ const handleSkipProtocol = useCallback(async () => {
+ try {
+ console.log('[ProtocolSetup] User chose to skip protocol setup');
+
+ // Update profile to indicate no protocol setup
+ if (profile) {
+ const updatedProfile = {
+ ...profile,
+ hasSetupProtocol: false,
+ };
+ await saveProfile(updatedProfile);
+ }
+
+ // Log skip event
+ logAnalyticsEvent('protocol_setup_skipped', {
+ flow: 'protocol_setup'
+ });
+
+ // Navigate to main app
+ router.replace('/(tabs)/new-dose');
+ } catch (error) {
+ console.error('[ProtocolSetup] Error skipping protocol:', error);
+ router.replace('/(tabs)/new-dose');
+ }
+ }, [profile, saveProfile, router]);
+
+ const handleComplete = useCallback(async () => {
+ try {
+ setIsCompleting(true);
+ setErrorMessage(null);
+ console.log('[ProtocolSetup] Starting protocol completion...');
+
+ // Validate required fields
+ const validation = validateCurrentStep();
+ if (!validation.isValid) {
+ setFieldErrors(validation.errors);
+ setErrorMessage('Please fix the errors below before continuing.');
+ setIsCompleting(false);
+ return;
+ }
+
+ // Clear any previous errors
+ setFieldErrors({});
+ setErrorMessage(null);
+
+ // Create protocol object
+ const protocol: Protocol = {
+ id: `protocol_${Date.now()}`,
+ name: PROTOCOL_TEMPLATES.find(t => t.type === setupData.type)?.name || 'Custom Protocol',
+ medication: setupData.medication.trim(),
+ dosage: setupData.dosage.trim(),
+ unit: setupData.unit,
+ frequency: setupData.frequency,
+ customFrequencyDays: setupData.customFrequencyDays,
+ startDate: new Date().toISOString(),
+ isActive: true,
+ dateCreated: new Date().toISOString(),
+ userId: user?.uid,
+ };
+
+ // Save protocol to storage
+ const existingProtocols = await AsyncStorage.getItem('userProtocols');
+ const protocols = existingProtocols ? JSON.parse(existingProtocols) : [];
+ protocols.push(protocol);
+ await AsyncStorage.setItem('userProtocols', JSON.stringify(protocols));
+
+ // Update user profile to indicate protocol setup is complete
+ if (profile) {
+ const updatedProfile = {
+ ...profile,
+ hasSetupProtocol: true,
+ };
+ await saveProfile(updatedProfile);
+ }
+
+ // Log completion
+ logAnalyticsEvent('protocol_setup_complete', {
+ protocol_type: setupData.type,
+ medication: setupData.medication,
+ frequency: setupData.frequency,
+ unit: setupData.unit,
+ flow: 'protocol_setup'
+ });
+
+ console.log('[ProtocolSetup] Protocol setup completed successfully');
+
+ // Small delay to show completion state
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ // Navigate to main app
+ router.replace('/(tabs)/new-dose');
+ } catch (error) {
+ console.error('[ProtocolSetup] Error completing protocol setup:', error);
+
+ let errorMsg = 'An unexpected error occurred. Please try again.';
+ if (error instanceof Error) {
+ if (error.message.includes('network') || error.message.includes('fetch')) {
+ errorMsg = 'Connection error. Please check your internet and try again.';
+ } else if (error.message.includes('storage') || error.message.includes('quota')) {
+ errorMsg = 'Storage error. Please try clearing some space and try again.';
+ }
+ }
+
+ setErrorMessage(errorMsg);
+ setIsCompleting(false);
+ }
+ }, [setupData, user?.uid, profile, saveProfile, router, validateCurrentStep]);
+
+ const validateCurrentStep = (): { isValid: boolean; errors: {[key: string]: string} } => {
+ const errors: {[key: string]: string} = {};
+
+ switch (currentStep) {
+ case 0:
+ if (!setupData.type) {
+ errors.type = 'Please select a protocol type';
+ }
+ break;
+ case 1:
+ if (!setupData.medication.trim()) {
+ errors.medication = 'Medication name is required';
+ }
+ if (!setupData.dosage.trim()) {
+ errors.dosage = 'Dosage amount is required';
+ } else {
+ // Validate that dosage is a valid number
+ const dosageNum = parseFloat(setupData.dosage.trim());
+ if (isNaN(dosageNum) || dosageNum <= 0) {
+ errors.dosage = 'Please enter a valid dosage amount';
+ }
+ }
+ break;
+ }
+
+ return {
+ isValid: Object.keys(errors).length === 0,
+ errors
+ };
+ };
+
+ const isCurrentStepComplete = (): boolean => {
+ const validation = validateCurrentStep();
+ return validation.isValid;
+ };
+
+ const getSelectedTemplate = (): ProtocolTemplate | null => {
+ return PROTOCOL_TEMPLATES.find(t => t.type === setupData.type) || null;
+ };
+
+ const renderProtocolTypeStep = () => (
+
+ Choose Your Protocol Type
+
+ Select the type of medication protocol you're following for personalized guidance.
+
+
+
+ {PROTOCOL_TEMPLATES.map((template) => {
+ const isSelected = setupData.type === template.type;
+ const IconComponent = template.type === 'trt' ? Activity :
+ template.type === 'peptides' ? Syringe :
+ template.type === 'insulin' ? Pill : Plus;
+
+ return (
+ handleProtocolTypeSelect(template.type)}
+ accessibilityRole="button"
+ accessibilityLabel={template.name}
+ >
+
+
+ {isSelected && }
+
+
+ {template.name}
+
+
+ {template.description}
+
+
+ );
+ })}
+
+
+ );
+
+ const renderMedicationDetailsStep = () => {
+ const selectedTemplate = getSelectedTemplate();
+
+ return (
+
+ Protocol Details
+
+ Enter your medication details to complete your {selectedTemplate?.name} protocol setup.
+
+
+ {/* Error Message Display */}
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+
+ {/* Medication Name */}
+
+ Medication Name
+ {
+ setSetupData(prev => ({ ...prev, medication: text }));
+ // Clear error when user starts typing
+ if (fieldErrors.medication) {
+ setFieldErrors(prev => ({ ...prev, medication: '' }));
+ }
+ }}
+ placeholder={selectedTemplate?.commonMedications[0] || "Enter medication name"}
+ placeholderTextColor="#A1A1A1"
+ />
+ {fieldErrors.medication && (
+ {fieldErrors.medication}
+ )}
+
+
+ {/* Dosage */}
+
+ Standard Dose
+
+ {
+ setSetupData(prev => ({ ...prev, dosage: text }));
+ // Clear error when user starts typing
+ if (fieldErrors.dosage) {
+ setFieldErrors(prev => ({ ...prev, dosage: '' }));
+ }
+ }}
+ placeholder="0.5"
+ placeholderTextColor="#A1A1A1"
+ keyboardType="decimal-pad"
+ />
+
+ {selectedTemplate?.defaultUnits.map((unit) => (
+ setSetupData(prev => ({ ...prev, unit }))}
+ >
+
+ {unit}
+
+
+ ))}
+
+
+ {fieldErrors.dosage && (
+ {fieldErrors.dosage}
+ )}
+
+
+ {/* Frequency */}
+
+ Frequency
+
+ {selectedTemplate?.commonFrequencies.map((freq) => (
+ setSetupData(prev => ({ ...prev, frequency: freq }))}
+ >
+
+ {freq.charAt(0).toUpperCase() + freq.slice(1)}
+
+
+ ))}
+
+
+
+
+ );
+ };
+
+ const renderCurrentStep = () => {
+ switch (currentStep) {
+ case 0: return renderProtocolTypeStep();
+ case 1: return renderMedicationDetailsStep();
+ default: return null;
+ }
+ };
+
+ const getProgressWidth = () => {
+ return ((currentStep + 1) / 2) * 100;
+ };
+
+ return (
+
+
+
+ Set Up Your Protocol
+
+ Step {currentStep + 1} of 2
+
+
+ {/* Progress bar */}
+
+
+
+
+
+ {Math.round(getProgressWidth())}% Complete
+
+
+
+
+ {renderCurrentStep()}
+
+
+
+
+
+
+ Back
+
+
+
+
+ Skip for now
+
+
+
+
+ {isCompleting ? 'Saving...' : (currentStep === 1 ? 'Complete' : 'Next')}
+
+ {!isCompleting && (
+
+ )}
+ {isCompleting && (
+
+ {/* Simple loading indicator */}
+ ⏳
+
+ )}
+
+
+
+
+
+ Your protocol information is stored securely and only used to provide personalized guidance.
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#FFFFFF',
+ },
+ scrollView: {
+ flex: 1,
+ },
+ scrollContent: {
+ paddingTop: 40,
+ paddingHorizontal: 24,
+ },
+ header: {
+ marginBottom: 24,
+ alignItems: 'center',
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: '700',
+ color: '#000000',
+ textAlign: 'center',
+ marginBottom: 12,
+ },
+ subtitle: {
+ fontSize: 17,
+ color: '#6B6B6B',
+ textAlign: 'center',
+ lineHeight: 24,
+ marginBottom: 12,
+ },
+ progressContainer: {
+ width: '100%',
+ alignItems: 'center',
+ marginTop: 16,
+ },
+ progressBackground: {
+ width: '100%',
+ height: 4,
+ backgroundColor: '#E5E5EA',
+ borderRadius: 2,
+ overflow: 'hidden',
+ },
+ progressBar: {
+ height: '100%',
+ backgroundColor: '#007AFF',
+ borderRadius: 2,
+ },
+ progressLabel: {
+ fontSize: 12,
+ color: '#6B6B6B',
+ marginTop: 8,
+ },
+ stepContainer: {
+ marginBottom: 24,
+ },
+ stepTitle: {
+ fontSize: 24,
+ fontWeight: '600',
+ color: '#000000',
+ textAlign: 'center',
+ marginBottom: 12,
+ },
+ stepDescription: {
+ fontSize: 16,
+ color: '#6B6B6B',
+ textAlign: 'center',
+ lineHeight: 22,
+ marginBottom: 32,
+ },
+ optionsContainer: {
+ gap: 16,
+ },
+ protocolCard: {
+ backgroundColor: '#FFFFFF',
+ borderWidth: 2,
+ borderColor: '#E5E5EA',
+ borderRadius: 16,
+ padding: 20,
+ marginBottom: 12,
+ },
+ protocolCardSelected: {
+ borderColor: '#007AFF',
+ backgroundColor: '#F7F9FF',
+ },
+ protocolCardHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ protocolTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#000000',
+ marginBottom: 8,
+ },
+ protocolTitleSelected: {
+ color: '#007AFF',
+ },
+ protocolDescription: {
+ fontSize: 14,
+ color: '#6B6B6B',
+ lineHeight: 20,
+ },
+ formContainer: {
+ gap: 24,
+ },
+ fieldContainer: {
+ gap: 8,
+ },
+ fieldLabel: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#000000',
+ },
+ textInput: {
+ borderWidth: 1,
+ borderColor: '#E5E5EA',
+ borderRadius: 12,
+ padding: 16,
+ fontSize: 16,
+ color: '#000000',
+ backgroundColor: '#FFFFFF',
+ },
+ dosageContainer: {
+ flexDirection: 'row',
+ gap: 12,
+ alignItems: 'flex-start',
+ },
+ dosageInput: {
+ flex: 1,
+ },
+ unitPicker: {
+ flexDirection: 'row',
+ backgroundColor: '#F2F2F7',
+ borderRadius: 12,
+ padding: 4,
+ },
+ unitOption: {
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 8,
+ },
+ unitOptionSelected: {
+ backgroundColor: '#007AFF',
+ },
+ unitOptionText: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: '#6B6B6B',
+ },
+ unitOptionTextSelected: {
+ color: '#FFFFFF',
+ },
+ frequencyContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 8,
+ },
+ frequencyOption: {
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ borderRadius: 20,
+ borderWidth: 1,
+ borderColor: '#E5E5EA',
+ backgroundColor: '#FFFFFF',
+ },
+ frequencyOptionSelected: {
+ borderColor: '#007AFF',
+ backgroundColor: '#007AFF',
+ },
+ frequencyOptionText: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: '#6B6B6B',
+ },
+ frequencyOptionTextSelected: {
+ color: '#FFFFFF',
+ },
+ footer: {
+ padding: 24,
+ paddingTop: 16,
+ backgroundColor: '#FFFFFF',
+ borderTopWidth: 1,
+ borderTopColor: '#E5E5EA',
+ },
+ buttonContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ backButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ },
+ backButtonText: {
+ fontSize: 16,
+ fontWeight: '500',
+ color: '#007AFF',
+ },
+ rightButtons: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ },
+ skipButton: {
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ },
+ skipButtonText: {
+ fontSize: 16,
+ fontWeight: '500',
+ color: '#6B6B6B',
+ },
+ nextButton: {
+ backgroundColor: '#007AFF',
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 24,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ minHeight: 48,
+ },
+ nextButtonDisabled: {
+ backgroundColor: '#E5E5EA',
+ },
+ nextButtonText: {
+ color: '#FFFFFF',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ nextButtonTextDisabled: {
+ color: '#A1A1A1',
+ },
+ privacyText: {
+ fontSize: 12,
+ color: '#A1A1A1',
+ textAlign: 'center',
+ lineHeight: 16,
+ },
+ // Mobile-specific styles
+ stepContainerMobile: {
+ marginBottom: 20,
+ },
+ headerMobile: {
+ marginBottom: 20,
+ },
+ titleMobile: {
+ fontSize: 24,
+ marginBottom: 8,
+ },
+ subtitleMobile: {
+ fontSize: 16,
+ marginBottom: 8,
+ },
+ stepTitleMobile: {
+ fontSize: 20,
+ marginBottom: 8,
+ },
+ stepDescriptionMobile: {
+ fontSize: 14,
+ marginBottom: 24,
+ },
+ scrollContentMobile: {
+ paddingHorizontal: 16,
+ paddingTop: 20,
+ },
+ optionsContainerMobile: {
+ gap: 12,
+ },
+ protocolCardMobile: {
+ padding: 16,
+ marginBottom: 8,
+ },
+ protocolTitleMobile: {
+ fontSize: 16,
+ },
+ protocolDescriptionMobile: {
+ fontSize: 13,
+ },
+ formContainerMobile: {
+ gap: 20,
+ },
+ fieldLabelMobile: {
+ fontSize: 14,
+ },
+ textInputMobile: {
+ padding: 12,
+ fontSize: 14,
+ },
+ footerMobile: {
+ padding: 16,
+ paddingTop: 12,
+ },
+ buttonContainerMobile: {
+ marginBottom: 12,
+ },
+ backButtonMobile: {
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+ },
+ backButtonTextMobile: {
+ fontSize: 14,
+ },
+ skipButtonMobile: {
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+ },
+ skipButtonTextMobile: {
+ fontSize: 14,
+ },
+ nextButtonMobile: {
+ paddingHorizontal: 20,
+ paddingVertical: 10,
+ minHeight: 42,
+ },
+ nextButtonTextMobile: {
+ fontSize: 14,
+ },
+ privacyTextMobile: {
+ fontSize: 11,
+ },
+ // Error and loading styles
+ errorContainer: {
+ backgroundColor: '#FFF2F2',
+ borderWidth: 1,
+ borderColor: '#FF6B6B',
+ borderRadius: 8,
+ padding: 12,
+ marginBottom: 16,
+ },
+ errorText: {
+ color: '#D63031',
+ fontSize: 14,
+ fontWeight: '500',
+ textAlign: 'center',
+ },
+ textInputError: {
+ borderColor: '#FF6B6B',
+ borderWidth: 2,
+ },
+ fieldErrorText: {
+ color: '#D63031',
+ fontSize: 12,
+ marginTop: 4,
+ marginLeft: 4,
+ },
+ loadingSpinner: {
+ marginLeft: 8,
+ },
+ loadingText: {
+ fontSize: 16,
+ },
+});
\ No newline at end of file
diff --git a/app/onboarding/userType.tsx b/app/onboarding/userType.tsx
index e0e00985..bad8ad32 100644
--- a/app/onboarding/userType.tsx
+++ b/app/onboarding/userType.tsx
@@ -16,7 +16,7 @@ export default function UserTypeSegmentation() {
const { user } = useAuth();
const { saveProfile } = useUserProfile();
const { submitOnboardingIntent } = useOnboardingIntentStorage();
- const { age, limitedFunctionality, reason } = useLocalSearchParams<{
+ const { age, limitedFunctionality } = useLocalSearchParams<{
age: string;
limitedFunctionality: string;
reason: string;
@@ -31,6 +31,7 @@ export default function UserTypeSegmentation() {
isRecoveryUse: null,
age: age ? parseInt(age) : null,
birthDate: null,
+ followsProtocol: null,
});
// Log analytics when step starts
@@ -45,7 +46,8 @@ export default function UserTypeSegmentation() {
switch (step) {
case 0: return 'background';
case 1: return 'use_type';
- case 2: return 'personal_use';
+ case 2: return 'protocol_decision';
+ case 3: return 'personal_use';
default: return 'unknown';
}
};
@@ -108,26 +110,40 @@ export default function UserTypeSegmentation() {
step_name: getStepName(currentStep)
});
- if (currentStep === 2) {
+ if (currentStep === 3) {
// For personal use question, set to null when skipped
setAnswers(prev => ({ ...prev, isPersonalUse: null }));
}
// Move to next step or complete
- if (currentStep < 2) {
+ if (currentStep < 3) {
+ // Special case: if user selected "I follow a protocol" in step 2, skip step 3 and go directly to completion
+ if (currentStep === 2 && answers.followsProtocol === true) {
+ console.log('[UserType] User selected protocol - skipping personal use step and completing directly');
+ handleComplete();
+ return;
+ }
+
setCurrentStep(currentStep + 1);
} else {
handleComplete();
}
- }, [currentStep]);
+ }, [currentStep, answers.followsProtocol, handleComplete]);
const handleNext = useCallback(() => {
- if (currentStep < 2) {
+ if (currentStep < 3) {
+ // Special case: if user selected "I follow a protocol" in step 2, skip step 3 and go directly to completion
+ if (currentStep === 2 && answers.followsProtocol === true) {
+ console.log('[UserType] User selected protocol - skipping personal use step and completing directly');
+ handleComplete();
+ return;
+ }
+
setCurrentStep(currentStep + 1);
} else {
handleComplete();
}
- }, [currentStep]);
+ }, [currentStep, answers.followsProtocol, handleComplete]);
const handleBack = useCallback(() => {
if (currentStep > 0) {
@@ -140,6 +156,7 @@ export default function UserTypeSegmentation() {
console.log('[UserType] ========== ONBOARDING COMPLETION START ==========');
console.log('[UserType] Current answers:', answers);
console.log('[UserType] Current user:', user?.uid || 'No user');
+ console.log('[UserType] followsProtocol value:', answers.followsProtocol);
// Submit onboarding intent data (for analytics and data collection)
// This happens first and independently of profile saving
@@ -162,6 +179,8 @@ export default function UserTypeSegmentation() {
age: answers.age || undefined, // Include age if provided
dateCreated: new Date().toISOString(),
userId: user?.uid,
+ followsProtocol: answers.followsProtocol ?? false,
+ hasSetupProtocol: false, // Will be set to true after protocol setup
};
console.log('[UserType] Created profile object:', profile);
@@ -184,6 +203,7 @@ export default function UserTypeSegmentation() {
isCosmeticUse: profile.isCosmeticUse,
isPerformanceUse: profile.isPerformanceUse,
isRecoveryUse: profile.isRecoveryUse,
+ followsProtocol: profile.followsProtocol,
skipped_personal_use: answers.isPersonalUse === null,
age: profile.age,
age_range: profile.age ? (profile.age < 18 ? 'minor' : profile.age < 65 ? 'adult' : 'senior') : 'unknown'
@@ -208,10 +228,15 @@ export default function UserTypeSegmentation() {
parsedProfile: storedProfile ? JSON.parse(storedProfile) : null
});
- // Navigate directly to intro screen instead of relying on index.tsx routing
- console.log('[UserType] 🚀 NAVIGATING DIRECTLY TO INTRO - calling router.replace("/(tabs)/new-dose")');
- console.log('[UserType] ========== BYPASSING INDEX.TSX ROUTING ==========');
- router.replace('/(tabs)/new-dose');
+ // Navigate based on whether user wants to set up a protocol
+ if (answers.followsProtocol === true) {
+ console.log('[UserType] 🚀 NAVIGATING TO PROTOCOL SETUP - user wants to follow a protocol');
+ router.replace('/onboarding/protocol');
+ } else {
+ console.log('[UserType] 🚀 NAVIGATING DIRECTLY TO INTRO - user wants quick use mode');
+ console.log('[UserType] ========== BYPASSING INDEX.TSX ROUTING ==========');
+ router.replace('/(tabs)/new-dose');
+ }
} catch (error) {
console.error('[UserType] ❌ ERROR during completion:', error);
console.error('[UserType] Error stack:', error instanceof Error ? error.stack : 'No stack');
@@ -225,7 +250,8 @@ export default function UserTypeSegmentation() {
switch (currentStep) {
case 0: return answers.isLicensedProfessional !== null || answers.isProfessionalAthlete !== null;
case 1: return answers.isCosmeticUse !== null || answers.isPerformanceUse !== null || answers.isRecoveryUse !== null;
- case 2: return true; // This step is always "complete" since it can be skipped
+ case 2: return answers.followsProtocol !== null;
+ case 3: return true; // This step is always "complete" since it can be skipped
default: return false;
}
};
@@ -422,6 +448,63 @@ export default function UserTypeSegmentation() {
);
+ const renderProtocolDecisionStep = () => (
+
+ Are you following a protocol?
+
+ This helps us determine whether to set up structured medication scheduling or provide quick one-off calculations.
+
+
+
+ handleAnswerChange('followsProtocol', true)}
+ accessibilityRole="button"
+ accessibilityLabel="Yes, I follow a structured protocol"
+ >
+ {answers.followsProtocol === true && }
+
+ Yes, I follow a protocol
+
+
+ I have a structured medication schedule (TRT, peptides, insulin, etc.)
+
+
+
+ handleAnswerChange('followsProtocol', false)}
+ accessibilityRole="button"
+ accessibilityLabel="No, I need quick calculations"
+ >
+ {answers.followsProtocol === false && }
+
+ No, quick calculations
+
+
+ I need one-off dose calculations and guidance
+
+
+
+
+ );
+
const renderPersonalUseStep = () => (
Who is this for?
@@ -492,13 +575,16 @@ export default function UserTypeSegmentation() {
switch (currentStep) {
case 0: return renderBackgroundStep();
case 1: return renderUseTypeStep();
- case 2: return renderPersonalUseStep();
+ case 2: return renderProtocolDecisionStep();
+ case 3: return renderPersonalUseStep();
default: return null;
}
};
const getProgressWidth = () => {
- return ((currentStep + 1) / 3) * 100;
+ // If user selected protocol in step 2, we skip step 3, so total steps is 3 instead of 4
+ const totalSteps = (currentStep >= 2 && answers.followsProtocol === true) ? 3 : 4;
+ return ((currentStep + 1) / totalSteps) * 100;
};
return (
@@ -507,7 +593,7 @@ export default function UserTypeSegmentation() {
Let's Personalize Your Experience
- Step {currentStep + 1} of 3
+ Step {currentStep + 1} of {(currentStep >= 2 && answers.followsProtocol === true) ? 3 : 4}
{/* Progress bar */}
diff --git a/types/protocol.ts b/types/protocol.ts
new file mode 100644
index 00000000..abfa81f8
--- /dev/null
+++ b/types/protocol.ts
@@ -0,0 +1,69 @@
+export interface Protocol {
+ id: string;
+ name: string;
+ medication: string;
+ dosage: string;
+ unit: 'mg' | 'mL' | 'IU' | 'mcg';
+ frequency: 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'custom';
+ customFrequencyDays?: number;
+ startDate: string; // ISO 8601 date
+ isActive: boolean;
+ dateCreated: string;
+ userId?: string;
+}
+
+export type ProtocolType = 'trt' | 'peptides' | 'insulin' | 'custom';
+
+export interface ProtocolTemplate {
+ type: ProtocolType;
+ name: string;
+ description: string;
+ commonMedications: string[];
+ defaultUnits: ('mg' | 'mL' | 'IU' | 'mcg')[];
+ commonFrequencies: ('daily' | 'weekly' | 'biweekly' | 'monthly')[];
+}
+
+export const PROTOCOL_TEMPLATES: ProtocolTemplate[] = [
+ {
+ type: 'trt',
+ name: 'Testosterone Replacement Therapy (TRT)',
+ description: 'Hormone replacement therapy for testosterone deficiency',
+ commonMedications: ['Testosterone Cypionate', 'Testosterone Enanthate', 'Testosterone Propionate'],
+ defaultUnits: ['mg', 'mL'],
+ commonFrequencies: ['weekly', 'biweekly']
+ },
+ {
+ type: 'peptides',
+ name: 'Peptides',
+ description: 'Peptide therapy for various health and wellness goals',
+ commonMedications: ['Semaglutide', 'Tirzepatide', 'BPC-157', 'TB-500', 'Ipamorelin'],
+ defaultUnits: ['mg', 'mcg', 'IU'],
+ commonFrequencies: ['daily', 'weekly']
+ },
+ {
+ type: 'insulin',
+ name: 'Insulin',
+ description: 'Insulin therapy for diabetes management',
+ commonMedications: ['Insulin Glargine', 'Insulin Lispro', 'Insulin Aspart', 'Insulin NPH'],
+ defaultUnits: ['IU', 'mL'],
+ commonFrequencies: ['daily']
+ },
+ {
+ type: 'custom',
+ name: 'Custom Protocol',
+ description: 'Create your own custom medication protocol',
+ commonMedications: [],
+ defaultUnits: ['mg', 'mL', 'IU', 'mcg'],
+ commonFrequencies: ['daily', 'weekly', 'biweekly', 'monthly']
+ }
+];
+
+export interface ProtocolScheduleEntry {
+ id: string;
+ protocolId: string;
+ scheduledDate: string; // ISO 8601 date
+ isCompleted: boolean;
+ completedDate?: string;
+ actualDose?: string;
+ notes?: string;
+}
\ No newline at end of file
diff --git a/types/userProfile.ts b/types/userProfile.ts
index d1fa8308..d6add087 100644
--- a/types/userProfile.ts
+++ b/types/userProfile.ts
@@ -9,6 +9,8 @@ export interface UserProfile {
birthDate?: string; // Birth date in YYYY-MM-DD format for more precise age calculation
dateCreated: string;
userId?: string; // Optional field to track which user this profile belongs to
+ followsProtocol: boolean; // Whether user follows a structured medication protocol
+ hasSetupProtocol?: boolean; // Whether user has completed protocol setup
}
export type UserProfileAnswers = {
@@ -20,6 +22,7 @@ export type UserProfileAnswers = {
isRecoveryUse: boolean | null;
age: number | null;
birthDate: string | null; // Birth date in YYYY-MM-DD format
+ followsProtocol: boolean | null; // Whether user follows a structured medication protocol
};
export enum WarningLevel {