From 60e27da57def7ace50c1df165d0ca8a8a50bfdba Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 4 Oct 2025 21:27:52 +0530 Subject: [PATCH] local backup improvements --- src/components/BackupRestoreSettings.tsx | 24 ++- src/screens/AuthScreen.tsx | 207 ++++++++++++++++++++--- src/screens/BackupScreen.tsx | 24 ++- src/screens/OnboardingScreen.tsx | 12 +- src/services/backupService.ts | 86 +++++++++- 5 files changed, 320 insertions(+), 33 deletions(-) diff --git a/src/components/BackupRestoreSettings.tsx b/src/components/BackupRestoreSettings.tsx index f30d7e95..603ab53e 100644 --- a/src/components/BackupRestoreSettings.tsx +++ b/src/components/BackupRestoreSettings.tsx @@ -10,6 +10,7 @@ import { import { MaterialIcons } from '@expo/vector-icons'; import * as DocumentPicker from 'expo-document-picker'; import * as Sharing from 'expo-sharing'; +import * as Updates from 'expo-updates'; import { backupService, BackupOptions } from '../services/backupService'; import { useTheme } from '../contexts/ThemeContext'; import { logger } from '../utils/logger'; @@ -40,6 +41,20 @@ const BackupRestoreSettings: React.FC = ({ isTablet setAlertVisible(true); }; + const restartApp = async () => { + try { + await Updates.reloadAsync(); + } catch (error) { + logger.error('[BackupRestoreSettings] Failed to restart app:', error); + // Fallback: show error message + openAlert( + 'Restart Failed', + 'Failed to restart the app. Please manually close and reopen the app to see your restored data.', + [{ label: 'OK', onPress: () => {} }] + ); + } + }; + // Create backup const handleCreateBackup = useCallback(async () => { @@ -162,7 +177,14 @@ const BackupRestoreSettings: React.FC = ({ isTablet openAlert( 'Restore Complete', 'Your data has been successfully restored. Please restart the app to see all changes.', - [{ label: 'OK', onPress: () => {} }] + [ + { label: 'Cancel', onPress: () => {} }, + { + label: 'Restart App', + onPress: restartApp, + style: { fontWeight: 'bold' } + } + ] ); } catch (error) { logger.error('[BackupRestoreSettings] Failed to restore backup:', error); diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx index ba08816e..edb36e5d 100644 --- a/src/screens/AuthScreen.tsx +++ b/src/screens/AuthScreen.tsx @@ -26,8 +26,11 @@ const AuthScreen: React.FC = () => { const [showPassword, setShowPassword] = useState(false); const [showConfirm, setShowConfirm] = useState(false); const [mode, setMode] = useState<'signin' | 'signup'>('signin'); + const signupDisabled = true; // Signup disabled due to upcoming system replacement const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const [showWarningDetails, setShowWarningDetails] = useState(false); + const authCardOpacity = useRef(new Animated.Value(1)).current; // Subtle, performant animations const introOpacity = useRef(new Animated.Value(0)).current; @@ -141,6 +144,16 @@ const AuthScreen: React.FC = () => { const handleSubmit = async () => { if (loading) return; + + // Prevent signup if disabled + if (mode === 'signup' && signupDisabled) { + const msg = 'Sign up is currently disabled due to upcoming system changes'; + setError(msg); + Toast.error(msg); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); + return; + } + if (!isEmailValid) { const msg = 'Enter a valid email address'; setError(msg); @@ -187,6 +200,27 @@ const AuthScreen: React.FC = () => { navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any); }; + const toggleWarningDetails = () => { + if (showWarningDetails) { + // Fade in auth card + Animated.timing(authCardOpacity, { + toValue: 1, + duration: 300, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }).start(); + } else { + // Fade out auth card + Animated.timing(authCardOpacity, { + toValue: 0, + duration: 300, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }).start(); + } + setShowWarningDetails(!showWarningDetails); + }; + // showToast helper replaced with direct calls to toast.* API return ( @@ -254,13 +288,72 @@ const AuthScreen: React.FC = () => { + {/* Important Warning Message */} + + + + + + Important Notice + + + This authentication system will be completely replaced by local backup/restore functionality by October 8th. Please create backup files as your cloud data will be permanently destroyed. + + + Read more {showWarningDetails ? '▼' : '▶'} + + + + + {/* Expanded Details */} + {showWarningDetails && ( + + + + Why is this system being discontinued? + + + • Lack of real-time support for addon synchronization{'\n'} + • Database synchronization issues with addons and settings{'\n'} + • Unreliable cloud data management{'\n'} + • Performance problems with remote data access + + + + Benefits of Local Backup System: + + + • Instant addon synchronization across devices{'\n'} + • Reliable offline access to all your data{'\n'} + • Complete control over your backup files{'\n'} + • Faster performance with local data storage{'\n'} + • No dependency on external servers{'\n'} + • Easy migration between devices + + + + )} + + - {/* Main Card */} - + {/* Main Card - Hide when warning details are expanded */} + { setMode('signup')} - activeOpacity={0.8} + onPress={() => !signupDisabled && setMode('signup')} + activeOpacity={signupDisabled ? 1 : 0.8} + disabled={signupDisabled} > - - Sign Up + + Sign Up {signupDisabled && '(Disabled)'} @@ -482,18 +582,29 @@ const AuthScreen: React.FC = () => { {/* Switch Mode */} - setMode(mode === 'signin' ? 'signup' : 'signin')} - activeOpacity={0.7} - style={{ marginTop: 16 }} - > - - {mode === 'signin' ? "Don't have an account? " : 'Already have an account? '} - - {mode === 'signin' ? 'Sign up' : 'Sign in'} + {!signupDisabled && ( + setMode(mode === 'signin' ? 'signup' : 'signin')} + activeOpacity={0.7} + style={{ marginTop: 16 }} + > + + {mode === 'signin' ? "Don't have an account? " : 'Already have an account? '} + + {mode === 'signin' ? 'Sign up' : 'Sign in'} + - - + + )} + + {/* Signup disabled message */} + {signupDisabled && mode === 'signin' && ( + + + New account creation is temporarily disabled + + + )} {/* Skip sign in - more prominent when coming from onboarding */} { - + {/* Toasts rendered globally in App root */} @@ -580,8 +691,9 @@ const styles = StyleSheet.create({ centerContainer: { flex: 1, alignItems: 'center', - justifyContent: 'center', + justifyContent: 'flex-start', paddingHorizontal: 20, + paddingTop: 20, paddingBottom: 28, }, card: { @@ -686,6 +798,63 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: '500', }, + warningContainer: { + paddingHorizontal: 20, + marginTop: 24, + marginBottom: 8, + }, + warningCard: { + flexDirection: 'row', + padding: 16, + borderRadius: 12, + borderWidth: 1, + alignItems: 'flex-start', + }, + warningIcon: { + marginRight: 12, + marginTop: 2, + }, + warningContent: { + flex: 1, + }, + warningTitle: { + fontSize: 16, + fontWeight: '700', + marginBottom: 6, + }, + warningText: { + fontSize: 14, + lineHeight: 20, + fontWeight: '500', + }, + disabledButton: { + opacity: 0.5, + }, + readMoreText: { + fontSize: 14, + fontWeight: '600', + marginTop: 8, + alignSelf: 'flex-start', + }, + warningDetails: { + marginTop: 8, + borderRadius: 12, + borderWidth: 1, + overflow: 'hidden', + }, + detailsContent: { + padding: 16, + }, + detailsTitle: { + fontSize: 15, + fontWeight: '700', + marginBottom: 8, + }, + detailsText: { + fontSize: 13, + lineHeight: 18, + fontWeight: '500', + }, }); export default AuthScreen; diff --git a/src/screens/BackupScreen.tsx b/src/screens/BackupScreen.tsx index e50feec3..c3665341 100644 --- a/src/screens/BackupScreen.tsx +++ b/src/screens/BackupScreen.tsx @@ -13,6 +13,7 @@ import { import { MaterialIcons } from '@expo/vector-icons'; import * as DocumentPicker from 'expo-document-picker'; import * as Sharing from 'expo-sharing'; +import * as Updates from 'expo-updates'; import { useNavigation } from '@react-navigation/native'; import { backupService, BackupOptions } from '../services/backupService'; import { useTheme } from '../contexts/ThemeContext'; @@ -41,6 +42,20 @@ const BackupScreen: React.FC = () => { setAlertVisible(true); }; + const restartApp = async () => { + try { + await Updates.reloadAsync(); + } catch (error) { + logger.error('[BackupScreen] Failed to restart app:', error); + // Fallback: show error message + openAlert( + 'Restart Failed', + 'Failed to restart the app. Please manually close and reopen the app to see your restored data.', + [{ label: 'OK', onPress: () => {} }] + ); + } + }; + // Create backup const handleCreateBackup = useCallback(async () => { try { @@ -162,7 +177,14 @@ const BackupScreen: React.FC = () => { openAlert( 'Restore Complete', 'Your data has been successfully restored. Please restart the app to see all changes.', - [{ label: 'OK', onPress: () => {} }] + [ + { label: 'Cancel', onPress: () => {} }, + { + label: 'Restart App', + onPress: restartApp, + style: { fontWeight: 'bold' } + } + ] ); } catch (error) { logger.error('[BackupScreen] Failed to restore backup:', error); diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index 3b445dd3..50215b6b 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -26,7 +26,6 @@ import Animated, { } from 'react-native-reanimated'; import { useTheme } from '../contexts/ThemeContext'; import { NavigationProp, useNavigation } from '@react-navigation/native'; -import { useAccount } from '../contexts/AccountContext'; import { RootStackParamList } from '../navigation/AppNavigator'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -79,7 +78,6 @@ const onboardingData: OnboardingSlide[] = [ const OnboardingScreen = () => { const { currentTheme } = useTheme(); const navigation = useNavigation>(); - const { user } = useAccount(); const [currentIndex, setCurrentIndex] = useState(0); const flatListRef = useRef(null); const progressValue = useSharedValue(0); @@ -137,15 +135,11 @@ const OnboardingScreen = () => { const handleGetStarted = async () => { try { await AsyncStorage.setItem('hasCompletedOnboarding', 'true'); - // After onboarding, route to login if no user; otherwise go to app - if (!user) { - navigation.reset({ index: 0, routes: [{ name: 'Account', params: { fromOnboarding: true } as any }] }); - } else { - navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] }); - } + // After onboarding, go directly to main app + navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] }); } catch (error) { if (__DEV__) console.error('Error saving onboarding status:', error); - navigation.reset({ index: 0, routes: [{ name: user ? 'MainTabs' : 'Account', params: !user ? ({ fromOnboarding: true } as any) : undefined }] }); + navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] }); } }; diff --git a/src/services/backupService.ts b/src/services/backupService.ts index 20369df8..15146db2 100644 --- a/src/services/backupService.ts +++ b/src/services/backupService.ts @@ -453,8 +453,43 @@ export class BackupService { private async getTraktSettings(): Promise { try { + // Get general Trakt settings const traktSettingsJson = await AsyncStorage.getItem('trakt_settings'); - return traktSettingsJson ? JSON.parse(traktSettingsJson) : {}; + const traktSettings = traktSettingsJson ? JSON.parse(traktSettingsJson) : {}; + + // Get authentication tokens + const [ + accessToken, + refreshToken, + tokenExpiry, + autosyncEnabled, + syncFrequency, + completionThreshold + ] = await Promise.all([ + AsyncStorage.getItem('trakt_access_token'), + AsyncStorage.getItem('trakt_refresh_token'), + AsyncStorage.getItem('trakt_token_expiry'), + AsyncStorage.getItem('trakt_autosync_enabled'), + AsyncStorage.getItem('trakt_sync_frequency'), + AsyncStorage.getItem('trakt_completion_threshold') + ]); + + return { + ...traktSettings, + authentication: { + accessToken, + refreshToken, + tokenExpiry: tokenExpiry ? parseInt(tokenExpiry, 10) : null + }, + autosync: { + enabled: autosyncEnabled ? (() => { + try { return JSON.parse(autosyncEnabled); } + catch { return true; } + })() : true, + frequency: syncFrequency ? parseInt(syncFrequency, 10) : 60000, + completionThreshold: completionThreshold ? parseInt(completionThreshold, 10) : 95 + } + }; } catch (error) { logger.error('[BackupService] Failed to get Trakt settings:', error); return {}; @@ -607,8 +642,53 @@ export class BackupService { private async restoreTraktSettings(traktSettings: any): Promise { try { - await AsyncStorage.setItem('trakt_settings', JSON.stringify(traktSettings)); - logger.info('[BackupService] Trakt settings restored'); + // Restore general Trakt settings + if (traktSettings && typeof traktSettings === 'object') { + const { authentication, autosync, ...generalSettings } = traktSettings; + + // Restore general settings + await AsyncStorage.setItem('trakt_settings', JSON.stringify(generalSettings)); + + // Restore authentication tokens if available + if (authentication) { + const tokenPromises = []; + + if (authentication.accessToken) { + tokenPromises.push(AsyncStorage.setItem('trakt_access_token', authentication.accessToken)); + } + + if (authentication.refreshToken) { + tokenPromises.push(AsyncStorage.setItem('trakt_refresh_token', authentication.refreshToken)); + } + + if (authentication.tokenExpiry) { + tokenPromises.push(AsyncStorage.setItem('trakt_token_expiry', authentication.tokenExpiry.toString())); + } + + await Promise.all(tokenPromises); + } + + // Restore autosync settings if available + if (autosync) { + const autosyncPromises = []; + + if (autosync.enabled !== undefined) { + autosyncPromises.push(AsyncStorage.setItem('trakt_autosync_enabled', JSON.stringify(autosync.enabled))); + } + + if (autosync.frequency !== undefined) { + autosyncPromises.push(AsyncStorage.setItem('trakt_sync_frequency', autosync.frequency.toString())); + } + + if (autosync.completionThreshold !== undefined) { + autosyncPromises.push(AsyncStorage.setItem('trakt_completion_threshold', autosync.completionThreshold.toString())); + } + + await Promise.all(autosyncPromises); + } + + logger.info('[BackupService] Trakt settings and authentication restored'); + } } catch (error) { logger.error('[BackupService] Failed to restore Trakt settings:', error); }