feat: enhance onboarding and settings screens with NAS authentication and configuration

This commit is contained in:
bobbert committed 2026-03-04 23:10:19 +00:00
1 parent 5158305086
commit 09a6d2a57b
2 files changed
+487 -242

No files matched your search

+179 -75
View File
@@ -1,11 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
StyleSheet, View, Text, ScrollView, TouchableOpacity, Alert, StyleSheet, View, Text, ScrollView, TouchableOpacity, Alert,
ActivityIndicator, Switch, ActivityIndicator, Switch, TextInput,
} from 'react-native'; } from 'react-native';
import { useTheme } from '../services/ThemeContext'; import { useTheme } from '../services/ThemeContext';
import DatabaseService from '../services/DatabaseService'; import DatabaseService from '../services/DatabaseService';
import ServiceConnectCard from '../components/ServiceConnectCard'; import GoogleAUTH from '../AUTH/GoogleAUTH';
import NASAUTH from '../AUTH/NASAUTH';
import NASSetupInstructions from '../components/NASSetupInstructions';
import ServiceHealthIndicator from '../components/ServiceHealthIndicator';
const OnboardingScreen = ({ navigation, route }) => { const OnboardingScreen = ({ navigation, route }) => {
const { userId } = route.params; const { userId } = route.params;
@@ -18,11 +21,21 @@ const OnboardingScreen = ({ navigation, route }) => {
const [iosEnabled, setIosEnabled] = useState(false); const [iosEnabled, setIosEnabled] = useState(false);
const [nasEnabled, setNasEnabled] = useState(false); const [nasEnabled, setNasEnabled] = useState(false);
// Step 1 — track which services completed connection via ServiceConnectCard callbacks // Step 1 — connection status
const [googleConnected, setGoogleConnected] = useState(false); const [googleConnected, setGoogleConnected] = useState(false);
const [iosConnected, setIosConnected] = useState(false); const [iosConnected, setIosConnected] = useState(false);
const [nasConnected, setNasConnected] = useState(false); const [nasConnected, setNasConnected] = useState(false);
// Step 1 — health indicators
const [googleHealth, setGoogleHealth] = useState('inactive');
const [iosHealth, setIosHealth] = useState('inactive');
const [nasHealth, setNasHealth] = useState('inactive');
// NAS fields
const [nasHost, setNasHost] = useState('');
const [nasPort, setNasPort] = useState('9500');
const [nasUsername, setNasUsername] = useState('');
// Step 2 — auto-backup checklist // Step 2 — auto-backup checklist
const [autoBackupServices, setAutoBackupServices] = useState([]); const [autoBackupServices, setAutoBackupServices] = useState([]);
@@ -40,6 +53,74 @@ const OnboardingScreen = ({ navigation, route }) => {
return services; return services;
}; };
const handleGoogleAuth = async () => {
setLoading(true);
setGoogleHealth('checking');
try {
const googleAuth = new GoogleAUTH();
const result = await googleAuth.authenticate();
await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', {
accessToken: result.accessToken,
refreshToken: result.refreshToken || null,
expiresAt: googleAuth.expiresAt || Date.now() + 3600000,
email: result.email || 'google-user',
});
setGoogleConnected(true);
setGoogleHealth('healthy');
Alert.alert('Success', 'Google Photos connected!');
} catch (error) {
setGoogleHealth('error');
Alert.alert('Error', 'Failed to connect Google Photos: ' + error.message);
} finally {
setLoading(false);
}
};
const handleIosAuth = async () => {
setLoading(true);
setIosHealth('checking');
try {
await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', {
status: 'placeholder',
note: 'iCloud integration pending Apple API access',
});
setIosConnected(true);
setIosHealth('healthy');
Alert.alert('Info', 'iCloud Photos registered. Full integration coming soon.');
} catch (error) {
setIosHealth('error');
Alert.alert('Error', 'Failed to set up iCloud: ' + error.message);
} finally {
setLoading(false);
}
};
const handleNasAuth = async () => {
if (!nasHost.trim() || !nasUsername.trim()) {
Alert.alert('Error', 'Please fill in the NAS host and username');
return;
}
setLoading(true);
setNasHealth('checking');
try {
const nasAuth = new NASAUTH();
await nasAuth.authenticate(nasHost.trim(), parseInt(nasPort) || 9500, nasUsername.trim());
await DatabaseService.linkAccount(userId, 'NAS', {
host: nasHost.trim(),
port: parseInt(nasPort) || 9500,
username: nasUsername.trim(),
});
setNasConnected(true);
setNasHealth('healthy');
Alert.alert('Success', 'NAS connected!');
} catch (error) {
setNasHealth('error');
Alert.alert('Error', 'Failed to connect NAS: ' + error.message);
} finally {
setLoading(false);
}
};
const toggleAutoBackup = (serviceType) => { const toggleAutoBackup = (serviceType) => {
setAutoBackupServices((prev) => setAutoBackupServices((prev) =>
prev.includes(serviceType) ? prev.filter((s) => s !== serviceType) : [...prev, serviceType] prev.includes(serviceType) ? prev.filter((s) => s !== serviceType) : [...prev, serviceType]
@@ -89,7 +170,7 @@ const OnboardingScreen = ({ navigation, route }) => {
<Text style={[styles.description, { color: theme.textSecondary }]}>{steps[currentStep].description}</Text> <Text style={[styles.description, { color: theme.textSecondary }]}>{steps[currentStep].description}</Text>
</View> </View>
{/* Step 0: Select services */} {/* ── Step 0: pick services ── */}
{currentStep === 0 && ( {currentStep === 0 && (
<View style={styles.cardsContainer}> <View style={styles.cardsContainer}>
<ServiceToggleCard theme={theme} icon="📷" name="Google Photos" <ServiceToggleCard theme={theme} icon="📷" name="Google Photos"
@@ -101,40 +182,88 @@ const OnboardingScreen = ({ navigation, route }) => {
<ServiceToggleCard theme={theme} icon="💾" name="NAS Storage" <ServiceToggleCard theme={theme} icon="💾" name="NAS Storage"
description="Connect to your NAS photo server (REST API)" description="Connect to your NAS photo server (REST API)"
enabled={nasEnabled} onToggle={() => setNasEnabled(!nasEnabled)} /> enabled={nasEnabled} onToggle={() => setNasEnabled(!nasEnabled)} />
{nasEnabled && <NASSetupInstructions />}
</View> </View>
)} )}
{/* Step 1: Configure each selected service via shared ServiceConnectCard */} {/* ── Step 1: connect each service ── */}
{currentStep === 1 && ( {currentStep === 1 && (
<View style={styles.cardsContainer}> <View style={styles.cardsContainer}>
{googleEnabled && ( {googleEnabled && (
<ServiceConnectCard <View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
serviceType="GOOGLE_PHOTOS" <View style={styles.configHeader}>
userId={userId} <ServiceHealthIndicator status={googleHealth} size={10} />
isLinked={googleConnected} <Text style={styles.configIcon}>📷</Text>
onLinked={() => setGoogleConnected(true)} <Text style={[styles.configTitle, { color: theme.text }]}>Google Photos</Text>
/> {googleConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
</View>
{!googleConnected && (
<TouchableOpacity
style={[styles.authButton, { backgroundColor: '#4285F4' }, loading && styles.buttonDisabled]}
onPress={handleGoogleAuth} disabled={loading}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.authButtonText}>Sign in with Google</Text>}
</TouchableOpacity>
)}
</View>
)} )}
{iosEnabled && ( {iosEnabled && (
<ServiceConnectCard <View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
serviceType="ICLOUD_PHOTOS" <View style={styles.configHeader}>
userId={userId} <ServiceHealthIndicator status={iosHealth} size={10} />
isLinked={iosConnected} <Text style={styles.configIcon}>🍎</Text>
onLinked={() => setIosConnected(true)} <Text style={[styles.configTitle, { color: theme.text }]}>iCloud Photos</Text>
/> {iosConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
</View>
{!iosConnected && (
<TouchableOpacity
style={[styles.authButton, { backgroundColor: '#333' }, loading && styles.buttonDisabled]}
onPress={handleIosAuth} disabled={loading}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.authButtonText}>Connect iCloud</Text>}
</TouchableOpacity>
)}
</View>
)} )}
{nasEnabled && ( {nasEnabled && (
<ServiceConnectCard <View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
serviceType="NAS" <View style={styles.configHeader}>
userId={userId} <ServiceHealthIndicator status={nasHealth} size={10} />
isLinked={nasConnected} <Text style={styles.configIcon}>💾</Text>
onLinked={() => setNasConnected(true)} <Text style={[styles.configTitle, { color: theme.text }]}>NAS Storage</Text>
/> {nasConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
</View>
{!nasConnected && (
<>
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Server Address</Text>
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
placeholder="192.168.1.100" placeholderTextColor={theme.textTertiary}
value={nasHost} onChangeText={setNasHost} editable={!loading} />
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Port</Text>
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
placeholder="9500" placeholderTextColor={theme.textTertiary}
value={nasPort} onChangeText={setNasPort} editable={!loading} keyboardType="numeric" />
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Username</Text>
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
placeholder="Your username" placeholderTextColor={theme.textTertiary}
value={nasUsername} onChangeText={setNasUsername} editable={!loading} autoCapitalize="none" />
<TouchableOpacity
style={[styles.authButton, { backgroundColor: '#0a7ea4' }, loading && styles.buttonDisabled]}
onPress={handleNasAuth} disabled={loading}
>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.authButtonText}>Test & Connect</Text>}
</TouchableOpacity>
</>
)}
</View>
)} )}
</View> </View>
)} )}
{/* Step 2: Auto-backup multi-checkbox */} {/* ── Step 2: auto-backup ── */}
{currentStep === 2 && ( {currentStep === 2 && (
<View style={styles.cardsContainer}> <View style={styles.cardsContainer}>
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}> <View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
@@ -142,31 +271,22 @@ const OnboardingScreen = ({ navigation, route }) => {
Select which services new photos should automatically upload to. You can select multiple or none. Select which services new photos should automatically upload to. You can select multiple or none.
</Text> </Text>
{getConnectedServices().map((svc) => ( {getConnectedServices().map((svc) => (
<TouchableOpacity <TouchableOpacity key={svc.type}
key={svc.type}
style={[styles.checkRow, { borderBottomColor: theme.borderLight }]} style={[styles.checkRow, { borderBottomColor: theme.borderLight }]}
onPress={() => toggleAutoBackup(svc.type)} onPress={() => toggleAutoBackup(svc.type)} activeOpacity={0.7}
activeOpacity={0.7}
> >
<View style={[ <View style={[styles.checkbox, { borderColor: theme.primary },
styles.checkbox, { borderColor: theme.primary }, autoBackupServices.includes(svc.type) && { backgroundColor: theme.primary }]}>
autoBackupServices.includes(svc.type) && { backgroundColor: theme.primary },
]}>
{autoBackupServices.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>} {autoBackupServices.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>}
</View> </View>
<Text style={styles.checkIcon}>{svc.icon}</Text> <Text style={styles.checkIcon}>{svc.icon}</Text>
<Text style={[styles.checkLabel, { color: theme.text }]}>{svc.name}</Text> <Text style={[styles.checkLabel, { color: theme.text }]}>{svc.name}</Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
<TouchableOpacity <TouchableOpacity style={[styles.checkRow, { borderBottomWidth: 0 }]}
style={[styles.checkRow, { borderBottomWidth: 0 }]} onPress={() => setAutoBackupServices([])} activeOpacity={0.7}>
onPress={() => setAutoBackupServices([])} <View style={[styles.checkbox, { borderColor: theme.border },
activeOpacity={0.7} autoBackupServices.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary }]}>
>
<View style={[
styles.checkbox, { borderColor: theme.border },
autoBackupServices.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary },
]}>
{autoBackupServices.length === 0 && <Text style={styles.checkmark}>✓</Text>} {autoBackupServices.length === 0 && <Text style={styles.checkmark}>✓</Text>}
</View> </View>
<Text style={[styles.checkLabel, { color: theme.text }]}>Don't automatically back up</Text> <Text style={[styles.checkLabel, { color: theme.text }]}>Don't automatically back up</Text>
@@ -183,17 +303,13 @@ const OnboardingScreen = ({ navigation, route }) => {
onPress={() => setCurrentStep(currentStep - 1)} disabled={loading}> onPress={() => setCurrentStep(currentStep - 1)} disabled={loading}>
<Text style={[styles.backButtonText, { color: theme.textSecondary }]}>Back</Text> <Text style={[styles.backButtonText, { color: theme.textSecondary }]}>Back</Text>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : <View style={{ flex: 1 }} />}
<View style={{ flex: 1 }} />
)}
<TouchableOpacity <TouchableOpacity
style={[styles.nextButton, { backgroundColor: theme.primary }, loading && styles.buttonDisabled]} style={[styles.nextButton, { backgroundColor: theme.primary }, loading && styles.buttonDisabled]}
onPress={handleNext} disabled={loading} onPress={handleNext} disabled={loading}
> >
{loading ? <ActivityIndicator color="#fff" /> : ( {loading ? <ActivityIndicator color="#fff" /> : (
<Text style={styles.nextButtonText}> <Text style={styles.nextButtonText}>{currentStep === steps.length - 1 ? 'Get Started' : 'Next'}</Text>
{currentStep === steps.length - 1 ? 'Get Started' : 'Next'}
</Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -210,21 +326,15 @@ const ServiceToggleCard = ({ theme, icon, name, description, enabled, onToggle }
<Text style={[styles.toggleDescription, { color: theme.textSecondary }]}>{description}</Text> <Text style={[styles.toggleDescription, { color: theme.textSecondary }]}>{description}</Text>
</View> </View>
</View> </View>
<Switch <Switch value={enabled} onValueChange={onToggle}
value={enabled}
onValueChange={onToggle}
trackColor={{ false: theme.border, true: '#a8dab5' }} trackColor={{ false: theme.border, true: '#a8dab5' }}
thumbColor={enabled ? '#34a853' : '#f4f3f4'} thumbColor={enabled ? '#34a853' : '#f4f3f4'} />
/>
</View> </View>
); );
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1 },
progressBar: { progressBar: { flexDirection: 'row', justifyContent: 'center', gap: 8, paddingTop: 60, paddingBottom: 8 },
flexDirection: 'row', justifyContent: 'center', gap: 8,
paddingTop: 60, paddingBottom: 8,
},
progressDot: { width: 32, height: 4, borderRadius: 2 }, progressDot: { width: 32, height: 4, borderRadius: 2 },
content: { flex: 1, paddingHorizontal: 20 }, content: { flex: 1, paddingHorizontal: 20 },
header: { marginBottom: 24 }, header: { marginBottom: 24 },
@@ -232,35 +342,29 @@ const styles = StyleSheet.create({
title: { fontSize: 26, fontWeight: '700', marginBottom: 6 }, title: { fontSize: 26, fontWeight: '700', marginBottom: 6 },
description: { fontSize: 14 }, description: { fontSize: 14 },
cardsContainer: { gap: 12 }, cardsContainer: { gap: 12 },
toggleCard: { toggleCard: { borderRadius: 12, padding: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderWidth: 1.5 },
borderRadius: 12, padding: 16, flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center', borderWidth: 1.5,
},
toggleCardContent: { flex: 1, flexDirection: 'row', alignItems: 'center' }, toggleCardContent: { flex: 1, flexDirection: 'row', alignItems: 'center' },
toggleIcon: { fontSize: 28, marginRight: 14 }, toggleIcon: { fontSize: 28, marginRight: 14 },
toggleInfo: { flex: 1 }, toggleInfo: { flex: 1 },
toggleName: { fontSize: 15, fontWeight: '600', marginBottom: 2 }, toggleName: { fontSize: 15, fontWeight: '600', marginBottom: 2 },
toggleDescription: { fontSize: 12 }, toggleDescription: { fontSize: 12 },
configCard: { borderRadius: 12, padding: 16, borderWidth: 1 }, configCard: { borderRadius: 12, padding: 16, borderWidth: 1 },
configHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
configIcon: { fontSize: 24, marginRight: 10 },
configTitle: { fontSize: 16, fontWeight: '600', flex: 1 },
connectedBadge: { fontSize: 13, fontWeight: '600' },
authButton: { borderRadius: 8, paddingVertical: 12, alignItems: 'center', marginTop: 4 },
authButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
fieldLabel: { fontSize: 12, fontWeight: '600', marginTop: 10, marginBottom: 4 },
fieldInput: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14 },
checklistInfo: { fontSize: 13, marginBottom: 8 }, checklistInfo: { fontSize: 13, marginBottom: 8 },
checkRow: { checkRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth },
flexDirection: 'row', alignItems: 'center', paddingVertical: 14, checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, justifyContent: 'center', alignItems: 'center', marginRight: 12 },
borderBottomWidth: StyleSheet.hairlineWidth,
},
checkbox: {
width: 22, height: 22, borderRadius: 4, borderWidth: 2,
justifyContent: 'center', alignItems: 'center', marginRight: 12,
},
checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' }, checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' },
checkIcon: { fontSize: 18, marginRight: 8 }, checkIcon: { fontSize: 18, marginRight: 8 },
checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 }, checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 },
footer: { footer: { flexDirection: 'row', paddingHorizontal: 20, paddingVertical: 14, borderTopWidth: 1, gap: 12 },
flexDirection: 'row', paddingHorizontal: 20, paddingVertical: 14, backButton: { flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center', borderWidth: 1 },
borderTopWidth: 1, gap: 12,
},
backButton: {
flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center', borderWidth: 1,
},
backButtonText: { fontSize: 15, fontWeight: '600' }, backButtonText: { fontSize: 15, fontWeight: '600' },
nextButton: { flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center' }, nextButton: { flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center' },
nextButtonText: { fontSize: 15, fontWeight: '600', color: '#fff' }, nextButtonText: { fontSize: 15, fontWeight: '600', color: '#fff' },
+308 -167
View File
@@ -1,32 +1,42 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { import {
StyleSheet, View, Text, ScrollView, TouchableOpacity, Switch, Alert, ActivityIndicator, StyleSheet, View, Text, ScrollView, TouchableOpacity, Switch, TextInput,
Alert, ActivityIndicator,
} from 'react-native'; } from 'react-native';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import { useTheme } from '../services/ThemeContext'; import { useTheme } from '../services/ThemeContext';
import DatabaseService from '../services/DatabaseService'; import DatabaseService from '../services/DatabaseService';
import ServiceConnectCard from '../components/ServiceConnectCard'; import NASSetupInstructions from '../components/NASSetupInstructions';
import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; import ServiceHealthIndicator from '../components/ServiceHealthIndicator';
import NASAUTH from '../AUTH/NASAUTH'; import NASAUTH from '../AUTH/NASAUTH';
import GoogleAUTH from '../AUTH/GoogleAUTH'; import GoogleAUTH from '../AUTH/GoogleAUTH';
import { getServiceIcon, getServiceName } from '../UI/ServiceHelpers'; import { getServiceIcon, getServiceName } from '../UI/ServiceHelpers';
// All services the app supports — add new ones here and they appear everywhere automatically // Master list — add a new service here and it appears in both sections automatically
const ALL_SERVICES = [ const ALL_SERVICES = [
{ type: 'GOOGLE_PHOTOS', icon: '📷', name: 'Google Photos', desc: 'Access your Google Photos library' }, { type: 'GOOGLE_PHOTOS', icon: '📷', name: 'Google Photos' },
{ type: 'ICLOUD_PHOTOS', icon: '🍎', name: 'iCloud Photos', desc: 'Access your iCloud photo library' }, { type: 'ICLOUD_PHOTOS', icon: '🍎', name: 'iCloud Photos' },
{ type: 'NAS', icon: '💾', name: 'NAS Storage', desc: 'Connect to your NAS photo server' }, { type: 'NAS', icon: '💾', name: 'NAS Storage' },
]; ];
const SettingsScreen = ({ navigation, route }) => { const SettingsScreen = ({ navigation, route }) => {
const userId = route?.params?.userId; const userId = route?.params?.userId;
const { theme, themeName, setThemeName } = useTheme(); const { theme, themeName, setThemeName } = useTheme();
const [loading, setLoading] = useState(false);
const [accounts, setAccounts] = useState([]); const [accounts, setAccounts] = useState([]);
const [autoBackup, setAutoBackup] = useState([]); const [autoBackup, setAutoBackup] = useState([]);
const [nasOffload, setNasOffload] = useState(false); const [nasOffload, setNasOffload] = useState(false);
const [serviceHealth, setServiceHealth] = useState({}); const [serviceHealth, setServiceHealth] = useState({});
// Track a reload token so ServiceConnectCards re-read existingData after linking
const [reloadKey, setReloadKey] = useState(0); // Which unlinked-service "add" form is expanded
const [expandedAdd, setExpandedAdd] = useState(null);
// Which linked-service edit form is expanded
const [expandedEdit, setExpandedEdit] = useState(null);
// NAS form fields (shared for both add and edit)
const [nasHost, setNasHost] = useState('');
const [nasPort, setNasPort] = useState('9500');
const [nasUsername, setNasUsername] = useState('');
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@@ -40,64 +50,72 @@ const SettingsScreen = ({ navigation, route }) => {
setAccounts(allAccounts); setAccounts(allAccounts);
const backup = await DatabaseService.getAutoBackupServices(userId); const backup = await DatabaseService.getAutoBackupServices(userId);
setAutoBackup(backup); setAutoBackup(backup);
const nasAcc = allAccounts.find((a) => a.service_type === 'NAS');
if (nasAcc?.account_data) {
setNasHost(nasAcc.account_data.host || '');
setNasPort(String(nasAcc.account_data.port || 9500));
setNasUsername(nasAcc.account_data.username || '');
}
checkAllServiceHealth(allAccounts); checkAllServiceHealth(allAccounts);
} catch (err) { } catch (err) {
console.error('Error loading settings:', err); console.error('Error loading settings:', err);
} }
}; };
// ─── Health checks ─────────────────────────────────────── // ─── Health ──────────────────────────────────────────────
const checkAllServiceHealth = async (accs) => { const checkAllServiceHealth = async (accs) => {
for (const acc of accs) { for (const acc of accs) {
if (!acc.is_enabled) { if (!acc.is_enabled) {
setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'inactive' })); setServiceHealth((p) => ({ ...p, [acc.service_type]: 'inactive' }));
continue; continue;
} }
setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'checking' })); setServiceHealth((p) => ({ ...p, [acc.service_type]: 'checking' }));
const status = await checkSingleHealth(acc); const s = await checkSingleHealth(acc);
setServiceHealth((prev) => ({ ...prev, [acc.service_type]: status })); setServiceHealth((p) => ({ ...p, [acc.service_type]: s }));
} }
}; };
const checkSingleHealth = async (acc) => { const checkSingleHealth = async (acc) => {
try { try {
if (acc.service_type === 'GOOGLE_PHOTOS') { if (acc.service_type === 'GOOGLE_PHOTOS') {
const gAuth = new GoogleAUTH(); const g = new GoogleAUTH();
gAuth.accessToken = acc.account_data?.accessToken; g.accessToken = acc.account_data?.accessToken;
gAuth.refreshToken = acc.account_data?.refreshToken; g.refreshToken = acc.account_data?.refreshToken;
gAuth.expiresAt = acc.account_data?.expiresAt || 0; g.expiresAt = acc.account_data?.expiresAt || 0;
const info = await gAuth.getUserInfo(); const info = await g.getUserInfo();
return info?.email ? 'healthy' : 'error'; return info?.email ? 'healthy' : 'error';
} }
if (acc.service_type === 'NAS') { if (acc.service_type === 'NAS') {
const nasAuth = new NASAUTH(); const n = new NASAUTH();
const d = acc.account_data || {}; const d = acc.account_data || {};
await nasAuth.authenticate(d.host, d.port, d.username); await n.authenticate(d.host, d.port, d.username);
const healthy = await nasAuth.healthCheck(); const ok = await n.healthCheck();
await nasAuth.revokeAccess(); await n.revokeAccess();
return healthy ? 'healthy' : 'error'; return ok ? 'healthy' : 'error';
} }
if (acc.service_type === 'ICLOUD_PHOTOS') return 'healthy'; if (acc.service_type === 'ICLOUD_PHOTOS') return 'healthy';
return 'inactive'; return 'inactive';
} catch { } catch { return 'error'; }
return 'error';
}
}; };
// ─── Service actions ───────────────────────────────────── const recheckHealth = async (acc) => {
setServiceHealth((p) => ({ ...p, [acc.service_type]: 'checking' }));
const s = await checkSingleHealth(acc);
setServiceHealth((p) => ({ ...p, [acc.service_type]: s }));
};
// ─── Actions on existing accounts ────────────────────────
const toggleService = async (accountId, currentEnabled) => { const toggleService = async (accountId, currentEnabled) => {
try { try {
await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled); await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled);
await loadData(); await loadData();
} catch { } catch { Alert.alert('Error', 'Failed to update service'); }
Alert.alert('Error', 'Failed to update service');
}
}; };
const deleteAccount = (acc) => { const deleteAccount = (acc) => {
Alert.alert( Alert.alert(
'Remove Service', 'Remove Service',
`Remove ${getServiceName(acc.service_type)} from your account?`, `Remove ${getServiceName(acc.service_type)} from your account? This cannot be undone.`,
[ [
{ text: 'Cancel', style: 'cancel' }, { text: 'Cancel', style: 'cancel' },
{ {
@@ -105,23 +123,99 @@ const SettingsScreen = ({ navigation, route }) => {
onPress: async () => { onPress: async () => {
try { try {
await DatabaseService.removeLinkedAccount(acc.id); await DatabaseService.removeLinkedAccount(acc.id);
setServiceHealth((prev) => { const n = { ...prev }; delete n[acc.service_type]; return n; }); setServiceHealth((p) => { const n = { ...p }; delete n[acc.service_type]; return n; });
setExpandedEdit(null);
await loadData(); await loadData();
} catch { } catch { Alert.alert('Error', 'Failed to remove service'); }
Alert.alert('Error', 'Failed to remove service');
}
}, },
}, },
] ]
); );
}; };
// ─── Re-authenticate / edit existing ─────────────────────
const saveReauth = async (serviceType) => {
setLoading(true);
try {
if (serviceType === 'GOOGLE_PHOTOS') {
const g = new GoogleAUTH();
const result = await g.authenticate();
await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', {
accessToken: result.accessToken,
refreshToken: result.refreshToken || null,
expiresAt: g.expiresAt || Date.now() + 3600000,
email: result.email || 'google-user',
});
Alert.alert('Success', 'Google Photos re-authenticated!');
} else if (serviceType === 'ICLOUD_PHOTOS') {
await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', {
status: 'placeholder', note: 'iCloud integration pending Apple API access',
});
Alert.alert('Info', 'iCloud Photos re-registered.');
} else if (serviceType === 'NAS') {
if (!nasHost.trim() || !nasUsername.trim()) {
Alert.alert('Error', 'Please fill in the NAS host and username');
setLoading(false); return;
}
const n = new NASAUTH();
await n.authenticate(nasHost.trim(), parseInt(nasPort) || 9500, nasUsername.trim());
await DatabaseService.linkAccount(userId, 'NAS', {
host: nasHost.trim(), port: parseInt(nasPort) || 9500, username: nasUsername.trim(),
});
Alert.alert('Success', 'NAS configuration saved!');
}
setExpandedEdit(null);
await loadData();
} catch (err) {
Alert.alert('Error', 'Failed: ' + err.message);
} finally { setLoading(false); }
};
// ─── Add new service ──────────────────────────────────────
const saveNewService = async (serviceType) => {
setLoading(true);
try {
if (serviceType === 'GOOGLE_PHOTOS') {
const g = new GoogleAUTH();
const result = await g.authenticate();
await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', {
accessToken: result.accessToken,
refreshToken: result.refreshToken || null,
expiresAt: g.expiresAt || Date.now() + 3600000,
email: result.email || 'google-user',
});
Alert.alert('Success', 'Google Photos connected!');
} else if (serviceType === 'ICLOUD_PHOTOS') {
await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', {
status: 'placeholder', note: 'iCloud integration pending Apple API access',
});
Alert.alert('Info', 'iCloud Photos registered.');
} else if (serviceType === 'NAS') {
if (!nasHost.trim() || !nasUsername.trim()) {
Alert.alert('Error', 'Please fill in the NAS host and username');
setLoading(false); return;
}
const n = new NASAUTH();
await n.authenticate(nasHost.trim(), parseInt(nasPort) || 9500, nasUsername.trim());
await DatabaseService.linkAccount(userId, 'NAS', {
host: nasHost.trim(), port: parseInt(nasPort) || 9500, username: nasUsername.trim(),
});
Alert.alert('Success', 'NAS connected!');
}
setExpandedAdd(null);
setNasHost(''); setNasPort('9500'); setNasUsername('');
await loadData();
} catch (err) {
Alert.alert('Error', 'Failed to connect: ' + err.message);
} finally { setLoading(false); }
};
const toggleAutoBackup = async (serviceType) => { const toggleAutoBackup = async (serviceType) => {
const updated = autoBackup.includes(serviceType) const updated = autoBackup.includes(serviceType)
? autoBackup.filter((s) => s !== serviceType) ? autoBackup.filter((s) => s !== serviceType)
: [...autoBackup, serviceType]; : [...autoBackup, serviceType];
setAutoBackup(updated); setAutoBackup(updated);
try { await DatabaseService.setAutoBackupServices(userId, updated); } catch (err) { console.error(err); } try { await DatabaseService.setAutoBackupServices(userId, updated); } catch (e) { console.error(e); }
}; };
const handleLogout = () => { const handleLogout = () => {
@@ -139,6 +233,10 @@ const SettingsScreen = ({ navigation, route }) => {
const hasNasConnected = accounts.some((a) => a.service_type === 'NAS' && a.is_enabled); const hasNasConnected = accounts.some((a) => a.service_type === 'NAS' && a.is_enabled);
// Split into linked and unlinked
const linkedAccounts = accounts.filter((a) => ALL_SERVICES.some((s) => s.type === a.service_type));
const unlinkedServices = ALL_SERVICES.filter((s) => !accounts.some((a) => a.service_type === s.type));
return ( return (
<View style={[styles.container, { backgroundColor: theme.background }]}> <View style={[styles.container, { backgroundColor: theme.background }]}>
<View style={[styles.header, { backgroundColor: theme.background, borderBottomColor: theme.border }]}> <View style={[styles.header, { backgroundColor: theme.background, borderBottomColor: theme.border }]}>
@@ -151,12 +249,11 @@ const SettingsScreen = ({ navigation, route }) => {
<ScrollView style={styles.scrollContent} showsVerticalScrollIndicator={false}> <ScrollView style={styles.scrollContent} showsVerticalScrollIndicator={false}>
{/* ── Appearance ───────────────────────────────── */} {/* ── Appearance ── */}
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>APPEARANCE</Text> <Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>APPEARANCE</Text>
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}> <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
{themeOptions.map((opt) => ( {themeOptions.map((opt) => (
<TouchableOpacity <TouchableOpacity key={opt.key}
key={opt.key}
style={[styles.themeRow, themeName === opt.key && { backgroundColor: theme.cardActive }, { borderBottomColor: theme.borderLight }]} style={[styles.themeRow, themeName === opt.key && { backgroundColor: theme.cardActive }, { borderBottomColor: theme.borderLight }]}
onPress={() => setThemeName(opt.key)} onPress={() => setThemeName(opt.key)}
> >
@@ -170,102 +267,166 @@ const SettingsScreen = ({ navigation, route }) => {
))} ))}
</View> </View>
{/* ── Services ─────────────────────────────────── */} {/* ── Connected services ── */}
{/* Always shows ALL services — connected ones get a manage strip, unconnected show ServiceConnectCard */} {linkedAccounts.length > 0 && (
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>SERVICES</Text> <>
<Text style={[styles.sectionHint, { color: theme.textTertiary }]}> <Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>CONNECTED SERVICES</Text>
Connect new services or manage existing ones at any time. <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
</Text> {linkedAccounts.map((acc, idx) => {
const health = serviceHealth[acc.service_type] || (acc.is_enabled ? 'checking' : 'inactive');
const isLastRow = idx === linkedAccounts.length - 1 && expandedEdit !== acc.service_type;
const isEditOpen = expandedEdit === acc.service_type;
return (
<View key={acc.id}>
{/* Main row */}
<View style={[styles.serviceRow, !isLastRow && { borderBottomColor: theme.borderLight, borderBottomWidth: StyleSheet.hairlineWidth }]}>
<TouchableOpacity onPress={() => recheckHealth(acc)} activeOpacity={0.6}>
<ServiceHealthIndicator status={health} size={10} />
</TouchableOpacity>
<Text style={styles.serviceIcon}>{getServiceIcon(acc.service_type)}</Text>
<View style={styles.serviceInfo}>
<Text style={[styles.serviceName, { color: theme.text }]}>{getServiceName(acc.service_type)}</Text>
<Text style={[styles.serviceStatus, { color: acc.is_enabled ? theme.accent : theme.textTertiary }]}>
{acc.is_enabled ? 'Enabled' : 'Disabled'}
</Text>
</View>
<Switch
value={!!acc.is_enabled}
onValueChange={() => toggleService(acc.id, acc.is_enabled)}
trackColor={{ false: theme.border, true: '#a8dab5' }}
thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'}
/>
</View>
{ALL_SERVICES.map((svc) => { {/* Action buttons */}
const acc = accounts.find((a) => a.service_type === svc.type); <View style={[styles.serviceActions, { borderBottomColor: theme.borderLight }]}>
const health = serviceHealth[svc.type] || (acc?.is_enabled ? 'checking' : 'inactive'); <TouchableOpacity
style={[styles.actionBtn, { borderColor: theme.primary }]}
onPress={() => {
if (acc.service_type === 'NAS') {
setExpandedEdit(isEditOpen ? null : acc.service_type);
} else {
saveReauth(acc.service_type);
}
}}
disabled={loading}
>
<Text style={[styles.actionBtnText, { color: theme.primary }]}>
{acc.service_type === 'NAS' ? (isEditOpen ? 'Cancel' : 'Edit') : (loading ? '…' : 'Re-auth')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionBtn, { borderColor: theme.danger }]}
onPress={() => deleteAccount(acc)}
>
<Text style={[styles.actionBtnText, { color: theme.danger }]}>Remove</Text>
</TouchableOpacity>
</View>
if (acc) { {/* Inline NAS edit form */}
// Already linked — show management card with enable/disable + remove + re-auth via ServiceConnectCard {isEditOpen && acc.service_type === 'NAS' && (
return ( <View style={[styles.inlineForm, { borderTopColor: theme.borderLight }]}>
<View key={svc.type} style={[styles.managedCard, { backgroundColor: theme.card, borderColor: theme.border }]}> <Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Server Address</Text>
{/* Top row: health, icon, name, enable toggle */} <TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
<View style={styles.managedHeader}> value={nasHost} onChangeText={setNasHost} placeholder="192.168.1.100" placeholderTextColor={theme.textTertiary} />
<TouchableOpacity onPress={() => checkSingleHealth(acc).then((s) => setServiceHealth((p) => ({ ...p, [svc.type]: s })))}> <Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Port</Text>
<ServiceHealthIndicator status={health} size={10} /> <TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
</TouchableOpacity> value={nasPort} onChangeText={setNasPort} keyboardType="numeric" placeholder="9500" placeholderTextColor={theme.textTertiary} />
<Text style={styles.managedIcon}>{svc.icon}</Text> <Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Username</Text>
<View style={styles.managedInfo}> <TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
<Text style={[styles.managedName, { color: theme.text }]}>{svc.name}</Text> value={nasUsername} onChangeText={setNasUsername} autoCapitalize="none" placeholder="Your username" placeholderTextColor={theme.textTertiary} />
<Text style={[styles.managedStatus, { color: acc.is_enabled ? theme.accent : theme.textTertiary }]}> <TouchableOpacity style={[styles.saveButton, { backgroundColor: theme.primary }]}
{acc.is_enabled ? 'Enabled' : 'Disabled'} onPress={() => saveReauth('NAS')} disabled={loading}>
</Text> {loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.saveButtonText}>Save Changes</Text>}
</TouchableOpacity>
</View>
)}
</View> </View>
<Switch );
value={!!acc.is_enabled} })}
onValueChange={() => toggleService(acc.id, acc.is_enabled)}
trackColor={{ false: theme.border, true: '#a8dab5' }}
thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'}
/>
</View>
{/* Re-auth / edit via shared ServiceConnectCard (starts collapsed since already linked) */}
<ServiceConnectCard
key={`${svc.type}-${reloadKey}`}
serviceType={svc.type}
userId={userId}
existingData={acc.account_data}
isLinked={true}
showRemove={true}
onLinked={() => { setReloadKey((k) => k + 1); loadData(); }}
onRemoved={() => deleteAccount(acc)}
/>
</View>
);
}
// Not yet linked — show ServiceConnectCard directly so user can add it
return (
<View key={svc.type}>
<View style={[styles.notLinkedHeader, { borderColor: theme.border }]}>
<Text style={styles.managedIcon}>{svc.icon}</Text>
<View style={styles.managedInfo}>
<Text style={[styles.managedName, { color: theme.text }]}>{svc.name}</Text>
<Text style={[styles.managedStatus, { color: theme.textTertiary }]}>Not connected</Text>
</View>
</View>
<ServiceConnectCard
key={`${svc.type}-${reloadKey}`}
serviceType={svc.type}
userId={userId}
isLinked={false}
onLinked={() => { setReloadKey((k) => k + 1); loadData(); }}
/>
</View> </View>
); </>
})} )}
{/* ── NAS Offloading ────────────────────────────── */} {/* ── Add services (only shows services not yet connected) ── */}
{unlinkedServices.length > 0 && (
<>
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>ADD SERVICES</Text>
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
{unlinkedServices.map((svc, idx) => {
const isLast = idx === unlinkedServices.length - 1;
const isOpen = expandedAdd === svc.type;
return (
<View key={svc.type}>
{/* Service row with Add button */}
<View style={[styles.serviceRow, !isLast && !isOpen && { borderBottomColor: theme.borderLight, borderBottomWidth: StyleSheet.hairlineWidth }]}>
<Text style={styles.serviceIcon}>{svc.icon}</Text>
<View style={styles.serviceInfo}>
<Text style={[styles.serviceName, { color: theme.text }]}>{svc.name}</Text>
<Text style={[styles.serviceStatus, { color: theme.textTertiary }]}>Not connected</Text>
</View>
<TouchableOpacity
style={[styles.addBtn, { backgroundColor: theme.primary }]}
onPress={() => {
if (svc.type === 'NAS') {
setNasHost(''); setNasPort('9500'); setNasUsername('');
setExpandedAdd(isOpen ? null : svc.type);
} else {
saveNewService(svc.type);
}
}}
disabled={loading}
>
<Text style={styles.addBtnText}>{loading && expandedAdd === svc.type ? '…' : 'Add'}</Text>
</TouchableOpacity>
</View>
{/* Inline NAS add form */}
{isOpen && svc.type === 'NAS' && (
<View style={[styles.inlineForm, { borderTopColor: theme.borderLight }]}>
<NASSetupInstructions />
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Server Address</Text>
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
value={nasHost} onChangeText={setNasHost} placeholder="192.168.1.100" placeholderTextColor={theme.textTertiary} />
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Port</Text>
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
value={nasPort} onChangeText={setNasPort} keyboardType="numeric" placeholder="9500" placeholderTextColor={theme.textTertiary} />
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Username</Text>
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
value={nasUsername} onChangeText={setNasUsername} autoCapitalize="none" placeholder="Your username" placeholderTextColor={theme.textTertiary} />
<TouchableOpacity style={[styles.saveButton, { backgroundColor: '#0a7ea4' }]}
onPress={() => saveNewService('NAS')} disabled={loading}>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.saveButtonText}>Test & Connect</Text>}
</TouchableOpacity>
</View>
)}
</View>
);
})}
</View>
</>
)}
{/* ── NAS Offloading ── */}
{hasNasConnected && ( {hasNasConnected && (
<> <>
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>NAS OFFLOADING</Text> <Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>NAS OFFLOADING</Text>
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}> <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
<View style={styles.serviceRow}> <View style={styles.serviceRow}>
<Text style={styles.managedIcon}>📷</Text> <Text style={styles.serviceIcon}>📷</Text>
<View style={styles.managedInfo}> <View style={styles.serviceInfo}>
<Text style={[styles.managedName, { color: theme.text }]}>Offload Google Photos to NAS</Text> <Text style={[styles.serviceName, { color: theme.text }]}>Offload Google Photos to NAS</Text>
<Text style={[styles.managedStatus, { color: theme.textSecondary }]}> <Text style={[styles.serviceStatus, { color: theme.textSecondary }]}>Fetch Google Photos data through NAS server</Text>
Fetch Google Photos data through NAS server
</Text>
</View> </View>
<Switch <Switch value={nasOffload} onValueChange={setNasOffload}
value={nasOffload}
onValueChange={setNasOffload}
trackColor={{ false: theme.border, true: '#a8dab5' }} trackColor={{ false: theme.border, true: '#a8dab5' }}
thumbColor={nasOffload ? theme.accent : '#f4f3f4'} thumbColor={nasOffload ? theme.accent : '#f4f3f4'} />
/>
</View> </View>
</View> </View>
</> </>
)} )}
{/* ── Auto Backup ───────────────────────────────── */} {/* ── Auto Backup ── */}
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>AUTOMATIC BACKUP</Text> <Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>AUTOMATIC BACKUP</Text>
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}> <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
<Text style={[styles.cardSubtext, { color: theme.textSecondary }]}> <Text style={[styles.cardSubtext, { color: theme.textSecondary }]}>
@@ -274,45 +435,34 @@ const SettingsScreen = ({ navigation, route }) => {
{ALL_SERVICES.map((svc) => { {ALL_SERVICES.map((svc) => {
const isConnected = accounts.some((a) => a.service_type === svc.type && a.is_enabled); const isConnected = accounts.some((a) => a.service_type === svc.type && a.is_enabled);
return ( return (
<TouchableOpacity <TouchableOpacity key={svc.type}
key={svc.type}
style={[styles.checkRow, { borderBottomColor: theme.borderLight }]} style={[styles.checkRow, { borderBottomColor: theme.borderLight }]}
onPress={() => isConnected && toggleAutoBackup(svc.type)} onPress={() => isConnected && toggleAutoBackup(svc.type)}
disabled={!isConnected} disabled={!isConnected} activeOpacity={0.7}
activeOpacity={0.7}
> >
<View style={[ <View style={[styles.checkbox, { borderColor: isConnected ? theme.primary : theme.border },
styles.checkbox, { borderColor: isConnected ? theme.primary : theme.border }, autoBackup.includes(svc.type) && { backgroundColor: theme.primary, borderColor: theme.primary }]}>
autoBackup.includes(svc.type) && { backgroundColor: theme.primary, borderColor: theme.primary },
]}>
{autoBackup.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>} {autoBackup.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>}
</View> </View>
<Text style={styles.checkIcon}>{svc.icon}</Text> <Text style={styles.checkIcon}>{svc.icon}</Text>
<Text style={[styles.checkLabel, { color: isConnected ? theme.text : theme.textTertiary }]}> <Text style={[styles.checkLabel, { color: isConnected ? theme.text : theme.textTertiary }]}>{svc.name}</Text>
{svc.name}
</Text>
{!isConnected && <Text style={[styles.notConnected, { color: theme.textTertiary }]}>Not connected</Text>} {!isConnected && <Text style={[styles.notConnected, { color: theme.textTertiary }]}>Not connected</Text>}
</TouchableOpacity> </TouchableOpacity>
); );
})} })}
<TouchableOpacity <TouchableOpacity style={[styles.checkRow, { borderBottomWidth: 0 }]}
style={[styles.checkRow, { borderBottomWidth: 0 }]}
onPress={() => { setAutoBackup([]); DatabaseService.setAutoBackupServices(userId, []); }} onPress={() => { setAutoBackup([]); DatabaseService.setAutoBackupServices(userId, []); }}
activeOpacity={0.7} activeOpacity={0.7}
> >
<View style={[ <View style={[styles.checkbox, { borderColor: theme.border },
styles.checkbox, { borderColor: theme.border }, autoBackup.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary }]}>
autoBackup.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary },
]}>
{autoBackup.length === 0 && <Text style={styles.checkmark}>✓</Text>} {autoBackup.length === 0 && <Text style={styles.checkmark}>✓</Text>}
</View> </View>
<Text style={[styles.checkLabel, { color: theme.text, marginLeft: 0 }]}> <Text style={[styles.checkLabel, { color: theme.text, marginLeft: 0 }]}>Don't automatically back up</Text>
Don't automatically back up
</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* ── Sign Out ──────────────────────────────────── */} {/* ── Sign Out ── */}
<TouchableOpacity style={[styles.logoutButton, { borderColor: theme.danger }]} onPress={handleLogout}> <TouchableOpacity style={[styles.logoutButton, { borderColor: theme.danger }]} onPress={handleLogout}>
<Text style={[styles.logoutText, { color: theme.danger }]}>Sign Out</Text> <Text style={[styles.logoutText, { color: theme.danger }]}>Sign Out</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -331,11 +481,9 @@ const styles = StyleSheet.create({
backArrow: { fontSize: 24 }, backArrow: { fontSize: 24 },
headerTitle: { fontSize: 18, fontWeight: '700' }, headerTitle: { fontSize: 18, fontWeight: '700' },
scrollContent: { flex: 1, paddingHorizontal: 16 }, scrollContent: { flex: 1, paddingHorizontal: 16 },
sectionTitle: { fontSize: 12, fontWeight: '700', marginTop: 24, marginBottom: 4, letterSpacing: 0.5 }, sectionTitle: { fontSize: 12, fontWeight: '700', marginTop: 24, marginBottom: 8, letterSpacing: 0.5 },
sectionHint: { fontSize: 12, marginBottom: 10 },
card: { borderRadius: 12, borderWidth: 1, overflow: 'hidden', marginBottom: 4 }, card: { borderRadius: 12, borderWidth: 1, overflow: 'hidden', marginBottom: 4 },
cardSubtext: { fontSize: 12, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 4 }, cardSubtext: { fontSize: 12, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 4 },
// Theme rows
themeRow: { themeRow: {
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14,
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
@@ -345,40 +493,33 @@ const styles = StyleSheet.create({
themeName: { fontSize: 14, fontWeight: '600', marginBottom: 2 }, themeName: { fontSize: 14, fontWeight: '600', marginBottom: 2 },
themeDesc: { fontSize: 12 }, themeDesc: { fontSize: 12 },
radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: '#dadce0' }, radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: '#dadce0' },
// Managed (already linked) service cards serviceRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12 },
managedCard: { serviceIcon: { fontSize: 22, marginRight: 8 },
borderRadius: 12, borderWidth: 1, marginBottom: 10, overflow: 'hidden', serviceInfo: { flex: 1 },
serviceName: { fontSize: 14, fontWeight: '600', marginBottom: 2 },
serviceStatus: { fontSize: 12 },
serviceActions: {
flexDirection: 'row', paddingHorizontal: 14, paddingVertical: 8, gap: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
}, },
managedHeader: { actionBtn: { borderWidth: 1, borderRadius: 6, paddingHorizontal: 14, paddingVertical: 6 },
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12, actionBtnText: { fontSize: 12, fontWeight: '600' },
gap: 8, addBtn: { borderRadius: 6, paddingHorizontal: 14, paddingVertical: 6 },
}, addBtnText: { color: '#fff', fontSize: 12, fontWeight: '600' },
managedIcon: { fontSize: 22 }, inlineForm: { paddingHorizontal: 14, paddingBottom: 14, borderTopWidth: StyleSheet.hairlineWidth },
managedInfo: { flex: 1 }, fieldLabel: { fontSize: 12, fontWeight: '600', marginTop: 10, marginBottom: 4 },
managedName: { fontSize: 14, fontWeight: '600', marginBottom: 2 }, fieldInput: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14 },
managedStatus: { fontSize: 12 }, saveButton: { borderRadius: 8, paddingVertical: 12, alignItems: 'center', marginTop: 14 },
// Not-yet-linked header strip saveButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
notLinkedHeader: {
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 4, paddingBottom: 2, gap: 8,
},
// Service row (offload section)
serviceRow: {
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12,
},
// Auto backup
checkRow: { checkRow: {
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
checkbox: { checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, justifyContent: 'center', alignItems: 'center', marginRight: 12 },
width: 22, height: 22, borderRadius: 4, borderWidth: 2,
justifyContent: 'center', alignItems: 'center', marginRight: 12,
},
checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' }, checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' },
checkIcon: { fontSize: 18, marginRight: 8 }, checkIcon: { fontSize: 18, marginRight: 8 },
checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 }, checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 },
notConnected: { fontSize: 11 }, notConnected: { fontSize: 11 },
// Logout
logoutButton: { borderWidth: 1.5, borderRadius: 8, paddingVertical: 14, alignItems: 'center', marginTop: 24 }, logoutButton: { borderWidth: 1.5, borderRadius: 8, paddingVertical: 14, alignItems: 'center', marginTop: 24 },
logoutText: { fontSize: 15, fontWeight: '600' }, logoutText: { fontSize: 15, fontWeight: '600' },
}); });