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:
104
apps/native/app/(auth)/reset-password.tsx
Normal file
104
apps/native/app/(auth)/reset-password.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useLocalSearchParams, 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 ResetPasswordScreen() {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { t } = useTranslation();
|
||||
const { token } = useLocalSearchParams<{ token: string }>();
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const mismatch = confirm.length > 0 && password !== confirm;
|
||||
const canSave = password.length >= 8 && password === confirm;
|
||||
|
||||
async function handleSave() {
|
||||
if (!canSave || !token) return;
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
const result = await authClient.resetPassword({ newPassword: password, token });
|
||||
setIsPending(false);
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? t('common.error'));
|
||||
} else {
|
||||
setDone(true);
|
||||
setTimeout(() => router.replace("/(auth)/login"), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
className="flex-1 bg-white"
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<View className="flex-1 px-6" style={{ paddingTop: insets.top + 40 }}>
|
||||
<Text className="text-2xl font-bold text-gray-900 mb-2">{t('resetPassword.title')}</Text>
|
||||
<Text className="text-sm text-gray-500 mb-8">{t('resetPassword.subtitle')}</Text>
|
||||
|
||||
{done ? (
|
||||
<View className="bg-green-50 rounded-2xl p-5 items-center" style={{ borderWidth: 1, borderColor: "#bbf7d0" }}>
|
||||
<Text className="text-base font-semibold text-green-800 text-center">{t('resetPassword.successMessage')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">{t('resetPassword.newPassword')}</Text>
|
||||
<TextInput
|
||||
className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-base text-gray-900 mb-4"
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">{t('resetPassword.confirmPassword')}</Text>
|
||||
<TextInput
|
||||
className={`bg-gray-50 border rounded-xl px-4 py-3 text-base text-gray-900 mb-1 ${mismatch ? "border-red-300" : "border-gray-200"}`}
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={confirm}
|
||||
onChangeText={setConfirm}
|
||||
secureTextEntry
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
{mismatch && <Text className="text-xs text-red-500 mb-3">{t('resetPassword.mismatch')}</Text>}
|
||||
{!mismatch && <View className="mb-4" />}
|
||||
|
||||
{error && <Text className="text-sm text-red-500 mb-4">{error}</Text>}
|
||||
|
||||
<Pressable
|
||||
onPress={handleSave}
|
||||
disabled={isPending || !canSave}
|
||||
className="rounded-2xl py-4 items-center active:opacity-80"
|
||||
style={{ backgroundColor: !canSave ? "#e5e7eb" : "#2563EB" }}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text className="text-base font-semibold" style={{ color: !canSave ? "#9ca3af" : "#fff" }}>
|
||||
{t('resetPassword.saveButton')}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user