Sync time entries with invoice lines and harden auth for mobile.

Link clocked time to invoice items with bidirectional sync, add entry editing on web, broaden session cookie detection for Expo clients, and handle API rate limits without signing users out.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-28 21:44:19 -04:00
co-authored by Cursor
parent c267b3e1c8
commit 31151e7f39
19 changed files with 849 additions and 111 deletions
+44 -34
View File
@@ -8,45 +8,55 @@ import { TRPCClientError } from "@trpc/client";
import { toast } from "sonner";
import SuperJSON from "superjson";
function isUnauthorized(error: unknown): boolean {
return (
error instanceof TRPCClientError &&
error.data != null &&
typeof error.data === "object" &&
"code" in error.data &&
(error.data as { code: string }).code === "UNAUTHORIZED"
);
}
function isRateLimited(error: unknown): boolean {
if (!(error instanceof TRPCClientError)) return false;
if (error.data != null && typeof error.data === "object" && "code" in error.data) {
if ((error.data as { code: string }).code === "TOO_MANY_REQUESTS") return true;
}
const message = error.message.toLowerCase();
return message.includes("too many") || message.includes("rate limit");
}
function handleQueryError(error: unknown) {
if (isRateLimited(error)) {
toast.error("Too many requests. Please wait a moment and try again.");
return;
}
if (isUnauthorized(error)) {
toast.error("Please sign in to continue");
if (typeof window !== "undefined") {
window.location.href = "/auth/signin";
}
}
}
export const createQueryClient = () =>
new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
if (
error instanceof TRPCClientError &&
error.data &&
typeof error.data === "object" &&
"code" in error.data &&
(error.data as { code: string }).code === "UNAUTHORIZED"
) {
toast.error("Please sign in to continue");
if (typeof window !== "undefined") {
window.location.href = "/auth/signin";
}
}
},
}),
mutationCache: new MutationCache({
onError: (error) => {
if (
error instanceof TRPCClientError &&
error.data &&
typeof error.data === "object" &&
"code" in error.data &&
(error.data as { code: string }).code === "UNAUTHORIZED"
) {
toast.error("Please sign in to continue");
if (typeof window !== "undefined") {
window.location.href = "/auth/signin";
}
}
},
}),
queryCache: new QueryCache({ onError: handleQueryError }),
mutationCache: new MutationCache({ onError: handleQueryError }),
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 30 * 1000,
retry: (failureCount, error) => {
if (isUnauthorized(error) || isRateLimited(error)) return false;
return failureCount < 1;
},
},
mutations: {
retry: (failureCount, error) => {
if (isRateLimited(error)) return false;
return failureCount < 1;
},
},
dehydrate: {
serializeData: SuperJSON.serialize,