Created
September 5, 2025 09:50
-
-
Save MendyLanda/1f7377ebfec18c0e23e077aee9bba87e to your computer and use it in GitHub Desktop.
better-auth & expo, offline support
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useEffect, useState } from "react"; | |
| import * as Network from "expo-network"; | |
| export function useOnline() { | |
| const [isOnline, setIsOnline] = useState(true); | |
| useEffect(() => { | |
| const checkNetworkStatus = async () => { | |
| const networkState = await Network.getNetworkStateAsync(); | |
| setIsOnline(networkState.isConnected ?? false); | |
| }; | |
| // Check initial network status | |
| void checkNetworkStatus(); | |
| // Listen for network state changes | |
| const subscription = Network.addNetworkStateListener((state) => { | |
| setIsOnline(state.isConnected ?? false); | |
| }); | |
| return () => subscription.remove(); | |
| }, []); | |
| return isOnline; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useEffect } from "react"; | |
| import { router } from "expo-router"; | |
| import * as SecureStore from "expo-secure-store"; | |
| import { useQuery } from "@tanstack/react-query"; | |
| import { addDays, isAfter } from "date-fns"; | |
| import { authClient } from "~/utils/auth"; | |
| import { useOnline } from "./use-online"; | |
| const STORAGE_KEY = "__session"; | |
| const EXPIRY_DAYS = 7; | |
| type Session = ReturnType<typeof authClient.useSession>["data"]; | |
| /** | |
| * Only called when the session is updated | |
| */ | |
| const storeSession = async (session: Session) => { | |
| try { | |
| const localExpiry = addDays(new Date(), EXPIRY_DAYS).getTime(); | |
| await SecureStore.setItemAsync( | |
| STORAGE_KEY, | |
| JSON.stringify({ session, localExpiry }), | |
| ); | |
| return { session, localExpiry }; | |
| } catch (error) { | |
| console.error(error); | |
| return null; | |
| } | |
| }; | |
| const clearStoredSession = async () => { | |
| try { | |
| await SecureStore.deleteItemAsync(STORAGE_KEY); | |
| } catch (error) { | |
| console.error(error); | |
| } | |
| }; | |
| const getStoredSession = async () => { | |
| try { | |
| const session = await SecureStore.getItemAsync(STORAGE_KEY); | |
| const parsed = session | |
| ? (JSON.parse(session) as { session: Session; localExpiry: number }) | |
| : null; | |
| if (!parsed) return null; | |
| return { | |
| ...parsed, | |
| localExpired: isAfter(new Date(), new Date(parsed.localExpiry)), | |
| }; | |
| } catch (error) { | |
| console.error(error); | |
| return null; | |
| } | |
| }; | |
| export interface UseSessionResult { | |
| session: Session | null; | |
| isPending: boolean; | |
| error: Error | null; | |
| mode: "server" | "local"; | |
| signOut: () => Promise<void>; | |
| } | |
| export const useSession = (): UseSessionResult => { | |
| const isOnline = useOnline(); | |
| const { | |
| data: serverSession, | |
| isPending: isLoadingServerSession, | |
| error: serverSessionError, | |
| } = authClient.useSession(); | |
| const { | |
| data: storedSession = null, | |
| isLoading: isLoadingStoredSession, | |
| error: storedSessionError, | |
| } = useQuery({ | |
| queryKey: [STORAGE_KEY], | |
| queryFn: getStoredSession, | |
| }); | |
| const signOut = async () => { | |
| await authClient.signOut(); | |
| await clearStoredSession(); | |
| router.replace("/login"); | |
| }; | |
| // Update stored session when server session changes | |
| useEffect(() => { | |
| if (serverSession) { | |
| void storeSession(serverSession); | |
| } | |
| }, [serverSession]); | |
| const storedSessionExpired = storedSession?.localExpired ?? false; | |
| const hasValidStoredSession = storedSession && !storedSessionExpired; | |
| const serverNotReady = isLoadingServerSession || serverSessionError; | |
| const createStoredSessionResult = (): UseSessionResult => ({ | |
| session: storedSession?.session ?? null, | |
| isPending: isLoadingStoredSession, | |
| error: storedSessionError, | |
| mode: "local", | |
| signOut, | |
| }); | |
| const createServerSessionResult = (): UseSessionResult => ({ | |
| session: serverSession, | |
| isPending: false, | |
| error: serverSessionError, | |
| mode: "server", | |
| signOut, | |
| }); | |
| const createNullSessionResult = (): UseSessionResult => ({ | |
| session: null, | |
| isPending: false, | |
| error: null, | |
| mode: "local", | |
| signOut, | |
| }); | |
| // When online: prefer server session, but fallback to stored if server isn't ready and stored is valid | |
| if (isOnline) { | |
| if (serverNotReady && hasValidStoredSession) { | |
| return createStoredSessionResult(); | |
| } | |
| return createServerSessionResult(); | |
| } | |
| // When offline: use stored session if valid, otherwise return null | |
| if (hasValidStoredSession) { | |
| return createStoredSessionResult(); | |
| } | |
| return createNullSessionResult(); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment