Services and APIs - PlugImt/transat-app GitHub Wiki

Services & APIs

๐Ÿ”— Backend Integration Overview

Transat 2.0 integrates with a Go backend (Transat_2.0_Backend) that provides all the campus services data. The frontend uses modern data fetching patterns with TanStack Query for efficient state management and caching.

๐Ÿ—๏ธ Backend Architecture

Technology Stack

  • Backend: Go with Gin framework
  • Database: PostgreSQL
  • Authentication: JWT tokens
  • API Design: RESTful with some real-time endpoints
  • File Storage: Local and cloud storage options
  • Caching: Redis for session management

API Base Structure

Backend Services:
โ”œโ”€โ”€ Authentication Service
โ”œโ”€โ”€ User Management Service
โ”œโ”€โ”€ Restaurant Service
โ”œโ”€โ”€ Washing Machine Service
โ”œโ”€โ”€ Weather Service
โ”œโ”€โ”€ Games & Entertainment Service
โ”œโ”€โ”€ Club Management Service
โ””โ”€โ”€ Notification Service

๐Ÿ“ก API Integration

HTTP Client Configuration

The app uses Axios with custom configuration for API communication:

// src/lib/api.ts
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';

const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'https://api.transat.local';

export const apiClient = axios.create({
  baseURL: API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor for authentication
apiClient.interceptors.request.use(async (config) => {
  const token = await AsyncStorage.getItem('auth_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor for error handling
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      // Handle token expiration
      await AsyncStorage.removeItem('auth_token');
      // Redirect to login
    }
    return Promise.reject(error);
  }
);

TanStack Query Configuration

// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      retry: 3,
      refetchOnWindowFocus: false,
    },
    mutations: {
      retry: 1,
    },
  },
});

๐Ÿ” Authentication Service

JWT Authentication Flow

// src/services/auth.ts
export interface LoginCredentials {
  email: string;
  password: string;
}

export interface AuthUser {
  id: string;
  email: string;
  name: string;
  studentId?: string;
  role: 'student' | 'staff' | 'admin';
}

export const authService = {
  async login(credentials: LoginCredentials): Promise<AuthUser> {
    const response = await apiClient.post('/auth/login', credentials);
    const { token, user } = response.data;
    
    await AsyncStorage.setItem('auth_token', token);
    return user;
  },

  async register(userData: RegisterData): Promise<AuthUser> {
    const response = await apiClient.post('/auth/register', userData);
    return response.data.user;
  },

  async logout(): Promise<void> {
    await apiClient.post('/auth/logout');
    await AsyncStorage.removeItem('auth_token');
  },

  async refreshToken(): Promise<string> {
    const response = await apiClient.post('/auth/refresh');
    const { token } = response.data;
    await AsyncStorage.setItem('auth_token', token);
    return token;
  },

  async getCurrentUser(): Promise<AuthUser> {
    const response = await apiClient.get('/auth/me');
    return response.data.user;
  }
};

Authentication Hooks

// src/hooks/auth/useAuth.ts
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

export const useAuth = () => {
  const queryClient = useQueryClient();

  const loginMutation = useMutation({
    mutationFn: authService.login,
    onSuccess: (user) => {
      queryClient.setQueryData(['current_user'], user);
    },
  });

  const logoutMutation = useMutation({
    mutationFn: authService.logout,
    onSuccess: () => {
      queryClient.clear();
    },
  });

  return {
    login: loginMutation.mutate,
    logout: logoutMutation.mutate,
    isLoggingIn: loginMutation.isPending,
    loginError: loginMutation.error,
  };
};

export const useUser = () => {
  return useQuery({
    queryKey: ['current_user'],
    queryFn: authService.getCurrentUser,
    staleTime: Infinity, // User data rarely changes
  });
};

๐Ÿฝ๏ธ Restaurant Service

API Endpoints

// src/services/restaurant.ts
export interface MenuItem {
  id: string;
  name: string;
  description: string;
  price: number;
  category: 'starter' | 'main' | 'dessert' | 'side';
  allergens: string[];
  available: boolean;
}

export interface DailyMenu {
  date: string;
  lunch: MenuItem[];
  dinner: MenuItem[];
  isWeekend: boolean;
}

export const restaurantService = {
  async getDailyMenu(date?: string): Promise<DailyMenu> {
    const response = await apiClient.get('/restaurant/menu', {
      params: { date: date || new Date().toISOString().split('T')[0] }
    });
    return response.data;
  },

  async getMenuForWeek(startDate: string): Promise<DailyMenu[]> {
    const response = await apiClient.get('/restaurant/menu/week', {
      params: { startDate }
    });
    return response.data;
  },

  async setFavoriteItem(itemId: string): Promise<void> {
    await apiClient.post(`/restaurant/favorites/${itemId}`);
  },

  async removeFavoriteItem(itemId: string): Promise<void> {
    await apiClient.delete(`/restaurant/favorites/${itemId}`);
  }
};

Restaurant Hooks

// src/hooks/useRestaurant.ts
export const useRestaurantMenu = (date?: string) => {
  return useQuery({
    queryKey: QUERY_KEYS.restaurantMenu(date),
    queryFn: () => restaurantService.getDailyMenu(date),
    staleTime: 30 * 60 * 1000, // 30 minutes
  });
};

export const useWeeklyMenu = (startDate: string) => {
  return useQuery({
    queryKey: QUERY_KEYS.weeklyMenu(startDate),
    queryFn: () => restaurantService.getMenuForWeek(startDate),
    staleTime: 60 * 60 * 1000, // 1 hour
  });
};

๐Ÿงบ Washing Machine Service

Real-time Machine Status

// src/services/washingMachine.ts
export interface WashingMachine {
  id: string;
  location: string;
  floor: number;
  type: 'washer' | 'dryer';
  status: 'available' | 'occupied' | 'out_of_order';
  timeRemaining?: number; // minutes
  currentUser?: string;
  lastUpdated: string;
}

export interface BookingRequest {
  machineId: string;
  startTime: string;
  duration: number; // minutes
}

export const washingMachineService = {
  async getAllMachines(): Promise<WashingMachine[]> {
    const response = await apiClient.get('/washing-machines');
    return response.data;
  },

  async getMachineStatus(machineId: string): Promise<WashingMachine> {
    const response = await apiClient.get(`/washing-machines/${machineId}`);
    return response.data;
  },

  async bookMachine(booking: BookingRequest): Promise<void> {
    await apiClient.post('/washing-machines/book', booking);
  },

  async cancelBooking(bookingId: string): Promise<void> {
    await apiClient.delete(`/washing-machines/bookings/${bookingId}`);
  },

  async reportIssue(machineId: string, issue: string): Promise<void> {
    await apiClient.post(`/washing-machines/${machineId}/report`, { issue });
  }
};

WebSocket Integration for Real-time Updates

// src/services/websocket.ts
export class WashingMachineWebSocket {
  private ws: WebSocket | null = null;
  private listeners: Map<string, (data: any) => void> = new Map();

  connect() {
    const wsUrl = process.env.EXPO_PUBLIC_WS_URL || 'ws://localhost:8080/ws';
    this.ws = new WebSocket(wsUrl);

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.listeners.forEach((callback) => callback(data));
    };
  }

  subscribe(callback: (data: any) => void) {
    const id = Math.random().toString(36);
    this.listeners.set(id, callback);
    return () => this.listeners.delete(id);
  }

  disconnect() {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

โ˜€๏ธ Weather Service

Weather Data Integration

// src/services/weather.ts
export interface WeatherData {
  temperature: number;
  condition: string;
  icon: string;
  humidity: number;
  windSpeed: number;
  location: string;
  forecast: WeatherForecast[];
}

export interface WeatherForecast {
  date: string;
  high: number;
  low: number;
  condition: string;
  icon: string;
}

export const weatherService = {
  async getCurrentWeather(): Promise<WeatherData> {
    const response = await apiClient.get('/weather/current');
    return response.data;
  },

  async getForecast(days: number = 5): Promise<WeatherForecast[]> {
    const response = await apiClient.get('/weather/forecast', {
      params: { days }
    });
    return response.data;
  }
};

๐ŸŽฎ Games Service

Game Management

// src/services/games.ts
export interface Game {
  id: string;
  name: string;
  description: string;
  type: 'trivia' | 'puzzle' | 'social';
  difficulty: 'easy' | 'medium' | 'hard';
  maxPlayers: number;
  duration: number; // minutes
  active: boolean;
}

export interface GameSession {
  id: string;
  gameId: string;
  players: Player[];
  status: 'waiting' | 'active' | 'completed';
  startTime: string;
  endTime?: string;
  scores: Record<string, number>;
}

export const gamesService = {
  async getAvailableGames(): Promise<Game[]> {
    const response = await apiClient.get('/games');
    return response.data;
  },

  async joinGame(gameId: string): Promise<GameSession> {
    const response = await apiClient.post(`/games/${gameId}/join`);
    return response.data;
  },

  async getLeaderboard(gameId: string): Promise<LeaderboardEntry[]> {
    const response = await apiClient.get(`/games/${gameId}/leaderboard`);
    return response.data;
  }
};

๐Ÿ“ข Notification Service

Push Notification Management

// src/services/notifications.ts
export interface NotificationPreferences {
  washingMachineAlerts: boolean;
  menuUpdates: boolean;
  gameInvitations: boolean;
  clubAnnouncements: boolean;
  systemNotifications: boolean;
}

export const notificationService = {
  async registerPushToken(token: string): Promise<void> {
    await apiClient.post('/notifications/register', { token });
  },

  async updatePreferences(preferences: NotificationPreferences): Promise<void> {
    await apiClient.put('/notifications/preferences', preferences);
  },

  async getPreferences(): Promise<NotificationPreferences> {
    const response = await apiClient.get('/notifications/preferences');
    return response.data;
  },

  async sendTestNotification(): Promise<void> {
    await apiClient.post('/notifications/test');
  }
};

๐Ÿ”ง Query Keys Management

Centralized query key management for consistent caching:

// src/lib/queryKeys.ts
export const QUERY_KEYS = {
  // User & Auth
  currentUser: ['current_user'] as const,
  
  // Restaurant
  restaurantMenu: (date?: string) => ['restaurant', 'menu', date] as const,
  weeklyMenu: (startDate: string) => ['restaurant', 'weekly', startDate] as const,
  favorites: ['restaurant', 'favorites'] as const,
  
  // Washing Machines
  washingMachines: ['washing_machines'] as const,
  machineStatus: (id: string) => ['washing_machines', id] as const,
  userBookings: ['washing_machines', 'bookings'] as const,
  
  // Weather
  weather: ['weather', 'current'] as const,
  forecast: (days: number) => ['weather', 'forecast', days] as const,
  
  // Games
  games: ['games'] as const,
  gameSession: (id: string) => ['games', 'session', id] as const,
  leaderboard: (gameId: string) => ['games', 'leaderboard', gameId] as const,
  
  // Notifications
  notificationPreferences: ['notifications', 'preferences'] as const,
} as const;

๐Ÿ”„ Data Synchronization

Optimistic Updates

// Example: Optimistic washing machine booking
export const useBookWashingMachine = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: washingMachineService.bookMachine,
    onMutate: async (booking) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: QUERY_KEYS.washingMachines });

      // Snapshot previous value
      const previousMachines = queryClient.getQueryData(QUERY_KEYS.washingMachines);

      // Optimistically update
      queryClient.setQueryData(QUERY_KEYS.washingMachines, (old: WashingMachine[]) =>
        old?.map(machine =>
          machine.id === booking.machineId
            ? { ...machine, status: 'occupied' as const }
            : machine
        )
      );

      return { previousMachines };
    },
    onError: (err, booking, context) => {
      // Rollback on error
      if (context?.previousMachines) {
        queryClient.setQueryData(QUERY_KEYS.washingMachines, context.previousMachines);
      }
    },
    onSettled: () => {
      // Refetch after mutation
      queryClient.invalidateQueries({ queryKey: QUERY_KEYS.washingMachines });
    },
  });
};

Background Sync

// src/services/backgroundSync.ts
export class BackgroundSyncService {
  private intervalId: NodeJS.Timeout | null = null;

  start() {
    this.intervalId = setInterval(() => {
      this.syncCriticalData();
    }, 30000); // Sync every 30 seconds
  }

  stop() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

  private async syncCriticalData() {
    try {
      // Sync washing machine status
      await queryClient.invalidateQueries({ 
        queryKey: QUERY_KEYS.washingMachines 
      });
      
      // Sync weather data
      await queryClient.invalidateQueries({ 
        queryKey: QUERY_KEYS.weather 
      });
    } catch (error) {
      console.error('Background sync failed:', error);
    }
  }
}

๐Ÿšซ Error Handling

Global Error Handling

// src/lib/errorHandler.ts
export class ApiError extends Error {
  constructor(
    message: string,
    public status: number,
    public code?: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

export const handleApiError = (error: any): ApiError => {
  if (error.response) {
    return new ApiError(
      error.response.data.message || 'An error occurred',
      error.response.status,
      error.response.data.code
    );
  }
  
  if (error.request) {
    return new ApiError('Network error', 0, 'NETWORK_ERROR');
  }
  
  return new ApiError(error.message || 'Unknown error', 0, 'UNKNOWN_ERROR');
};

Retry Logic

// src/lib/retryConfig.ts
export const retryConfig = {
  retry: (failureCount: number, error: any) => {
    // Don't retry on 4xx errors (client errors)
    if (error.status >= 400 && error.status < 500) {
      return false;
    }
    
    // Retry up to 3 times for other errors
    return failureCount < 3;
  },
  retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000),
};

Next Steps: Learn about mobile-specific features in Mobile Features documentation.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ