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 * 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<BackupRestoreSettingsProps> = ({ 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<BackupRestoreSettingsProps> = ({ 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);

View file

@ -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<string | null>(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 = () => {
</Text>
</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
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? headerHeight : 0}
>
{/* Main Card */}
<View style={styles.centerContainer}>
{/* Main Card - Hide when warning details are expanded */}
<Animated.View style={[styles.centerContainer, { opacity: authCardOpacity }]}>
<Animated.View style={[styles.card, {
backgroundColor: Platform.OS === 'android' ? '#121212' : 'rgba(255,255,255,0.02)',
borderColor: Platform.OS === 'android' ? '#1f1f1f' : 'rgba(255,255,255,0.06)',
@ -312,12 +405,19 @@ const AuthScreen: React.FC = () => {
<TouchableOpacity
style={[
styles.switchButton,
signupDisabled && styles.disabledButton,
]}
onPress={() => setMode('signup')}
activeOpacity={0.8}
onPress={() => !signupDisabled && setMode('signup')}
activeOpacity={signupDisabled ? 1 : 0.8}
disabled={signupDisabled}
>
<Text style={[styles.switchText, { color: mode === 'signup' ? '#fff' : currentTheme.colors.textMuted }]}>
Sign Up
<Text style={[
styles.switchText,
{
color: mode === 'signup' ? '#fff' : (signupDisabled ? 'rgba(255,255,255,0.3)' : currentTheme.colors.textMuted)
}
]}>
Sign Up {signupDisabled && '(Disabled)'}
</Text>
</TouchableOpacity>
</View>
@ -482,18 +582,29 @@ const AuthScreen: React.FC = () => {
</Animated.View>
{/* Switch Mode */}
<TouchableOpacity
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
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={{ color: currentTheme.colors.primary, fontWeight: '600' }}>
{mode === 'signin' ? 'Sign up' : 'Sign in'}
{!signupDisabled && (
<TouchableOpacity
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
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={{ color: currentTheme.colors.primary, fontWeight: '600' }}>
{mode === 'signin' ? 'Sign up' : 'Sign in'}
</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 */}
<TouchableOpacity
@ -520,7 +631,7 @@ const AuthScreen: React.FC = () => {
</TouchableOpacity>
</Animated.View>
</View>
</Animated.View>
</KeyboardAvoidingView>
{/* Toasts rendered globally in App root */}
</SafeAreaView>
@ -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;

View file

@ -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);

View file

@ -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<NavigationProp<RootStackParamList>>();
const { user } = useAccount();
const [currentIndex, setCurrentIndex] = useState(0);
const flatListRef = useRef<FlatList>(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' }] });
}
};

View file

@ -453,8 +453,43 @@ export class BackupService {
private async getTraktSettings(): Promise<any> {
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<void> {
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);
}