Zachoo
Zachoo4w ago

Logout after period of Inactivity Convex + Clerk - Expo APP

Still experiencing an issue with the user getting logged out. I've put all auth logic in a wrapper hook that I have around each auth route, as putting it in the root layout causes it to attempt navigation before routing is ready, which throws an error. Any help or if someone has an example of how to implement auth flow with onboarding check with convex & clerk.
40 Replies
Zachoo
ZachooOP4w ago
@erquhart - Have added new thread
erquhart
erquhart4w ago
Where in this logic is the user getting unexpectedly logged out
Zachoo
ZachooOP4w ago
I'm trying to figure this out—I think the issue stems from throwing an error in the Convex backend when the user isn't authenticated. It seems like queries are being executed before the user is authenticated, and since all backend functions are wrapped with an auth check, a large number of errors during initialization might be triggering a logout. If anyone has an example flow or proposed solution with onboarding logic id love to hear it ❤️
erquhart
erquhart4w ago
I'd definitely recommend using logs to track down what's happening leading up to the user getting logged out, helps clarify things I'm a neanderthal when it comes to this, but it works - I'd pepper console.log(0), console.log(1), etc. throughout this component, then check the logs. You can follow the logic, rerenders, figure out things that are running that you thought were being skipped, etc.
djbalin
djbalin4w ago
We've been experiencing huge issues with exactly this as well. We think, however, that the issue may be Expo SecureStore. We found through Sentry monitoring that a User interaction is not allowed error was being thrown often by the Apple Keychain (which is what Expo SecureStore etc. use on iOS to store passwords etc. securely on the iOS device). This appeared to be after a longer period (>few hours) of the app being in the background, and the user then changing e.g. charging state or orientation, which somehow triggered Clerk or Expo to try to re-fetch authentication state while the app was in the background, which was disallowed due to Keychain security constraints. This eventually resulted in a "Error: You are signed out" from Clerk, which we assume then caused users to lose their local authentication state. We also experience quite a lot of Convex errors stating that our users are not authenticated (we also use auth'ed wrappers), but think the error above is the original culprit. Do you have Sentry set up? Otherwise maybe try to do that and see if it helps - literally takes less than an hour. We ended up moving away from Expo SecureStore, as there were several open GitHub issues mentioning this exact same issue, but no Expo fix seems to have come yet. We now use https://www.npmjs.com/package/react-native-keychain, and the error has completely disappeared from Sentry, so we are waiting and hoping to hear less and less about this issue from our users 🤣 as it's pretty much impossible to replicate on our side.. @Zachoo
erquhart
erquhart4w ago
Zachoo's issue here is a chronic logout, like the user can't even get logged in and use the app without getting logged out. I don't think it's the same. For what you're describing @djbalin, I've been troubleshooting web and Expo/RN logout/unauth issues a lot - especially the ones related to backgrounded apps/tabs - over the past few months. The native app I test with uses Sentry as well. I have never seen an issue that could be traced back to SecureStore, so I suspect what you're seeing may be Clerk specific. There is potential for logout after coming back from the background due to a remaining race condition or two with the Convex client, I have a fix for it that I've been running successfully for like a month, but I hate how the fix is implemented. Need to find a palatable way to introduce it. But when that's out it should take care of logout issues not specifically stemming from Clerk. My test app also doesn't request permission to run in the background, so perhaps the SecureStore error isn't Clerk specific but only surfaces for apps that run in the background. I'll have to try that. Lol just checked Sentry for something unrelated and that User interaction is not allowed from getValueWithKeyAsync has been happening regularly for months. It's not actually causing any noticeable issues, logouts, or anything in the app, though. So I suspect it's the client race conditions that are actually causing your logouts, and it just all coincides with coming out of long background periods.
djbalin
djbalin4w ago
Haha yea exactly that trace has been our only clue @erquhart! I found a range of GitHub issues describing the same issue so we investigated this. Does that trace not lead to "Error: You are signed out" for you? The two events are separated by ~30 other events (http stuff) occuring between them, but it's a common pattern. See my screenshot That's what seemed like the smoking gun for us - this "you are signed out" error comes from Clerk as far as we can tell. We rationalized that the background getValueWithKeyAsync fails due to Apple Keychain stuff (also indicated in the trace), and that therefore our Clerk provider cannot confirm authentication, and the user gets logged out. Interestingly, now that we changed our implementation of the tokenCache passed to the ConvexClerk provider to just use react-native-keychain (and not Expo SecureStore), these errors have completely vanished from our Sentry logs. Have you been able to reproduce this users-getting-logged-out problem reliably on your end? We haven't, and given the nature of the delay necessary, it's been a headache to try to troubleshoot this.
No description
djbalin
djbalin3w ago
Ah interesting I just looked into background permissions that you mentioned - I wasn't aware of explicitly having set any, but I can see that our expo-video actually allows background playback of videos. Also noticed in Sentry that virtually all of the You are signed out error traces begin by the user playing a video and then, presumably, locking their device while still on that video. So expo-video keeps the app alive in the background, but then maybe our Expo SecureStore settings did not allow for this background check https://discord.com/channels/695411232856997968/1265984246309327001/1265984248217866322 This thread in the Expo discord also mentions this problem, where a solution was seemingly to relax the Apple Keychain access rules. We also implemented this in our switch from Expo SecureStore to react-native-keychain, and it's very likely that this has been what solved it for us
erquhart
erquhart3w ago
Ah okay, this is good info thanks for following up with details
Zachoo
ZachooOP3w ago
Just seen this sorry have been away huge thanks will check it out and update here So you are logged in and then after a period of inactivity sometimes several days or hours are logged out sounds similar to me but the logout is so unpredictable I will address sentry aswell
djbalin
djbalin3w ago
Let's keep each other updated on our findings about this pernicious thing :)) @Zachoo @erquhart
Zachoo
ZachooOP2w ago
I'm experiencing an authentication/routing issue with my React/Expo app using Convex. The primary issue is that users are getting logged out after leaving the app in the background for extended periods (e.g., 3 days). Steps to Reproduce Open the app while signed in Leave app in background for several days (or simulate by disabling WiFi, clearing from background, and reopening) Return to app Observed Behavior App shows loading/splash screen for ~30 seconds Incorrectly redirects to sign-in route Console error:
May 16, 09:47:49.256 failure 12ms QprojectFeatures:getProjectFeatures
Uncaught Error: Unauthenticated at handler (../convex/projectFeatures.tsx:45:7)
May 16, 09:47:49.256 failure 12ms QprojectFeatures:getProjectFeatures
Uncaught Error: Unauthenticated at handler (../convex/projectFeatures.tsx:45:7)
if i then re-enable wifi If I try to sign in, it says 'session already exists' -> returned from clerk If I clear app from background and reopen with WiFi on, it works fine, user is logged in perfectly fine My Guess I believe the issue is in the auth flow and network handling: When the app opens after being in background for days, the auth session with Convex has timed out My auth guard (useAuthNavigation hook) checks authentication status The Convex auth check fails or times out The guard redirects to sign-in route as it thinks the user is unauthenticated But the local Clerk session is still valid, causing the "session already exists" error Relevant Code
import { useConvexAuth, useQuery } from "convex/react";
import { usePathname } from "expo-router";
import { api } from "@packages/backend/convex/_generated/api";

// Define routes as constants
export const ROUTES = {
AUTH: {
SIGN_IN: "/sign-in",
SIGN_UP: "/sign-up",
},
ONBOARDING: "/onboarding",
APP: "/(app)",
};

export function useAuthNavigation() {
const { isAuthenticated, isLoading } = useConvexAuth();
const pathname = usePathname();

// Define auth exempted paths
const authExemptedPaths = [ROUTES.AUTH.SIGN_IN, ROUTES.AUTH.SIGN_UP];

// Current path state helpers
const isAuthPath = authExemptedPaths.includes(pathname);
const isOnboardingPath = pathname === ROUTES.ONBOARDING;

// Fetch current user data if authenticated and not in loading state
const authUser = useQuery(
api.users.currentUserState,
isAuthenticated || isLoading ? {} : "skip"
);

// Still loading, don't navigate yet
if (isLoading || (isAuthenticated && authUser === undefined)) {
return { isReady: false };
}

// Ready to determine navigation
const userAccess = {
isOnboarded: authUser?.isOnboarded ?? false,
};

// Route determination logic
let targetRoute = null;

// Priority 1: Authentication state
if (!isAuthenticated && !isAuthPath) {
targetRoute = ROUTES.AUTH.SIGN_IN;
}
// Priority 3: Handle authenticated users on auth pages
else if (isAuthPath && isAuthenticated) {
if (!userAccess.isOnboarded) {
targetRoute = ROUTES.ONBOARDING;
} else {
targetRoute = ROUTES.APP;
}
}
// Priority 4: Onboarding state
else if (
!isAuthPath &&
!isOnboardingPath &&
isAuthenticated &&
!userAccess.isOnboarded
) {
targetRoute = ROUTES.ONBOARDING;
}
// Priority 5: Already onboarded users on onboarding page
else if (isOnboardingPath && userAccess.isOnboarded) {
targetRoute = ROUTES.APP;
}

return {
isReady: true,
targetRoute,
userAccess,
isAuthenticated,
};
}
import { useConvexAuth, useQuery } from "convex/react";
import { usePathname } from "expo-router";
import { api } from "@packages/backend/convex/_generated/api";

// Define routes as constants
export const ROUTES = {
AUTH: {
SIGN_IN: "/sign-in",
SIGN_UP: "/sign-up",
},
ONBOARDING: "/onboarding",
APP: "/(app)",
};

export function useAuthNavigation() {
const { isAuthenticated, isLoading } = useConvexAuth();
const pathname = usePathname();

// Define auth exempted paths
const authExemptedPaths = [ROUTES.AUTH.SIGN_IN, ROUTES.AUTH.SIGN_UP];

// Current path state helpers
const isAuthPath = authExemptedPaths.includes(pathname);
const isOnboardingPath = pathname === ROUTES.ONBOARDING;

// Fetch current user data if authenticated and not in loading state
const authUser = useQuery(
api.users.currentUserState,
isAuthenticated || isLoading ? {} : "skip"
);

// Still loading, don't navigate yet
if (isLoading || (isAuthenticated && authUser === undefined)) {
return { isReady: false };
}

// Ready to determine navigation
const userAccess = {
isOnboarded: authUser?.isOnboarded ?? false,
};

// Route determination logic
let targetRoute = null;

// Priority 1: Authentication state
if (!isAuthenticated && !isAuthPath) {
targetRoute = ROUTES.AUTH.SIGN_IN;
}
// Priority 3: Handle authenticated users on auth pages
else if (isAuthPath && isAuthenticated) {
if (!userAccess.isOnboarded) {
targetRoute = ROUTES.ONBOARDING;
} else {
targetRoute = ROUTES.APP;
}
}
// Priority 4: Onboarding state
else if (
!isAuthPath &&
!isOnboardingPath &&
isAuthenticated &&
!userAccess.isOnboarded
) {
targetRoute = ROUTES.ONBOARDING;
}
// Priority 5: Already onboarded users on onboarding page
else if (isOnboardingPath && userAccess.isOnboarded) {
targetRoute = ROUTES.APP;
}

return {
isReady: true,
targetRoute,
userAccess,
isAuthenticated,
};
}
How should I modify my auth navigation logic (if needed) to handle these background timeout scenarios and prevent false redirects to the sign-in screen when there's a session mismatch between Clerk and Convex? @erquhart @djbalin or could it be bause i throw the error from convex ? @Pedro Martinez chat moved here could it be here in convex provider with clerk " function useAuthFromClerk() { const { isLoaded, isSignedIn, getToken, orgId, orgRole } = useAuth(); const fetchAccessToken = useCallback( async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => { try { return getToken({ template: "convex", skipCache: forceRefreshToken, }); } catch { return null; } }, // Build a new fetchAccessToken to trigger setAuth() whenever these change. // Anything else from the JWT Clerk wants to be reactive goes here too. // Clerk's Expo useAuth hook is not memoized so we don't include getToken. // eslint-disable-next-line react-hooks/exhaustive-deps [orgId, orgRole], );" when returning from background from a period of inactivity we may need a refresh ?
erquhart
erquhart2w ago
I would consider disabling wifi and extended background to be different cases. I honestly haven't looked much at what happens when you turn off data. And if you leave an app in the background for days in iOS without opening it, it's pretty much guaranteed to get shut down to reclaim memory. When you open an app that's been in the background that long, it's the same as opening it cold. Some of what you described sounds very specific to disabling wifi. Can you describe just what happens in the extended background scenario? Is it the same?
Zachoo
ZachooOP2w ago
that is just the best case i have found to replicate exact issue users are still reporting where they are logged in fine they use app close it come back a couple days later and it shows plash screen and then straight to /sign-in route when they login it says session allready exisits which makes sense as clerk should keep for 2 years as per my config. user cannont or is not routed away from this /signin route until they clear app from app draw half swipe up from bottom and clear app. upon re opening they are logged in fine again without ever needing to reenter password is quite an infuriating but as it leads to negative reviews for our software with no clear understanding of whats happening no clear sentryb logs eithier just seems there is a mismatch between the state of clerk and the state of convex upon re opening but i would assume this is handle by convex clerk provider
erquhart
erquhart2w ago
I just have not seen a condition like this that occurs when the app is closed out and reopens. If you see a splash screen, everything is starting from scratch. If there's a token in storage, Convex will still force refresh it initially, no matter what. I obviously can't say for certain, but this really sounds like a routing logic bug. Have you been able to reproduce this in your own use of the app organically?
Zachoo
ZachooOP2w ago
no i havent been aple to replicate but have seen it happen to a user in front of me i shared routing logic above does it look okay ?
erquhart
erquhart2w ago
Hard to say, other things may end up influencing it beyond just that hook. Within the hook, though, things are hard to reason about due to the waterfall of variable reassignment. If I may, I suggest returning early for each route to make things more clear.
Zachoo
ZachooOP2w ago
is there any example code docs about for it with an onboarding flow ?
erquhart
erquhart2w ago
It just needs to be more clear. If you return for each case you get a lot of clarity. Eg.,:
export function useAuthNavigation() {
const { isAuthenticated, isLoading } = useConvexAuth();
const pathname = usePathname();

// Define auth exempted paths
const authExemptedPaths = [ROUTES.AUTH.SIGN_IN, ROUTES.AUTH.SIGN_UP];

// Current path state helpers
const isAuthPath = authExemptedPaths.includes(pathname);
const isOnboardingPath = pathname === ROUTES.ONBOARDING;

// Fetch current user data if authenticated and not in loading state
const authUser = useQuery(
api.users.currentUserState,
isAuthenticated || isLoading ? {} : "skip"
);

// Still loading, don't navigate yet
if (isLoading || (isAuthenticated && authUser === undefined)) {
return { isReady: false };
}

// Ready to determine navigation
const userAccess = {
isOnboarded: authUser?.isOnboarded ?? false,
};

const getRouteState = (path: string) => ({
isReady: true,
targetRoute: path,
userAccess,
isAuthenticated,
})

if (isOnboardingPath && userAccess.isOnboarded) {
return getRouteState(ROUTES.APP);
}

// Priority 1: Authentication state
if (!isAuthenticated && !isAuthPath) {
return getRouteState(ROUTES.AUTH.SIGN_IN);
}

// Priority 3: Handle authenticated users on auth pages
if (isAuthPath && isAuthenticated) {
if (!userAccess.isOnboarded) {
return getRouteState(ROUTES.ONBOARDING);
}
return getRouteState(ROUTES.APP);
}
// Priority 4: Onboarding state
if (
!isAuthPath &&
!isOnboardingPath &&
isAuthenticated &&
!userAccess.isOnboarded
) {
return getRouteState(ROUTES.ONBOARDING);
}
// Priority 5: Already onboarded users on onboarding page
if (isOnboardingPath && userAccess.isOnboarded) {
return getRouteState(ROUTES.APP);
}
}
export function useAuthNavigation() {
const { isAuthenticated, isLoading } = useConvexAuth();
const pathname = usePathname();

// Define auth exempted paths
const authExemptedPaths = [ROUTES.AUTH.SIGN_IN, ROUTES.AUTH.SIGN_UP];

// Current path state helpers
const isAuthPath = authExemptedPaths.includes(pathname);
const isOnboardingPath = pathname === ROUTES.ONBOARDING;

// Fetch current user data if authenticated and not in loading state
const authUser = useQuery(
api.users.currentUserState,
isAuthenticated || isLoading ? {} : "skip"
);

// Still loading, don't navigate yet
if (isLoading || (isAuthenticated && authUser === undefined)) {
return { isReady: false };
}

// Ready to determine navigation
const userAccess = {
isOnboarded: authUser?.isOnboarded ?? false,
};

const getRouteState = (path: string) => ({
isReady: true,
targetRoute: path,
userAccess,
isAuthenticated,
})

if (isOnboardingPath && userAccess.isOnboarded) {
return getRouteState(ROUTES.APP);
}

// Priority 1: Authentication state
if (!isAuthenticated && !isAuthPath) {
return getRouteState(ROUTES.AUTH.SIGN_IN);
}

// Priority 3: Handle authenticated users on auth pages
if (isAuthPath && isAuthenticated) {
if (!userAccess.isOnboarded) {
return getRouteState(ROUTES.ONBOARDING);
}
return getRouteState(ROUTES.APP);
}
// Priority 4: Onboarding state
if (
!isAuthPath &&
!isOnboardingPath &&
isAuthenticated &&
!userAccess.isOnboarded
) {
return getRouteState(ROUTES.ONBOARDING);
}
// Priority 5: Already onboarded users on onboarding page
if (isOnboardingPath && userAccess.isOnboarded) {
return getRouteState(ROUTES.APP);
}
}
You're troubleshooting users being on the sign in page when they don't belong there. This way the other conditions after authentication state don't have an opportunity to overwrite the target route, so it's easier to reason about. This is just an example, I didn't pay enough attention to your logic to make sure nothing was depending on the other approach. But hopefully this demonstrates the value of returning early and treating variables immutably wherever possible.
Zachoo
ZachooOP2w ago
Have made more readable and still have same error root layout
import { ClerkProvider, useAuth } from "@clerk/clerk-expo";
import { Slot } from "expo-router";
import "../global.css";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import {
ConvexReactClient,
} from "convex/react";
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { ConvexQueryCacheProvider } from "convex-helpers/react/cache";
import { tokenCache } from "cache";

// Initialize Convex client
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, {
verbose: true,
});

export default function RootLayout() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!;

return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ConvexProviderWithClerk useAuth={useAuth} client={convex}>
<ConvexQueryCacheProvider>
<BottomSheetModalProvider>
<Slot />
</BottomSheetModalProvider>
</ConvexQueryCacheProvider>
</ConvexProviderWithClerk>
</ClerkProvider>
</GestureHandlerRootView>
);
}
import { ClerkProvider, useAuth } from "@clerk/clerk-expo";
import { Slot } from "expo-router";
import "../global.css";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import {
ConvexReactClient,
} from "convex/react";
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { ConvexQueryCacheProvider } from "convex-helpers/react/cache";
import { tokenCache } from "cache";

// Initialize Convex client
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, {
verbose: true,
});

export default function RootLayout() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!;

return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ConvexProviderWithClerk useAuth={useAuth} client={convex}>
<ConvexQueryCacheProvider>
<BottomSheetModalProvider>
<Slot />
</BottomSheetModalProvider>
</ConvexQueryCacheProvider>
</ConvexProviderWithClerk>
</ClerkProvider>
</GestureHandlerRootView>
);
}
.tsx
import { Stack } from "expo-router/stack";
import { View, Platform } from "react-native";
import { Navbar } from "./_components/navbar";
import useNotifications from "providers/notification-provider";
import { useQuery } from "convex/react";
import LoadingScreen from "components/shared/loader";
// import { useAuthNavigation } from "app/authProvider";
import { useConvexAuth } from "convex/react";
import { api } from "@packages/backend/convex/_generated/api";
import { Redirect } from "expo-router";

export default function AppLayout() {
// Initialize notifications if user is authenticated and we have user data
const { permissionGranted } = useNotifications();

const { isAuthenticated, isLoading } = useConvexAuth();
const currentUser = useQuery(api.users.currentUserState);

if (isLoading) {
return <LoadingScreen text="Fetching user data ..." />;
}

if (!isAuthenticated) {
return <Redirect href="/sign-in" />;
}

if (currentUser && !currentUser.isOnboarded) {
return <Redirect href="/onboarding" />;
}

return (
<View
className="flex-1 bg-background"
style={{
paddingTop: Platform.OS === "android" ? 25 : 0,
paddingBottom: Platform.OS === "android" ? 35 : 0,
}}
>
<Stack
screenOptions={{
headerShown: false,
animation: "fade",
contentStyle: { backgroundColor: "transparent" },
}}
/>
<Navbar />
</View>
);
}
.tsx
import { Stack } from "expo-router/stack";
import { View, Platform } from "react-native";
import { Navbar } from "./_components/navbar";
import useNotifications from "providers/notification-provider";
import { useQuery } from "convex/react";
import LoadingScreen from "components/shared/loader";
// import { useAuthNavigation } from "app/authProvider";
import { useConvexAuth } from "convex/react";
import { api } from "@packages/backend/convex/_generated/api";
import { Redirect } from "expo-router";

export default function AppLayout() {
// Initialize notifications if user is authenticated and we have user data
const { permissionGranted } = useNotifications();

const { isAuthenticated, isLoading } = useConvexAuth();
const currentUser = useQuery(api.users.currentUserState);

if (isLoading) {
return <LoadingScreen text="Fetching user data ..." />;
}

if (!isAuthenticated) {
return <Redirect href="/sign-in" />;
}

if (currentUser && !currentUser.isOnboarded) {
return <Redirect href="/onboarding" />;
}

return (
<View
className="flex-1 bg-background"
style={{
paddingTop: Platform.OS === "android" ? 25 : 0,
paddingBottom: Platform.OS === "android" ? 35 : 0,
}}
>
<Stack
screenOptions={{
headerShown: false,
animation: "fade",
contentStyle: { backgroundColor: "transparent" },
}}
/>
<Navbar />
</View>
);
}
if for any reason the user is redirected to sign in they are not redirected from there unless the app is cleared.
Pedro Martinez
@Zachoo i had that issue as well, didn't get redirect from the AppLayout after the user was logged in, so in my SignInScreen added a useEffect listening to isAuthenticated from useConvexAuth hook. Something like this: export default function SignInScreen() { const router = useRouter(); const { isAuthenticated, isLoading: isAuthenticating } = useConvexAuth(); useEffect(() => { if (isAuthenticated) { router.push('/'); } }, [isAuthenticated]); .....
Zachoo
ZachooOP5d ago
hmm i have the same logic but in my _layout file for sign in route did you have same in my mind the logic is the same ???
// import { useAuthNavigation } from "app/authProvider";
import LoadingScreen from "components/shared/loader";
import { useConvexAuth } from "convex/react";
import { Redirect, Stack } from "expo-router";
import { View, Platform } from "react-native";
import { useNetInfo } from "@react-native-community/netinfo";

export default function AuthLayout() {
const { isAuthenticated, isLoading } = useConvexAuth();
const netInfo = useNetInfo();

if (isLoading) {
return <LoadingScreen text="Fetching user data ..." />;
}

if (isAuthenticated) {
return <Redirect href="/(app)/(home)" />;
}

const isOffline = !netInfo.isConnected;

if (isOffline) {
return (
<View className="flex-1 items-center justify-center bg-background p-10">
<LoadingScreen text=" 🛠️ No internet connection. Please check your network settings." />
</View>
);
}

return (
<View
className="flex-1 bg-background"
style={{
paddingTop: Platform.OS === "android" ? 25 : 0,
paddingBottom: Platform.OS === "android" ? 35 : 0,
}}
>
<Stack
screenOptions={{
headerShown: false,
animation: "fade",
contentStyle: { backgroundColor: "transparent" },
}}
/>
</View>
);
}
// import { useAuthNavigation } from "app/authProvider";
import LoadingScreen from "components/shared/loader";
import { useConvexAuth } from "convex/react";
import { Redirect, Stack } from "expo-router";
import { View, Platform } from "react-native";
import { useNetInfo } from "@react-native-community/netinfo";

export default function AuthLayout() {
const { isAuthenticated, isLoading } = useConvexAuth();
const netInfo = useNetInfo();

if (isLoading) {
return <LoadingScreen text="Fetching user data ..." />;
}

if (isAuthenticated) {
return <Redirect href="/(app)/(home)" />;
}

const isOffline = !netInfo.isConnected;

if (isOffline) {
return (
<View className="flex-1 items-center justify-center bg-background p-10">
<LoadingScreen text=" 🛠️ No internet connection. Please check your network settings." />
</View>
);
}

return (
<View
className="flex-1 bg-background"
style={{
paddingTop: Platform.OS === "android" ? 25 : 0,
paddingBottom: Platform.OS === "android" ? 35 : 0,
}}
>
<Stack
screenOptions={{
headerShown: false,
animation: "fade",
contentStyle: { backgroundColor: "transparent" },
}}
/>
</View>
);
}
@Pedro Martinez as sign in screen will be wrapped by auth layout i also am redirected every time fine except for if app is in background for several days and then reopened the user is forced to clear app from background to re-sync with convex is this issue you had and did use effect code resolve ?? added use effect still have same issue with app returning from background after an extended period of inactivity.
erquhart
erquhart4d ago
When you say return from background, it's also happening if in iOS the user swipes up to close the app and then opens it, right?
Zachoo
ZachooOP4d ago
Yes as long as the app has not been cleared from background and has been sitting there for an extended period @erquhart
erquhart
erquhart4d ago
If they clear it from the background and then open it, everything works?
Zachoo
ZachooOP4d ago
Yes correct @erquhart I have shared a video of exactly what user sees sometimes if they leave app in background for a while and then reopen in pm If a user opens the app and then leaves it in the background for a day or two—timing which seems random and possibly related to iOS unloading apps to save memory (not a full clear)—then reopens it, they are shown the sign-in screen. However, the logs show that Clerk is still authenticated (it fetches the token from storage), but Convex appears to treat the user as unauthenticated. Happy to temporarily add you to GitHub so you can see structure ?
Pedro Martinez
@Zachoo I had the same issue with users being logged out after returning from the background (at random intervals), and I made two changes that have been working so far: * Switched from expo-secure-store to react-native-keychain. * In my _layout, I was originally using Clerk for auth with const { isSignedIn, isLoaded } = useAuth();. I then switched to using useConvexAuth, but that introduced a new issue: after logging in, users couldn't navigate to the app's home screen. To fix that, I added a redirect on the sign-in screen, and that made it work. I noticed your _layout is slightly different from mine. Here's what I currently have:
export default function DefaultLayout() { const { isAuthenticated, isLoading } = useConvexAuth(); // Get the tint color for the current color scheme const tintColor = Colors.primary; if (isLoading) { return <Loader color={Colors.border} />; } if (!isAuthenticated) { return <Redirect href="/sign-in" />; } return ( <Stack screenOptions={{ headerTitle: '', headerBackVisible: false, headerShadowVisible: false, headerTintColor: tintColor, }} > <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="request" options={{ headerShown: false }} /> </Stack> ); }
Zachoo
ZachooOP4d ago
Hmm maybe different issue I have no problem navigating user from sign in screen it is just that when app has been in background for say 3 days and then reopened user is show /sign in even tho there is an active clerk session only way to get back in is to entirely clearing app from background and reopening user is redirect and signed in fine
erquhart
erquhart4d ago
Can you add logs from your sign in page/component to print the authenticated state provided by both Convex and Clerk
Zachoo
ZachooOP4d ago
Sure but I don’t know how to replicate to get console logs because it happens randomly can only get into the same state by disabling wifi @erquhart nothing shows in sentry but i am on the home sign in route and when i click sign in it says session exists ?
erquhart
erquhart3d ago
Assuming you can repro this locally, have your component render the authenticated state info from Convex and Clerk. It's going to take this sort of digging to understand what's happening - testing assumptions, printing things on the screen (since you don't have a browser console to log to). Determining what the actual state is when the authenticated user is on the sign in screen is a good start.
Zachoo
ZachooOP3d ago
The yeah I can show the sentry logs however they don’t trigger on the home screen or n this case but I am still not redirected so it is difficult to say what issue is. I have exact implementation as docs and sentry should trigger if there is a state mismatch which has been tested however the n this case it does not enter that error trigger but I am still not authentic which makes no sense as else I would be redirected. This is the same logic that executes when you clear app from background. The issue only appears after booting with wifi disabled waiting 30s then enabling wifi else leaving app in background for say 48h
erquhart
erquhart3d ago
Right, don't use Sentry for what I'm talking about here. Instead, in your sign in component or page component, whichever component has authentication state for Convex and Clerk, render those state values right in your component locally so you can view them on your iOS device when you're reproducing this bug. then you can look directly at the state. Does that make sense? But also, you can use console.log and look at your expo cli to output them for you, or in the debugger console (hit "j" while the expo cli is running)
Zachoo
ZachooOP3d ago
This is strange session exists but clerk use auth hook returns false
Zachoo
ZachooOP3d ago
No description
Zachoo
ZachooOP3d ago
@erquhart this is what im shown but if i clear app from multi tasking UI and login everything is fine ...
import {
View,
SafeAreaView,
Image,
KeyboardAvoidingView,
Platform,
ScrollView,
ImageBackground,
} from "react-native";
import React, { useEffect } from "react";
import SignInForm from "../../forms/authentication/sign-in-form";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import SimpleAuthForm from "forms/authentication/unified-auth-form";
import { useConvexAuth } from "convex/react";
import { Redirect, useRouter } from "expo-router";
import { useAuth } from "@clerk/clerk-expo";
import { Text } from "react-native";

// import UnifiedAuthForm from "forms/authentication/unified-auth-form";

export default function Page() {
const backgroundImage = require("../../assets/background.png");
const dividerImage = require("../../assets/divider.png");

const router = useRouter();
const { isAuthenticated, isLoading: isAuthenticating } = useConvexAuth();
const { isLoaded, isSignedIn, userId, sessionId, getToken } = useAuth();

useEffect(() => {
console.log("isAuthenticated", isAuthenticated);
console.log("isSignedIn", isSignedIn);
if (isAuthenticated) {
router.push("/");
}
}, [isAuthenticated, isSignedIn]);

return (
<KeyboardAwareScrollView
contentContainerStyle={{ flexGrow: 1 }}
bounces={false}
>
<View className="flex-1 pb-10 bg-white">
<ImageBackground
source={backgroundImage}
className="absolute w-full h-full"
resizeMode="cover"
/>
<SafeAreaView className="flex-1 justify-end">
<Image
source={dividerImage}
className="w-full"
style={{ width: "100%" }}
/>
<View className="justify-center items-center bg-white">
<Text>is convex authenticated: {isAuthenticated.toString()} </Text>
<Text>is clerk signed in: {isSignedIn?.toString()} </Text>
<Text>is clerk loaded: {isLoaded.toString()} </Text>
<Text>is convex auth loading: {isAuthenticated.toString()} </Text>
<Text>user id: {userId} </Text>
<Text>session id: {sessionId} </Text>
</View>
<SimpleAuthForm />
{/* <SignInForm /> */}
{/* <UnifiedAuthForm /> */}
</SafeAreaView>
</View>
</KeyboardAwareScrollView>
);
}
import {
View,
SafeAreaView,
Image,
KeyboardAvoidingView,
Platform,
ScrollView,
ImageBackground,
} from "react-native";
import React, { useEffect } from "react";
import SignInForm from "../../forms/authentication/sign-in-form";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import SimpleAuthForm from "forms/authentication/unified-auth-form";
import { useConvexAuth } from "convex/react";
import { Redirect, useRouter } from "expo-router";
import { useAuth } from "@clerk/clerk-expo";
import { Text } from "react-native";

// import UnifiedAuthForm from "forms/authentication/unified-auth-form";

export default function Page() {
const backgroundImage = require("../../assets/background.png");
const dividerImage = require("../../assets/divider.png");

const router = useRouter();
const { isAuthenticated, isLoading: isAuthenticating } = useConvexAuth();
const { isLoaded, isSignedIn, userId, sessionId, getToken } = useAuth();

useEffect(() => {
console.log("isAuthenticated", isAuthenticated);
console.log("isSignedIn", isSignedIn);
if (isAuthenticated) {
router.push("/");
}
}, [isAuthenticated, isSignedIn]);

return (
<KeyboardAwareScrollView
contentContainerStyle={{ flexGrow: 1 }}
bounces={false}
>
<View className="flex-1 pb-10 bg-white">
<ImageBackground
source={backgroundImage}
className="absolute w-full h-full"
resizeMode="cover"
/>
<SafeAreaView className="flex-1 justify-end">
<Image
source={dividerImage}
className="w-full"
style={{ width: "100%" }}
/>
<View className="justify-center items-center bg-white">
<Text>is convex authenticated: {isAuthenticated.toString()} </Text>
<Text>is clerk signed in: {isSignedIn?.toString()} </Text>
<Text>is clerk loaded: {isLoaded.toString()} </Text>
<Text>is convex auth loading: {isAuthenticated.toString()} </Text>
<Text>user id: {userId} </Text>
<Text>session id: {sessionId} </Text>
</View>
<SimpleAuthForm />
{/* <SignInForm /> */}
{/* <UnifiedAuthForm /> */}
</SafeAreaView>
</View>
</KeyboardAwareScrollView>
);
}
erquhart
erquhart3d ago
What are user id and session id when the bug happens (I mean defined or undefined)
Zachoo
ZachooOP2d ago
undefined
Zachoo
ZachooOP2d ago
No description
erquhart
erquhart2d ago
I see you have verbose logging on, can you share the cli logs that are being output when you reproduce this issue You can also add a telemetry prop to ClerkProvider that may provide some additional insight in the logs telemetry={{ debug: true, disabled: false }} I want to keep the troubleshooting conversation here so it benefits others, but if you want to share logs via dm that's fine

Did you find this page helpful?