Files
HausApp/apps/native/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

97 lines
3.3 KiB
TypeScript

import "@/global.css";
import "@/src/i18n";
import { QueryClientProvider } from "@tanstack/react-query";
import { Stack, useRouter } from "expo-router";
import { HeroUINativeProvider } from "heroui-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { useEffect } from "react";
import { Linking, Alert } from "react-native";
import { AppThemeProvider } from "@/contexts/app-theme-context";
import { queryClient } from "@/src/lib/query-client";
import { authClient } from "@/src/lib/auth-client";
import { useAuthStore } from "@/src/stores/auth.store";
if (__DEV__) {
const originalConsoleError = console.error;
console.error = (...args: unknown[]) => {
if (typeof args[0] === "string" && args[0].includes("Maximum update depth")) return;
originalConsoleError(...args);
};
}
function DeepLinkHandler() {
const router = useRouter();
async function handleUrl(url: string) {
// Match haushaltsApp://invite?invitationId=xxx
const match = url.match(/haushaltsApp:\/\/invite\?invitationId=([^&]+)/i);
if (!match) return;
const invitationId = match[1]!;
const isLoggedIn = !!useAuthStore.getState().user;
if (!isLoggedIn) {
// Store invitationId and redirect after login
useAuthStore.getState().setPendingInvitationId(invitationId);
router.replace("/(auth)/login");
return;
}
try {
const result = await authClient.organization.acceptInvitation({ invitationId });
if (result.error) throw new Error(result.error.message ?? "Fehler");
// Refresh households list
const { apiRequest } = await import("@/src/lib/api-client");
const householdsResponse = await apiRequest<{ households: { id: string; name: string; role: string }[] }>("/api/households");
useAuthStore.getState().setHouseholds(householdsResponse.households);
await queryClient.invalidateQueries();
Alert.alert("Einladung angenommen", "Du bist jetzt Mitglied des Haushalts.");
router.replace("/(app)/haushalt");
} catch (err) {
Alert.alert("Fehler", (err as Error).message ?? "Einladung konnte nicht angenommen werden.");
}
}
useEffect(() => {
// Handle cold start URL
Linking.getInitialURL().then((url) => {
if (url) void handleUrl(url);
});
// Handle foreground/background URL
const sub = Linking.addEventListener("url", ({ url }) => {
void handleUrl(url);
});
return () => sub.remove();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
}
export default function Layout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<AppThemeProvider>
<QueryClientProvider client={queryClient}>
<KeyboardProvider>
<HeroUINativeProvider>
<DeepLinkHandler />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="(auth)" />
<Stack.Screen name="(app)" />
<Stack.Screen name="+not-found" />
</Stack>
</HeroUINativeProvider>
</KeyboardProvider>
</QueryClientProvider>
</AppThemeProvider>
</GestureHandlerRootView>
);
}