193 lines
5.9 KiB
TypeScript
193 lines
5.9 KiB
TypeScript
import { ClerkProvider, ClerkLoaded } from "@clerk/clerk-expo";
|
|
import { Stack } from "expo-router";
|
|
import * as SecureStore from "expo-secure-store";
|
|
import { View, Text } from "react-native";
|
|
import { useEffect, useState } from "react";
|
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
import { validateEnv } from "../utils/env";
|
|
import { ThemeProvider } from "../contexts/ThemeContext";
|
|
import { StatisticsProvider } from "../contexts/StatisticsContext";
|
|
import { FitnessGoalsProvider } from "../contexts/FitnessGoalsContext";
|
|
import { RecommendationsProvider } from "../contexts/RecommendationsContext";
|
|
import { NotificationsProvider } from "../contexts/NotificationsContext";
|
|
import log from "../utils/logger";
|
|
|
|
// Wrapper to use notification permissions hook after ClerkLoaded
|
|
function AppContent() {
|
|
// Import here to avoid hook execution before Clerk is loaded
|
|
const {
|
|
useNotificationPermissions,
|
|
} = require("../hooks/useNotificationPermissions");
|
|
useNotificationPermissions();
|
|
|
|
return (
|
|
<Stack>
|
|
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
<Stack.Screen name="welcome" options={{ headerShown: false }} />
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
// Validate environment variables on app startup
|
|
try {
|
|
const env = validateEnv();
|
|
log.info("Environment validation passed", {
|
|
appName: env.EXPO_PUBLIC_APP_NAME,
|
|
apiUrl: env.EXPO_PUBLIC_API_URL,
|
|
});
|
|
} catch (error) {
|
|
log.error("Environment validation failed", error);
|
|
// In development, show the error but allow app to continue
|
|
// In production, this will crash the app (as it should)
|
|
if (!__DEV__) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Token cache for Clerk
|
|
const tokenCache = {
|
|
async getToken(key: string) {
|
|
try {
|
|
const value = await SecureStore.getItemAsync(key);
|
|
log.debug("Getting token from cache", {
|
|
key,
|
|
exists: !!value,
|
|
preview:
|
|
value && value.length > 50
|
|
? value.substring(0, 50) + "..."
|
|
: undefined,
|
|
});
|
|
return value;
|
|
} catch (err) {
|
|
log.error("Failed to get token from cache", err, { key });
|
|
return null;
|
|
}
|
|
},
|
|
async saveToken(key: string, value: string) {
|
|
try {
|
|
log.debug("Saving token to cache", {
|
|
key,
|
|
preview:
|
|
value && value.length > 50
|
|
? value.substring(0, 50) + "..."
|
|
: undefined,
|
|
});
|
|
return SecureStore.setItemAsync(key, value);
|
|
} catch (err) {
|
|
log.error("Failed to save token to cache", err, { key });
|
|
}
|
|
},
|
|
};
|
|
|
|
export default function RootLayout() {
|
|
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
|
const [cacheCleared, setCacheCleared] = useState(false);
|
|
|
|
log.debug("RootLayout rendering", {
|
|
hasPublishableKey: !!publishableKey,
|
|
});
|
|
|
|
// TEMPORARY: Clear Clerk cache on app start to fix instance mismatch
|
|
useEffect(() => {
|
|
log.debug("Starting cache cleanup");
|
|
|
|
const clearOldClerkCache = async () => {
|
|
try {
|
|
log.debug("Checking for old Clerk tokens");
|
|
|
|
const keysToCheck = [
|
|
"__clerk_client_jwt",
|
|
"__clerk_db_jwt",
|
|
"__clerk_client_uat",
|
|
"__clerk_session_id",
|
|
"__clerk_refresh_token",
|
|
"__clerk_session_jwt",
|
|
];
|
|
|
|
for (const key of keysToCheck) {
|
|
try {
|
|
const value = await SecureStore.getItemAsync(key);
|
|
if (value) {
|
|
log.debug("Found token", {
|
|
key,
|
|
preview: value.substring(0, 80) + "...",
|
|
});
|
|
|
|
// Check if it's from the old instance
|
|
if (value.includes("pleasing-pheasant-20")) {
|
|
log.debug("Deleting old token", { key });
|
|
await SecureStore.deleteItemAsync(key);
|
|
} else if (value.includes("needed-elephant-64")) {
|
|
log.debug("Token is from correct instance");
|
|
} else {
|
|
log.warn("Token doesn't match known instances", { key });
|
|
}
|
|
}
|
|
} catch (e) {
|
|
log.error("Error checking token key", e, { key });
|
|
}
|
|
}
|
|
|
|
log.info("Old token cleanup complete");
|
|
setCacheCleared(true);
|
|
} catch (error) {
|
|
log.error("Error clearing old cache", error);
|
|
setCacheCleared(true); // Continue anyway
|
|
}
|
|
};
|
|
|
|
clearOldClerkCache();
|
|
}, []);
|
|
|
|
if (!publishableKey) {
|
|
log.error("No Clerk publishable key found");
|
|
return (
|
|
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
|
<Text>Missing Clerk Publishable Key</Text>
|
|
<Text
|
|
style={{ marginTop: 8, textAlign: "center", paddingHorizontal: 20 }}
|
|
>
|
|
Please add EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to your .env file
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (!cacheCleared) {
|
|
log.debug("Waiting for cache to clear");
|
|
return (
|
|
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
|
<Text>Clearing old authentication cache...</Text>
|
|
<Text style={{ marginTop: 8, fontSize: 12, color: "#666" }}>
|
|
Check the terminal for logs...
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
log.debug("Rendering ClerkProvider", {
|
|
keyPreview: publishableKey?.substring(0, 20) + "...",
|
|
});
|
|
|
|
return (
|
|
<SafeAreaProvider>
|
|
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
|
|
<ClerkLoaded>
|
|
<ThemeProvider>
|
|
<NotificationsProvider>
|
|
<StatisticsProvider>
|
|
<FitnessGoalsProvider>
|
|
<RecommendationsProvider>
|
|
<AppContent />
|
|
</RecommendationsProvider>
|
|
</FitnessGoalsProvider>
|
|
</StatisticsProvider>
|
|
</NotificationsProvider>
|
|
</ThemeProvider>
|
|
</ClerkLoaded>
|
|
</ClerkProvider>
|
|
</SafeAreaProvider>
|
|
);
|
|
}
|