// 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,
},
});