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>
155 lines
4.7 KiB
TypeScript
155 lines
4.7 KiB
TypeScript
import { router } from "expo-router";
|
|
import { useState } from "react";
|
|
import {
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from "react-native";
|
|
import { FullScreen } from "@/components/Screen";
|
|
import { AuthBackground } from "@/components/AppBackground";
|
|
import { AuthServerPicker } from "@/components/AuthServerPicker";
|
|
import { HeadingText, Logo } from "@/components/Logo";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Card } from "@/components/ui/Card";
|
|
import { Input } from "@/components/ui/Input";
|
|
import { fonts, spacing } from "@/constants/theme";
|
|
import { useAppTheme } from "@/contexts/ThemeContext";
|
|
import { requestPasswordReset } from "@/lib/auth-api";
|
|
import { isValidEmail, useFieldVisibility } from "@/lib/form-validation";
|
|
|
|
export default function ForgotPasswordScreen() {
|
|
const { colors } = useAppTheme();
|
|
const [email, setEmail] = useState("");
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [serverReady, setServerReady] = useState(true);
|
|
const { touch, visible, markSubmitted } = useFieldVisibility();
|
|
|
|
const emailValidationError = !email.trim()
|
|
? "Email is required"
|
|
: isValidEmail(email)
|
|
? undefined
|
|
: "Enter a valid email";
|
|
const canSubmit = isValidEmail(email) && serverReady;
|
|
|
|
async function handleSubmit() {
|
|
markSubmitted();
|
|
if (!canSubmit) return;
|
|
setError(null);
|
|
setMessage(null);
|
|
setLoading(true);
|
|
|
|
try {
|
|
const result = await requestPasswordReset(email.trim());
|
|
setMessage(result);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Request failed");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<AuthBackground>
|
|
<FullScreen style={styles.safe}>
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
|
style={styles.flex}
|
|
>
|
|
<ScrollView contentContainerStyle={styles.container}>
|
|
<Pressable onPress={() => router.back()}>
|
|
<Text style={[styles.back, { color: colors.mutedForeground }]}>← Back</Text>
|
|
</Pressable>
|
|
|
|
<AuthServerPicker onReadyChange={setServerReady} />
|
|
|
|
<Card style={styles.card}>
|
|
<View style={styles.header}>
|
|
<Logo size="md" />
|
|
<HeadingText style={styles.title}>Reset password</HeadingText>
|
|
<Text style={[styles.subtitle, { color: colors.mutedForeground }]}>
|
|
Enter your email and we'll send reset instructions if an account exists.
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.form}>
|
|
<Input
|
|
label="Email"
|
|
autoCapitalize="none"
|
|
autoComplete="email"
|
|
keyboardType="email-address"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
onBlur={() => touch("email")}
|
|
placeholder="you@example.com"
|
|
required
|
|
error={visible("email") ? emailValidationError : undefined}
|
|
/>
|
|
|
|
{error ? (
|
|
<Text style={[styles.error, { color: colors.destructive }]}>{error}</Text>
|
|
) : null}
|
|
{message ? (
|
|
<Text style={[styles.success, { color: colors.foreground }]}>{message}</Text>
|
|
) : null}
|
|
|
|
<Button
|
|
title="Send reset link"
|
|
loading={loading}
|
|
disabled={!canSubmit}
|
|
onPress={handleSubmit}
|
|
/>
|
|
<Button
|
|
title="Have a reset token?"
|
|
variant="ghost"
|
|
onPress={() => router.push("/(auth)/reset-password")}
|
|
/>
|
|
</View>
|
|
</Card>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</FullScreen>
|
|
</AuthBackground>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
safe: { flex: 1 },
|
|
flex: { flex: 1 },
|
|
container: {
|
|
flexGrow: 1,
|
|
padding: spacing.lg,
|
|
paddingBottom: spacing.md,
|
|
gap: spacing.md,
|
|
justifyContent: "center",
|
|
},
|
|
back: {
|
|
fontFamily: fonts.bodyMedium,
|
|
fontSize: 16,
|
|
marginBottom: spacing.sm,
|
|
},
|
|
card: { gap: spacing.lg },
|
|
header: { gap: spacing.sm },
|
|
title: { fontSize: 28 },
|
|
subtitle: {
|
|
fontSize: 14,
|
|
fontFamily: fonts.body,
|
|
lineHeight: 20,
|
|
},
|
|
form: { gap: spacing.md },
|
|
error: {
|
|
fontSize: 14,
|
|
fontFamily: fonts.body,
|
|
},
|
|
success: {
|
|
fontSize: 14,
|
|
fontFamily: fonts.body,
|
|
lineHeight: 20,
|
|
},
|
|
});
|