Health: consent gate (Art. 9 DSGVO) plus weight/vet-visit/vaccine log sections, gated behind health.getConsentStatus like the web app. Milestones: view a pet's milestone posts and create the 5 user-initiated types with an optional photo. Messaging: chat detail/compose screen (messages/[conversationId].tsx) polling while focused, reachable from the conversations list and from a pet profile's new "Nachricht" entry point. Push: usePushNotifications requests permission and registers the Expo push token with owner.registerPushToken; real delivery still needs an EAS project id (`eas init`), tracked in the README. All four verified live on a real Android emulator: health CRUD (add/list/ delete a weight log), Health/Milestones/Nachricht entry points gated correctly by own-pet vs. other-pet, and full chat history loading via an existing conversation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { ActivityIndicator, View } from "react-native";
|
|
import { Slot, useRouter, useSegments } from "expo-router";
|
|
import { ClerkProvider, useAuth } from "@clerk/expo";
|
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
|
import {
|
|
useFonts,
|
|
Inter_400Regular,
|
|
Inter_600SemiBold,
|
|
Inter_700Bold,
|
|
Inter_800ExtraBold,
|
|
} from "@expo-google-fonts/inter";
|
|
import { secureTokenCache } from "../src/lib/tokenCache";
|
|
import { env } from "../src/lib/env";
|
|
import { TRPCProvider, createMobileTRPCClient, makeQueryClient } from "../src/api/trpc";
|
|
import { queryPersister } from "../src/lib/storage";
|
|
import { ActivePetProvider } from "../src/context/ActivePetContext";
|
|
import { usePushNotifications } from "../src/hooks/usePushNotifications";
|
|
import { colors } from "../src/theme/tokens";
|
|
|
|
/**
|
|
* Redirects between the (auth) and (app) route groups based on Clerk's
|
|
* session state. Mirrors Clerk's official expo-router integration pattern —
|
|
* plain useAuth()/useSegments() rather than expo-router's typed route
|
|
* guards, since (auth)/(app) group membership isn't otherwise derivable.
|
|
*/
|
|
function useProtectedRoute() {
|
|
const { isLoaded, isSignedIn } = useAuth();
|
|
const segments = useSegments();
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
if (!isLoaded) return;
|
|
const inAuthGroup = segments[0] === "(auth)";
|
|
|
|
if (!isSignedIn && !inAuthGroup) {
|
|
router.replace("/(auth)/sign-in");
|
|
} else if (isSignedIn && inAuthGroup) {
|
|
router.replace("/(app)");
|
|
}
|
|
}, [isLoaded, isSignedIn, segments, router]);
|
|
}
|
|
|
|
function ApiProviders({ children }: { children: React.ReactNode }) {
|
|
const { getToken } = useAuth();
|
|
const [queryClient] = useState(() => makeQueryClient());
|
|
const trpcClient = useMemo(() => createMobileTRPCClient(() => getToken()), [getToken]);
|
|
|
|
return (
|
|
<PersistQueryClientProvider client={queryClient} persistOptions={{ persister: queryPersister }}>
|
|
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
|
|
<ActivePetProvider>
|
|
<PushNotificationRegistrar />
|
|
{children}
|
|
</ActivePetProvider>
|
|
</TRPCProvider>
|
|
</PersistQueryClientProvider>
|
|
);
|
|
}
|
|
|
|
/** No UI — just runs the push-token registration side effect once signed in. */
|
|
function PushNotificationRegistrar() {
|
|
usePushNotifications();
|
|
return null;
|
|
}
|
|
|
|
function LoadingScreen() {
|
|
return (
|
|
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: colors.porchWhite }}>
|
|
<ActivityIndicator color={colors.marigoldOrange} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function InitialLayout() {
|
|
const { isLoaded } = useAuth();
|
|
useProtectedRoute();
|
|
|
|
if (!isLoaded) {
|
|
return <LoadingScreen />;
|
|
}
|
|
|
|
return (
|
|
<ApiProviders>
|
|
<Slot />
|
|
</ApiProviders>
|
|
);
|
|
}
|
|
|
|
export default function RootLayout() {
|
|
const [fontsLoaded] = useFonts({
|
|
Inter_400Regular,
|
|
Inter_600SemiBold,
|
|
Inter_700Bold,
|
|
Inter_800ExtraBold,
|
|
});
|
|
|
|
if (!fontsLoaded) {
|
|
return <LoadingScreen />;
|
|
}
|
|
|
|
return (
|
|
<ClerkProvider publishableKey={env.clerkPublishableKey} tokenCache={secureTokenCache}>
|
|
<SafeAreaProvider>
|
|
<InitialLayout />
|
|
</SafeAreaProvider>
|
|
</ClerkProvider>
|
|
);
|
|
}
|