goon/mobile/src/screens/AppLockScreen.tsx
goon-foss 7deda639bf fix(theme): rozdziel role akcentu i dodaj ramke, ktora cokolwiek oddziela
Pomiar WCAG na realnych parach z apki (2026-08-13):

    accent #B23A48 jako tekst:  3,21 na bg | 2,96 na bgElevated | 2,75 na card
    border #3A3128 jako granica: 1,48 na bg | 1,27 na card

Czyli kolor MARKI byl ponizej nawet progu 3:1 dla elementow interfejsu,
a nie tylko ponizej 4,5 dla tekstu. Dotyczylo to aktywnych zakladek,
gwiazdek ulubionych, linkow i chipow. Ramki przy 1,27 nie byly granica,
tylko cieniem.

NIE zmieniam koloru marki. Podniesienie samego `accent` do progu tekstu
wymagaloby zejscia w roz (#CE6975), co likwiduje tozsamosc, ktorej broni
naglowek theme.ts. Zamiast tego rozdzielam ROLE tego samego koloru:

  accent      zostaje do WYPELNIEN — jasny tekst na nim ma 5,03, przechodzi
  accentText  #C75260 dla tekstu i obwodek — 4,30 / 3,96 / 3,68

62 podmiany w 24 plikach (34 color, 26 borderColor, 2 thumbColor).
38 wypelnien nietknietych. Sprawdzone skryptem, ze zaden zmieniony tekst
nie stoi na tle akcentu.

borderStrong #7B7065 dla granic NIOSACYCH ZNACZENIE (pola tekstowe, 12
miejsc); dekoracyjne wloski zostaja na `border`. Wartosc dobrana pod obie
powierzchnie, bo pola leza i na bg, i na card: #6B6157 dawal na karcie
2,66, a #73695E dokladnie 2,999, czyli wciaz ponizej progu.
2026-08-14 09:50:08 +02:00

218 lines
6.4 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
Animated,
Pressable,
StyleSheet,
Text,
View,
Vibration,
} from 'react-native';
import * as Haptics from 'expo-haptics';
import {
authenticateBiometric,
biometricAvailable,
getSettings,
verifyPin,
} from '../lib/applock';
import { theme } from '../theme';
interface Props {
onUnlock: () => void;
onLogout?: () => void;
}
const KEYS: (string | null)[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', null, '0', '⌫'];
export function AppLockScreen({ onUnlock, onLogout }: Props) {
const [pin, setPin] = useState('');
const [bioAvailable, setBioAvailable] = useState(false);
const [bioEnabled, setBioEnabled] = useState(false);
const [verifying, setVerifying] = useState(false);
const [error, setError] = useState(false);
const shake = useRef(new Animated.Value(0)).current;
useEffect(() => {
(async () => {
const [settings, available] = await Promise.all([getSettings(), biometricAvailable()]);
setBioEnabled(settings.bioEnabled);
setBioAvailable(available);
if (settings.bioEnabled && available) {
try {
const ok = await authenticateBiometric();
if (ok) onUnlock();
} catch {
// fall through to pin entry
}
}
})();
}, [onUnlock]);
async function tryUnlock(candidate: string) {
setVerifying(true);
const ok = await verifyPin(candidate);
setVerifying(false);
if (ok) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
onUnlock();
} else {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
setError(true);
setPin('');
Vibration.vibrate(50);
Animated.sequence([
Animated.timing(shake, { toValue: 10, duration: 50, useNativeDriver: true }),
Animated.timing(shake, { toValue: -10, duration: 50, useNativeDriver: true }),
Animated.timing(shake, { toValue: 10, duration: 50, useNativeDriver: true }),
Animated.timing(shake, { toValue: 0, duration: 50, useNativeDriver: true }),
]).start();
}
}
function press(key: string) {
if (verifying) return;
setError(false);
if (key === '⌫') {
setPin((p) => p.slice(0, -1));
Haptics.selectionAsync().catch(() => {});
return;
}
if (pin.length >= 8) return;
const next = pin + key;
setPin(next);
Haptics.selectionAsync().catch(() => {});
if (next.length >= 4) {
// Auto-verify when at least 4 digits, nicer UX. User can keep typing if PIN longer.
tryUnlock(next);
}
}
async function biometricRetry() {
if (!bioEnabled || !bioAvailable) return;
const ok = await authenticateBiometric();
if (ok) onUnlock();
}
return (
<View style={styles.root}>
<View style={styles.header}>
<View style={styles.lockBadge}>
<Text style={styles.lockGlyph}>g</Text>
</View>
<Text style={styles.title}>Goon locked</Text>
<Text style={styles.subtitle}>
{bioEnabled && bioAvailable ? 'Use fingerprint or PIN' : 'Enter PIN'}
</Text>
</View>
<Animated.View style={[styles.dots, { transform: [{ translateX: shake }] }]}>
{[0, 1, 2, 3].map((i) => {
const filled = i < pin.length;
return (
<View
key={i}
style={[
styles.dot,
filled && styles.dotFilled,
error && styles.dotError,
]}
/>
);
})}
</Animated.View>
{verifying ? <ActivityIndicator color={theme.muted} style={{ marginTop: 12 }} /> : <View style={{ height: 28 }} />}
<View style={styles.pad}>
{KEYS.map((k, i) => {
if (k === null) {
return bioEnabled && bioAvailable ? (
<Pressable key={i} style={styles.key} onPress={biometricRetry}>
<Text style={styles.keyBio}>👆</Text>
</Pressable>
) : (
<View key={i} style={[styles.key, styles.keyEmpty]} />
);
}
return (
<Pressable
key={i}
style={({ pressed }) => [styles.key, pressed && styles.keyPressed]}
onPress={() => press(k)}
>
<Text style={k === '⌫' ? styles.keyBack : styles.keyText}>{k}</Text>
</Pressable>
);
})}
</View>
{onLogout ? (
<Pressable onPress={onLogout} hitSlop={12} style={styles.logout}>
<Text style={styles.logoutText}>Log out</Text>
</Pressable>
) : null}
</View>
);
}
const KEY_SIZE = 76;
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: theme.bg,
paddingTop: 80,
alignItems: 'center',
paddingHorizontal: 24,
},
header: { alignItems: 'center', marginBottom: 36 },
lockBadge: {
width: 72,
height: 72,
borderRadius: 18,
backgroundColor: theme.card,
borderWidth: 1,
borderColor: theme.borderFocus,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
lockGlyph: { color: theme.accentGlow, fontSize: 38, fontWeight: '900', lineHeight: 42 },
title: { color: theme.fg, fontSize: 22, fontWeight: '700' },
subtitle: { color: theme.muted, fontSize: 13, marginTop: 4 },
dots: { flexDirection: 'row', gap: 16, marginTop: 8 },
dot: {
width: 14,
height: 14,
borderRadius: 7,
borderWidth: 1.5,
borderColor: theme.border,
},
dotFilled: { backgroundColor: theme.accent, borderColor: theme.accentText },
dotError: { borderColor: theme.bad, backgroundColor: theme.bad },
pad: {
marginTop: 20,
flexDirection: 'row',
flexWrap: 'wrap',
width: KEY_SIZE * 3 + 32,
justifyContent: 'space-between',
rowGap: 14,
},
key: {
width: KEY_SIZE,
height: KEY_SIZE,
borderRadius: KEY_SIZE / 2,
backgroundColor: theme.card,
borderWidth: 1,
borderColor: theme.border,
alignItems: 'center',
justifyContent: 'center',
},
keyEmpty: { backgroundColor: 'transparent', borderColor: 'transparent' },
keyPressed: { backgroundColor: theme.bgElevated, borderColor: theme.borderFocus },
keyText: { color: theme.fg, fontSize: 28, fontWeight: '500' },
keyBack: { color: theme.muted, fontSize: 22 },
keyBio: { fontSize: 28 },
logout: { marginTop: 28, padding: 12 },
logoutText: { color: theme.muted, fontSize: 13 },
});