import React, { useState, useRef, useEffect } from 'react'; import { View, Text, StyleSheet, TextInput, Pressable, KeyboardAvoidingView, Platform, Keyboard, ScrollView, } from 'react-native'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import Animated, { FadeIn, FadeInUp, FadeOut, FadeOutLeft, SlideInRight, SlideOutLeft, Layout, } from 'react-native-reanimated'; import { GradientBackground } from '../../src/components/ui/GradientBackground'; import { GlassCard } from '../../src/components/ui/GlassCard'; import { GlassButton } from '../../src/components/ui/GlassButton'; import { IntensitySlider } from '../../src/components/ui/IntensitySlider'; import { colors, gradients, spacing, typography, radius, shadows } from '../../src/theme'; import { useAuthStore } from '../../src/store'; import { supabase } from '../../src/lib/supabase'; import { getProfileError } from '../../src/lib/errors'; import { registerForPushNotificationsAsync, savePushToken } from '../../src/lib/notifications'; import { Events } from '../../src/lib/analytics'; import type { Gender, IntensityLevel } from '../../src/types'; const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [ { value: 'male', label: 'Male', icon: 'male' }, { value: 'female', label: 'Female', icon: 'female' }, { value: 'non-binary', label: 'Non-binary', icon: 'male-female' }, { value: 'prefer-not-to-say', label: 'Skip', icon: 'remove-circle-outline' }, ]; type UsageReason = 'improve_communication' | 'spice_up_intimacy' | 'deeper_connection' | 'have_fun' | 'strengthen_relationship'; const PURPOSE_OPTIONS: { value: UsageReason; label: string; icon: string }[] = [ { value: 'improve_communication', label: 'Improve communication', icon: 'chatbubbles' }, { value: 'spice_up_intimacy', label: 'Spice up intimacy', icon: 'flame' }, { value: 'deeper_connection', label: 'Build deeper connection', icon: 'heart' }, { value: 'have_fun', label: 'Have fun together', icon: 'happy' }, { value: 'strengthen_relationship', label: 'Strengthen our relationship', icon: 'shield-checkmark' }, ]; const DEFAULT_MAX_INTENSITY: IntensityLevel = 2; const normalizeIntensity = (value: number | null | undefined): IntensityLevel | null => { if (value === 1 || value === 2 || value === 3 || value === 4 || value === 5) return value; return null; }; const getInitialIntensity = (profile?: { max_intensity?: number | null; show_explicit_content?: boolean | null } | null): IntensityLevel => { const normalized = normalizeIntensity(profile?.max_intensity); if (normalized) return normalized; return profile?.show_explicit_content ? 5 : DEFAULT_MAX_INTENSITY; }; type Stage = 'name' | 'gender' | 'purpose' | 'explicit' | 'notifications'; export default function OnboardingScreen() { const router = useRouter(); const { user, fetchUser } = useAuthStore(); const [stage, setStage] = useState('name'); const [name, setName] = useState(user?.name || ''); const [gender, setGender] = useState(user?.gender || null); const [usageReason, setUsageReason] = useState(null); const [maxIntensity, setMaxIntensity] = useState(() => getInitialIntensity(user)); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const inputRef = useRef(null); useEffect(() => { Events.onboardingStart(); }, []); const handleNameSubmit = () => { if (!name.trim()) { setError('Please enter your name'); return; } Keyboard.dismiss(); setError(null); setStage('gender'); Events.onboardingStageComplete('name'); }; const handleGenderSelect = (selectedGender: Gender) => { setGender(selectedGender); setError(null); }; const handleGenderContinue = () => { if (!gender) { setError('Please select a gender or skip'); return; } setStage('purpose'); Events.onboardingStageComplete('gender'); }; const handlePurposeSelect = (selectedReason: UsageReason) => { setUsageReason(selectedReason); setError(null); }; const handlePurposeContinue = () => { if (!usageReason) { setError('Please select what brings you to Sauci'); return; } setStage('explicit'); Events.onboardingStageComplete('purpose'); }; const handleIntensityContinue = () => { setStage('notifications'); Events.onboardingStageComplete('explicit'); }; const handleEnableNotifications = async () => { setIsLoading(true); try { const token = await registerForPushNotificationsAsync(); if (token && user?.id) { await savePushToken(user.id, token); } } catch (err) { console.error('Failed to enable notifications:', err); } finally { setIsLoading(false); await handleComplete(); } }; const handleComplete = async () => { console.log('[Onboarding] handleComplete called, user:', user?.id, 'name:', name); if (!user?.id) { console.error('[Onboarding] No user ID available'); setError('Not logged in. Please restart the app.'); return; } setIsLoading(true); setError(null); try { const showExplicitContent = maxIntensity >= 3; const { error: updateError } = await supabase .from('profiles') .update({ name: name.trim(), gender, usage_reason: usageReason, max_intensity: maxIntensity, show_explicit_content: showExplicitContent, onboarding_completed: true, updated_at: new Date().toISOString(), }) .eq('id', user.id); if (updateError) { console.error('[Onboarding] Profile update error:', updateError); throw updateError; } console.log('[Onboarding] Profile updated successfully, fetching user...'); await fetchUser(); console.log('[Onboarding] User fetched, navigating to home...'); Events.profileUpdated(["name", "gender", "usage_reason", "max_intensity", "show_explicit_content"]); Events.onboardingComplete(); router.replace('/'); } catch (err: any) { console.error('Onboarding error:', err); setError(getProfileError(err)); } finally { setIsLoading(false); } }; const renderStage = () => { switch (stage) { case 'name': return ( Welcome to Sauci Let's get to know you What's your name? {error && ( {error} )} Continue ); case 'gender': return ( Hey, {name.trim()}! This helps us show you the right questions What's your gender? {GENDER_OPTIONS.map((option) => ( handleGenderSelect(option.value)} > {option.label} ))} {error && ( {error} )} Continue ); case 'purpose': return ( What brings you here? This helps us personalize your experience {PURPOSE_OPTIONS.map((option) => ( handlePurposeSelect(option.value)} > {option.label} {usageReason === option.value && } ))} {error && ( {error} )} Continue ); case 'explicit': return ( Set Your Comfort Zone Choose the intimacy level you're comfortable with Start where you're comfortable — you can always adjust later {error && ( {error} )} Continue ); case 'notifications': return ( Stay in the Loop Get notified when you and your partner match on answers Match Alerts Know instantly when you both agree Messages Never miss a message from your partner {error && ( {error} )} Enable Notifications Maybe Later ); } }; return ( {/* Header with back button and progress dots */} {stage !== 'name' ? ( { if (stage === 'notifications') setStage('explicit'); else if (stage === 'explicit') setStage('purpose'); else if (stage === 'purpose') setStage('gender'); else setStage('name'); }} > ) : ( )} {renderStage()} ); } const styles = StyleSheet.create({ container: { flex: 1, }, scrollContent: { flexGrow: 1, }, headerBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingTop: 60, paddingBottom: spacing.md, paddingHorizontal: spacing.md, }, backButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: colors.glass.background, alignItems: 'center', justifyContent: 'center', }, backButtonPlaceholder: { width: 40, }, progressContainer: { flexDirection: 'row', justifyContent: 'center', gap: spacing.sm, }, progressDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: colors.glass.border, }, progressDotActive: { backgroundColor: colors.primary, width: 24, }, stageContainer: { flex: 1, padding: spacing.lg, paddingBottom: 40, }, header: { alignItems: 'center', marginBottom: spacing.xl, }, iconContainer: { width: 80, height: 80, borderRadius: 40, alignItems: 'center', justifyContent: 'center', marginBottom: spacing.lg, ...shadows.lg, }, title: { ...typography.largeTitle, color: colors.text, marginBottom: spacing.sm, textAlign: 'center', }, greeting: { ...typography.largeTitle, color: colors.primary, marginBottom: spacing.sm, textAlign: 'center', }, subtitle: { ...typography.body, color: colors.textSecondary, textAlign: 'center', }, card: { marginBottom: spacing.lg, }, label: { ...typography.headline, color: colors.text, marginBottom: spacing.md, }, inputContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.glass.background, borderRadius: radius.md, borderWidth: 1, borderColor: colors.glass.border, paddingHorizontal: spacing.md, }, inputIcon: { marginRight: spacing.sm, }, input: { flex: 1, ...typography.body, color: colors.text, paddingVertical: spacing.md, }, genderGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.sm, }, genderOption: { width: '48%', alignItems: 'center', backgroundColor: colors.glass.background, borderRadius: radius.lg, borderWidth: 2, borderColor: colors.glass.border, paddingVertical: spacing.lg, paddingHorizontal: spacing.md, gap: spacing.sm, }, genderOptionSelected: { borderColor: colors.primary, backgroundColor: colors.primaryLight, }, genderLabel: { ...typography.subhead, color: colors.textSecondary, textAlign: 'center', }, genderLabelSelected: { color: colors.primary, fontWeight: '600', }, purposeList: { gap: spacing.sm, }, purposeOption: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.glass.background, borderRadius: radius.lg, borderWidth: 2, borderColor: colors.glass.border, padding: spacing.md, }, purposeOptionSelected: { borderColor: colors.primary, backgroundColor: colors.primaryLight, }, purposeIcon: { width: 44, height: 44, borderRadius: 22, backgroundColor: colors.glass.background, alignItems: 'center', justifyContent: 'center', marginRight: spacing.md, }, purposeIconSelected: { backgroundColor: colors.primaryLight, }, purposeLabel: { ...typography.subhead, color: colors.textSecondary, flex: 1, }, purposeLabelSelected: { color: colors.text, fontWeight: '600', }, intensityNote: { ...typography.caption1, color: colors.textSecondary, textAlign: 'center', marginTop: spacing.md, }, radioOuter: { width: 24, height: 24, borderRadius: 12, borderWidth: 2, borderColor: colors.glass.border, alignItems: 'center', justifyContent: 'center', }, radioOuterSelected: { borderColor: colors.primary, }, radioInner: { width: 12, height: 12, borderRadius: 6, backgroundColor: colors.primary, }, errorContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(231, 76, 60, 0.1)', borderRadius: radius.md, padding: spacing.md, gap: spacing.sm, marginTop: spacing.sm, }, errorText: { ...typography.caption1, color: colors.error, flex: 1, }, footer: { marginTop: 'auto', }, notificationFeature: { flexDirection: 'row', alignItems: 'center', paddingVertical: spacing.md, borderBottomWidth: 1, borderBottomColor: colors.glass.border, }, notificationFeatureLast: { borderBottomWidth: 0, }, notificationIcon: { width: 48, height: 48, borderRadius: 24, backgroundColor: colors.primaryLight, alignItems: 'center', justifyContent: 'center', marginRight: spacing.md, }, notificationText: { flex: 1, }, notificationTitle: { ...typography.headline, color: colors.text, marginBottom: spacing.xs, }, notificationDescription: { ...typography.caption1, color: colors.textSecondary, }, skipButton: { alignItems: 'center', paddingVertical: spacing.md, marginTop: spacing.sm, }, skipText: { ...typography.subhead, color: colors.textSecondary, }, });