diff --git a/apps/mobile/src/api/client.ts b/apps/mobile/src/api/client.ts index 35ac7f9..00c1a80 100644 --- a/apps/mobile/src/api/client.ts +++ b/apps/mobile/src/api/client.ts @@ -1,18 +1,52 @@ -import axios from 'axios'; -import { API_BASE_URL } from '../config/api'; +import axios, { type AxiosRequestConfig } from "axios"; +import { API_BASE_URL } from "../config/api"; +import log from "../utils/logger"; export const apiClient = axios.create({ - baseURL: API_BASE_URL, - headers: { - 'Content-Type': 'application/json', - }, + baseURL: API_BASE_URL, + timeout: 15000, + headers: { + "Content-Type": "application/json", + }, }); +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + if (status === 401) { + log.warn("API unauthorized response", { url: error.config?.url }); + } else if (status === 403) { + log.warn("API forbidden response", { url: error.config?.url }); + } else if (status && status >= 500) { + log.error("API server error", error, { + status, + url: error.config?.url, + }); + } + } + return Promise.reject(error); + }, +); + +export function withAuth(token?: string | null): AxiosRequestConfig { + if (!token) { + return {}; + } + + return { + headers: { + Authorization: `Bearer ${token}`, + }, + }; +} + // Helper to set the auth token for a request export const setAuthToken = (token: string) => { - if (token) { - apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`; - } else { - delete apiClient.defaults.headers.common['Authorization']; - } + if (token) { + apiClient.defaults.headers.common.Authorization = `Bearer ${token}`; + } else { + delete apiClient.defaults.headers.common.Authorization; + } }; diff --git a/apps/mobile/src/api/gyms.ts b/apps/mobile/src/api/gyms.ts new file mode 100644 index 0000000..3988597 --- /dev/null +++ b/apps/mobile/src/api/gyms.ts @@ -0,0 +1,56 @@ +import { isAxiosError } from "axios"; +import { API_ENDPOINTS } from "../config/api"; +import { apiClient, withAuth } from "./client"; + +export interface Gym { + id: string; + name: string; + location?: string; +} + +export const gymsApi = { + getGyms: async (token: string | null): Promise => { + try { + const response = await apiClient.get( + API_ENDPOINTS.GYMS, + withAuth(token), + ); + + const payload = response.data; + if (Array.isArray(payload)) { + return payload; + } + + if (payload && Array.isArray(payload.data)) { + return payload.data; + } + + return []; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error(`Failed to fetch gyms: ${error.response.status}`); + } + throw new Error("Failed to fetch gyms"); + } + }, + + updateUserGym: async ( + gymId: string | null, + token: string | null, + ): Promise => { + try { + await apiClient.patch( + API_ENDPOINTS.USERS.GYM, + { gymId }, + withAuth(token), + ); + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error( + `Failed to update gym selection: ${error.response.status}`, + ); + } + throw new Error("Failed to update gym selection"); + } + }, +}; diff --git a/apps/mobile/src/api/index.ts b/apps/mobile/src/api/index.ts index 34d72ff..528eba6 100644 --- a/apps/mobile/src/api/index.ts +++ b/apps/mobile/src/api/index.ts @@ -13,3 +13,4 @@ export * from "./recommendations"; export * from "./nutrition"; export * from "./hydration"; export * from "./client"; +export * from "./gyms"; diff --git a/apps/mobile/src/api/notifications.ts b/apps/mobile/src/api/notifications.ts index 22c32bd..4ddaf60 100644 --- a/apps/mobile/src/api/notifications.ts +++ b/apps/mobile/src/api/notifications.ts @@ -1,4 +1,5 @@ -import { API_BASE_URL } from "../config/api"; +import { isAxiosError } from "axios"; +import { apiClient, withAuth } from "./client"; export interface Notification { id: string; @@ -25,66 +26,49 @@ interface ApiResponse { export async function fetchNotifications( token: string | null, ): Promise { - const headers: Record = { - "Content-Type": "application/json", - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - - const response = await fetch(`${API_BASE_URL}/api/notifications`, { - method: "GET", - headers, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error( - `Failed to fetch notifications: ${response.status} - ${errorText}`, + try { + const response = await apiClient.get>( + "/api/notifications", + withAuth(token), ); + + const result = response.data; + + if (result.success && result.data) { + return result.data.map((notification) => ({ + ...notification, + createdAt: new Date(notification.createdAt), + })); + } + + return []; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error( + `Failed to fetch notifications: ${error.response.status}`, + ); + } + throw new Error("Failed to fetch notifications"); } - - const result: ApiResponse = await response.json(); - - if (result.success && result.data) { - // Convert date strings to Date objects - return result.data.map((notification) => ({ - ...notification, - createdAt: new Date(notification.createdAt), - })); - } - - return []; } /** * Get unread notification count */ export async function fetchUnreadCount(token: string | null): Promise { - const headers: Record = { - "Content-Type": "application/json", - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; + try { + const response = await apiClient.get>( + "/api/notifications/unread-count", + withAuth(token), + ); + const result = response.data; + return result.success && result.data ? result.data.count : 0; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error(`Failed to fetch unread count: ${error.response.status}`); + } + throw new Error("Failed to fetch unread count"); } - - const response = await fetch( - `${API_BASE_URL}/api/notifications/unread-count`, - { - method: "GET", - headers, - }, - ); - - if (!response.ok) { - throw new Error(`Failed to fetch unread count: ${response.status}`); - } - - const result: ApiResponse<{ count: number }> = await response.json(); - - return result.success && result.data ? result.data.count : 0; } /** @@ -94,62 +78,51 @@ export async function markAsRead( notificationId: string, token: string | null, ): Promise { - const headers: Record = { - "Content-Type": "application/json", - }; + try { + const response = await apiClient.put>( + `/api/notifications/${notificationId}`, + {}, + withAuth(token), + ); - if (token) { - headers["Authorization"] = `Bearer ${token}`; + const result = response.data; + if (result.success && result.data) { + return { + ...result.data, + createdAt: new Date(result.data.createdAt), + }; + } + throw new Error("Invalid response format"); + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error( + `Failed to mark notification as read: ${error.response.status}`, + ); + } + if (error instanceof Error && error.message === "Invalid response format") { + throw error; + } + throw new Error("Failed to mark notification as read"); } - - const response = await fetch( - `${API_BASE_URL}/api/notifications/${notificationId}`, - { - method: "PUT", - headers, - }, - ); - - if (!response.ok) { - throw new Error(`Failed to mark notification as read: ${response.status}`); - } - - const result: ApiResponse = await response.json(); - - if (result.success && result.data) { - return { - ...result.data, - createdAt: new Date(result.data.createdAt), - }; - } - - throw new Error("Invalid response format"); } /** * Mark all notifications as read */ export async function markAllAsRead(token: string | null): Promise { - const headers: Record = { - "Content-Type": "application/json", - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - - const response = await fetch( - `${API_BASE_URL}/api/notifications/mark-all-read`, - { - method: "POST", - headers, - }, - ); - - if (!response.ok) { - throw new Error( - `Failed to mark all notifications as read: ${response.status}`, + try { + await apiClient.post( + "/api/notifications/mark-all-read", + {}, + withAuth(token), ); + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error( + `Failed to mark all notifications as read: ${error.response.status}`, + ); + } + throw new Error("Failed to mark all notifications as read"); } } @@ -160,24 +133,18 @@ export async function deleteNotification( notificationId: string, token: string | null, ): Promise { - const headers: Record = { - "Content-Type": "application/json", - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - - const response = await fetch( - `${API_BASE_URL}/api/notifications/${notificationId}`, - { - method: "DELETE", - headers, - }, - ); - - if (!response.ok) { - throw new Error(`Failed to delete notification: ${response.status}`); + try { + await apiClient.delete( + `/api/notifications/${notificationId}`, + withAuth(token), + ); + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error( + `Failed to delete notification: ${error.response.status}`, + ); + } + throw new Error("Failed to delete notification"); } } @@ -189,21 +156,16 @@ export async function savePushToken( deviceType: "ios" | "android", token: string | null, ): Promise { - const headers: Record = { - "Content-Type": "application/json", - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - - const response = await fetch(`${API_BASE_URL}/api/notifications/save-token`, { - method: "POST", - headers, - body: JSON.stringify({ expoPushToken, deviceType }), - }); - - if (!response.ok) { - throw new Error(`Failed to save push token: ${response.status}`); + try { + await apiClient.post( + "/api/notifications/save-token", + { expoPushToken, deviceType }, + withAuth(token), + ); + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error(`Failed to save push token: ${error.response.status}`); + } + throw new Error("Failed to save push token"); } } diff --git a/apps/mobile/src/api/recommendations.ts b/apps/mobile/src/api/recommendations.ts index 4186ab5..3e42eda 100644 --- a/apps/mobile/src/api/recommendations.ts +++ b/apps/mobile/src/api/recommendations.ts @@ -1,4 +1,6 @@ -import { API_BASE_URL, API_ENDPOINTS } from "../config/api"; +import { isAxiosError } from "axios"; +import { apiClient, withAuth } from "./client"; +import { API_ENDPOINTS } from "../config/api"; export interface Recommendation { id: string; @@ -21,6 +23,15 @@ export interface GenerateRecommendationRequest { modelProvider?: "openai" | "deepseek" | "ollama"; } +interface ApiResponse { + success?: boolean; + data?: T; +} + +function isApiResponse(value: unknown): value is ApiResponse { + return typeof value === "object" && value !== null && "success" in value; +} + /** * Get recommendations for a user * @@ -32,28 +43,29 @@ export async function getRecommendations( userId: string, token: string | null, ): Promise { - const headers: any = { - "Content-Type": "application/json", - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; + let result: ApiResponse | Recommendation[]; + try { + const response = await apiClient.get(API_ENDPOINTS.RECOMMENDATIONS, { + params: { userId }, + ...withAuth(token), + }); + result = response.data; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error( + `Failed to fetch recommendations: ${error.response.status}`, + ); + } + throw new Error("Failed to fetch recommendations"); } - const response = await fetch( - `${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}?userId=${userId}`, - { headers }, - ); - - if (!response.ok) { - throw new Error(`Failed to fetch recommendations: ${response.status}`); - } - - const result = await response.json(); - // Handle standardized API response format // API returns: { success: true, data: [...], meta: {...} } - if (result.success && result.data) { + if ( + isApiResponse(result) && + result.success && + result.data + ) { return Array.isArray(result.data) ? result.data : []; } @@ -72,36 +84,30 @@ export async function generateRecommendation( data: GenerateRecommendationRequest, token: string | null, ): Promise { - const headers: any = { - "Content-Type": "application/json", - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; + let result: ApiResponse | Recommendation; + try { + const response = await apiClient.post( + `${API_ENDPOINTS.RECOMMENDATIONS}/generate`, + data, + withAuth(token), + ); + result = response.data; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error( + `Failed to generate recommendation: ${error.response.status}`, + ); + } + throw new Error("Failed to generate recommendation"); } - const response = await fetch( - `${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}/generate`, - { - method: "POST", - headers, - body: JSON.stringify(data), - }, - ); - - if (!response.ok) { - throw new Error(`Failed to generate recommendation: ${response.status}`); - } - - const result = await response.json(); - // Handle standardized API response format - if (result.success && result.data) { + if (isApiResponse(result) && result.success && result.data) { return result.data; } // Fallback for legacy format - return result; + return result as Recommendation; } /** @@ -115,40 +121,32 @@ export async function generateRecommendation( export async function approveRecommendation( recommendationId: string, token: string | null, - approvedBy?: string, ): Promise { - const headers: any = { - "Content-Type": "application/json", - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - - const response = await fetch( - `${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}/approve`, - { - method: "POST", - headers, - body: JSON.stringify({ + let result: ApiResponse | Recommendation; + try { + const response = await apiClient.post( + `${API_ENDPOINTS.RECOMMENDATIONS}/approve`, + { recommendationId, status: "approved", - approvedBy, - }), - }, - ); - - if (!response.ok) { - throw new Error(`Failed to approve recommendation: ${response.status}`); + }, + withAuth(token), + ); + result = response.data; + } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error( + `Failed to approve recommendation: ${error.response.status}`, + ); + } + throw new Error("Failed to approve recommendation"); } - const result = await response.json(); - // Handle standardized API response format - if (result.success && result.data) { + if (isApiResponse(result) && result.success && result.data) { return result.data; } // Fallback for legacy format - return result; + return result as Recommendation; } diff --git a/apps/mobile/src/app/(auth)/onboarding.tsx b/apps/mobile/src/app/(auth)/onboarding.tsx index 5f6c661..343a12c 100644 --- a/apps/mobile/src/app/(auth)/onboarding.tsx +++ b/apps/mobile/src/app/(auth)/onboarding.tsx @@ -12,7 +12,7 @@ import { import { useUser, useAuth } from "@clerk/clerk-expo"; import { useRouter } from "expo-router"; import { fitnessProfileApi } from "@/api/fitnessProfile"; -import { API_BASE_URL } from "@/config/api"; +import { gymsApi, type Gym } from "@/api/gyms"; import log from "../../utils/logger"; export default function OnboardingScreen() { @@ -20,9 +20,7 @@ export default function OnboardingScreen() { const { getToken } = useAuth(); const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); - const [gyms, setGyms] = useState< - Array<{ id: string; name: string; location?: string }> - >([]); + const [gyms, setGyms] = useState([]); const [gymsLoading, setGymsLoading] = useState(false); const [selectedGymId, setSelectedGymId] = useState(null); @@ -31,13 +29,8 @@ export default function OnboardingScreen() { try { setGymsLoading(true); const token = await getToken(); - const res = await fetch(`${API_BASE_URL}/api/gyms`, { - headers: token ? { Authorization: `Bearer ${token}` } : undefined, - }); - const data = await res.json(); - if (Array.isArray(data)) { - setGyms(data); - } + const data = await gymsApi.getGyms(token); + setGyms(data); } catch (e) { log.error("Failed to fetch gyms", e); } finally { @@ -82,14 +75,7 @@ export default function OnboardingScreen() { // If gym was selected or cleared, patch user's gym selection first // selectedGymId: string gym id, or null to proceed without gym try { - await fetch(`${API_BASE_URL}/api/users/gym`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ gymId: selectedGymId }), - }); + await gymsApi.updateUserGym(selectedGymId, token); } catch (e) { log.warn("Failed to update gym selection", { gymId: selectedGymId }); } diff --git a/apps/mobile/src/app/(tabs)/profile.tsx b/apps/mobile/src/app/(tabs)/profile.tsx index d6808a3..b460ce0 100644 --- a/apps/mobile/src/app/(tabs)/profile.tsx +++ b/apps/mobile/src/app/(tabs)/profile.tsx @@ -18,8 +18,8 @@ import { ListItem } from "../../components/ListItem"; import { MinimalButton } from "../../components/MinimalButton"; import { Badge } from "../../components/Badge"; import { IconContainer } from "../../components/IconContainer"; -import { API_BASE_URL, API_ENDPOINTS } from "../../config/api"; import { fitnessProfileApi, FitnessProfile } from "../../api/fitnessProfile"; +import { gymsApi, type Gym } from "../../api/gyms"; import log from "../../utils/logger"; export default function ProfileScreen() { @@ -29,9 +29,7 @@ export default function ProfileScreen() { const { colors, typography, theme: activeTheme, setTheme } = useTheme(); const { getToken } = useAuth(); - const [gyms, setGyms] = useState< - Array<{ id: string; name: string; location?: string }> - >([]); + const [gyms, setGyms] = useState([]); const [gymsLoading, setGymsLoading] = useState(false); const [selectedGymId, setSelectedGymId] = useState(null); const [currentGymId, setCurrentGymId] = useState(null); @@ -76,51 +74,14 @@ export default function ProfileScreen() { try { setGymsLoading(true); const token = await getToken(); - const url = `${API_BASE_URL}${API_ENDPOINTS.GYMS}`; - log.debug("Loading gyms", { url }); - const res = await fetch(url, { - headers: token ? { Authorization: `Bearer ${token}` } : undefined, - }); - const contentType = res.headers.get("content-type") || ""; - if (!res.ok) { - const text = await res.text().catch(() => ""); - log.error( - "Failed to fetch gyms - non-OK response", - new Error(text.slice(0, 200)), - { status: res.status }, - ); - setGyms([]); - return; - } - if (!contentType.includes("application/json")) { - const text = await res.text().catch(() => ""); - log.error( - "Failed to fetch gyms - expected JSON", - new Error(text.slice(0, 200)), - { contentType }, - ); - setGyms([]); - return; - } - let data: any = null; - try { - data = await res.json(); - } catch (e) { - const text = await res.text().catch(() => ""); - log.error("Failed to parse gyms JSON", e, { - bodyPreview: text?.slice(0, 200), - }); - setGyms([]); - return; - } - const list = Array.isArray(data) ? data : []; + const list = await gymsApi.getGyms(token); setGyms(list); const gid = currentGymId ?? ((user?.publicMetadata as any)?.gymId as string | undefined) ?? null; if (gid) { - const g = list.find((x: any) => x.id === gid); + const g = list.find((x) => x.id === gid); setCurrentGymId(gid); setCurrentGymName(g?.name ?? null); if (selectedGymId === null) setSelectedGymId(gid); @@ -136,42 +97,7 @@ export default function ProfileScreen() { const handleApplyGym = async () => { try { const token = await getToken(); - const url = `${API_BASE_URL}${API_ENDPOINTS.USERS.GYM}`; - log.debug("Updating gym selection", { - url, - gymId: selectedGymId, - token: token ? "present" : "missing", - }); - const res = await fetch(url, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: JSON.stringify({ gymId: selectedGymId }), - }); - const contentType = res.headers.get("content-type") || ""; - if (!res.ok) { - const text = await res.text().catch(() => ""); - log.error( - "Failed to update gym selection - non-OK response", - new Error(text.slice(0, 200)), - { status: res.status }, - ); - Alert.alert("Error", "Failed to update gym selection"); - return; - } - if (contentType.includes("application/json")) { - try { - const data = await res.json(); - log.debug("Gym selection updated", { data }); - } catch (e) { - const text = await res.text().catch(() => ""); - log.error("Failed to parse update response JSON", e, { - bodyPreview: text?.slice(0, 200), - }); - } - } + await gymsApi.updateUserGym(selectedGymId, token); setCurrentGymId(selectedGymId); setCurrentGymName( selectedGymId diff --git a/apps/mobile/src/services/fitnessGoals.ts b/apps/mobile/src/services/fitnessGoals.ts index 15c4867..e64e9d8 100644 --- a/apps/mobile/src/services/fitnessGoals.ts +++ b/apps/mobile/src/services/fitnessGoals.ts @@ -1,4 +1,6 @@ -import { API_BASE_URL, API_ENDPOINTS } from "../config/api"; +import { isAxiosError } from "axios"; +import { apiClient, withAuth } from "../api/client"; +import { API_ENDPOINTS } from "../config/api"; import log from "../utils/logger"; export interface FitnessGoal { @@ -41,38 +43,21 @@ export interface CreateGoalData { } export class FitnessGoalsService { - private async getAuthHeaders(token: string | null): Promise { - const headers: any = { - "Content-Type": "application/json", - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - - return headers; - } - async getGoals( userId: string, token: string | null, status?: string, ): Promise { try { - const headers = await this.getAuthHeaders(token); - let url = `${API_BASE_URL}${API_ENDPOINTS.FITNESS_GOALS.LIST}?userId=${userId}`; + const response = await apiClient.get(API_ENDPOINTS.FITNESS_GOALS.LIST, { + params: { + userId, + ...(status && status !== "all" ? { status } : {}), + }, + ...withAuth(token), + }); - if (status && status !== "all") { - url += `&status=${status}`; - } - - const response = await fetch(url, { headers }); - - if (!response.ok) { - throw new Error(`Failed to fetch goals: ${response.status}`); - } - - const result = await response.json(); + const result = response.data; // Handle standardized API response format // API returns: { success: true, data: [...], meta: {...} } @@ -83,6 +68,9 @@ export class FitnessGoalsService { // Fallback for legacy format (direct array) return Array.isArray(result) ? result : []; } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error(`Failed to fetch goals: ${error.response.status}`); + } log.error("Failed to fetch fitness goals", error); throw error; } @@ -93,22 +81,13 @@ export class FitnessGoalsService { token: string | null, ): Promise { try { - const headers = await this.getAuthHeaders(token); - const response = await fetch( - `${API_BASE_URL}${API_ENDPOINTS.FITNESS_GOALS.CREATE}`, - { - method: "POST", - headers, - body: JSON.stringify(goalData), - }, + const response = await apiClient.post( + API_ENDPOINTS.FITNESS_GOALS.CREATE, + goalData, + withAuth(token), ); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to create goal"); - } - - const result = await response.json(); + const result = response.data; // Handle standardized API response format if (result.success && result.data) { @@ -118,6 +97,14 @@ export class FitnessGoalsService { // Fallback for legacy format return result; } catch (error) { + if (isAxiosError(error)) { + const message = + (error.response?.data as { error?: string } | undefined)?.error || + (error.response + ? `Failed to create goal: ${error.response.status}` + : "Failed to create goal"); + throw new Error(message); + } log.error("Failed to create fitness goal", error); throw error; } @@ -129,21 +116,13 @@ export class FitnessGoalsService { token: string | null, ): Promise { try { - const headers = await this.getAuthHeaders(token); - const response = await fetch( - `${API_BASE_URL}${API_ENDPOINTS.FITNESS_GOALS.UPDATE(id)}`, - { - method: "PUT", - headers, - body: JSON.stringify(updates), - }, + const response = await apiClient.put( + API_ENDPOINTS.FITNESS_GOALS.UPDATE(id), + updates, + withAuth(token), ); - if (!response.ok) { - throw new Error("Failed to update goal"); - } - - const result = await response.json(); + const result = response.data; // Handle standardized API response format if (result.success && result.data) { @@ -153,6 +132,9 @@ export class FitnessGoalsService { // Fallback for legacy format return result; } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error(`Failed to update goal: ${error.response.status}`); + } log.error("Failed to update fitness goal", error); throw error; } @@ -168,20 +150,13 @@ export class FitnessGoalsService { async completeGoal(id: string, token: string | null): Promise { try { - const headers = await this.getAuthHeaders(token); - const response = await fetch( - `${API_BASE_URL}${API_ENDPOINTS.FITNESS_GOALS.COMPLETE(id)}`, - { - method: "POST", - headers, - }, + const response = await apiClient.post( + API_ENDPOINTS.FITNESS_GOALS.COMPLETE(id), + {}, + withAuth(token), ); - if (!response.ok) { - throw new Error("Failed to complete goal"); - } - - const result = await response.json(); + const result = response.data; // Note: Complete endpoint returns direct object (legacy format) // Handle standardized API response format (if migrated) @@ -192,6 +167,9 @@ export class FitnessGoalsService { // Fallback for legacy format (current implementation) return result; } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error(`Failed to complete goal: ${error.response.status}`); + } log.error("Failed to complete fitness goal", error); throw error; } @@ -199,22 +177,17 @@ export class FitnessGoalsService { async deleteGoal(id: string, token: string | null): Promise { try { - const headers = await this.getAuthHeaders(token); - const response = await fetch( - `${API_BASE_URL}${API_ENDPOINTS.FITNESS_GOALS.DELETE(id)}`, - { - method: "DELETE", - headers, - }, + await apiClient.delete( + API_ENDPOINTS.FITNESS_GOALS.DELETE(id), + withAuth(token), ); - if (!response.ok) { - throw new Error("Failed to delete goal"); - } - // DELETE endpoint returns: { success: true, data: { deleted: true }, meta: {...} } // No need to parse the result for void return type } catch (error) { + if (isAxiosError(error) && error.response) { + throw new Error(`Failed to delete goal: ${error.response.status}`); + } log.error("Failed to delete fitness goal", error); throw error; }