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:
@@ -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 4–6 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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user