32ffe782ea
Flatten widget layouts and use system colors so banner and expanded regions render on vibrant lock screens; migrate auth sessions per account to prevent double sign-in; scope app lock PIN to accounts; default clock description to "Clock In"; add architecture docs and deferred form validation on auth screens. Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
4.3 KiB
TypeScript
179 lines
4.3 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import {
|
|
Modal,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from "react-native";
|
|
|
|
import { Logo } from "@/components/Logo";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { fonts, spacing } from "@/constants/theme";
|
|
import { useAppLock } from "@/contexts/AppLockContext";
|
|
import { useAppTheme } from "@/contexts/ThemeContext";
|
|
|
|
export function AppLockOverlay() {
|
|
const { colors } = useAppTheme();
|
|
const {
|
|
enabled,
|
|
isLocked,
|
|
biometricEnabled,
|
|
biometricAvailable,
|
|
biometricLabel,
|
|
unlockWithPin,
|
|
unlockWithBiometric,
|
|
} = useAppLock();
|
|
const [pin, setPin] = useState("");
|
|
const [error, setError] = useState("");
|
|
const promptedRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (!isLocked) {
|
|
setPin("");
|
|
setError("");
|
|
promptedRef.current = false;
|
|
}
|
|
}, [isLocked]);
|
|
|
|
useEffect(() => {
|
|
if (!enabled || !isLocked || !biometricEnabled || !biometricAvailable) {
|
|
return;
|
|
}
|
|
if (promptedRef.current) return;
|
|
|
|
const timer = setTimeout(() => {
|
|
promptedRef.current = true;
|
|
void unlockWithBiometric().then((success) => {
|
|
if (!success) return;
|
|
setPin("");
|
|
setError("");
|
|
});
|
|
}, 400);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [enabled, isLocked, biometricEnabled, biometricAvailable, unlockWithBiometric]);
|
|
|
|
if (!enabled || !isLocked) {
|
|
return null;
|
|
}
|
|
|
|
async function submitPin() {
|
|
const success = await unlockWithPin(pin);
|
|
if (success) {
|
|
setPin("");
|
|
setError("");
|
|
return;
|
|
}
|
|
setError("Incorrect PIN");
|
|
setPin("");
|
|
}
|
|
|
|
async function tryBiometric() {
|
|
promptedRef.current = true;
|
|
const success = await unlockWithBiometric();
|
|
if (!success) {
|
|
setError(`Could not unlock with ${biometricLabel}`);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Modal visible animationType="fade" transparent={false}>
|
|
<View style={[styles.screen, { backgroundColor: colors.background }]}>
|
|
<View style={styles.content}>
|
|
<Logo size="md" />
|
|
<Text style={[styles.title, { color: colors.foreground }]}>Locked</Text>
|
|
<Text style={[styles.subtitle, { color: colors.mutedForeground }]}>
|
|
Enter your PIN to continue
|
|
</Text>
|
|
|
|
<TextInput
|
|
value={pin}
|
|
onChangeText={(value) => {
|
|
setError("");
|
|
setPin(value.replace(/\D/g, "").slice(0, 6));
|
|
}}
|
|
keyboardType="number-pad"
|
|
secureTextEntry
|
|
maxLength={6}
|
|
style={[
|
|
styles.pinInput,
|
|
{
|
|
color: colors.foreground,
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.card,
|
|
},
|
|
]}
|
|
placeholder="PIN"
|
|
placeholderTextColor={colors.mutedForeground}
|
|
onSubmitEditing={() => void submitPin()}
|
|
/>
|
|
|
|
{error ? <Text style={[styles.error, { color: colors.destructive }]}>{error}</Text> : null}
|
|
|
|
<View style={styles.actions}>
|
|
<Button title="Unlock" onPress={() => void submitPin()} disabled={pin.length < 4} />
|
|
|
|
{biometricAvailable ? (
|
|
<Button
|
|
title={`Unlock with ${biometricLabel}`}
|
|
variant="secondary"
|
|
onPress={() => void tryBiometric()}
|
|
style={styles.biometricButton}
|
|
/>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
screen: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
padding: spacing.lg,
|
|
},
|
|
content: {
|
|
alignItems: "center",
|
|
gap: spacing.md,
|
|
width: "100%",
|
|
maxWidth: 320,
|
|
alignSelf: "center",
|
|
},
|
|
title: {
|
|
fontSize: 22,
|
|
fontFamily: fonts.heading,
|
|
textAlign: "center",
|
|
},
|
|
subtitle: {
|
|
fontSize: 14,
|
|
fontFamily: fonts.body,
|
|
textAlign: "center",
|
|
lineHeight: 20,
|
|
},
|
|
pinInput: {
|
|
width: "100%",
|
|
borderWidth: 1,
|
|
borderRadius: 12,
|
|
minHeight: 52,
|
|
paddingHorizontal: spacing.md,
|
|
fontSize: 20,
|
|
fontFamily: fonts.bodySemiBold,
|
|
textAlign: "center",
|
|
},
|
|
error: {
|
|
fontFamily: fonts.bodyMedium,
|
|
fontSize: 13,
|
|
textAlign: "center",
|
|
},
|
|
actions: {
|
|
width: "100%",
|
|
gap: spacing.sm,
|
|
},
|
|
biometricButton: {
|
|
width: "100%",
|
|
},
|
|
});
|