From 3273e7a0fbbce82f4ce6cacbcdb7b6d6848f6c1b Mon Sep 17 00:00:00 2001 From: JustZvan Date: Mon, 6 Apr 2026 15:32:51 +0200 Subject: feat: gallery scanning preferences --- app/(auth)/_layout.tsx | 1 + app/(auth)/reset-password.tsx | 267 ++++++++++++++++++++++++++++++++++++++++++ app/(auth)/signin.tsx | 40 ++++++- app/(auth)/signup.tsx | 43 +++++-- app/(tabs)/controls.tsx | 90 ++++++++++++-- 5 files changed, 421 insertions(+), 20 deletions(-) create mode 100644 app/(auth)/reset-password.tsx (limited to 'app') diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx index e18b629..3d5e604 100644 --- a/app/(auth)/_layout.tsx +++ b/app/(auth)/_layout.tsx @@ -24,6 +24,7 @@ export default function AuthLayout() { + ); } diff --git a/app/(auth)/reset-password.tsx b/app/(auth)/reset-password.tsx new file mode 100644 index 0000000..a5f8388 --- /dev/null +++ b/app/(auth)/reset-password.tsx @@ -0,0 +1,267 @@ +import { Ionicons } from "@expo/vector-icons"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useMemo, useState } from "react"; +import { + KeyboardAvoidingView, + Platform, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { apiClient } from "../../api/client"; +import { t } from "../../lib/locales"; +import { colors } from "../../lib/theme"; +import { Button, H1, Muted, TextInput } from "../../lib/ui"; + +type Mode = "request" | "confirm"; + +export default function ResetPassword() { + const params = useLocalSearchParams<{ token?: string | string[] }>(); + const tokenFromParams = useMemo( + () => (Array.isArray(params.token) ? params.token[0] : params.token) || "", + [params.token], + ); + const [mode, setMode] = useState(tokenFromParams ? "confirm" : "request"); + const [email, setEmail] = useState(""); + const [token, setToken] = useState(tokenFromParams); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [emailError, setEmailError] = useState(""); + const [tokenError, setTokenError] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [confirmPasswordError, setConfirmPasswordError] = useState(""); + + useEffect(() => { + if (!tokenFromParams) return; + setToken(tokenFromParams); + setMode("confirm"); + }, [tokenFromParams]); + + const resetFieldErrors = () => { + setEmailError(""); + setTokenError(""); + setPasswordError(""); + setConfirmPasswordError(""); + }; + + const validateRequest = () => { + resetFieldErrors(); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + setEmailError(t("invalidEmail")); + return false; + } + + return true; + }; + + const validateConfirm = () => { + resetFieldErrors(); + let valid = true; + + if (!token.trim()) { + setTokenError(t("resetTokenRequired")); + valid = false; + } + + if (!password) { + setPasswordError(t("passwordRequired")); + valid = false; + } else if (password.length < 8) { + setPasswordError(t("passwordTooShort")); + valid = false; + } + + if (password !== confirmPassword) { + setConfirmPasswordError(t("passwordsDoNotMatch")); + valid = false; + } + + return valid; + }; + + const handleRequestReset = async () => { + if (!validateRequest()) return; + + setLoading(true); + setError(""); + setSuccess(""); + + try { + const result = await apiClient.requestPasswordReset(email.trim()); + if (!result.success) { + setError(result.reason || t("resetPasswordError")); + return; + } + + setSuccess(t("passwordResetEmailSent")); + } catch { + setError(t("resetPasswordError")); + } finally { + setLoading(false); + } + }; + + const handleConfirmReset = async () => { + if (!validateConfirm()) return; + + setLoading(true); + setError(""); + setSuccess(""); + + try { + const result = await apiClient.confirmPasswordReset(token.trim(), password); + if (!result.success) { + setError(result.reason || t("resetPasswordError")); + return; + } + + setSuccess(t("passwordResetSuccess")); + setTimeout(() => router.replace("/(auth)/signin"), 500); + } catch { + setError(t("resetPasswordError")); + } finally { + setLoading(false); + } + }; + + const switchMode = () => { + resetFieldErrors(); + setError(""); + setSuccess(""); + setMode((current) => (current === "request" ? "confirm" : "request")); + }; + + return ( + + + + router.back()}> + + + + +

{t("resetPassword")}

+ {t("resetPasswordHelp")} +
+ + + {mode === "request" ? ( + + ) : ( + <> + + + + + + + )} + + {error ? {error} : null} + {success ? {success} : null} + +