Production deployment setup + feature complete

- 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>
This commit is contained in:
René Schober
2026-03-20 11:54:22 +01:00
parent 4e34270786
commit 9ddc7c6d7a
194 changed files with 55961 additions and 305 deletions

View File

@@ -0,0 +1,83 @@
import { authClient } from "@/src/lib/auth-client";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useState } from "react";
import { ActivityIndicator, Pressable, Text, View } from "react-native";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
export default function VerifyEmailScreen() {
const router = useRouter();
const { t } = useTranslation();
const { email } = useLocalSearchParams<{ email: string }>();
const [isResending, setIsResending] = useState(false);
const [resent, setResent] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleResend() {
if (!email) return;
setIsResending(true);
setError(null);
try {
await authClient.sendVerificationEmail({ email, callbackURL: "/" });
setResent(true);
setTimeout(() => setResent(false), 4000);
} catch {
setError(t('verifyEmail.resendError'));
} finally {
setIsResending(false);
}
}
return (
<View className="flex-1 bg-white items-center justify-center px-6">
<View className="w-16 h-16 rounded-full bg-blue-50 items-center justify-center mb-6">
<Ionicons name="mail-outline" size={32} color="#2563EB" />
</View>
<Text className="text-2xl font-bold text-gray-900 mb-2 text-center">
{t('verifyEmail.title')}
</Text>
<Text className="text-base text-gray-500 mb-2 text-center">
{t('verifyEmail.hint')}
</Text>
{email && (
<Text className="text-base font-semibold text-gray-900 mb-8 text-center">
{email}
</Text>
)}
{error && (
<View className="mb-4 rounded-lg bg-red-50 p-3 w-full">
<Text className="text-sm text-red-600 text-center">{error}</Text>
</View>
)}
{resent && (
<View className="mb-4 rounded-lg bg-green-50 p-3 w-full">
<Text className="text-sm text-green-700 text-center">{t('verifyEmail.resentConfirm')}</Text>
</View>
)}
<Pressable
onPress={handleResend}
disabled={isResending}
className="w-full items-center rounded-xl bg-blue-600 py-4 mb-4 active:opacity-80"
>
{isResending ? (
<ActivityIndicator color="white" />
) : (
<Text className="text-base font-semibold text-white">
{t('verifyEmail.resend')}
</Text>
)}
</Pressable>
<Pressable
onPress={() => router.replace("/(auth)/login")}
className="py-2 active:opacity-60"
>
<Text className="text-sm text-gray-500">{t('verifyEmail.backToLogin')}</Text>
</Pressable>
</View>
);
}