local backup improvements

This commit is contained in:
tapframe 2025-10-04 21:27:52 +05:30
parent 88313e6d06
commit 60e27da57d
5 changed files with 320 additions and 33 deletions

View file

@ -10,6 +10,7 @@ import {
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import * as DocumentPicker from 'expo-document-picker'; import * as DocumentPicker from 'expo-document-picker';
import * as Sharing from 'expo-sharing'; import * as Sharing from 'expo-sharing';
import * as Updates from 'expo-updates';
import { backupService, BackupOptions } from '../services/backupService'; import { backupService, BackupOptions } from '../services/backupService';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
@ -40,6 +41,20 @@ const BackupRestoreSettings: React.FC<BackupRestoreSettingsProps> = ({ isTablet
setAlertVisible(true); 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 // Create backup
const handleCreateBackup = useCallback(async () => { const handleCreateBackup = useCallback(async () => {
@ -162,7 +177,14 @@ const BackupRestoreSettings: React.FC<BackupRestoreSettingsProps> = ({ isTablet
openAlert( openAlert(
'Restore Complete', 'Restore Complete',
'Your data has been successfully restored. Please restart the app to see all changes.', '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) { } catch (error) {
logger.error('[BackupRestoreSettings] Failed to restore backup:', error); logger.error('[BackupRestoreSettings] Failed to restore backup:', error);

View file

@ -26,8 +26,11 @@ const AuthScreen: React.FC = () => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
const [mode, setMode] = useState<'signin' | 'signup'>('signin'); const [mode, setMode] = useState<'signin' | 'signup'>('signin');
const signupDisabled = true; // Signup disabled due to upcoming system replacement
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showWarningDetails, setShowWarningDetails] = useState(false);
const authCardOpacity = useRef(new Animated.Value(1)).current;
// Subtle, performant animations // Subtle, performant animations
const introOpacity = useRef(new Animated.Value(0)).current; const introOpacity = useRef(new Animated.Value(0)).current;
@ -141,6 +144,16 @@ const AuthScreen: React.FC = () => {
const handleSubmit = async () => { const handleSubmit = async () => {
if (loading) return; 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) { if (!isEmailValid) {
const msg = 'Enter a valid email address'; const msg = 'Enter a valid email address';
setError(msg); setError(msg);
@ -187,6 +200,27 @@ const AuthScreen: React.FC = () => {
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any); 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 // showToast helper replaced with direct calls to toast.* API
return ( return (
@ -254,13 +288,72 @@ const AuthScreen: React.FC = () => {
</Text> </Text>
</Animated.View> </Animated.View>
{/* Important Warning Message */}
<Animated.View
style={[
styles.warningContainer,
{
opacity: introOpacity,
transform: [{ translateY: introTranslateY }],
},
]}
>
<TouchableOpacity
style={[styles.warningCard, { backgroundColor: 'rgba(255, 193, 7, 0.1)', borderColor: 'rgba(255, 193, 7, 0.3)' }]}
onPress={toggleWarningDetails}
activeOpacity={0.8}
>
<MaterialIcons name="warning" size={20} color="#FFC107" style={styles.warningIcon} />
<View style={styles.warningContent}>
<Text style={[styles.warningTitle, { color: '#FFC107' }]}>
Important Notice
</Text>
<Text style={[styles.warningText, { color: currentTheme.colors.white }]}>
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.
</Text>
<Text style={[styles.readMoreText, { color: '#FFC107' }]}>
Read more {showWarningDetails ? '▼' : '▶'}
</Text>
</View>
</TouchableOpacity>
{/* Expanded Details */}
{showWarningDetails && (
<Animated.View style={[styles.warningDetails, { backgroundColor: 'rgba(255, 193, 7, 0.05)', borderColor: 'rgba(255, 193, 7, 0.2)' }]}>
<View style={styles.detailsContent}>
<Text style={[styles.detailsTitle, { color: '#FFC107' }]}>
Why is this system being discontinued?
</Text>
<Text style={[styles.detailsText, { color: currentTheme.colors.white }]}>
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
</Text>
<Text style={[styles.detailsTitle, { color: '#FFC107', marginTop: 16 }]}>
Benefits of Local Backup System:
</Text>
<Text style={[styles.detailsText, { color: currentTheme.colors.white }]}>
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
</Text>
</View>
</Animated.View>
)}
</Animated.View>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? headerHeight : 0} keyboardVerticalOffset={Platform.OS === 'ios' ? headerHeight : 0}
> >
{/* Main Card */} {/* Main Card - Hide when warning details are expanded */}
<View style={styles.centerContainer}> <Animated.View style={[styles.centerContainer, { opacity: authCardOpacity }]}>
<Animated.View style={[styles.card, { <Animated.View style={[styles.card, {
backgroundColor: Platform.OS === 'android' ? '#121212' : 'rgba(255,255,255,0.02)', backgroundColor: Platform.OS === 'android' ? '#121212' : 'rgba(255,255,255,0.02)',
borderColor: Platform.OS === 'android' ? '#1f1f1f' : 'rgba(255,255,255,0.06)', borderColor: Platform.OS === 'android' ? '#1f1f1f' : 'rgba(255,255,255,0.06)',
@ -312,12 +405,19 @@ const AuthScreen: React.FC = () => {
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.switchButton, styles.switchButton,
signupDisabled && styles.disabledButton,
]} ]}
onPress={() => setMode('signup')} onPress={() => !signupDisabled && setMode('signup')}
activeOpacity={0.8} activeOpacity={signupDisabled ? 1 : 0.8}
disabled={signupDisabled}
> >
<Text style={[styles.switchText, { color: mode === 'signup' ? '#fff' : currentTheme.colors.textMuted }]}> <Text style={[
Sign Up styles.switchText,
{
color: mode === 'signup' ? '#fff' : (signupDisabled ? 'rgba(255,255,255,0.3)' : currentTheme.colors.textMuted)
}
]}>
Sign Up {signupDisabled && '(Disabled)'}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -482,18 +582,29 @@ const AuthScreen: React.FC = () => {
</Animated.View> </Animated.View>
{/* Switch Mode */} {/* Switch Mode */}
<TouchableOpacity {!signupDisabled && (
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')} <TouchableOpacity
activeOpacity={0.7} onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
style={{ marginTop: 16 }} activeOpacity={0.7}
> style={{ marginTop: 16 }}
<Text style={[styles.switchModeText, { color: currentTheme.colors.textMuted }]}> >
{mode === 'signin' ? "Don't have an account? " : 'Already have an account? '} <Text style={[styles.switchModeText, { color: currentTheme.colors.textMuted }]}>
<Text style={{ color: currentTheme.colors.primary, fontWeight: '600' }}> {mode === 'signin' ? "Don't have an account? " : 'Already have an account? '}
{mode === 'signin' ? 'Sign up' : 'Sign in'} <Text style={{ color: currentTheme.colors.primary, fontWeight: '600' }}>
{mode === 'signin' ? 'Sign up' : 'Sign in'}
</Text>
</Text> </Text>
</Text> </TouchableOpacity>
</TouchableOpacity> )}
{/* Signup disabled message */}
{signupDisabled && mode === 'signin' && (
<View style={{ marginTop: 16, alignItems: 'center' }}>
<Text style={[styles.switchModeText, { color: 'rgba(255,255,255,0.5)', fontSize: 13 }]}>
New account creation is temporarily disabled
</Text>
</View>
)}
{/* Skip sign in - more prominent when coming from onboarding */} {/* Skip sign in - more prominent when coming from onboarding */}
<TouchableOpacity <TouchableOpacity
@ -520,7 +631,7 @@ const AuthScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
</View> </Animated.View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
{/* Toasts rendered globally in App root */} {/* Toasts rendered globally in App root */}
</SafeAreaView> </SafeAreaView>
@ -580,8 +691,9 @@ const styles = StyleSheet.create({
centerContainer: { centerContainer: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'flex-start',
paddingHorizontal: 20, paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 28, paddingBottom: 28,
}, },
card: { card: {
@ -686,6 +798,63 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
fontWeight: '500', 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; export default AuthScreen;

View file

@ -13,6 +13,7 @@ import {
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import * as DocumentPicker from 'expo-document-picker'; import * as DocumentPicker from 'expo-document-picker';
import * as Sharing from 'expo-sharing'; import * as Sharing from 'expo-sharing';
import * as Updates from 'expo-updates';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { backupService, BackupOptions } from '../services/backupService'; import { backupService, BackupOptions } from '../services/backupService';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
@ -41,6 +42,20 @@ const BackupScreen: React.FC = () => {
setAlertVisible(true); 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 // Create backup
const handleCreateBackup = useCallback(async () => { const handleCreateBackup = useCallback(async () => {
try { try {
@ -162,7 +177,14 @@ const BackupScreen: React.FC = () => {
openAlert( openAlert(
'Restore Complete', 'Restore Complete',
'Your data has been successfully restored. Please restart the app to see all changes.', '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) { } catch (error) {
logger.error('[BackupScreen] Failed to restore backup:', error); logger.error('[BackupScreen] Failed to restore backup:', error);

View file

@ -26,7 +26,6 @@ import Animated, {
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { NavigationProp, useNavigation } from '@react-navigation/native'; import { NavigationProp, useNavigation } from '@react-navigation/native';
import { useAccount } from '../contexts/AccountContext';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
@ -79,7 +78,6 @@ const onboardingData: OnboardingSlide[] = [
const OnboardingScreen = () => { const OnboardingScreen = () => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { user } = useAccount();
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const flatListRef = useRef<FlatList>(null); const flatListRef = useRef<FlatList>(null);
const progressValue = useSharedValue(0); const progressValue = useSharedValue(0);
@ -137,15 +135,11 @@ const OnboardingScreen = () => {
const handleGetStarted = async () => { const handleGetStarted = async () => {
try { try {
await AsyncStorage.setItem('hasCompletedOnboarding', 'true'); await AsyncStorage.setItem('hasCompletedOnboarding', 'true');
// After onboarding, route to login if no user; otherwise go to app // After onboarding, go directly to main app
if (!user) { navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] });
navigation.reset({ index: 0, routes: [{ name: 'Account', params: { fromOnboarding: true } as any }] });
} else {
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] });
}
} catch (error) { } catch (error) {
if (__DEV__) console.error('Error saving onboarding status:', 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' }] });
} }
}; };

View file

@ -453,8 +453,43 @@ export class BackupService {
private async getTraktSettings(): Promise<any> { private async getTraktSettings(): Promise<any> {
try { try {
// Get general Trakt settings
const traktSettingsJson = await AsyncStorage.getItem('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) { } catch (error) {
logger.error('[BackupService] Failed to get Trakt settings:', error); logger.error('[BackupService] Failed to get Trakt settings:', error);
return {}; return {};
@ -607,8 +642,53 @@ export class BackupService {
private async restoreTraktSettings(traktSettings: any): Promise<void> { private async restoreTraktSettings(traktSettings: any): Promise<void> {
try { try {
await AsyncStorage.setItem('trakt_settings', JSON.stringify(traktSettings)); // Restore general Trakt settings
logger.info('[BackupService] Trakt settings restored'); 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) { } catch (error) {
logger.error('[BackupService] Failed to restore Trakt settings:', error); logger.error('[BackupService] Failed to restore Trakt settings:', error);
} }