import React, { useEffect, useState } from 'react'; import { ActivityIndicator, Alert, Linking, Pressable, ScrollView, StyleSheet, Switch, Text, View, } from 'react-native'; import { AppLockSettings, biometricAvailable, clearPin, DEFAULT_TIMEOUT_SECONDS, getSettings, setBioEnabled, setEnabled, setPin, setTimeoutSeconds, verifyPin, } from '../lib/applock'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { APP_VERSION } from '../lib/appVersion'; import { replayOnboarding } from '../lib/onboardingBus'; import { usePreferences } from '../PreferencesContext'; import type { RootStackParamList } from '../navigation'; import { theme } from '../theme'; import { PinEntry } from './PinEntry'; const TIMEOUT_OPTIONS: { label: string; seconds: number }[] = [ { label: 'Immediately', seconds: 0 }, { label: '30 sec', seconds: 30 }, { label: '1 min', seconds: 60 }, { label: '5 min', seconds: 300 }, { label: '15 min', seconds: 900 }, ]; type Stage = 'menu' | 'enter-current' | 'set-new' | 'confirm-new' | 'disable-confirm'; export function AppLockSettingsScreen() { const [settings, setSettings] = useState(null); const [bioAvailable, setBioAvailable] = useState(false); const [stage, setStage] = useState('menu'); const [newPin, setNewPin] = useState(''); const [errorText, setErrorText] = useState(null); const { gridColumns, setGridColumns } = usePreferences(); const navigation = useNavigation>(); async function refresh() { const [s, bio] = await Promise.all([getSettings(), biometricAvailable()]); setSettings(s); setBioAvailable(bio); } useEffect(() => { refresh(); }, []); if (!settings) { return ( ); } if (stage === 'enter-current') { return ( { setStage('menu'); setErrorText(null); }} onSubmit={async (candidate) => { const ok = await verifyPin(candidate); if (!ok) { setErrorText('Incorrect PIN'); return; } setErrorText(null); setStage('set-new'); }} /> ); } if (stage === 'set-new') { return ( { setStage('menu'); setErrorText(null); setNewPin(''); }} onSubmit={async (candidate) => { if (candidate.length < 4) { setErrorText('Min. 4 digits'); return; } setNewPin(candidate); setErrorText(null); setStage('confirm-new'); }} /> ); } if (stage === 'confirm-new') { return ( { setStage('menu'); setErrorText(null); setNewPin(''); }} onSubmit={async (candidate) => { if (candidate !== newPin) { setErrorText('PINs do not match'); return; } await setPin(newPin); await setEnabled(true); setNewPin(''); setErrorText(null); setStage('menu'); await refresh(); Alert.alert('Done', 'PIN saved. App lock enabled.'); }} /> ); } if (stage === 'disable-confirm') { return ( { setStage('menu'); setErrorText(null); }} onSubmit={async (candidate) => { const ok = await verifyPin(candidate); if (!ok) { setErrorText('Incorrect PIN'); return; } await setEnabled(false); await setBioEnabled(false); await clearPin(); setErrorText(null); setStage('menu'); await refresh(); }} /> ); } return ( App lock Enabled {settings.enabled ? 'Requires PIN at startup and after a break' : 'Disabled'} { if (v) { setStage('set-new'); } else if (settings.hasPin) { setStage('disable-confirm'); } else { setEnabled(false).then(refresh); } }} trackColor={{ true: theme.accent, false: theme.border }} thumbColor={theme.fg} /> {settings.enabled && settings.hasPin ? ( { setStage('enter-current'); setErrorText(null); }} > Change PIN ) : null} {settings.enabled ? ( <> Biometrics Fingerprint / Face Unlock {bioAvailable ? 'Faster unlocking, PIN as fallback' : 'No biometrics on this device'} { await setBioEnabled(v); await refresh(); }} trackColor={{ true: theme.accent, false: theme.border }} thumbColor={theme.fg} /> Time until lock How long to wait after leaving the app before locking {TIMEOUT_OPTIONS.map((opt) => { const active = settings.timeoutSeconds === opt.seconds; return ( { await setTimeoutSeconds(opt.seconds); await refresh(); }} > {opt.label} ); })} ) : null} The app is also hidden in the recent apps list and blocks screenshots. Content navigation.navigate('Blacklist')}> Hidden content Hide tags, performers or studios you never want to see Grid Thumbnails per row in scene lists {[1, 2, 3].map((n) => { const active = gridColumns === n; return ( setGridColumns(n)} > {n === 1 ? '1 column' : `${n} columns`} ); })} About Version Current JS bundle (OTA-updated) {APP_VERSION} Linking.openURL(REPO_URL).catch(() => {})} > Source code Open source (MIT) — audit it or self-host {REPO_LABEL} › replayOnboarding()}> Replay tutorial Show the first-launch tour again ); } // Publiczne repo OSS (zgłoszenie usera 4c5066b8: "no source code repo linked"). // Sygnał zaufania dla sideloadowanej apki 18+: audyt kodu / self-host / kontrybucja. const REPO_URL = 'https://github.com/goon-foss/goon'; const REPO_LABEL = 'goon-foss/goon'; const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: theme.bg }, center: { flex: 1, backgroundColor: theme.bg, alignItems: 'center', justifyContent: 'center' }, section: { backgroundColor: theme.card, marginHorizontal: 14, marginTop: 14, padding: 16, borderRadius: 14, borderWidth: 1, borderColor: theme.border, }, sectionTitle: { color: theme.fg, fontSize: 13, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 10, }, row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8, }, label: { color: theme.fg, fontSize: 15, fontWeight: '600' }, hint: { color: theme.muted, fontSize: 12, marginTop: 2 }, actionBtn: { marginTop: 12, paddingVertical: 12, paddingHorizontal: 16, borderRadius: 10, backgroundColor: theme.bgElevated, borderWidth: 1, borderColor: theme.border, alignItems: 'center', }, actionText: { color: theme.accent, fontSize: 14, fontWeight: '600' }, chipRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10 }, chip: { paddingVertical: 8, paddingHorizontal: 14, borderRadius: 999, backgroundColor: theme.bgElevated, borderWidth: 1, borderColor: theme.border, }, chipActive: { backgroundColor: theme.accent, borderColor: theme.accent }, versionValue: { color: theme.fg, fontSize: 15, fontWeight: '700', fontVariant: ['tabular-nums'] }, linkValue: { color: theme.accent, fontSize: 15, fontWeight: '700' }, chipText: { color: theme.muted, fontSize: 13 }, chipTextActive: { color: '#fff', fontWeight: '700' }, });