react-native-sop-docs/05-state-management-navigation/api-integration.md

12 KiB

API Integration

RTK Query Setup

// src/services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import Config from 'react-native-config';
import { RootState } from '@redux/store';

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: Config.API_BASE_URL,
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.token;
      if (token) {
        headers.set('authorization', `Bearer ${token}`);
      }
      headers.set('Content-Type', 'application/json');
      return headers;
    },
  }),
  tagTypes: ['User', 'NGO', 'Donation', 'Request'],
  endpoints: () => ({}),
});

Auth API

// src/services/authAPI.ts
import { api } from './api';

interface LoginRequest {
  email: string;
  password: string;
}

interface LoginResponse {
  user: {
    id: string;
    email: string;
    firstName: string;
    lastName: string;
  };
  token: string;
  refreshToken: string;
}

interface RegisterRequest {
  firstName: string;
  lastName: string;
  email: string;
  password: string;
  phone: string;
}

export const authAPI = api.injectEndpoints({
  endpoints: (builder) => ({
    login: builder.mutation<LoginResponse, LoginRequest>({
      query: (credentials) => ({
        url: '/auth/login',
        method: 'POST',
        body: credentials,
      }),
    }),
    register: builder.mutation<LoginResponse, RegisterRequest>({
      query: (userData) => ({
        url: '/auth/register',
        method: 'POST',
        body: userData,
      }),
    }),
    refreshToken: builder.mutation<{ token: string }, { refreshToken: string }>(
      {
        query: ({ refreshToken }) => ({
          url: '/auth/refresh',
          method: 'POST',
          body: { refreshToken },
        }),
      },
    ),
    logout: builder.mutation<void, void>({
      query: () => ({
        url: '/auth/logout',
        method: 'POST',
      }),
    }),
    forgotPassword: builder.mutation<void, { email: string }>({
      query: ({ email }) => ({
        url: '/auth/forgot-password',
        method: 'POST',
        body: { email },
      }),
    }),
    verifyOTP: builder.mutation<void, { email: string; otp: string }>({
      query: ({ email, otp }) => ({
        url: '/auth/verify-otp',
        method: 'POST',
        body: { email, otp },
      }),
    }),
  }),
});

export const {
  useLoginMutation,
  useRegisterMutation,
  useRefreshTokenMutation,
  useLogoutMutation,
  useForgotPasswordMutation,
  useVerifyOTPMutation,
} = authAPI;

User API

// src/services/userAPI.ts
import { api } from './api';

interface User {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  phone?: string;
  avatar?: string;
  isNGO: boolean;
  ngoDetails?: NGODetails;
}

interface NGODetails {
  ngoName: string;
  registrationNumber: string;
  address: string;
  description: string;
  causes: string[];
}

interface UpdateProfileRequest {
  firstName?: string;
  lastName?: string;
  phone?: string;
  avatar?: string;
}

export const userAPI = api.injectEndpoints({
  endpoints: (builder) => ({
    getProfile: builder.query<User, void>({
      query: () => '/user/profile',
      providesTags: ['User'],
    }),
    updateProfile: builder.mutation<User, UpdateProfileRequest>({
      query: (updates) => ({
        url: '/user/profile',
        method: 'PUT',
        body: updates,
      }),
      invalidatesTags: ['User'],
    }),
    uploadAvatar: builder.mutation<{ avatarUrl: string }, FormData>({
      query: (formData) => ({
        url: '/user/avatar',
        method: 'POST',
        body: formData,
      }),
      invalidatesTags: ['User'],
    }),
    deleteAccount: builder.mutation<void, void>({
      query: () => ({
        url: '/user/account',
        method: 'DELETE',
      }),
    }),
  }),
});

export const {
  useGetProfileQuery,
  useUpdateProfileMutation,
  useUploadAvatarMutation,
  useDeleteAccountMutation,
} = userAPI;

NGO API

// src/services/ngoAPI.ts
import { api } from './api';

interface NGO {
  id: string;
  ngoName: string;
  description: string;
  registrationNumber: string;
  address: string;
  causes: string[];
  coverImage?: string;
  isVerified: boolean;
  totalDonations: number;
  activeRequests: number;
}

interface CreateNGORequest {
  ngoName: string;
  description: string;
  registrationNumber: string;
  address: string;
  causes: string[];
  documents: string[];
}

export const ngoAPI = api.injectEndpoints({
  endpoints: (builder) => ({
    getNGOList: builder.query<
      NGO[],
      { page?: number; limit?: number; search?: string }
    >({
      query: ({ page = 1, limit = 10, search }) => ({
        url: '/ngo',
        params: { page, limit, search },
      }),
      providesTags: ['NGO'],
    }),
    getNGOById: builder.query<NGO, string>({
      query: (id) => `/ngo/${id}`,
      providesTags: ['NGO'],
    }),
    createNGO: builder.mutation<NGO, CreateNGORequest>({
      query: (ngoData) => ({
        url: '/ngo',
        method: 'POST',
        body: ngoData,
      }),
      invalidatesTags: ['NGO', 'User'],
    }),
    updateNGO: builder.mutation<
      NGO,
      { id: string; updates: Partial<CreateNGORequest> }
    >({
      query: ({ id, updates }) => ({
        url: `/ngo/${id}`,
        method: 'PUT',
        body: updates,
      }),
      invalidatesTags: ['NGO'],
    }),
    getFavoriteNGOs: builder.query<NGO[], void>({
      query: () => '/ngo/favorites',
      providesTags: ['NGO'],
    }),
    toggleFavoriteNGO: builder.mutation<void, string>({
      query: (ngoId) => ({
        url: `/ngo/${ngoId}/favorite`,
        method: 'POST',
      }),
      invalidatesTags: ['NGO'],
    }),
  }),
});

export const {
  useGetNGOListQuery,
  useGetNGOByIdQuery,
  useCreateNGOMutation,
  useUpdateNGOMutation,
  useGetFavoriteNGOsQuery,
  useToggleFavoriteNGOMutation,
} = ngoAPI;

Request API

// src/services/requestAPI.ts
import { api } from './api';

interface FundraisingRequest {
  id: string;
  title: string;
  description: string;
  category: string;
  targetAmount: number;
  currentAmount: number;
  images: string[];
  location: {
    latitude: number;
    longitude: number;
    address: string;
  };
  status: 'active' | 'completed' | 'cancelled';
  createdBy: string;
  ngoId?: string;
  isUrgent: boolean;
  endDate: string;
}

interface CreateRequestData {
  title: string;
  description: string;
  category: string;
  targetAmount: number;
  images: string[];
  location: {
    latitude: number;
    longitude: number;
    address: string;
  };
  isUrgent: boolean;
  endDate: string;
}

export const requestAPI = api.injectEndpoints({
  endpoints: (builder) => ({
    getRequests: builder.query<
      FundraisingRequest[],
      {
        page?: number;
        limit?: number;
        category?: string;
        location?: string;
        urgent?: boolean;
      }
    >({
      query: (params) => ({
        url: '/requests',
        params,
      }),
      providesTags: ['Request'],
    }),
    getRequestById: builder.query<FundraisingRequest, string>({
      query: (id) => `/requests/${id}`,
      providesTags: ['Request'],
    }),
    createRequest: builder.mutation<FundraisingRequest, CreateRequestData>({
      query: (requestData) => ({
        url: '/requests',
        method: 'POST',
        body: requestData,
      }),
      invalidatesTags: ['Request'],
    }),
    updateRequest: builder.mutation<
      FundraisingRequest,
      {
        id: string;
        updates: Partial<CreateRequestData>;
      }
    >({
      query: ({ id, updates }) => ({
        url: `/requests/${id}`,
        method: 'PUT',
        body: updates,
      }),
      invalidatesTags: ['Request'],
    }),
    deleteRequest: builder.mutation<void, string>({
      query: (id) => ({
        url: `/requests/${id}`,
        method: 'DELETE',
      }),
      invalidatesTags: ['Request'],
    }),
    getMyRequests: builder.query<FundraisingRequest[], void>({
      query: () => '/requests/my-requests',
      providesTags: ['Request'],
    }),
  }),
});

export const {
  useGetRequestsQuery,
  useGetRequestByIdQuery,
  useCreateRequestMutation,
  useUpdateRequestMutation,
  useDeleteRequestMutation,
  useGetMyRequestsQuery,
} = requestAPI;

Error Handling

// src/utils/errorHandler.ts
import { SerializedError } from '@reduxjs/toolkit';
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';

export interface APIError {
  message: string;
  status?: number;
  code?: string;
}

export const handleAPIError = (
  error: FetchBaseQueryError | SerializedError,
): APIError => {
  if ('status' in error) {
    // FetchBaseQueryError
    const status = error.status;
    const data = error.data as any;

    switch (status) {
      case 400:
        return {
          message: data?.message || 'Bad request',
          status: 400,
          code: 'BAD_REQUEST',
        };
      case 401:
        return {
          message: 'Authentication required',
          status: 401,
          code: 'UNAUTHORIZED',
        };
      case 403:
        return {
          message: 'Access denied',
          status: 403,
          code: 'FORBIDDEN',
        };
      case 404:
        return {
          message: 'Resource not found',
          status: 404,
          code: 'NOT_FOUND',
        };
      case 500:
        return {
          message: 'Server error. Please try again later.',
          status: 500,
          code: 'SERVER_ERROR',
        };
      default:
        return {
          message: data?.message || 'An unexpected error occurred',
          status: typeof status === 'number' ? status : undefined,
          code: 'UNKNOWN_ERROR',
        };
    }
  } else {
    // SerializedError
    return {
      message: error.message || 'Network error',
      code: 'NETWORK_ERROR',
    };
  }
};

API Hooks

// src/hooks/useAPI.ts
import { useState, useCallback } from 'react';
import { handleAPIError, APIError } from '@utils/errorHandler';

interface UseAPIState<T> {
  data: T | null;
  loading: boolean;
  error: APIError | null;
}

export const useAPI = <T>() => {
  const [state, setState] = useState<UseAPIState<T>>({
    data: null,
    loading: false,
    error: null,
  });

  const execute = useCallback(async (apiCall: () => Promise<T>) => {
    setState((prev) => ({ ...prev, loading: true, error: null }));

    try {
      const data = await apiCall();
      setState({ data, loading: false, error: null });
      return data;
    } catch (error: any) {
      const apiError = handleAPIError(error);
      setState((prev) => ({ ...prev, loading: false, error: apiError }));
      throw apiError;
    }
  }, []);

  const reset = useCallback(() => {
    setState({ data: null, loading: false, error: null });
  }, []);

  return {
    ...state,
    execute,
    reset,
  };
};

Store Integration

// src/redux/store/index.ts (updated)
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { combineReducers } from '@reduxjs/toolkit';
import { api } from '@services/api';
import authSlice from '../slices/authSlice';

const persistConfig = {
  key: 'root',
  storage: AsyncStorage,
  whitelist: ['auth'],
};

const rootReducer = combineReducers({
  auth: authSlice,
  [api.reducerPath]: api.reducer,
});

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
      },
    }).concat(api.middleware),
});

export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;