fix: Centralize API clients and add global ErrorBoundary (Phase 3)
Migrate 6 services from duplicate Axios instances to centralized apiClient (auto-refresh, multi-tab sync, proper 401 handling): - trading.service.ts, notification.service.ts, payment.service.ts - education.service.ts, chat.service.ts, investment.service.ts Add global ErrorBoundary wrapping all App routes. Move assistant module ErrorBoundary to shared components/ErrorBoundary.tsx. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
42d18759b5
commit
1d76747e9b
@ -4,6 +4,7 @@ import { Suspense, lazy } from 'react';
|
|||||||
// Layout
|
// Layout
|
||||||
import MainLayout from './components/layout/MainLayout';
|
import MainLayout from './components/layout/MainLayout';
|
||||||
import AuthLayout from './components/layout/AuthLayout';
|
import AuthLayout from './components/layout/AuthLayout';
|
||||||
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
|
|
||||||
// Loading component
|
// Loading component
|
||||||
const LoadingSpinner = () => (
|
const LoadingSpinner = () => (
|
||||||
@ -68,6 +69,7 @@ const PredictionsPage = lazy(() => import('./modules/admin/pages/PredictionsPage
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Auth routes */}
|
{/* Auth routes */}
|
||||||
@ -149,6 +151,7 @@ function App() {
|
|||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
206
src/components/ErrorBoundary.tsx
Normal file
206
src/components/ErrorBoundary.tsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Global ErrorBoundary Component
|
||||||
|
* Catches JavaScript errors in child components and displays fallback UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
RefreshCw,
|
||||||
|
Home,
|
||||||
|
Bug,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export interface ErrorBoundaryProps {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||||||
|
onReset?: () => void;
|
||||||
|
showDetails?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
errorInfo: ErrorInfo | null;
|
||||||
|
showStack: boolean;
|
||||||
|
copied: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
errorInfo: null,
|
||||||
|
showStack: false,
|
||||||
|
copied: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||||
|
this.setState({ errorInfo });
|
||||||
|
|
||||||
|
if (this.props.onError) {
|
||||||
|
this.props.onError(error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReset = (): void => {
|
||||||
|
this.setState({
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
errorInfo: null,
|
||||||
|
showStack: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.props.onReset) {
|
||||||
|
this.props.onReset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRefresh = (): void => {
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleGoHome = (): void => {
|
||||||
|
window.location.href = '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleStack = (): void => {
|
||||||
|
this.setState((prev) => ({ showStack: !prev.showStack }));
|
||||||
|
};
|
||||||
|
|
||||||
|
copyError = async (): Promise<void> => {
|
||||||
|
const { error, errorInfo } = this.state;
|
||||||
|
const errorText = `Error: ${error?.message}\n\nStack: ${error?.stack}\n\nComponent Stack: ${errorInfo?.componentStack}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(errorText);
|
||||||
|
this.setState({ copied: true });
|
||||||
|
setTimeout(() => this.setState({ copied: false }), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { hasError, error, errorInfo, showStack, copied } = this.state;
|
||||||
|
const { children, fallback, showDetails = true } = this.props;
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
if (fallback) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-[400px] flex items-center justify-center p-6 bg-gray-900/50 rounded-xl border border-gray-700">
|
||||||
|
<div className="max-w-lg w-full">
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<div className="p-4 bg-red-500/20 rounded-full">
|
||||||
|
<AlertTriangle className="w-12 h-12 text-red-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-xl font-semibold text-white text-center mb-2">
|
||||||
|
Something went wrong
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 text-center mb-6">
|
||||||
|
An unexpected error occurred. You can try refreshing the page or return to the home screen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{error && showDetails && (
|
||||||
|
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Bug className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-red-400 mb-1">Error Message</p>
|
||||||
|
<p className="text-sm text-gray-300 break-words">{error.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error.stack && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<button
|
||||||
|
onClick={this.toggleStack}
|
||||||
|
className="flex items-center gap-2 text-sm text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{showStack ? (
|
||||||
|
<ChevronUp className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
{showStack ? 'Hide' : 'Show'} Stack Trace
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showStack && (
|
||||||
|
<div className="mt-2 relative">
|
||||||
|
<pre className="text-xs text-gray-400 bg-gray-800 p-3 rounded overflow-x-auto max-h-40">
|
||||||
|
{error.stack}
|
||||||
|
</pre>
|
||||||
|
<button
|
||||||
|
onClick={this.copyError}
|
||||||
|
className="absolute top-2 right-2 p-1.5 bg-gray-700 hover:bg-gray-600 rounded transition-colors"
|
||||||
|
title="Copy error"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="w-4 h-4 text-green-400" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
|
<button
|
||||||
|
onClick={this.handleReset}
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-5 h-5" />
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={this.handleRefresh}
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-5 h-5" />
|
||||||
|
Refresh Page
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={this.handleGoHome}
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<Home className="w-5 h-5" />
|
||||||
|
Go Home
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500 text-center mt-4">
|
||||||
|
If the problem persists, please contact support with the error details above.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
@ -63,9 +63,9 @@ export type { LLMConfig, ModelInfo, ConfigPreset, ModelId, ReasoningStyle, Analy
|
|||||||
export { default as ContextMemoryDisplay } from './ContextMemoryDisplay';
|
export { default as ContextMemoryDisplay } from './ContextMemoryDisplay';
|
||||||
export type { ContextMessage, ContextSummary, ContextMemoryState } from './ContextMemoryDisplay';
|
export type { ContextMessage, ContextSummary, ContextMemoryState } from './ContextMemoryDisplay';
|
||||||
|
|
||||||
// Error Handling & Status (OQI-007)
|
// Error Handling & Status (OQI-007) - Re-export from shared component
|
||||||
export { default as ErrorBoundary } from './ErrorBoundary';
|
export { default as ErrorBoundary } from '../../../components/ErrorBoundary';
|
||||||
export type { ErrorBoundaryProps, ErrorBoundaryState } from './ErrorBoundary';
|
export type { ErrorBoundaryProps, ErrorBoundaryState } from '../../../components/ErrorBoundary';
|
||||||
|
|
||||||
export { default as ConnectionStatus } from './ConnectionStatus';
|
export { default as ConnectionStatus } from './ConnectionStatus';
|
||||||
export type { ConnectionState, ConnectionMetrics, ConnectionStatusProps } from './ConnectionStatus';
|
export type { ConnectionState, ConnectionMetrics, ConnectionStatusProps } from './ConnectionStatus';
|
||||||
|
|||||||
@ -3,52 +3,15 @@
|
|||||||
* API client for LLM Copilot chat endpoints
|
* API client for LLM Copilot chat endpoints
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import { apiClient } from '../lib/apiClient';
|
||||||
import type {
|
import type {
|
||||||
ChatSession,
|
ChatSession,
|
||||||
SendMessageResponse,
|
SendMessageResponse,
|
||||||
CreateSessionResponse,
|
CreateSessionResponse,
|
||||||
} from '../types/chat.types';
|
} from '../types/chat.types';
|
||||||
|
|
||||||
// ============================================================================
|
// Uses centralized apiClient from lib/apiClient.ts (auto-refresh, multi-tab sync)
|
||||||
// API Configuration
|
// All paths prefixed with /llm/ since apiClient baseURL is /api/v1
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || 'http://localhost:3000';
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: `${API_BASE_URL}/api/v1/llm`,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add request interceptor for auth token
|
|
||||||
api.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add response interceptor for error handling
|
|
||||||
api.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
(error) => {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
// Token expired or invalid
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Chat Service
|
// Chat Service
|
||||||
@ -59,7 +22,7 @@ export const chatService = {
|
|||||||
* Create a new chat session
|
* Create a new chat session
|
||||||
*/
|
*/
|
||||||
async createSession(): Promise<CreateSessionResponse> {
|
async createSession(): Promise<CreateSessionResponse> {
|
||||||
const response = await api.post('/sessions');
|
const response = await apiClient.post('/llm/sessions');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -67,7 +30,7 @@ export const chatService = {
|
|||||||
* Get all chat sessions for the current user
|
* Get all chat sessions for the current user
|
||||||
*/
|
*/
|
||||||
async getSessions(): Promise<ChatSession[]> {
|
async getSessions(): Promise<ChatSession[]> {
|
||||||
const response = await api.get('/sessions');
|
const response = await apiClient.get('/llm/sessions');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -75,7 +38,7 @@ export const chatService = {
|
|||||||
* Get a specific chat session with all messages
|
* Get a specific chat session with all messages
|
||||||
*/
|
*/
|
||||||
async getSession(sessionId: string): Promise<ChatSession> {
|
async getSession(sessionId: string): Promise<ChatSession> {
|
||||||
const response = await api.get(`/sessions/${sessionId}`);
|
const response = await apiClient.get(`/llm/sessions/${sessionId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -86,7 +49,7 @@ export const chatService = {
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
message: string
|
message: string
|
||||||
): Promise<SendMessageResponse> {
|
): Promise<SendMessageResponse> {
|
||||||
const response = await api.post(`/sessions/${sessionId}/chat`, {
|
const response = await apiClient.post(`/llm/sessions/${sessionId}/chat`, {
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@ -96,14 +59,14 @@ export const chatService = {
|
|||||||
* Delete a chat session
|
* Delete a chat session
|
||||||
*/
|
*/
|
||||||
async deleteSession(sessionId: string): Promise<void> {
|
async deleteSession(sessionId: string): Promise<void> {
|
||||||
await api.delete(`/sessions/${sessionId}`);
|
await apiClient.delete(`/llm/sessions/${sessionId}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quick analysis of a symbol (public endpoint, no auth required)
|
* Quick analysis of a symbol (public endpoint, no auth required)
|
||||||
*/
|
*/
|
||||||
async analyzeSymbol(symbol: string): Promise<{ analysis: string }> {
|
async analyzeSymbol(symbol: string): Promise<{ analysis: string }> {
|
||||||
const response = await api.get(`/analyze/${symbol}`);
|
const response = await apiClient.get(`/llm/analyze/${symbol}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* API client for courses, lessons, quizzes, and gamification
|
* API client for courses, lessons, quizzes, and gamification
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import { apiClient as api } from '../lib/apiClient';
|
||||||
import type {
|
import type {
|
||||||
CourseListItem,
|
CourseListItem,
|
||||||
CourseDetail,
|
CourseDetail,
|
||||||
@ -27,23 +27,7 @@ import type {
|
|||||||
GamificationSummary,
|
GamificationSummary,
|
||||||
} from '../types/education.types';
|
} from '../types/education.types';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || '/api/v1';
|
// Uses centralized apiClient from lib/apiClient.ts (auto-refresh, multi-tab sync)
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add auth token to requests
|
|
||||||
api.interceptors.request.use((config) => {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Categories
|
// Categories
|
||||||
|
|||||||
@ -3,9 +3,10 @@
|
|||||||
* API client for investment accounts, products, transactions, and withdrawals
|
* API client for investment accounts, products, transactions, and withdrawals
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import { apiClient } from '../lib/apiClient';
|
||||||
|
|
||||||
const API_BASE = '/api/v1/investment';
|
// apiClient baseURL is /api/v1, so prefix paths with /investment
|
||||||
|
const API_PREFIX = '/investment';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@ -120,12 +121,12 @@ export interface AccountDetail extends InvestmentAccount {
|
|||||||
|
|
||||||
export async function getProducts(riskProfile?: string): Promise<Product[]> {
|
export async function getProducts(riskProfile?: string): Promise<Product[]> {
|
||||||
const params = riskProfile ? { riskProfile } : {};
|
const params = riskProfile ? { riskProfile } : {};
|
||||||
const response = await axios.get(`${API_BASE}/products`, { params });
|
const response = await apiClient.get(`${API_PREFIX}/products`, { params });
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProductById(productId: string): Promise<Product> {
|
export async function getProductById(productId: string): Promise<Product> {
|
||||||
const response = await axios.get(`${API_BASE}/products/${productId}`);
|
const response = await apiClient.get(`${API_PREFIX}/products/${productId}`);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ export async function getProductPerformance(
|
|||||||
productId: string,
|
productId: string,
|
||||||
period: 'week' | 'month' | '3months' | 'year' = 'month'
|
period: 'week' | 'month' | '3months' | 'year' = 'month'
|
||||||
): Promise<ProductPerformance[]> {
|
): Promise<ProductPerformance[]> {
|
||||||
const response = await axios.get(`${API_BASE}/products/${productId}/performance`, {
|
const response = await apiClient.get(`${API_PREFIX}/products/${productId}/performance`, {
|
||||||
params: { period },
|
params: { period },
|
||||||
});
|
});
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
@ -144,27 +145,27 @@ export async function getProductPerformance(
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export async function getUserAccounts(): Promise<InvestmentAccount[]> {
|
export async function getUserAccounts(): Promise<InvestmentAccount[]> {
|
||||||
const response = await axios.get(`${API_BASE}/accounts`);
|
const response = await apiClient.get(`${API_PREFIX}/accounts`);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAccountSummary(): Promise<AccountSummary> {
|
export async function getAccountSummary(): Promise<AccountSummary> {
|
||||||
const response = await axios.get(`${API_BASE}/accounts/summary`);
|
const response = await apiClient.get(`${API_PREFIX}/accounts/summary`);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAccountById(accountId: string): Promise<AccountDetail> {
|
export async function getAccountById(accountId: string): Promise<AccountDetail> {
|
||||||
const response = await axios.get(`${API_BASE}/accounts/${accountId}`);
|
const response = await apiClient.get(`${API_PREFIX}/accounts/${accountId}`);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAccount(productId: string, initialDeposit: number): Promise<InvestmentAccount> {
|
export async function createAccount(productId: string, initialDeposit: number): Promise<InvestmentAccount> {
|
||||||
const response = await axios.post(`${API_BASE}/accounts`, { productId, initialDeposit });
|
const response = await apiClient.post(`${API_PREFIX}/accounts`, { productId, initialDeposit });
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function closeAccount(accountId: string): Promise<void> {
|
export async function closeAccount(accountId: string): Promise<void> {
|
||||||
await axios.post(`${API_BASE}/accounts/${accountId}/close`);
|
await apiClient.post(`${API_PREFIX}/accounts/${accountId}/close`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -180,14 +181,14 @@ export async function getTransactions(
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
}
|
}
|
||||||
): Promise<{ transactions: Transaction[]; total: number }> {
|
): Promise<{ transactions: Transaction[]; total: number }> {
|
||||||
const response = await axios.get(`${API_BASE}/accounts/${accountId}/transactions`, {
|
const response = await apiClient.get(`${API_PREFIX}/accounts/${accountId}/transactions`, {
|
||||||
params: options,
|
params: options,
|
||||||
});
|
});
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDeposit(accountId: string, amount: number): Promise<Transaction> {
|
export async function createDeposit(accountId: string, amount: number): Promise<Transaction> {
|
||||||
const response = await axios.post(`${API_BASE}/accounts/${accountId}/deposit`, { amount });
|
const response = await apiClient.post(`${API_PREFIX}/accounts/${accountId}/deposit`, { amount });
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +200,7 @@ export async function createWithdrawal(
|
|||||||
cryptoInfo?: { network: string; address: string };
|
cryptoInfo?: { network: string; address: string };
|
||||||
}
|
}
|
||||||
): Promise<Withdrawal> {
|
): Promise<Withdrawal> {
|
||||||
const response = await axios.post(`${API_BASE}/accounts/${accountId}/withdraw`, {
|
const response = await apiClient.post(`${API_PREFIX}/accounts/${accountId}/withdraw`, {
|
||||||
amount,
|
amount,
|
||||||
...destination,
|
...destination,
|
||||||
});
|
});
|
||||||
@ -211,7 +212,7 @@ export async function createWithdrawal(
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export async function getDistributions(accountId: string): Promise<Distribution[]> {
|
export async function getDistributions(accountId: string): Promise<Distribution[]> {
|
||||||
const response = await axios.get(`${API_BASE}/accounts/${accountId}/distributions`);
|
const response = await apiClient.get(`${API_PREFIX}/accounts/${accountId}/distributions`);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +222,7 @@ export async function getDistributions(accountId: string): Promise<Distribution[
|
|||||||
|
|
||||||
export async function getWithdrawals(status?: string): Promise<Withdrawal[]> {
|
export async function getWithdrawals(status?: string): Promise<Withdrawal[]> {
|
||||||
const params = status ? { status } : {};
|
const params = status ? { status } : {};
|
||||||
const response = await axios.get(`${API_BASE}/withdrawals`, { params });
|
const response = await apiClient.get(`${API_PREFIX}/withdrawals`, { params });
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,25 +3,7 @@
|
|||||||
* API client for notifications management
|
* API client for notifications management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import { apiClient as api } from '../lib/apiClient';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || '/api/v1';
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add auth token to requests
|
|
||||||
api.interceptors.request.use((config) => {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* API client for subscriptions, payments, billing, and wallet
|
* API client for subscriptions, payments, billing, and wallet
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import { apiClient as api } from '../lib/apiClient';
|
||||||
import type {
|
import type {
|
||||||
PricingPlan,
|
PricingPlan,
|
||||||
Subscription,
|
Subscription,
|
||||||
@ -24,23 +24,7 @@ import type {
|
|||||||
PlanInterval,
|
PlanInterval,
|
||||||
} from '../types/payment.types';
|
} from '../types/payment.types';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || '/api/v1';
|
// Uses centralized apiClient from lib/apiClient.ts (auto-refresh, multi-tab sync)
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add auth token to requests
|
|
||||||
api.interceptors.request.use((config) => {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Pricing Plans
|
// Pricing Plans
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* API client for trading and market data endpoints
|
* API client for trading and market data endpoints
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import { apiClient } from '../lib/apiClient';
|
||||||
import type {
|
import type {
|
||||||
Candle,
|
Candle,
|
||||||
Ticker,
|
Ticker,
|
||||||
@ -26,43 +26,8 @@ import type {
|
|||||||
AccountSummary,
|
AccountSummary,
|
||||||
} from '../types/trading.types';
|
} from '../types/trading.types';
|
||||||
|
|
||||||
// ============================================================================
|
// Uses centralized apiClient from lib/apiClient.ts (auto-refresh, multi-tab sync)
|
||||||
// API Configuration
|
const api = apiClient;
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || '/api/v1';
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add request interceptor for auth token
|
|
||||||
api.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
(error) => Promise.reject(error)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add response interceptor for error handling
|
|
||||||
api.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
(error) => {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
// Token expired or invalid
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Market Data API
|
// Market Data API
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user