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, FadeOut, SlideInRight, SlideOutLeft, } 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 { Paywall } from '../../src/components/paywall'; import { colors, gradients, spacing, typography, radius, shadows } from '../../src/theme'; import { useAuthStore } from '../../src/store'; import { profileSettingsApi } from '../../src/lib/profileSettingsApi'; import { getProfileError } from '../../src/lib/errors'; import { registerForPushNotificationsAsync, savePushToken } from '../../src/lib/notifications'; import { Events } from '../../src/lib/analytics'; import { hasSeenOnboardingPaywall, markOnboardingPaywallSeen } from '../../src/lib/onboardingPaywallSeen'; import { REQUIRED_ONBOARDING_VERSION, ONBOARDING_OFFERING_ID } from '../../src/constants/onboarding'; import { activeOnboardingStage, initialOnboardingStage, previousOnboardingStage, type OnboardingStage } from '../../src/lib/onboardingStages'; import type { Gender, UsageReason } 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' }, ]; const PURPOSE_OPTIONS: { value: UsageReason; label: string; icon: string }[] = [ { value: 'improve_communication', label: 'Improve communication', icon: 'chatbubbles' }, { value: 'reconnect', label: 'Feel more connected', 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' }, ]; type Stage = OnboardingStage; export default function OnboardingScreen() { const router = useRouter(); // Only subscribe to stable values to prevent re-renders from background fetchUser calls const userId = useAuthStore((s) => s.user?.id); const persistedUserName = useAuthStore((s) => s.user?.name); const pendingAppleDisplayName = useAuthStore((s) => s.pendingAppleDisplayName); const fetchUser = useAuthStore((s) => s.fetchUser); const initialUser = useRef(useAuthStore.getState().user); const [stage, setStage] = useState(() => initialOnboardingStage(initialUser.current?.name)); const [name, setName] = useState(initialUser.current?.name || ''); const [gender, setGender] = useState(initialUser.current?.gender || null); const [usageReason, setUsageReason] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [showPaywall, setShowPaywall] = useState(false); const inputRef = useRef(null); const paywallNavigationRef = useRef(false); const pendingDisplayName = pendingAppleDisplayName?.userId === userId ? pendingAppleDisplayName?.name : undefined; const displayName = persistedUserName?.trim() || pendingDisplayName || name; const hasStoredDisplayName = Boolean(persistedUserName?.trim() || pendingDisplayName); const activeStage = activeOnboardingStage(stage, persistedUserName, pendingDisplayName); const previousStage = previousOnboardingStage(activeStage, hasStoredDisplayName); 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('notifications'); Events.onboardingStageComplete('purpose'); }; const handleEnableNotifications = async () => { setIsLoading(true); try { const token = await registerForPushNotificationsAsync(); if (token && userId) { await savePushToken(userId, token); } } catch (err) { console.error('Failed to enable notifications:', err); } finally { setIsLoading(false); await handleComplete(); } }; const handleComplete = async () => { console.log('[Onboarding] handleComplete called, user:', userId, 'name:', displayName); if (!userId) { console.error('[Onboarding] No user ID available'); setError('Not logged in. Please restart the app.'); return; } setIsLoading(true); setError(null); try { const updatedFields: string[] = ["name", "gender", "usage_reason", "max_intensity", "show_explicit_content", "hide_nsfw"]; const updateData = { name: displayName.trim(), gender, usage_reason: usageReason, // Preserve the safest legacy profile values. Catalogue // eligibility is enforced centrally by reviewed status. max_intensity: 2 as const, show_explicit_content: false, hide_nsfw: true, onboarding_completed: true, onboarding_version: REQUIRED_ONBOARDING_VERSION, }; await profileSettingsApi.updateProfile(updateData); console.log('[Onboarding] Profile updated successfully, fetching user...'); await fetchUser(); console.log('[Onboarding] User fetched, navigating to home...'); Events.profileUpdated(updatedFields); Events.onboardingComplete(); const currentUser = useAuthStore.getState().user; if (currentUser?.id && !currentUser.is_premium) { const hasSeenPaywall = await hasSeenOnboardingPaywall(currentUser.id); if (!hasSeenPaywall) { paywallNavigationRef.current = true; setShowPaywall(true); return; } } router.replace('/'); } catch (err: any) { console.error('Onboarding error:', err); setError(getProfileError(err)); } finally { setIsLoading(false); } }; const handlePaywallClose = () => { setShowPaywall(false); const currentUser = useAuthStore.getState().user; if (currentUser?.id) { void markOnboardingPaywallSeen(currentUser.id); } if (paywallNavigationRef.current) { paywallNavigationRef.current = false; router.replace('/'); } }; const renderStage = () => { switch (activeStage) { case 'name': return ( What can we call you? Nicknames and pet names are welcome too! {error && ( {error} )} Continue ); case 'gender': return ( Hey, {displayName.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 '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 */} {previousStage ? ( { setError(null); setStage(previousStage); }} > ) : ( )} {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.backgroundLight, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: colors.border, }, // ... progressDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: colors.border, }, // ... inputContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background, // Flat background borderRadius: radius.md, borderWidth: 1, borderColor: colors.border, paddingHorizontal: spacing.md, }, // ... genderOption: { width: '48%', alignItems: 'center', backgroundColor: colors.backgroundLight, borderRadius: radius.lg, borderWidth: 2, borderColor: colors.border, paddingVertical: spacing.lg, paddingHorizontal: spacing.md, gap: spacing.sm, }, // ... purposeOption: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.backgroundLight, borderRadius: radius.lg, borderWidth: 2, borderColor: colors.border, padding: spacing.md, }, // ... purposeIcon: { width: 44, height: 44, borderRadius: 22, backgroundColor: colors.background, alignItems: 'center', justifyContent: 'center', marginRight: spacing.md, borderWidth: 1, borderColor: colors.border, }, // ... radioOuter: { width: 24, height: 24, borderRadius: 12, borderWidth: 2, borderColor: colors.border, alignItems: 'center', justifyContent: 'center', }, // ... notificationFeature: { flexDirection: 'row', alignItems: 'center', paddingVertical: spacing.md, borderBottomWidth: 1, borderBottomColor: colors.border, }, backButtonPlaceholder: { width: 40, }, progressContainer: { flexDirection: 'row', justifyContent: 'center', gap: spacing.sm, }, 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, }, inputIcon: { marginRight: spacing.sm, }, input: { flex: 1, ...typography.body, color: colors.text, paddingVertical: spacing.md, }, genderGrid: { flexDirection: 'row', flexWrap: 'wrap', 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, }, purposeOptionSelected: { borderColor: colors.primary, backgroundColor: colors.primaryLight, }, purposeIconSelected: { backgroundColor: colors.primaryLight, }, purposeLabel: { ...typography.subhead, color: colors.textSecondary, flex: 1, }, purposeLabelSelected: { color: colors.text, fontWeight: '600', }, 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', }, 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, }, });