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:
265
apps/native/app/(auth)/setup.tsx
Normal file
265
apps/native/app/(auth)/setup.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
import { useUpdateHouseholdSettings } from "@/src/hooks/useHouseholdSettings";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const ACCENT = "#2563EB";
|
||||
const SHARE_PRESETS = [50, 60, 75, 100];
|
||||
|
||||
export default function SetupScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { mutate: updateSettings, isPending } = useUpdateHouseholdSettings();
|
||||
|
||||
const [step, setStep] = useState<1 | 2 | 3 | 4>(1);
|
||||
const [ownerName, setOwnerName] = useState("");
|
||||
const [partnerName, setPartnerName] = useState("");
|
||||
const [userSharePercent, setUserSharePercent] = useState(50);
|
||||
const [monthlyBudget, setMonthlyBudget] = useState("400");
|
||||
const [splitChildCosts, setSplitChildCosts] = useState(true);
|
||||
|
||||
function handleSkip() {
|
||||
finalize();
|
||||
}
|
||||
|
||||
function finalize() {
|
||||
const input = {
|
||||
ownerName: ownerName.trim() || "Ich",
|
||||
partnerName: partnerName.trim() || "Partner",
|
||||
userSharePercent,
|
||||
monthlyBudget: parseFloat(monthlyBudget.replace(",", ".")) || 400,
|
||||
splitChildCosts,
|
||||
onboardingComplete: true,
|
||||
};
|
||||
updateSettings(input, {
|
||||
onSuccess: () => router.replace("/(app)/haushalt"),
|
||||
onError: () => router.replace("/(app)/haushalt"),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white" style={{ paddingTop: insets.top }}>
|
||||
{/* Progress bar */}
|
||||
<View className="flex-row px-6 pt-4 pb-2 gap-2">
|
||||
{([1, 2, 3, 4] as const).map((s) => (
|
||||
<View
|
||||
key={s}
|
||||
className="flex-1 h-1 rounded-full"
|
||||
style={{ backgroundColor: s <= step ? ACCENT : "#e5e7eb" }}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Skip */}
|
||||
<View className="flex-row justify-end px-6 py-2">
|
||||
<Pressable onPress={handleSkip} className="py-1 px-2 active:opacity-50">
|
||||
<Text className="text-sm text-gray-400">{t('onboarding.skip')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
className="flex-1 px-6"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={{ paddingBottom: 40 }}
|
||||
>
|
||||
{/* Step 1 — Willkommen */}
|
||||
{step === 1 && (
|
||||
<View className="flex-1 items-center justify-center pt-16">
|
||||
<View
|
||||
className="w-20 h-20 rounded-3xl items-center justify-center mb-6"
|
||||
style={{ backgroundColor: "#dbeafe" }}
|
||||
>
|
||||
<Text style={{ fontSize: 40 }}>💰</Text>
|
||||
</View>
|
||||
<Text className="text-3xl font-bold text-gray-900 text-center mb-3">
|
||||
{t('onboarding.welcome')}
|
||||
</Text>
|
||||
<Text className="text-base text-gray-500 text-center leading-6">
|
||||
{t('onboarding.subtitle')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Step 2 — Namen */}
|
||||
{step === 2 && (
|
||||
<View className="pt-8">
|
||||
<Text className="text-2xl font-bold text-gray-900 mb-2">{t('setup.namesTitle')}</Text>
|
||||
<Text className="text-base text-gray-500 mb-8">
|
||||
{t('setup.namesHint')}
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm font-medium text-gray-700 mb-1.5">{t('onboarding.yourName')}</Text>
|
||||
<TextInput
|
||||
className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-base text-gray-900 mb-4"
|
||||
placeholder={t('onboarding.yourNamePlaceholder')}
|
||||
value={ownerName}
|
||||
onChangeText={setOwnerName}
|
||||
autoCapitalize="words"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Text className="text-sm font-medium text-gray-700 mb-1.5">
|
||||
{t('settings.household.partnerName')}
|
||||
</Text>
|
||||
<TextInput
|
||||
className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-base text-gray-900"
|
||||
placeholder={t('onboarding.partnerNamePlaceholder')}
|
||||
value={partnerName}
|
||||
onChangeText={setPartnerName}
|
||||
autoCapitalize="words"
|
||||
returnKeyType="next"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Step 3 — Kostenaufteilung */}
|
||||
{step === 3 && (
|
||||
<View className="pt-8">
|
||||
<Text className="text-2xl font-bold text-gray-900 mb-2">{t('setup.costSplitTitle')}</Text>
|
||||
<Text className="text-base text-gray-500 mb-6">
|
||||
{t('setup.costSplitHint')}
|
||||
</Text>
|
||||
|
||||
{/* Preset buttons */}
|
||||
<View className="flex-row gap-2 mb-6">
|
||||
{SHARE_PRESETS.map((p) => (
|
||||
<Pressable
|
||||
key={p}
|
||||
onPress={() => setUserSharePercent(p)}
|
||||
className="flex-1 py-3 rounded-xl items-center"
|
||||
style={{
|
||||
backgroundColor: userSharePercent === p ? ACCENT : "#f3f4f6",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: userSharePercent === p ? "#fff" : "#374151" }}
|
||||
>
|
||||
{p}%
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Preview */}
|
||||
<View
|
||||
className="rounded-xl p-4 mb-6"
|
||||
style={{ backgroundColor: "#eff6ff", borderWidth: 1, borderColor: "#bfdbfe" }}
|
||||
>
|
||||
<Text className="text-sm text-blue-700">
|
||||
{t('onboarding.preview', {
|
||||
own: userSharePercent,
|
||||
partner: partnerName.trim() || 'Partner',
|
||||
rest: 100 - userSharePercent,
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Monthly budget */}
|
||||
<Text className="text-sm font-medium text-gray-700 mb-1.5">
|
||||
{t('setup.monthlyBudgetLabel')}
|
||||
</Text>
|
||||
<View className="flex-row items-center bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 mb-6">
|
||||
<Text className="text-base text-gray-400 mr-2">€</Text>
|
||||
<TextInput
|
||||
className="flex-1 text-base text-gray-900"
|
||||
placeholder="400"
|
||||
value={monthlyBudget}
|
||||
onChangeText={setMonthlyBudget}
|
||||
keyboardType="decimal-pad"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Split child costs */}
|
||||
<Text className="text-sm font-medium text-gray-700 mb-3">
|
||||
{t('setup.splitChildCostsLabel')}
|
||||
</Text>
|
||||
<View className="flex-row gap-3">
|
||||
<Pressable
|
||||
onPress={() => setSplitChildCosts(true)}
|
||||
className="flex-1 py-3 rounded-xl items-center"
|
||||
style={{ backgroundColor: splitChildCosts ? ACCENT : "#f3f4f6" }}
|
||||
>
|
||||
<Text
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: splitChildCosts ? "#fff" : "#374151" }}
|
||||
>
|
||||
{t('common.yes')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => setSplitChildCosts(false)}
|
||||
className="flex-1 py-3 rounded-xl items-center"
|
||||
style={{ backgroundColor: !splitChildCosts ? ACCENT : "#f3f4f6" }}
|
||||
>
|
||||
<Text
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: !splitChildCosts ? "#fff" : "#374151" }}
|
||||
>
|
||||
{t('common.no')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Step 4 — Fertig */}
|
||||
{step === 4 && (
|
||||
<View className="flex-1 items-center justify-center pt-16">
|
||||
<View
|
||||
className="w-20 h-20 rounded-3xl items-center justify-center mb-6"
|
||||
style={{ backgroundColor: "#dcfce7" }}
|
||||
>
|
||||
<Ionicons name="checkmark-circle" size={44} color="#16a34a" />
|
||||
</View>
|
||||
<Text className="text-3xl font-bold text-gray-900 text-center mb-3">
|
||||
{t('onboarding.done')}
|
||||
</Text>
|
||||
<Text className="text-base text-gray-500 text-center leading-6">
|
||||
{t('onboarding.doneHint')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<View className="px-6 pb-8" style={{ paddingBottom: insets.bottom + 24 }}>
|
||||
{step < 4 ? (
|
||||
<Pressable
|
||||
onPress={() => setStep(((step + 1) as 1 | 2 | 3 | 4))}
|
||||
className="rounded-2xl py-4 items-center active:opacity-80"
|
||||
style={{ backgroundColor: ACCENT }}
|
||||
>
|
||||
<Text className="text-base font-semibold text-white">
|
||||
{step === 1 ? t('onboarding.start') : t('common.next')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Pressable
|
||||
onPress={finalize}
|
||||
disabled={isPending}
|
||||
className="rounded-2xl py-4 items-center active:opacity-80"
|
||||
style={{ backgroundColor: "#16a34a" }}
|
||||
>
|
||||
{isPending ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text className="text-base font-semibold text-white">{t('onboarding.startApp')}</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user