Skip to content

Commit

Permalink
docs: update documentation for balance management and SSE integration
Browse files Browse the repository at this point in the history
- Add documentation for new balance components (BalanceDisplay, BalanceHandler)
- Create documentation for ContractSSEHandler component
- Add balance service REST API documentation
- Update llms.txt with new balance management section
- Update component and service cross-references
  • Loading branch information
shafin-deriv committed Feb 4, 2025
1 parent 19891e6 commit 4a8fee6
Show file tree
Hide file tree
Showing 30 changed files with 778 additions and 262 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ src/

## Development

### Conditional Rendering & Testing
Components such as Footer and BottomNav render differently based on the user's login status. Tests now use clientStore to simulate logged-in and logged-out views.

### Development Methodology

#### Test-Driven Development (TDD)
Expand Down
3 changes: 3 additions & 0 deletions STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ The Champion Trader application follows a modular architecture with clear separa
```
src/
├── components/ # Reusable UI components
│ ├── BalanceDisplay/ # Displays the user balance.
│ ├── BalanceHandler/ # Manages balance state.
│ └── ContractSSEHandler/ # Handles contract SSE streaming.
├── hooks/ # Custom React hooks
│ ├── sse/ # SSE hooks for real-time data
│ └── websocket/ # Legacy WebSocket hooks
Expand Down
6 changes: 6 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The application is structured around modular, self-contained components and uses
- [WebSocket Architecture](src/services/api/websocket/README.md): Documentation of the legacy WebSocket implementation (to be deprecated)
- [SSE Services](src/services/api/sse/README.md): Documentation of the Server-Sent Events (SSE) implementation for real-time market and contract price updates
- [State Management](src/stores/README.md): Detailed guide on Zustand store implementation, TDD approach, and state management patterns
- [Balance Management](#balance-management): Overview of balance management including [Balance Service](src/services/api/rest/balance/README.md) and components such as [BalanceDisplay](src/components/BalanceDisplay/README.md), [BalanceHandler](src/components/BalanceHandler/README.md), and [ContractSSEHandler](src/components/ContractSSEHandler/README.md)

## Development Methodology

Expand All @@ -30,18 +31,23 @@ The application is structured around modular, self-contained components and uses
- [REST API Documentation](src/services/api/rest/README.md): Available endpoints and usage examples for REST API integration
- [WebSocket Hooks](src/hooks/websocket/README.md): Legacy WebSocket hooks (to be deprecated)
- [SSE Services](src/services/api/sse/README.md): Real-time data streaming services leveraging SSE for improved reliability and performance
- [Balance Service](src/services/api/rest/balance/README.md): Documentation for balance-related REST endpoints.

## State Management

- [Store Organization](src/stores/README.md#store-organization): Detailed store structure and implementation
- [TDD for Stores](src/stores/README.md#test-driven-development): Test-first approach for store development
- [Store Guidelines](src/stores/README.md#store-guidelines): Best practices and patterns for state management
- [Client Store](src/stores/clientStore.ts): Manages client configuration including balance integration.

## Component Development

- [Component Guidelines](src/components/README.md#component-guidelines): Detailed component development process
- [Testing Patterns](src/components/README.md#test-first-implementation): Comprehensive testing examples and patterns
- [Best Practices](src/components/README.md#best-practices): Component design, testing, performance, and accessibility guidelines
- [BalanceDisplay Component](src/components/BalanceDisplay/README.md)
- [BalanceHandler Component](src/components/BalanceHandler/README.md)
- [ContractSSEHandler Component](src/components/ContractSSEHandler/README.md)

## Optional

Expand Down
105 changes: 62 additions & 43 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { lazy, Suspense, useEffect } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { lazy, Suspense, useEffect, useState } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { MainLayout } from "@/layouts/MainLayout";
import { useMarketWebSocket } from "@/hooks/websocket";
import { useContractSSE } from "@/hooks/sse";
import { ContractPriceRequest } from "@/services/api/websocket/types";
import { useClientStore } from "@/stores/clientStore";
import { ContractSSEHandler } from "@/components/ContractSSEHandler";
import { BalanceHandler } from "@/components/BalanceHandler";

const TradePage = lazy(() =>
import("@/screens/TradePage").then((module) => ({
Expand All @@ -20,42 +20,15 @@ const MenuPage = lazy(() =>
import("@/screens/MenuPage").then((module) => ({ default: module.MenuPage }))
);

// Initialize contract SSE for default parameters
const contractParams: ContractPriceRequest = {
duration: "1m",
instrument: "R_100",
trade_type: "CALL",
currency: "USD",
payout: "100",
strike: "1234.56",
};

export const App = () => {
const AppContent = () => {
// Initialize market websocket for default instrument
const { isConnected } = useMarketWebSocket("R_100", {
onConnect: () => console.log("Market WebSocket Connected"),
onError: (error) => console.log("Market WebSocket Error:", error),
onPrice: (price) => console.log("Price Update:", price),
});

const { token } = useClientStore();

const { price } = token
? useContractSSE(
contractParams,
token,
{
onPrice: (price) => console.log("Contract Price Update:", price),
onError: (error) => console.log("Contract SSE Error:", error),
}
)
: { price: null };

useEffect(() => {
if (price) {
console.log("Contract Price:", price);
}
}, [price]);
const { token, isLoggedIn } = useClientStore();

// Log connection status changes
useEffect(() => {
Expand All @@ -65,17 +38,63 @@ export const App = () => {
}, [isConnected]);

return (
<BrowserRouter>
<MainLayout>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<TradePage />} />
<Route path="/trade" element={<TradePage />} />
<MainLayout>
{token && (
<>
<ContractSSEHandler token={token} />
<BalanceHandler token={token} />
</>
)}
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<TradePage />} />
<Route path="/trade" element={<TradePage />} />
{isLoggedIn ? (
<Route path="/positions" element={<PositionsPage />} />
<Route path="/menu" element={<MenuPage />} />
</Routes>
</Suspense>
</MainLayout>
) : (
<Route path="/positions" element={<Navigate to="/menu" />} />
)}
<Route path="/menu" element={<MenuPage />} />
</Routes>
</Suspense>
</MainLayout>
);
};

export const App = () => {
const [isInitialized, setIsInitialized] = useState(false);
const { setToken } = useClientStore();

// Handle login token
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const tokenFromUrl = params.get('token');
const tokenFromStorage = localStorage.getItem('loginToken');

if (tokenFromUrl) {
localStorage.setItem('loginToken', tokenFromUrl);
setToken(tokenFromUrl);

// Remove token from URL
params.delete('token');
const newUrl = params.toString()
? `${window.location.pathname}?${params.toString()}`
: window.location.pathname;
window.history.replaceState({}, '', newUrl);
} else if (tokenFromStorage) {
setToken(tokenFromStorage);
}

setIsInitialized(true);
}, [setToken]);

if (!isInitialized) {
return <div>Initializing...</div>;
}

return (
<BrowserRouter>
<AppContent />
</BrowserRouter>
);
};
46 changes: 46 additions & 0 deletions src/components/BalanceDisplay/BalanceDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import { useClientStore } from '@/stores/clientStore';

interface BalanceDisplayProps {
onDeposit?: () => void;
depositLabel?: string;
className?: string;
loginUrl?: string;
}

export const BalanceDisplay: React.FC<BalanceDisplayProps> = ({
onDeposit,
depositLabel = 'Deposit',
className = '',
loginUrl = 'https://options-trading.deriv.ai/',
}) => {
const { isLoggedIn, balance, currency } = useClientStore();

if (!isLoggedIn) {
return (
<div className={`w-full flex items-center justify-end ${className}`}>
<a
href={loginUrl}
className="px-4 py-2 font-bold text-white bg-teal-500 rounded-3xl hover:bg-teal-600"
>
Log in
</a>
</div>
);
}

return (
<div className={`flex items-center justify-between landscape:gap-4 ${className}`}>
<div className="flex flex-col">
<span className="text-sm text-gray-700">Real</span>
<span className="text-xl font-bold text-teal-500">{balance} {currency}</span>
</div>
<button
className="px-4 py-2 font-bold border border-gray-700 rounded-3xl"
onClick={onDeposit}
>
{depositLabel}
</button>
</div>
);
};
35 changes: 35 additions & 0 deletions src/components/BalanceDisplay/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# BalanceDisplay Component

The BalanceDisplay component is responsible for presenting the user's current balance in a clear and concise manner.

## Features
- Displays the current balance along with the currency.
- Integrates with the clientStore for real-time updates.
- Built following atomic component design principles.

## Props
- **balance**: *number* — The current balance value.
- **currency**: *string* — The currency symbol or code (e.g., USD, EUR).

## Usage Example

```tsx
import { BalanceDisplay } from '@/components/BalanceDisplay';

function App() {
return (
<div>
<BalanceDisplay balance={1000} currency="USD" />
</div>
);
}

export default App;
```

## Testing
- Unit tests are located in the __tests__ folder (`__tests__/BalanceDisplay.test.tsx`), covering rendering scenarios and prop validations.

## Integration Notes
- This component retrieves balance data from the global clientStore.
- Designed with TDD in mind, ensuring reliability and ease of maintenance.
110 changes: 110 additions & 0 deletions src/components/BalanceDisplay/__tests__/BalanceDisplay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { BalanceDisplay } from '../BalanceDisplay';

jest.mock('@/stores/clientStore', () => ({
useClientStore: jest.fn(() => ({
token: 'test-token',
isLoggedIn: true,
balance: '1,000',
currency: 'USD',
setBalance: jest.fn(),
setToken: jest.fn(),
logout: jest.fn()
})),
getState: () => ({
token: 'test-token',
isLoggedIn: true,
balance: '1,000',
currency: 'USD',
setBalance: jest.fn(),
setToken: jest.fn(),
logout: jest.fn()
})
}));

describe('BalanceDisplay', () => {
beforeEach(() => {
// Reset all mocks before each test
jest.clearAllMocks();
});

describe('when logged in', () => {
beforeEach(() => {
// Import the mocked module inside the test
const { useClientStore } = require('@/stores/clientStore');
(useClientStore as jest.Mock).mockReturnValue({
isLoggedIn: true,
balance: '1,000',
currency: 'USD'
});
});

it('renders balance from store and default deposit label', () => {
render(<BalanceDisplay />);

expect(screen.getByText('Real')).toBeInTheDocument();
expect(screen.getByText('1,000 USD')).toBeInTheDocument(); // matches combined balance and currency
expect(screen.getByText('Deposit')).toBeInTheDocument();
});

it('renders custom deposit label when provided', () => {
render(<BalanceDisplay depositLabel="Add Funds" />);

expect(screen.getByText('Add Funds')).toBeInTheDocument();
});

it('calls onDeposit when deposit button is clicked', () => {
const mockOnDeposit = jest.fn();
render(<BalanceDisplay onDeposit={mockOnDeposit} />);

fireEvent.click(screen.getByText('Deposit'));
expect(mockOnDeposit).toHaveBeenCalledTimes(1);
});

it('applies custom className when provided', () => {
render(<BalanceDisplay className="custom-class" />);

expect(screen.getByText('Real').parentElement?.parentElement).toHaveClass('custom-class');
});
});

describe('when logged out', () => {
beforeEach(() => {
const { useClientStore } = require('@/stores/clientStore');
(useClientStore as jest.Mock).mockReturnValue({
isLoggedIn: false,
balance: '0',
currency: 'USD'
});
});

it('renders login button', () => {
render(<BalanceDisplay />);

expect(screen.getByText('Log in')).toBeInTheDocument();
expect(screen.queryByText('Real')).not.toBeInTheDocument();
expect(screen.queryByText('0 USD')).not.toBeInTheDocument();
});

it('renders login button with default login URL', () => {
render(<BalanceDisplay />);

const loginLink = screen.getByText('Log in');
expect(loginLink).toHaveAttribute('href', 'https://options-trading.deriv.ai/');
});

it('renders login button with custom login URL when provided', () => {
const customUrl = 'https://custom-login.example.com';
render(<BalanceDisplay loginUrl={customUrl} />);

const loginLink = screen.getByText('Log in');
expect(loginLink).toHaveAttribute('href', customUrl);
});

it('applies custom className when provided', () => {
render(<BalanceDisplay className="custom-class" />);

expect(screen.getByText('Log in').parentElement).toHaveClass('custom-class');
});
});
});
1 change: 1 addition & 0 deletions src/components/BalanceDisplay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BalanceDisplay } from './BalanceDisplay';
Loading

0 comments on commit 4a8fee6

Please sign in to comment.