|
| 1 | +import { useQuery } from '@tanstack/react-query' |
| 2 | + |
| 3 | +import { getAuthToken } from '../utils/auth' |
| 4 | +import { logger as defaultLogger } from '../utils/logger' |
| 5 | + |
| 6 | +import type { Logger } from '@codebuff/common/types/contracts/logger' |
| 7 | + |
| 8 | +// Valid fields that can be fetched from /api/v1/me |
| 9 | +export type UserField = |
| 10 | + | 'id' |
| 11 | + | 'email' |
| 12 | + | 'discord_id' |
| 13 | + | 'referral_code' |
| 14 | + | 'referral_link' |
| 15 | + |
| 16 | +// Query keys for type-safe cache management |
| 17 | +export const userDetailsQueryKeys = { |
| 18 | + all: ['userDetails'] as const, |
| 19 | + fields: (fields: readonly UserField[]) => |
| 20 | + [...userDetailsQueryKeys.all, ...fields] as const, |
| 21 | +} |
| 22 | + |
| 23 | +export type UserDetails<T extends UserField> = { |
| 24 | + [K in T]: K extends 'discord_id' | 'referral_code' | 'referral_link' |
| 25 | + ? string | null |
| 26 | + : string |
| 27 | +} |
| 28 | + |
| 29 | +interface FetchUserDetailsParams<T extends UserField> { |
| 30 | + authToken: string |
| 31 | + fields: readonly T[] |
| 32 | + logger?: Logger |
| 33 | +} |
| 34 | + |
| 35 | +/** |
| 36 | + * Fetches specific user details from the /api/v1/me endpoint |
| 37 | + */ |
| 38 | +export async function fetchUserDetails<T extends UserField>({ |
| 39 | + authToken, |
| 40 | + fields, |
| 41 | + logger = defaultLogger, |
| 42 | +}: FetchUserDetailsParams<T>): Promise<UserDetails<T> | null> { |
| 43 | + const appUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL |
| 44 | + if (!appUrl) { |
| 45 | + throw new Error('NEXT_PUBLIC_CODEBUFF_APP_URL is not set') |
| 46 | + } |
| 47 | + |
| 48 | + const fieldsParam = fields.join(',') |
| 49 | + const response = await fetch(`${appUrl}/api/v1/me?fields=${fieldsParam}`, { |
| 50 | + method: 'GET', |
| 51 | + headers: { |
| 52 | + 'Content-Type': 'application/json', |
| 53 | + Authorization: `Bearer ${authToken}`, |
| 54 | + }, |
| 55 | + }) |
| 56 | + |
| 57 | + if (!response.ok) { |
| 58 | + logger.error( |
| 59 | + { status: response.status, fields }, |
| 60 | + 'Failed to fetch user details from /api/v1/me', |
| 61 | + ) |
| 62 | + return null |
| 63 | + } |
| 64 | + |
| 65 | + const data = (await response.json()) as UserDetails<T> |
| 66 | + return data |
| 67 | +} |
| 68 | + |
| 69 | +export interface UseUserDetailsQueryDeps<T extends UserField> { |
| 70 | + fields: readonly T[] |
| 71 | + logger?: Logger |
| 72 | + enabled?: boolean |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * Hook to fetch specific user details |
| 77 | + */ |
| 78 | +export function useUserDetailsQuery<T extends UserField>({ |
| 79 | + fields, |
| 80 | + logger = defaultLogger, |
| 81 | + enabled = true, |
| 82 | +}: UseUserDetailsQueryDeps<T>) { |
| 83 | + const authToken = getAuthToken() |
| 84 | + |
| 85 | + return useQuery({ |
| 86 | + queryKey: userDetailsQueryKeys.fields(fields), |
| 87 | + queryFn: () => fetchUserDetails({ authToken: authToken!, fields, logger }), |
| 88 | + enabled: enabled && !!authToken, |
| 89 | + staleTime: 5 * 60 * 1000, // 5 minutes |
| 90 | + gcTime: 30 * 60 * 1000, // 30 minutes |
| 91 | + retry: false, |
| 92 | + refetchOnMount: false, |
| 93 | + refetchOnWindowFocus: false, |
| 94 | + refetchOnReconnect: false, |
| 95 | + }) |
| 96 | +} |
0 commit comments