BUS-Ticket-client/src/services/ApiClient.ts
user 299a91c83e Fix expo-local-authentication import, update ApiClient
- Remove expo-local-authentication dynamic import (causes build failure)
- Add stub biometric functions (disabled in this build)
- Update ApiClient login/register to match AuthContext usage
- Add requestOtp method to ApiClient

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 09:21:47 +00:00

254 lines
6.3 KiB
TypeScript

/**
* BUS-Tickets - API Client
* Copyright (c) 2024-2026 IT Enterprise
*/
import type { AuthTokens, Trip, Ticket, User } from '@/types';
interface ApiClientConfig {
baseUrl: string;
timeout?: number;
onTokenRefresh?: (tokens: AuthTokens) => Promise<void>;
onAuthError?: () => Promise<void>;
}
export class BusTicketsApiClient {
private config: ApiClientConfig;
private tokens: AuthTokens | null = null;
constructor(config: ApiClientConfig) {
this.config = config;
}
setTokens(tokens: AuthTokens): void {
this.tokens = tokens;
}
clearTokens(): void {
this.tokens = null;
}
getAccessToken(): string | null {
return this.tokens?.accessToken ?? null;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.config.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
};
if (this.tokens?.accessToken) {
headers['Authorization'] = `Bearer ${this.tokens.accessToken}`;
}
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
this.config.timeout ?? 30000
);
try {
const response = await fetch(url, {
...options,
headers,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (response.status === 401) {
if (this.config.onAuthError) {
await this.config.onAuthError();
}
throw new Error('Unauthorized');
}
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// Auth endpoints
async login(params: {
provider: 'email' | 'google' | 'facebook' | 'apple' | 'phone';
email?: string;
password?: string;
idToken?: string;
phone?: string;
otp?: string;
}): Promise<{ user: User; tokens: AuthTokens }> {
const response = await this.request<{ data: { user: User; tokens: AuthTokens } }>(
'/api/v1/auth/login',
{
method: 'POST',
body: JSON.stringify(params),
}
);
this.tokens = response.data.tokens;
return response.data;
}
async register(data: {
email: string;
password: string;
name: string;
phone?: string;
}): Promise<{ user: User; tokens: AuthTokens }> {
const response = await this.request<{ data: { user: User; tokens: AuthTokens } }>(
'/api/v1/auth/register',
{
method: 'POST',
body: JSON.stringify(data),
}
);
this.tokens = response.data.tokens;
return response.data;
}
async requestOtp(email?: string, phone?: string): Promise<void> {
await this.request('/api/v1/auth/otp/request', {
method: 'POST',
body: JSON.stringify({ email, phone }),
});
}
async logout(): Promise<void> {
await this.request('/api/v1/auth/logout', { method: 'POST' });
this.clearTokens();
}
async refreshToken(): Promise<AuthTokens> {
if (!this.tokens?.refreshToken) {
throw new Error('No refresh token available');
}
const response = await this.request<{ data: AuthTokens }>(
'/api/v1/auth/refresh',
{
method: 'POST',
body: JSON.stringify({ refreshToken: this.tokens.refreshToken }),
}
);
this.tokens = response.data;
if (this.config.onTokenRefresh) {
await this.config.onTokenRefresh(response.data);
}
return response.data;
}
// User endpoints
async getCurrentUser(): Promise<User> {
const response = await this.request<{ data: User }>('/api/v1/auth/me');
return response.data;
}
async updateProfile(data: Partial<User>): Promise<User> {
const response = await this.request<{ data: User }>('/api/v1/auth/profile', {
method: 'PUT',
body: JSON.stringify(data),
});
return response.data;
}
// Trip endpoints
async searchTrips(params: {
originId: number;
destinationId: number;
date: string;
passengers?: number;
}): Promise<Trip[]> {
const searchParams = new URLSearchParams({
origin_id: params.originId.toString(),
destination_id: params.destinationId.toString(),
date: params.date,
passengers: (params.passengers ?? 1).toString(),
});
const response = await this.request<{ data: Trip[] }>(
`/api/v1/trips?${searchParams}`
);
return response.data;
}
async getTripById(tripId: number): Promise<Trip> {
const response = await this.request<{ data: Trip }>(
`/api/v1/trips/${tripId}`
);
return response.data;
}
// Ticket endpoints
async getMyTickets(): Promise<Ticket[]> {
const response = await this.request<{ data: Ticket[] }>(
'/api/v1/tickets/my'
);
return response.data;
}
async getTicketById(ticketId: number): Promise<Ticket> {
const response = await this.request<{ data: Ticket }>(
`/api/v1/tickets/${ticketId}`
);
return response.data;
}
async bookTicket(data: {
tripId: number;
passengers: Array<{
name: string;
email: string;
phone?: string;
seat?: number;
}>;
}): Promise<Ticket[]> {
const response = await this.request<{ data: Ticket[] }>(
'/api/v1/tickets/book',
{
method: 'POST',
body: JSON.stringify(data),
}
);
return response.data;
}
async cancelTicket(ticketId: number): Promise<void> {
await this.request(`/api/v1/tickets/${ticketId}/cancel`, {
method: 'POST',
});
}
// Station endpoints
async searchStations(query: string): Promise<Array<{ id: number; name: string; city: string }>> {
const response = await this.request<{ data: Array<{ id: number; name: string; city: string }> }>(
`/api/v1/stations/search?q=${encodeURIComponent(query)}`
);
return response.data;
}
async getPopularStations(): Promise<Array<{ id: number; name: string; city: string }>> {
const response = await this.request<{ data: Array<{ id: number; name: string; city: string }> }>(
'/api/v1/stations/popular'
);
return response.data;
}
}
export function createApiClient(config: ApiClientConfig): BusTicketsApiClient {
return new BusTicketsApiClient(config);
}