import React, { useCallback, useEffect, useState } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, SafeAreaView, ScrollView, StatusBar, Platform, Linking, Switch, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { makeRedirectUri, useAuthRequest, ResponseType, Prompt, CodeChallengeMethod } from 'expo-auth-session'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import FastImage from '@d11/react-native-fast-image'; import { traktService, TraktUser } from '../services/traktService'; import { useSettings } from '../hooks/useSettings'; import { logger } from '../utils/logger'; import TraktIcon from '../../assets/rating-icons/trakt.svg'; import { useTheme } from '../contexts/ThemeContext'; import { useTraktIntegration } from '../hooks/useTraktIntegration'; import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings'; import { colors } from '../styles'; import CustomAlert from '../components/CustomAlert'; import { useTranslation } from 'react-i18next'; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; // Trakt configuration const TRAKT_CLIENT_ID = process.env.EXPO_PUBLIC_TRAKT_CLIENT_ID as string; if (!TRAKT_CLIENT_ID) { throw new Error('Missing EXPO_PUBLIC_TRAKT_CLIENT_ID environment variable'); } const discovery = { authorizationEndpoint: 'https://trakt.tv/oauth/authorize', tokenEndpoint: 'https://api.trakt.tv/oauth/token', }; // For use with deep linking const redirectUri = makeRedirectUri({ scheme: 'nuvio', path: 'auth/trakt', }); const TraktSettingsScreen: React.FC = () => { const { t } = useTranslation(); const { settings, updateSetting } = useSettings(); const isDarkMode = settings.enableDarkMode; const navigation = useNavigation(); const [isLoading, setIsLoading] = useState(true); const [isAuthenticated, setIsAuthenticated] = useState(false); const [userProfile, setUserProfile] = useState(null); const { currentTheme } = useTheme(); const { settings: autosyncSettings, isSyncing, setAutosyncEnabled, performManualSync } = useTraktAutosyncSettings(); const { isLoading: traktLoading, refreshAuthStatus } = useTraktIntegration(); const [showSyncFrequencyModal, setShowSyncFrequencyModal] = useState(false); const [showThresholdModal, setShowThresholdModal] = useState(false); const [alertVisible, setAlertVisible] = useState(false); const [alertTitle, setAlertTitle] = useState(''); const [alertMessage, setAlertMessage] = useState(''); const [alertActions, setAlertActions] = useState void; style?: object }>>([ { label: t('common.ok'), onPress: () => setAlertVisible(false) }, ]); const openAlert = ( title: string, message: string, actions?: Array<{ label: string; onPress?: () => void; style?: object }> ) => { setAlertTitle(title); setAlertMessage(message); if (actions && actions.length > 0) { setAlertActions( actions.map(a => ({ label: a.label, style: a.style, onPress: () => { a.onPress?.(); }, })) ); } else { setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]); } setAlertVisible(true); }; const checkAuthStatus = useCallback(async () => { setIsLoading(true); try { const authenticated = await traktService.isAuthenticated(); setIsAuthenticated(authenticated); if (authenticated) { const profile = await traktService.getUserProfile(); setUserProfile(profile); } else { setUserProfile(null); } } catch (error) { logger.error('[TraktSettingsScreen] Error checking auth status:', error); } finally { setIsLoading(false); } }, []); useEffect(() => { checkAuthStatus(); }, [checkAuthStatus]); // Setup expo-auth-session hook with PKCE const [request, response, promptAsync] = useAuthRequest( { clientId: TRAKT_CLIENT_ID, scopes: [], redirectUri: redirectUri, responseType: ResponseType.Code, usePKCE: true, codeChallengeMethod: CodeChallengeMethod.S256, }, discovery ); const [isExchangingCode, setIsExchangingCode] = useState(false); // Handle the response from the auth request useEffect(() => { if (response) { setIsExchangingCode(true); if (response.type === 'success' && request?.codeVerifier) { const { code } = response.params; logger.log('[TraktSettingsScreen] Auth code received:', code); traktService.exchangeCodeForToken(code, request.codeVerifier) .then(success => { if (success) { logger.log('[TraktSettingsScreen] Token exchange successful'); checkAuthStatus().then(() => { // Show success message openAlert( t('trakt.auth_success_title'), t('trakt.auth_success_msg'), [ { label: t('common.ok'), onPress: () => navigation.goBack(), } ] ); }); } else { logger.error('[TraktSettingsScreen] Token exchange failed'); openAlert(t('trakt.auth_error_title'), t('trakt.auth_error_msg')); } }) .catch(error => { logger.error('[TraktSettingsScreen] Token exchange error:', error); openAlert(t('trakt.auth_error_title'), t('trakt.auth_error_generic')); }) .finally(() => { setIsExchangingCode(false); }); } else if (response.type === 'error') { logger.error('[TraktSettingsScreen] Authentication error:', response.error); openAlert(t('trakt.auth_error_title'), response.error?.message || t('trakt.auth_error_generic')); setIsExchangingCode(false); } else { logger.log('[TraktSettingsScreen] Auth response type:', response.type); setIsExchangingCode(false); } } }, [response, checkAuthStatus, request?.codeVerifier, navigation]); const handleSignIn = () => { promptAsync(); // Trigger the authentication flow }; const handleSignOut = async () => { openAlert( t('trakt.sign_out'), t('trakt.sign_out_confirm'), [ { label: t('common.cancel'), onPress: () => { } }, { label: t('trakt.sign_out'), onPress: async () => { setIsLoading(true); try { await traktService.logout(); setIsAuthenticated(false); setUserProfile(null); // Refresh auth status in the integration hook to ensure UI consistency await refreshAuthStatus(); } catch (error) { logger.error('[TraktSettingsScreen] Error signing out:', error); openAlert(t('common.error'), t('trakt.sign_out_error')); } finally { setIsLoading(false); } } } ] ); }; return ( navigation.goBack()} style={styles.backButton} > {t('settings.title')} {/* Empty for now, but ready for future actions */} {t('trakt.settings_title')} {/* Maintenance Mode Banner */} {traktService.isMaintenanceMode() && ( {t('trakt.maintenance_title')} {traktService.getMaintenanceMessage()} )} {isLoading ? ( ) : traktService.isMaintenanceMode() ? ( {t('trakt.maintenance_unavailable')} {t('trakt.maintenance_desc')} {t('trakt.maintenance_button')} ) : isAuthenticated && userProfile ? ( {userProfile.avatar ? ( ) : ( {userProfile.name?.charAt(0) || userProfile.username.charAt(0)} )} {userProfile.name || userProfile.username} @{userProfile.username} {userProfile.vip && ( VIP )} {t('trakt.joined', { date: new Date(userProfile.joined_at).toLocaleDateString() })} {t('trakt.sign_out')} ) : ( {t('trakt.connect_title')} {t('trakt.connect_desc')} {isExchangingCode ? ( ) : ( {t('trakt.sign_in')} )} )} {isAuthenticated && ( {t('trakt.sync_settings_title')} {t('trakt.sync_info')} {t('trakt.auto_sync_label')} {t('trakt.auto_sync_desc')} {t('trakt.import_history_label')} {t('trakt.import_history_desc')} { const success = await performManualSync(); openAlert( t('trakt.sync_complete_title'), success ? t('trakt.sync_success_msg') : t('trakt.sync_error_msg') ); }} > {isSyncing ? ( ) : ( {t('trakt.sync_now_button')} )} {/* Display Settings Section */} {t('trakt.display_settings_title')} {t('trakt.show_comments_label')} {t('trakt.show_comments_desc')} updateSetting('showTraktComments', value)} trackColor={{ false: currentTheme.colors.border, true: currentTheme.colors.primary + '80' }} thumbColor={settings.showTraktComments ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis} /> )} This product uses the Trakt API but is not endorsed or certified by Trakt. setAlertVisible(false)} actions={alertActions} /> ); }; const styles = StyleSheet.create({ container: { flex: 1, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8, }, backButton: { flexDirection: 'row', alignItems: 'center', padding: 8, }, backText: { fontSize: 17, marginLeft: 8, }, headerActions: { flexDirection: 'row', alignItems: 'center', }, headerButton: { padding: 8, marginLeft: 8, }, headerTitle: { fontSize: 34, fontWeight: 'bold', paddingHorizontal: 16, marginBottom: 24, }, scrollView: { flex: 1, }, scrollContent: { paddingHorizontal: 16, paddingBottom: 32, }, card: { borderRadius: 12, overflow: 'hidden', marginBottom: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2, }, loadingContainer: { padding: 40, alignItems: 'center', justifyContent: 'center', }, signInContainer: { padding: 24, alignItems: 'center', }, traktLogo: { width: 120, height: 120, marginBottom: 20, }, signInTitle: { fontSize: 20, fontWeight: '600', marginBottom: 8, textAlign: 'center', }, signInDescription: { fontSize: 15, textAlign: 'center', marginBottom: 24, paddingHorizontal: 20, }, button: { width: '100%', height: 44, borderRadius: 8, alignItems: 'center', justifyContent: 'center', marginTop: 8, }, signOutButton: { marginTop: 20, }, buttonText: { fontSize: 16, fontWeight: '500', color: 'white', }, profileContainer: { padding: 20, }, profileHeader: { flexDirection: 'row', alignItems: 'center', }, avatar: { width: 64, height: 64, borderRadius: 32, }, avatarPlaceholder: { width: 64, height: 64, borderRadius: 32, alignItems: 'center', justifyContent: 'center', }, avatarText: { fontSize: 26, fontWeight: 'bold', color: 'white', }, profileInfo: { marginLeft: 16, flex: 1, }, profileName: { fontSize: 18, fontWeight: '600', marginBottom: 4, }, profileUsername: { fontSize: 14, }, vipBadge: { marginTop: 4, paddingHorizontal: 8, paddingVertical: 2, backgroundColor: '#FFD700', borderRadius: 4, alignSelf: 'flex-start', }, vipText: { fontSize: 10, fontWeight: 'bold', color: '#000', }, statsContainer: { marginTop: 16, paddingTop: 16, borderTopWidth: 0.5, borderTopColor: 'rgba(150,150,150,0.2)', }, joinedDate: { fontSize: 14, }, settingsSection: { padding: 20, }, sectionTitle: { fontSize: 18, fontWeight: '600', marginBottom: 16, marginTop: 8, }, settingItem: { marginBottom: 16, }, settingContent: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', minHeight: 60, }, settingTextContainer: { flex: 1, marginRight: 16, }, settingToggleContainer: { justifyContent: 'center', alignItems: 'center', }, settingLabel: { fontSize: 15, fontWeight: '500', marginBottom: 4, }, settingDescription: { fontSize: 14, }, infoBox: { padding: 12, borderRadius: 8, borderWidth: 1, marginBottom: 16, }, infoText: { fontSize: 13, lineHeight: 18, }, // Maintenance mode styles maintenanceBanner: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#E67E22', marginHorizontal: 16, marginBottom: 16, padding: 16, borderRadius: 12, }, maintenanceBannerTextContainer: { marginLeft: 12, flex: 1, }, maintenanceBannerTitle: { fontSize: 16, fontWeight: '600', color: '#FFF', marginBottom: 4, }, maintenanceBannerMessage: { fontSize: 13, color: '#FFF', opacity: 0.9, }, disclaimer: { fontSize: 12, textAlign: 'center', marginVertical: 20, paddingHorizontal: 20, }, }); export default TraktSettingsScreen;