// app/(tabs)/settings.tsx — Full Settings Screen with PIN setup import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, ScrollView, Switch, TouchableOpacity, Alert, Modal, SafeAreaView, ActivityIndicator, } from 'react-native'; import { router } from 'expo-router'; import { User, Shield, Fingerprint, RefreshCw, BookOpen, Download, Upload, Trash2, Info, ChevronRight, X, } from 'lucide-react-native'; import * as LocalAuthentication from 'expo-local-authentication'; import { useUserStore } from '@/store/useUserStore'; import { isPinEnabled, setPin, verifyPin, disablePin, isBiometricEnabled, setBiometricEnabled, } from '@/lib/pinService'; import { getDatabase } from '@/lib/database'; import PinPad from '@/components/ui/PinPad'; // ── PIN Flow Modal ──────────────────────────────────────────────────────────── type PinFlowMode = 'set' | 'verify'; interface PinFlowProps { mode: PinFlowMode; onSuccess: (pin?: string) => void; onCancel: () => void; title: string; subtitle?: string; } function PinFlowModal({ mode, onSuccess, onCancel, title, subtitle }: PinFlowProps) { const [step, setStep] = useState<'enter' | 'confirm'>('enter'); const [pin, setCurrentPin] = useState(''); const [firstPin, setFirstPin] = useState(''); const [error, setError] = useState(''); const handlePinChange = async (newPin: string) => { setCurrentPin(newPin); setError(''); if (newPin.length < 6) return; if (mode === 'verify') { const ok = await verifyPin(newPin); if (ok) { onSuccess(newPin); } else { setError('Incorrect PIN. Try again.'); setTimeout(() => setCurrentPin(''), 600); } return; } // mode === 'set' if (step === 'enter') { setFirstPin(newPin); setCurrentPin(''); setStep('confirm'); } else { // confirm step if (newPin === firstPin) { onSuccess(newPin); } else { setError('PINs do not match. Try again.'); setStep('enter'); setFirstPin(''); setTimeout(() => setCurrentPin(''), 400); } } }; const displayTitle = mode === 'set' ? step === 'enter' ? 'Set PIN' : 'Confirm PIN' : title; const displaySubtitle = mode === 'set' ? step === 'enter' ? 'Enter a 6-digit PIN' : 'Re-enter your PIN to confirm' : subtitle; return ( {displayTitle} {displaySubtitle ? ( {displaySubtitle} ) : null} {error ? {error} : null} ); } const pinStyles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#0F172A' }, header: { alignItems: 'flex-end', padding: 16 }, closeBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: 'rgba(255,255,255,0.08)', justifyContent: 'center', alignItems: 'center', }, content: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 24, gap: 28, }, title: { fontSize: 24, fontWeight: '700', color: '#F9FAFB' }, subtitle: { fontSize: 15, color: '#9CA3AF', textAlign: 'center' }, error: { fontSize: 14, color: '#F87171', textAlign: 'center' }, }); // ── Settings Screen ─────────────────────────────────────────────────────────── export default function SettingsScreen() { const user = useUserStore((s) => s.user); const clearUser = useUserStore((s) => s.clearUser); const [pinEnabled, setPinEnabledState] = useState(false); const [biometricEnabled, setBiometricEnabledState] = useState(false); const [biometricSupported, setBiometricSupported] = useState(false); const [loading, setLoading] = useState(true); const [pinFlowVisible, setPinFlowVisible] = useState(false); const [pinFlowMode, setPinFlowMode] = useState<'set' | 'verify'>('set'); const [pinFlowCallback, setPinFlowCallback] = useState<((pin?: string) => void) | null>(null); const [pinFlowTitle, setPinFlowTitle] = useState(''); const [pinFlowSubtitle, setPinFlowSubtitle] = useState(''); useEffect(() => { loadSecurityState(); }, []); const loadSecurityState = async () => { try { const [pinOn, bioOn, hw, enrolled] = await Promise.all([ isPinEnabled(), isBiometricEnabled(), LocalAuthentication.hasHardwareAsync(), LocalAuthentication.isEnrolledAsync(), ]); setPinEnabledState(pinOn); setBiometricEnabledState(bioOn); setBiometricSupported(hw && enrolled); } catch { // ignore } finally { setLoading(false); } }; const openPinFlow = useCallback( ( mode: 'set' | 'verify', title: string, subtitle: string, callback: (pin?: string) => void ) => { setPinFlowMode(mode); setPinFlowTitle(title); setPinFlowSubtitle(subtitle); setPinFlowCallback(() => callback); setPinFlowVisible(true); }, [] ); const handleTogglePin = async (value: boolean) => { if (value) { // Turning ON: show Set PIN flow openPinFlow('set', 'Set PIN', 'Enter a 6-digit PIN', async (pin) => { if (pin) { await setPin(pin); setPinEnabledState(true); setPinFlowVisible(false); Alert.alert('PIN Set', 'Your app is now protected with a PIN.'); } }); } else { // Turning OFF: verify first openPinFlow( 'verify', 'Disable PIN Lock', 'Enter your current PIN to disable', async () => { await disablePin(); setPinEnabledState(false); setBiometricEnabledState(false); setPinFlowVisible(false); Alert.alert('PIN Disabled', 'App lock has been turned off.'); } ); } }; const handleToggleBiometric = async (value: boolean) => { await setBiometricEnabled(value); setBiometricEnabledState(value); }; const handleClearData = () => { Alert.alert( 'Clear All Data', 'This will permanently delete all contacts, visits, territories, and sync history. This cannot be undone.', [ { text: 'Cancel', style: 'cancel' }, { text: 'Delete Everything', style: 'destructive', onPress: async () => { try { const db = await getDatabase(); await db.execAsync(` DELETE FROM contacts; DELETE FROM visits; DELETE FROM territories; DELETE FROM sync_log; `); Alert.alert('Done', 'All data has been cleared.'); } catch { Alert.alert('Error', 'Failed to clear data.'); } }, }, ] ); }; const handleExportBackup = () => { Alert.alert('Export Backup', 'Export feature coming in a future sprint.'); }; const handleImportBackup = () => { Alert.alert('Import Backup', 'Import feature coming in a future sprint.'); }; if (loading) { return ( ); } return ( {/* Header */} Settings {/* ── Profile ── */} {user?.displayName ?? '—'} Share ID: {user?.shareId ?? '—'} {/* ── Security ── */} } label="App Lock (PIN)" right={ } /> {pinEnabled && biometricSupported && ( <> } label="Biometric Unlock" right={ } /> )} {/* ── Sync ── */} } label="Sync with Device" right={} onPress={() => router.push('/(tabs)/sync')} /> {/* ── Data ── */} } label="Topic Library" right={} onPress={() => Alert.alert('Topic Library', 'Manage visit topics in a future update.') } /> } label="Export Backup" right={} onPress={handleExportBackup} /> } label="Import Backup" right={} onPress={handleImportBackup} /> } label="Clear All Data" labelStyle={{ color: '#EF4444' }} right={} onPress={handleClearData} /> {/* ── About ── */} } label="TerritoryLog" right={v1.0.0 (Sprint 5)} /> A local-first ministry record keeping app. All data stays on your device. {/* PIN Flow Modal */} {pinFlowVisible && pinFlowCallback && ( pinFlowCallback(pin)} onCancel={() => setPinFlowVisible(false)} /> )} ); } // ── Sub-components ────────────────────────────────────────────────────────── function SectionHeader({ title }: { title: string }) { return {title}; } function Divider() { return ; } interface SettingsRowProps { icon: React.ReactNode; label: string; labelStyle?: object; right?: React.ReactNode; onPress?: () => void; } function SettingsRow({ icon, label, labelStyle, right, onPress }: SettingsRowProps) { const Row = onPress ? TouchableOpacity : View; return ( {icon} {label} {right && {right}} ); } // ── Styles ────────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: '#F8F4EF', }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F8F4EF', }, container: { flex: 1, }, content: { paddingBottom: 16, }, pageTitle: { fontSize: 28, fontWeight: '700', color: '#111827', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 4, }, sectionHeader: { fontSize: 12, fontWeight: '600', color: '#6B7280', letterSpacing: 0.8, textTransform: 'uppercase', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 6, }, card: { backgroundColor: '#fff', marginHorizontal: 16, borderRadius: 16, paddingVertical: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 4, elevation: 2, }, profileRow: { flexDirection: 'row', alignItems: 'center', padding: 16, gap: 14, }, avatar: { width: 52, height: 52, borderRadius: 26, backgroundColor: '#EFF9F9', justifyContent: 'center', alignItems: 'center', borderWidth: 2, borderColor: '#B2DFDF', }, profileName: { fontSize: 17, fontWeight: '700', color: '#111827', }, profileShareId: { fontSize: 13, color: '#6B7280', }, shareIdMono: { fontFamily: 'monospace', color: '#1A6B72', fontWeight: '600', }, settingsRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 14, }, settingsRowLeft: { flexDirection: 'row', alignItems: 'center', gap: 12, flex: 1, }, rowIcon: { width: 32, height: 32, borderRadius: 8, backgroundColor: '#F3F4F6', justifyContent: 'center', alignItems: 'center', }, rowLabel: { fontSize: 15, color: '#111827', fontWeight: '500', }, settingsRowRight: { flexShrink: 0, marginLeft: 8, }, divider: { height: 1, backgroundColor: '#F3F4F6', marginHorizontal: 16, }, versionText: { fontSize: 13, color: '#9CA3AF', }, aboutRow: { paddingHorizontal: 16, paddingVertical: 12, }, aboutText: { fontSize: 13, color: '#6B7280', lineHeight: 20, }, });