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:
181
apps/native/app/(auth)/register.tsx
Normal file
181
apps/native/app/(auth)/register.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user