Files
beenvoice-app/components/AppLockOverlay.tsx
T
soconnor 14c880123c Add beenvoice mobile companion app with full dark mode support.
Expo app with dashboard, time clock, invoices, and settings — native tabs, glass UI, theme-aware components, and iOS Live Activities.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 22:36:37 -04:00

175 lines
4.3 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { useEffect, useState } from "react";
import {
Modal,
Pressable,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { LogoMark } 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("");
useEffect(() => {
if (!isLocked) {
setPin("");
setError("");
}
}, [isLocked]);
useEffect(() => {
if (!enabled || !isLocked || !biometricEnabled || !biometricAvailable) {
return;
}
void unlockWithBiometric().then((success) => {
if (!success) return;
setPin("");
setError("");
});
}, [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() {
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}>
<LogoMark size={56} />
<Text style={[styles.title, { color: colors.foreground }]}>beenvoice is 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}
<Button title="Unlock" onPress={() => void submitPin()} disabled={pin.length < 4} />
{biometricEnabled && biometricAvailable ? (
<Pressable
accessibilityRole="button"
onPress={() => void tryBiometric()}
style={styles.biometricButton}
>
<Ionicons name="finger-print-outline" size={20} color={colors.primary} />
<Text style={[styles.biometricLabel, { color: colors.primary }]}>
Unlock with {biometricLabel}
</Text>
</Pressable>
) : null}
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
justifyContent: "center",
padding: spacing.lg,
},
content: {
alignItems: "center",
gap: spacing.md,
},
title: {
fontSize: 22,
fontFamily: fonts.heading,
textAlign: "center",
},
subtitle: {
fontSize: 14,
fontFamily: fonts.body,
textAlign: "center",
lineHeight: 20,
},
pinInput: {
width: "100%",
maxWidth: 280,
borderWidth: 1,
borderRadius: 12,
minHeight: 52,
paddingHorizontal: spacing.md,
fontSize: 24,
fontFamily: fonts.bodySemiBold,
textAlign: "center",
letterSpacing: 8,
},
error: {
fontFamily: fonts.bodyMedium,
fontSize: 13,
},
biometricButton: {
flexDirection: "row",
alignItems: "center",
gap: spacing.xs,
paddingVertical: spacing.sm,
},
biometricLabel: {
fontSize: 14,
fontFamily: fonts.bodyMedium,
},
});