Skip to content

Instantly share code, notes, and snippets.

@MendyLanda
Created September 5, 2025 09:50
Show Gist options
  • Save MendyLanda/1f7377ebfec18c0e23e077aee9bba87e to your computer and use it in GitHub Desktop.
Save MendyLanda/1f7377ebfec18c0e23e077aee9bba87e to your computer and use it in GitHub Desktop.
better-auth & expo, offline support
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;
}
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