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>
158 lines
4.0 KiB
TypeScript
158 lines
4.0 KiB
TypeScript
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",
|
||
},
|
||
error: {
|
||
fontSize: 13,
|
||
fontFamily: fonts.bodyMedium,
|
||
},
|
||
actions: {
|
||
flexDirection: "row",
|
||
gap: spacing.sm,
|
||
},
|
||
});
|