Files
HausApp/apps/native/app/(app)/_layout.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

122 lines
4.8 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { useSession, authClient } from "@/src/lib/auth-client";
import { useAuthStore } from "@/src/stores/auth.store";
import { TAB_COLORS } from "@/src/constants/colors";
import { apiRequest } from "@/src/lib/api-client";
import { Redirect, Tabs, useRouter } from "expo-router";
import React, { useEffect } from "react";
import { Alert } from "react-native";
import { queryClient } from "@/src/lib/query-client";
import { useTranslation } from "react-i18next";
function PendingInvitationHandler() {
const router = useRouter();
const pendingInvitationId = useAuthStore((s) => s.pendingInvitationId);
useEffect(() => {
if (!pendingInvitationId) return;
authClient.organization.acceptInvitation({ invitationId: pendingInvitationId })
.then(async (result) => {
useAuthStore.getState().setPendingInvitationId(null);
if (result.error) {
Alert.alert("Fehler", result.error.message ?? "Einladung konnte nicht angenommen werden.");
return;
}
const householdsResponse = await apiRequest<{ households: { id: string; name: string; role: string }[] }>("/api/households");
const newHouseholds = householdsResponse.households;
useAuthStore.getState().setHouseholds(newHouseholds);
if (newHouseholds[0] && !useAuthStore.getState().activeHouseholdId) {
useAuthStore.getState().setActiveHousehold(newHouseholds[0].id);
}
await queryClient.invalidateQueries();
Alert.alert("Einladung angenommen", "Du bist jetzt Mitglied des Haushalts.");
router.replace("/(app)/haushalt");
})
.catch((err: Error) => {
useAuthStore.getState().setPendingInvitationId(null);
Alert.alert("Fehler", err.message ?? "Einladung konnte nicht angenommen werden.");
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pendingInvitationId]);
return null;
}
export default function AppLayout() {
const { data: session, isPending } = useSession();
const households = useAuthStore((s) => s.households);
const { t } = useTranslation();
if (isPending) return null;
if (!session) return <Redirect href="/(auth)/login" />;
if (households.length === 0) return <Redirect href="/(auth)/onboarding" />;
return (
<>
<PendingInvitationHandler />
<Tabs
screenOptions={{
tabBarInactiveTintColor: "#9ca3af",
headerShown: false,
tabBarStyle: { borderTopColor: "#f3f4f6" },
}}
>
<Tabs.Screen
name="haushalt/index"
options={{
title: t('tabs.household'),
tabBarActiveTintColor: TAB_COLORS.household,
tabBarIcon: ({ color, size }) => <Ionicons name="home-outline" size={size} color={color} />,
}}
/>
<Tabs.Screen
name="ich/index"
options={{
title: t('tabs.me'),
tabBarActiveTintColor: TAB_COLORS.private,
tabBarIcon: ({ color, size }) => <Ionicons name="person-outline" size={size} color={color} />,
}}
/>
<Tabs.Screen
name="kinder/index"
options={{
title: t('tabs.children'),
tabBarActiveTintColor: TAB_COLORS.children,
tabBarIcon: ({ color, size }) => <Ionicons name="happy-outline" size={size} color={color} />,
}}
/>
<Tabs.Screen
name="shopping-list/index"
options={{
title: t('tabs.shopping'),
tabBarActiveTintColor: TAB_COLORS.shopping,
tabBarIcon: ({ color, size }) => <Ionicons name="cart-outline" size={size} color={color} />,
}}
/>
<Tabs.Screen
name="mehr/index"
options={{
title: t('tabs.more'),
tabBarActiveTintColor: TAB_COLORS.more,
tabBarIcon: ({ color, size }) => <Ionicons name="grid-outline" size={size} color={color} />,
}}
/>
{/* Hidden — not in tab bar */}
<Tabs.Screen name="dashboard/index" options={{ href: null }} />
<Tabs.Screen name="transactions/index" options={{ href: null }} />
<Tabs.Screen name="urlaub/index" options={{ href: null }} />
<Tabs.Screen name="urlaub/[id]" options={{ href: null }} />
<Tabs.Screen name="settings/index" options={{ href: null }} />
<Tabs.Screen name="settings/categories" options={{ href: null }} />
<Tabs.Screen name="settings/fixed-costs" options={{ href: null }} />
<Tabs.Screen name="settings/transfer-line-items" options={{ href: null }} />
<Tabs.Screen name="settings/household" options={{ href: null }} />
<Tabs.Screen name="months/close" options={{ href: null }} />
<Tabs.Screen name="scanner" options={{ href: null }} />
</Tabs>
</>
);
}