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:
83
apps/native/app/(auth)/verify-email.tsx
Normal file
83
apps/native/app/(auth)/verify-email.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user