Files
soconnor 32ffe782ea Fix Live Activity lock screen rendering and polish multi-account auth.
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>
2026-06-18 01:23:36 -04:00

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