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:
René Schober
2026-03-20 11:54:22 +01:00
parent 4e34270786
commit 9ddc7c6d7a
194 changed files with 55961 additions and 305 deletions

View File

@@ -1,34 +1,96 @@
import "@/global.css";
import { Stack } from "expo-router";
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";
export const unstable_settings = {
initialRouteName: "(drawer)",
};
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 StackLayout() {
return (
<Stack screenOptions={{}}>
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ title: "Modal", presentation: "modal" }} />
</Stack>
);
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 }}>
<KeyboardProvider>
<AppThemeProvider>
<HeroUINativeProvider>
<StackLayout />
</HeroUINativeProvider>
</AppThemeProvider>
</KeyboardProvider>
<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>
);
}