- Dockerfile + deploy.sh for Hetzner server - Email verification via Better Auth + Resend - Invite code flow (6-digit OTP, generate/join) - Settlement share percent fix (payer vs debtor) - OCR scanner fixes (date display, retry, viewfinder) - app.json icon/splash/adaptive-icon configured - iOS deployment target 15.5 (ML Kit requirement) - DB migration 0014: household_invitations table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import { useRouter } from "expo-router";
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
ActivityIndicator,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Pressable,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { authClient } from "@/src/lib/auth-client";
|
|
|
|
export default function ForgotPasswordScreen() {
|
|
const router = useRouter();
|
|
const insets = useSafeAreaInsets();
|
|
const { t } = useTranslation();
|
|
const [email, setEmail] = useState("");
|
|
const [isPending, setIsPending] = useState(false);
|
|
const [sent, setSent] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function handleSend() {
|
|
if (!email.trim()) return;
|
|
setIsPending(true);
|
|
setError(null);
|
|
const result = await authClient.requestPasswordReset({
|
|
email: email.trim(),
|
|
redirectTo: "haushaltsApp://reset-password",
|
|
});
|
|
setIsPending(false);
|
|
if (result.error) {
|
|
setError(result.error.message ?? t('common.error'));
|
|
} else {
|
|
setSent(true);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
className="flex-1 bg-white"
|
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
>
|
|
<View style={{ paddingTop: insets.top }} className="px-6 pt-4 pb-2 flex-row items-center">
|
|
<Pressable onPress={() => router.back()} className="p-1 mr-2 active:opacity-50">
|
|
<Ionicons name="chevron-back" size={22} color="#374151" />
|
|
</Pressable>
|
|
</View>
|
|
|
|
<View className="flex-1 px-6 pt-8">
|
|
<Text className="text-2xl font-bold text-gray-900 mb-2">{t('forgotPassword.title')}</Text>
|
|
<Text className="text-sm text-gray-500 mb-8">{t('forgotPassword.subtitle')}</Text>
|
|
|
|
{sent ? (
|
|
<View className="bg-green-50 rounded-2xl p-5 items-center" style={{ borderWidth: 1, borderColor: "#bbf7d0" }}>
|
|
<Ionicons name="checkmark-circle" size={48} color="#16a34a" style={{ marginBottom: 12 }} />
|
|
<Text className="text-base font-semibold text-green-800 text-center mb-1">{t('forgotPassword.sentTitle')}</Text>
|
|
<Text className="text-sm text-green-600 text-center">{t('forgotPassword.sentHint')}</Text>
|
|
</View>
|
|
) : (
|
|
<>
|
|
<Text className="text-sm font-medium text-gray-700 mb-2">{t('login.emailLabel')}</Text>
|
|
<TextInput
|
|
className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-base text-gray-900 mb-4"
|
|
placeholder={t('login.emailPlaceholder')}
|
|
placeholderTextColor="#9ca3af"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
autoComplete="email"
|
|
/>
|
|
|
|
{error && (
|
|
<Text className="text-sm text-red-500 mb-4">{error}</Text>
|
|
)}
|
|
|
|
<Pressable
|
|
onPress={handleSend}
|
|
disabled={isPending || !email.trim()}
|
|
className="rounded-2xl py-4 items-center active:opacity-80"
|
|
style={{ backgroundColor: isPending || !email.trim() ? "#e5e7eb" : "#2563EB" }}
|
|
>
|
|
{isPending ? (
|
|
<ActivityIndicator color="#fff" />
|
|
) : (
|
|
<Text className="text-base font-semibold" style={{ color: !email.trim() ? "#9ca3af" : "#fff" }}>
|
|
{t('forgotPassword.sendButton')}
|
|
</Text>
|
|
)}
|
|
</Pressable>
|
|
</>
|
|
)}
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|