@daveyplate/better-auth-tanstack
Version:
Tanstack hooks for better-auth
367 lines (357 loc) • 12.3 kB
JavaScript
import { createContext, useContext, useMemo, useEffect, useCallback } from 'react';
import { jsx } from 'react/jsx-runtime';
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query';
import { skipToken } from '@tanstack/query-core';
// src/lib/auth-query-provider.tsx
var defaultAuthQueryOptions = {
sessionKey: ["session"],
tokenKey: ["token"],
listAccountsKey: ["list-accounts"],
listSessionsKey: ["list-sessions"],
listDeviceSessionsKey: ["list-device-sessions"],
listPasskeysKey: ["list-passkeys"],
optimistic: true,
refetchOnMutate: true
};
var AuthQueryContext = createContext(defaultAuthQueryOptions);
var AuthQueryProvider = ({
children,
sessionQueryOptions,
tokenQueryOptions,
...props
}) => {
return /* @__PURE__ */ jsx(
AuthQueryContext.Provider,
{
value: {
sessionQueryOptions: {
staleTime: 60 * 1e3,
...sessionQueryOptions
},
tokenQueryOptions: {
staleTime: 600 * 1e3,
...tokenQueryOptions
},
...defaultAuthQueryOptions,
...props
},
children
}
);
};
function useSession(authClient, options) {
var _a, _b;
const { sessionQueryOptions, sessionKey: queryKey, queryOptions } = useContext(AuthQueryContext);
const mergedOptions = { ...queryOptions, ...sessionQueryOptions, ...options };
const result = useQuery({
queryKey,
queryFn: () => authClient.getSession({ fetchOptions: { throw: true } }),
...mergedOptions
});
return {
...result,
session: (_a = result.data) == null ? void 0 : _a.session,
user: (_b = result.data) == null ? void 0 : _b.user
};
}
// src/hooks/shared/use-auth-query.ts
function useAuthQuery({
authClient,
queryKey,
queryFn,
options
}) {
const { data: sessionData } = useSession(authClient);
const { queryOptions } = useContext(AuthQueryContext);
const mergedOptions = { ...queryOptions, ...options };
return useQuery({
queryKey,
queryFn: sessionData ? () => queryFn({ fetchOptions: { throw: true } }) : skipToken,
...mergedOptions
});
}
// src/hooks/accounts/use-list-accounts.ts
function useListAccounts(authClient, options) {
const { listAccountsKey: queryKey } = useContext(AuthQueryContext);
return useAuthQuery({
authClient,
queryKey,
queryFn: authClient.listAccounts,
options
});
}
var useOnMutateError = () => {
const queryClient = useQueryClient();
const { optimistic } = useContext(AuthQueryContext);
const onMutateError = (error, queryKey, context) => {
var _a, _b;
if (error) {
console.error(error);
(_b = (_a = queryClient.getQueryCache().config).onError) == null ? void 0 : _b.call(_a, error, { queryKey });
}
if (!optimistic || !(context == null ? void 0 : context.previousData)) return;
queryClient.setQueryData(queryKey, context.previousData);
};
return { onMutateError };
};
// src/hooks/shared/use-auth-mutation.ts
function useAuthMutation({
queryKey,
mutationFn,
optimisticData,
options
}) {
const queryClient = useQueryClient();
const context = useContext(AuthQueryContext);
const { optimistic } = { ...context, ...options };
const { onMutateError } = useOnMutateError();
const mutation = useMutation({
mutationFn: ({ fetchOptions = { throw: true }, ...params }) => mutationFn({ fetchOptions, ...params }),
onMutate: async (params) => {
if (!optimistic || !optimisticData) return;
await queryClient.cancelQueries({ queryKey });
const previousData = queryClient.getQueryData(queryKey);
if (!previousData) return;
queryClient.setQueryData(queryKey, () => optimisticData(params, previousData));
return { previousData };
},
onError: (error2, _, context2) => onMutateError(error2, queryKey, context2),
onSettled: () => queryClient.invalidateQueries({ queryKey })
});
const { mutate, isPending, error } = mutation;
async function mutateAsync(params) {
return await mutation.mutateAsync(params);
}
return {
...mutation,
mutate,
mutateAsync,
isPending,
error
};
}
// src/hooks/accounts/use-unlink-account.ts
function useUnlinkAccount(authClient, options) {
const { listAccountsKey: queryKey } = useContext(AuthQueryContext);
return useAuthMutation({
queryKey,
mutationFn: authClient.unlinkAccount,
options
});
}
function useListDeviceSessions(authClient, options) {
const { listDeviceSessionsKey: queryKey } = useContext(AuthQueryContext);
return useAuthQuery({
authClient,
queryKey,
queryFn: authClient.multiSession.listDeviceSessions,
options
});
}
function useRevokeDeviceSession(authClient, options) {
const { listDeviceSessionsKey: queryKey } = useContext(AuthQueryContext);
return useAuthMutation({
queryKey,
mutationFn: authClient.multiSession.revoke,
options
});
}
function useSetActiveSession(authClient, options) {
const queryClient = useQueryClient();
const { onMutateError } = useOnMutateError();
const context = useContext(AuthQueryContext);
const { listDeviceSessionsKey: queryKey } = { ...context, ...options };
const mutation = useMutation({
mutationFn: ({ fetchOptions = { throw: true }, ...params }) => authClient.multiSession.setActive({ fetchOptions, ...params }),
onError: (error) => onMutateError(error, queryKey),
onSettled: () => queryClient.clear()
});
const {
mutate: setActiveSession,
mutateAsync: setActiveSessionAsync,
isPending: setActiveSessionPending,
error: setActiveSessionError
} = mutation;
return {
...mutation,
setActiveSession,
setActiveSessionAsync,
setActiveSessionPending,
setActiveSessionError
};
}
function useDeletePasskey(authClient, options) {
const { listPasskeysKey: queryKey } = useContext(AuthQueryContext);
return useAuthMutation({
queryKey,
mutationFn: authClient.passkey.deletePasskey,
options
});
}
function useListPasskeys(authClient, options) {
const { listPasskeysKey: queryKey } = useContext(AuthQueryContext);
return useAuthQuery({
authClient,
queryKey,
queryFn: authClient.passkey.listUserPasskeys,
options
});
}
function useUpdateUser(authClient, options) {
const { sessionKey: queryKey } = useContext(AuthQueryContext);
return useAuthMutation({
queryKey,
mutationFn: authClient.updateUser,
optimisticData: (params, previousSession) => ({
...previousSession,
user: { ...previousSession.user, ...params }
}),
options
});
}
function useListSessions(authClient, options) {
const { listSessionsKey: queryKey } = useContext(AuthQueryContext);
return useAuthQuery({ authClient, queryKey, queryFn: authClient.listSessions, options });
}
function useRevokeOtherSessions(authClient, options) {
const { listSessionsKey: queryKey } = useContext(AuthQueryContext);
const { data: sessionData } = useSession(authClient);
return useAuthMutation({
queryKey,
mutationFn: authClient.revokeOtherSessions,
options
});
}
function useRevokeSession(authClient, options) {
const { listSessionsKey: queryKey } = useContext(AuthQueryContext);
return useAuthMutation({
queryKey,
mutationFn: authClient.revokeSession,
options
});
}
function useRevokeSessions(authClient, options) {
const { listSessionsKey: queryKey } = useContext(AuthQueryContext);
return useAuthMutation({
queryKey,
mutationFn: authClient.revokeSessions,
options
});
}
var decodeJwt = (token) => {
const decode = (data) => {
if (typeof Buffer === "undefined") {
return atob(data);
}
return Buffer.from(data, "base64").toString();
};
const parts = token.split(".").map((part) => decode(part.replace(/-/g, "+").replace(/_/g, "/")));
return JSON.parse(parts[1]);
};
function useToken(authClient, options) {
const { data: sessionData } = useSession(authClient, options);
const { tokenKey, tokenQueryOptions, queryOptions } = useContext(AuthQueryContext);
const mergedOptions = { ...queryOptions, ...tokenQueryOptions, ...options };
const queryResult = useAuthQuery({
authClient,
queryKey: tokenKey,
queryFn: ({ fetchOptions }) => authClient.$fetch("/token", fetchOptions),
options: {
enabled: !!sessionData && (mergedOptions.enabled ?? true)
}
});
const { data, refetch, ...rest } = queryResult;
const payload = useMemo(() => data ? decodeJwt(data.token) : null, [data]);
useEffect(() => {
if (!(data == null ? void 0 : data.token)) return;
const payload2 = decodeJwt(data.token);
if (!(payload2 == null ? void 0 : payload2.exp)) return;
const expiresAt = payload2.exp * 1e3;
const expiresIn = expiresAt - Date.now();
const timeout = setTimeout(() => refetch(), expiresIn);
return () => clearTimeout(timeout);
}, [data, refetch]);
const isTokenExpired = useCallback(() => {
if (!(data == null ? void 0 : data.token)) return true;
const payload2 = decodeJwt(data.token);
if (!(payload2 == null ? void 0 : payload2.exp)) return true;
return payload2.exp < Date.now() / 1e3;
}, [data]);
useEffect(() => {
if (!sessionData) return;
if ((payload == null ? void 0 : payload.sub) !== sessionData.user.id) {
refetch();
}
}, [payload, sessionData, refetch]);
const tokenData = useMemo(
() => !sessionData || isTokenExpired() || (sessionData == null ? void 0 : sessionData.user.id) !== (payload == null ? void 0 : payload.sub) ? void 0 : data,
[sessionData, isTokenExpired, payload, data]
);
return { ...rest, data: tokenData, token: tokenData == null ? void 0 : tokenData.token, payload };
}
// src/lib/prefetch-session.ts
async function prefetchSession(authClient, queryClient, queryOptions, options) {
const { error, data } = await authClient.getSession();
const mergedOptions = {
...queryOptions == null ? void 0 : queryOptions.queryOptions,
...queryOptions == null ? void 0 : queryOptions.sessionQueryOptions,
...options
};
await queryClient.prefetchQuery({
...mergedOptions,
queryKey: queryOptions == null ? void 0 : queryOptions.sessionKey,
queryFn: () => data
});
return {
error,
data,
session: data == null ? void 0 : data.session,
user: data == null ? void 0 : data.user
};
}
// src/lib/create-auth-hooks.ts
function createAuthHooks(authClient) {
return {
useSession: (options) => useSession(authClient, options),
usePrefetchSession: (options) => {
const queryClient = useQueryClient();
const queryOptions = useContext(AuthQueryContext);
return {
prefetch: () => prefetchSession(authClient, queryClient, queryOptions, options)
};
},
useUpdateUser: (options) => useUpdateUser(authClient, options),
useToken: (options) => useToken(authClient, options),
useAuthQuery: ({
queryKey,
queryFn,
options
}) => useAuthQuery({ authClient, queryKey, queryFn, options }),
useListAccounts: (options) => useListAccounts(authClient, options),
useUnlinkAccount: () => useUnlinkAccount(authClient),
useListSessions: (options) => useListSessions(authClient, options),
useRevokeSession: (options) => useRevokeSession(authClient, options),
useRevokeSessions: (options) => useRevokeSessions(authClient, options),
useRevokeOtherSessions: (options) => useRevokeOtherSessions(authClient, options),
useListDeviceSessions: (options) => useListDeviceSessions(authClient, options),
useRevokeDeviceSession: (options) => useRevokeDeviceSession(authClient, options),
useSetActiveSession: (options) => useSetActiveSession(authClient, options),
useListPasskeys: (options) => useListPasskeys(authClient, options),
useDeletePasskey: (options) => useDeletePasskey(authClient, options),
useAuthMutation
};
}
// src/lib/create-auth-prefetches.ts
function createAuthPrefetches(authClient, queryOptions) {
return {
prefetchSession: (queryClient, options) => {
return prefetchSession(
authClient,
queryClient,
{ ...defaultAuthQueryOptions, ...queryOptions },
options
);
}
};
}
export { AuthQueryContext, AuthQueryProvider, createAuthHooks, createAuthPrefetches, defaultAuthQueryOptions, prefetchSession };