- 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>
97 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|