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