Files
HausApp/apps/native/app/(auth)/register.tsx
René Schober 9ddc7c6d7a 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>
2026-03-20 11:54:22 +01:00

182 lines
5.8 KiB
TypeScript

import { signUp, authClient } from "@/src/lib/auth-client";
import { useRouter } from "expo-router";
import { useState } from "react";
import * as AppleAuthentication from "expo-apple-authentication";
import {
ActivityIndicator,
KeyboardAvoidingView,
Platform,
Pressable,
Text,
TextInput,
View,
} from "react-native";
import { useTranslation } from "react-i18next";
export default function RegisterScreen() {
const router = useRouter();
const { t } = useTranslation();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleEmailRegister() {
if (!name || !email || !password) {
setError("Bitte alle Felder ausfüllen");
return;
}
if (password.length < 8) {
setError("Passwort muss mindestens 8 Zeichen lang sein");
return;
}
setIsLoading(true);
setError(null);
try {
const result = await signUp.email({
name,
email,
password,
callbackURL: "haushaltsApp://onboarding",
});
if (result.error) {
setError(result.error.message ?? "Registrierung fehlgeschlagen");
return;
}
// Email verification required — don't set user/session yet
router.replace({ pathname: "/(auth)/verify-email", params: { email } });
} catch {
setError("Registrierung fehlgeschlagen");
} finally {
setIsLoading(false);
}
}
async function handleAppleRegister() {
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
if (!credential.identityToken) return;
const result = await authClient.signIn.social({
provider: "apple",
idToken: { token: credential.identityToken },
});
if (result.error) {
setError(result.error.message ?? t('common.error'));
return;
}
// session is handled by authClient interceptor
router.replace("/(app)/haushalt");
} catch (err: unknown) {
if ((err as { code?: string }).code !== "ERR_CANCELED") {
setError(t('login.appleSignInError'));
}
}
}
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1 bg-white"
>
<View className="flex-1 justify-center px-6">
<Text className="mb-2 text-3xl font-bold text-gray-900">
Konto erstellen
</Text>
<Text className="mb-8 text-base text-gray-500">
Starte deinen Haushalts-Manager
</Text>
{error && (
<View className="mb-4 rounded-lg bg-red-50 p-3">
<Text className="text-sm text-red-600">{error}</Text>
</View>
)}
{Platform.OS === "ios" && (
<>
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_UP}
buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={12}
style={{ width: "100%", height: 50 }}
onPress={handleAppleRegister}
/>
<View className="flex-row items-center gap-3 my-4">
<View className="flex-1 h-px bg-gray-200" />
<Text className="text-xs text-gray-400">{t('common.or')}</Text>
<View className="flex-1 h-px bg-gray-200" />
</View>
</>
)}
<View className="mb-4">
<Text className="mb-1.5 text-sm font-medium text-gray-700">Name</Text>
<TextInput
className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-base text-gray-900"
placeholder="Dein Name"
value={name}
onChangeText={setName}
autoComplete="name"
/>
</View>
<View className="mb-4">
<Text className="mb-1.5 text-sm font-medium text-gray-700">
E-Mail
</Text>
<TextInput
className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-base text-gray-900"
placeholder="deine@email.de"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
/>
</View>
<View className="mb-6">
<Text className="mb-1.5 text-sm font-medium text-gray-700">
Passwort
</Text>
<TextInput
className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-base text-gray-900"
placeholder="Mindestens 8 Zeichen"
value={password}
onChangeText={setPassword}
secureTextEntry
autoComplete="new-password"
/>
</View>
<Pressable
onPress={handleEmailRegister}
disabled={isLoading}
className="mb-3 items-center rounded-xl bg-blue-600 py-4 active:opacity-80"
>
{isLoading ? (
<ActivityIndicator color="white" />
) : (
<Text className="text-base font-semibold text-white">
Konto erstellen
</Text>
)}
</Pressable>
<View className="flex-row justify-center">
<Text className="text-sm text-gray-500">Bereits ein Konto? </Text>
<Pressable onPress={() => router.push("/(auth)/login")}>
<Text className="text-sm font-semibold text-blue-600">Anmelden</Text>
</Pressable>
</View>
</View>
</KeyboardAvoidingView>
);
}