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>
This commit is contained in:
2026-06-17 22:36:37 -04:00
parent 8a7a8df477
commit 14c880123c
93 changed files with 8849 additions and 7849 deletions
+158
View File
@@ -0,0 +1,158 @@
import { useEffect, useState } from "react";
import {
Modal,
Pressable,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { Button } from "@/components/ui/Button";
import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { isValidPin } from "@/lib/app-lock";
type PinPromptProps = {
visible: boolean;
title: string;
message: string;
confirmLabel?: string;
requireConfirmation?: boolean;
onCancel: () => void;
onSubmit: (pin: string) => void;
};
export function PinPrompt({
visible,
title,
message,
confirmLabel = "Continue",
requireConfirmation = false,
onCancel,
onSubmit,
}: PinPromptProps) {
const { colors } = useAppTheme();
const [pin, setPin] = useState("");
const [confirmPin, setConfirmPin] = useState("");
const [error, setError] = useState("");
useEffect(() => {
if (!visible) {
setPin("");
setConfirmPin("");
setError("");
}
}, [visible]);
function handleSubmit() {
if (!isValidPin(pin)) {
setError("PIN must be 46 digits");
return;
}
if (requireConfirmation && pin !== confirmPin) {
setError("PINs do not match");
return;
}
onSubmit(pin);
}
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onCancel}>
<Pressable style={styles.backdrop} onPress={onCancel}>
<Pressable
style={[styles.sheet, { backgroundColor: colors.card, borderColor: colors.border }]}
onPress={(event) => event.stopPropagation()}
>
<Text style={[styles.title, { color: colors.foreground }]}>{title}</Text>
<Text style={[styles.message, { color: colors.mutedForeground }]}>{message}</Text>
<TextInput
value={pin}
onChangeText={(value) => {
setError("");
setPin(value.replace(/\D/g, "").slice(0, 6));
}}
keyboardType="number-pad"
secureTextEntry
maxLength={6}
placeholder="PIN"
placeholderTextColor={colors.mutedForeground}
style={[
styles.input,
{ color: colors.foreground, borderColor: colors.border, backgroundColor: colors.background },
]}
/>
{requireConfirmation ? (
<TextInput
value={confirmPin}
onChangeText={(value) => {
setError("");
setConfirmPin(value.replace(/\D/g, "").slice(0, 6));
}}
keyboardType="number-pad"
secureTextEntry
maxLength={6}
placeholder="Confirm PIN"
placeholderTextColor={colors.mutedForeground}
style={[
styles.input,
{ color: colors.foreground, borderColor: colors.border, backgroundColor: colors.background },
]}
/>
) : null}
{error ? <Text style={[styles.error, { color: colors.destructive }]}>{error}</Text> : null}
<View style={styles.actions}>
<Button title="Cancel" variant="secondary" onPress={onCancel} />
<Button title={confirmLabel} onPress={handleSubmit} />
</View>
</Pressable>
</Pressable>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: {
flex: 1,
justifyContent: "center",
padding: spacing.lg,
backgroundColor: "rgba(0,0,0,0.35)",
},
sheet: {
borderWidth: 1,
borderRadius: 16,
padding: spacing.lg,
gap: spacing.md,
},
title: {
fontSize: 18,
fontFamily: fonts.bodySemiBold,
},
message: {
fontSize: 14,
fontFamily: fonts.body,
lineHeight: 20,
},
input: {
borderWidth: 1,
borderRadius: 12,
minHeight: 48,
paddingHorizontal: spacing.md,
fontSize: 20,
fontFamily: fonts.bodySemiBold,
textAlign: "center",
letterSpacing: 6,
},
error: {
fontSize: 13,
fontFamily: fonts.bodyMedium,
},
actions: {
flexDirection: "row",
gap: spacing.sm,
},
});