Services and APIs - PlugImt/transat-app GitHub Wiki
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: 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
Backend Services:
โโโ Authentication Service
โโโ User Management Service
โโโ Restaurant Service
โโโ Washing Machine Service
โโโ Weather Service
โโโ Games & Entertainment Service
โโโ Club Management Service
โโโ Notification Service
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);
}
);
// 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,
},
},
});
// 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;
}
};
// 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
});
};
// 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}`);
}
};
// 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
});
};
// 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 });
}
};
// 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;
}
}
}
// 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;
}
};
// 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;
}
};
// 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');
}
};
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;
// 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 });
},
});
};
// 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);
}
}
}
// 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');
};
// 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.