Skip to content

Commit cc778a1

Browse files
feat(perps): add optimistic updates for take profit and stop loss prices
1 parent c77dd1a commit cc778a1

4 files changed

Lines changed: 731 additions & 1 deletion

File tree

app/components/UI/Perps/hooks/usePerpsTPSLUpdate.test.ts

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { ToastContext } from '../../../../component-library/components/Toast';
44
import { usePerpsTPSLUpdate } from './usePerpsTPSLUpdate';
55
import { usePerpsTrading } from './usePerpsTrading';
66
import usePerpsToasts from './usePerpsToasts';
7+
import { usePerpsStream } from '../providers/PerpsStreamManager';
78

89
jest.mock('./usePerpsTrading');
910
jest.mock('./usePerpsToasts');
11+
jest.mock('../providers/PerpsStreamManager');
1012
jest.mock('../../../../../locales/i18n', () => ({
1113
strings: jest.fn((key) => key),
1214
}));
@@ -40,6 +42,7 @@ jest.mock('expo-haptics', () => ({
4042
describe('usePerpsTPSLUpdate', () => {
4143
const mockUpdatePositionTPSL = jest.fn();
4244
const mockShowToast = jest.fn();
45+
const mockUpdatePositionTPSLOptimistic = jest.fn();
4346
const mockToastContext = {
4447
toastRef: {
4548
current: {
@@ -49,6 +52,12 @@ describe('usePerpsTPSLUpdate', () => {
4952
},
5053
};
5154

55+
const mockStream = {
56+
positions: {
57+
updatePositionTPSLOptimistic: mockUpdatePositionTPSLOptimistic,
58+
},
59+
};
60+
5261
const mockPerpsToastOptions = {
5362
positionManagement: {
5463
tpsl: {
@@ -126,6 +135,8 @@ describe('usePerpsTPSLUpdate', () => {
126135
showToast: mockShowToast,
127136
PerpsToastOptions: mockPerpsToastOptions,
128137
});
138+
139+
(usePerpsStream as jest.Mock).mockReturnValue(mockStream);
129140
});
130141

131142
const renderHookWithToast = (options = {}) => {
@@ -267,7 +278,7 @@ describe('usePerpsTPSLUpdate', () => {
267278
);
268279
});
269280

270-
it('should use toast configurations with correct haptics types', () => {
281+
it('uses toast configurations with correct haptics types', () => {
271282
// Verify success toast has correct haptics type
272283
expect(
273284
mockPerpsToastOptions.positionManagement.tpsl.updateTPSLSuccess
@@ -281,4 +292,145 @@ describe('usePerpsTPSLUpdate', () => {
281292
);
282293
expect(errorToast.hapticsType).toBe('error');
283294
});
295+
296+
describe('optimistic updates', () => {
297+
it('applies optimistic update to position cache on successful API response', async () => {
298+
const { result } = renderHookWithToast();
299+
const position = createMockPosition();
300+
const takeProfitPrice = '3300';
301+
const stopLossPrice = '2700';
302+
303+
mockUpdatePositionTPSL.mockResolvedValue({ success: true });
304+
305+
await act(async () => {
306+
await result.current.handleUpdateTPSL(
307+
position,
308+
takeProfitPrice,
309+
stopLossPrice,
310+
);
311+
});
312+
313+
expect(mockUpdatePositionTPSLOptimistic).toHaveBeenCalledWith(
314+
'ETH',
315+
takeProfitPrice,
316+
stopLossPrice,
317+
);
318+
});
319+
320+
it('applies optimistic update with undefined take profit price', async () => {
321+
const { result } = renderHookWithToast();
322+
const position = createMockPosition();
323+
const stopLossPrice = '2700';
324+
325+
mockUpdatePositionTPSL.mockResolvedValue({ success: true });
326+
327+
await act(async () => {
328+
await result.current.handleUpdateTPSL(
329+
position,
330+
undefined,
331+
stopLossPrice,
332+
);
333+
});
334+
335+
expect(mockUpdatePositionTPSLOptimistic).toHaveBeenCalledWith(
336+
'ETH',
337+
undefined,
338+
stopLossPrice,
339+
);
340+
});
341+
342+
it('applies optimistic update with undefined stop loss price', async () => {
343+
const { result } = renderHookWithToast();
344+
const position = createMockPosition();
345+
const takeProfitPrice = '3300';
346+
347+
mockUpdatePositionTPSL.mockResolvedValue({ success: true });
348+
349+
await act(async () => {
350+
await result.current.handleUpdateTPSL(
351+
position,
352+
takeProfitPrice,
353+
undefined,
354+
);
355+
});
356+
357+
expect(mockUpdatePositionTPSLOptimistic).toHaveBeenCalledWith(
358+
'ETH',
359+
takeProfitPrice,
360+
undefined,
361+
);
362+
});
363+
364+
it('applies optimistic update with both TP/SL undefined (removing both)', async () => {
365+
const { result } = renderHookWithToast();
366+
const position = createMockPosition({
367+
takeProfitPrice: '3500',
368+
stopLossPrice: '2500',
369+
});
370+
371+
mockUpdatePositionTPSL.mockResolvedValue({ success: true });
372+
373+
await act(async () => {
374+
await result.current.handleUpdateTPSL(position, undefined, undefined);
375+
});
376+
377+
expect(mockUpdatePositionTPSLOptimistic).toHaveBeenCalledWith(
378+
'ETH',
379+
undefined,
380+
undefined,
381+
);
382+
});
383+
384+
it('does not apply optimistic update on API failure response', async () => {
385+
const { result } = renderHookWithToast();
386+
const position = createMockPosition();
387+
388+
mockUpdatePositionTPSL.mockResolvedValue({
389+
success: false,
390+
error: 'Network error',
391+
});
392+
393+
await act(async () => {
394+
await result.current.handleUpdateTPSL(position, '3300', '2700');
395+
});
396+
397+
expect(mockUpdatePositionTPSLOptimistic).not.toHaveBeenCalled();
398+
});
399+
400+
it('does not apply optimistic update on exception', async () => {
401+
const { result } = renderHookWithToast();
402+
const position = createMockPosition();
403+
const error = new Error('Network error');
404+
405+
mockUpdatePositionTPSL.mockRejectedValue(error);
406+
407+
await act(async () => {
408+
await result.current.handleUpdateTPSL(position, '3300', '2700');
409+
});
410+
411+
expect(mockUpdatePositionTPSLOptimistic).not.toHaveBeenCalled();
412+
});
413+
414+
it('applies optimistic update before showing success toast', async () => {
415+
const callOrder: string[] = [];
416+
417+
mockUpdatePositionTPSLOptimistic.mockImplementation(() => {
418+
callOrder.push('optimistic');
419+
});
420+
mockShowToast.mockImplementation(() => {
421+
callOrder.push('toast');
422+
});
423+
424+
const { result } = renderHookWithToast();
425+
const position = createMockPosition();
426+
427+
mockUpdatePositionTPSL.mockResolvedValue({ success: true });
428+
429+
await act(async () => {
430+
await result.current.handleUpdateTPSL(position, '3300', '2700');
431+
});
432+
433+
expect(callOrder).toEqual(['optimistic', 'toast']);
434+
});
435+
});
284436
});

app/components/UI/Perps/hooks/usePerpsTPSLUpdate.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { usePerpsTrading } from './usePerpsTrading';
55
import type { Position, TPSLTrackingData } from '../controllers/types';
66
import { captureException } from '@sentry/react-native';
77
import usePerpsToasts from './usePerpsToasts';
8+
import { usePerpsStream } from '../providers/PerpsStreamManager';
89

910
interface UseTPSLUpdateOptions {
1011
onSuccess?: () => void;
@@ -19,6 +20,7 @@ interface UseTPSLUpdateOptions {
1920
export function usePerpsTPSLUpdate(options?: UseTPSLUpdateOptions) {
2021
const { updatePositionTPSL } = usePerpsTrading();
2122
const [isUpdating, setIsUpdating] = useState(false);
23+
const stream = usePerpsStream();
2224

2325
const { showToast, PerpsToastOptions } = usePerpsToasts();
2426

@@ -43,6 +45,14 @@ export function usePerpsTPSLUpdate(options?: UseTPSLUpdateOptions) {
4345
if (result.success) {
4446
DevLogger.log('Position TP/SL updated successfully:', result);
4547

48+
// Apply optimistic update immediately for better UX
49+
// This updates the UI before the WebSocket confirms the change
50+
stream.positions.updatePositionTPSLOptimistic(
51+
position.coin,
52+
takeProfitPrice,
53+
stopLossPrice,
54+
);
55+
4656
showToast(
4757
PerpsToastOptions.positionManagement.tpsl.updateTPSLSuccess,
4858
);
@@ -112,6 +122,7 @@ export function usePerpsTPSLUpdate(options?: UseTPSLUpdateOptions) {
112122
showToast,
113123
PerpsToastOptions.positionManagement.tpsl,
114124
options,
125+
stream,
115126
],
116127
);
117128

0 commit comments

Comments
 (0)