diff --git a/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx b/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx index 1397eb9149b..64a62234b80 100644 --- a/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx +++ b/app/components/UI/Charts/AdvancedChart/AdvancedChart.tsx @@ -120,6 +120,8 @@ const AdvancedChart = forwardRef( const prevChartTypeRef = useRef(chartType); const prevOhlcvDataRef = useRef([]); const prevOhlcvSeriesKeyRef = useRef(undefined); + /** When non-null, `ohlcvData` is still the previous series' array; skip sync until the hook replaces it. */ + const ohlcvSeriesStaleSnapshotRef = useRef(null); const tradingViewOpenInterceptRef = useRef(0); const htmlContent = useMemo( @@ -141,6 +143,7 @@ const AdvancedChart = forwardRef( prevChartTypeRef.current = undefined; prevOhlcvDataRef.current = []; prevOhlcvSeriesKeyRef.current = undefined; + ohlcvSeriesStaleSnapshotRef.current = null; }, [htmlContent]); // eslint-disable-line react-hooks/exhaustive-deps // ---- Helpers ---- @@ -171,6 +174,22 @@ const AdvancedChart = forwardRef( }, LAYOUT_SETTLE_FALLBACK_MS); }, [isChartReady, clearLayoutSettleTimeout]); + // WebView remounts when `key` changes; parent state would otherwise still look "ready". + // `CHART_READY` clears indicator/position/chart-type refs once the new instance loads. + useEffect(() => { + if (ohlcvSeriesKey === undefined) { + return; + } + setChartReadyCount(0); + setWebViewLoaded(false); + setLayoutSettling(false); + clearLayoutSettleTimeout(); + ohlcvSeriesStaleSnapshotRef.current = null; + activeIndicatorsRef.current.clear(); + prevPositionLinesRef.current = undefined; + prevChartTypeRef.current = undefined; + }, [ohlcvSeriesKey, clearLayoutSettleTimeout]); + useEffect( () => () => { clearLayoutSettleTimeout(); @@ -381,6 +400,7 @@ const AdvancedChart = forwardRef( prevChartTypeRef.current = undefined; prevOhlcvDataRef.current = []; prevOhlcvSeriesKeyRef.current = undefined; + ohlcvSeriesStaleSnapshotRef.current = null; webViewRef.current?.reload(); }, }), @@ -397,6 +417,14 @@ const AdvancedChart = forwardRef( useEffect(() => { if (ohlcvData.length === 0 || !webViewLoaded) return; + if (ohlcvSeriesStaleSnapshotRef.current !== null) { + if (ohlcvData !== ohlcvSeriesStaleSnapshotRef.current) { + ohlcvSeriesStaleSnapshotRef.current = null; + } else { + return; + } + } + const prevData = prevOhlcvDataRef.current; if ( @@ -404,12 +432,8 @@ const AdvancedChart = forwardRef( ohlcvSeriesKey !== prevOhlcvSeriesKeyRef.current ) { if (prevOhlcvSeriesKeyRef.current !== undefined) { - // Time range switch: ohlcvData is still stale from the previous - // period (fetch is in progress). Show skeleton, mark the key, and - // clear prevData so the fresh data triggers the length-diff branch - // on arrival — avoiding sending stale data to the WebView which - // causes a resolution race condition in TradingView. beginFullOhlcvLayoutSettle(); + ohlcvSeriesStaleSnapshotRef.current = ohlcvData; prevOhlcvSeriesKeyRef.current = ohlcvSeriesKey; prevOhlcvDataRef.current = []; return; @@ -561,6 +585,7 @@ const AdvancedChart = forwardRef( { expect(getByTestId('advanced-chart-skeleton')).toBeOnTheScreen(); + const webViewAfterRerender = getByTestId('mock-webview'); act(() => { - webView.props.onMessage({ + webViewAfterRerender.props.onLoadEnd(); + }); + act(() => { + webViewAfterRerender.props.onMessage({ + nativeEvent: { + data: JSON.stringify({ type: 'CHART_READY', payload: {} }), + }, + }); + }); + + act(() => { + webViewAfterRerender.props.onMessage({ nativeEvent: { data: JSON.stringify({ type: 'CHART_LAYOUT_SETTLED', payload: {} }), }, @@ -201,6 +213,22 @@ describe('AdvancedChart', () => { ); expect(setOhlcvCallsAfterKeyChange).toHaveLength(0); + // Series key remounts the WebView; load must finish before sync runs. Stale wait still applies. + const webViewAfterKeyChange = getByTestId('mock-webview'); + act(() => { + webViewAfterKeyChange.props.onLoadEnd(); + }); + + expect( + mockPostMessage.mock.calls.filter((call) => { + try { + return JSON.parse(call[0] as string).type === 'SET_OHLCV_DATA'; + } catch { + return false; + } + }), + ).toHaveLength(0); + // Fresh data arrives (same key, different bars) — NOW it should send mockPostMessage.mockClear(); rerender(); @@ -222,6 +250,59 @@ describe('AdvancedChart', () => { expect(realtimeCalls).toHaveLength(0); }); + it('reset() clears stale series snapshot so OHLCV sync runs after reload with the same data ref', () => { + const staleBars: OHLCVBar[] = [ + { time: 1000000, open: 10, high: 12, low: 9, close: 11, volume: 100 }, + ]; + const ref = React.createRef(); + const { getByTestId, rerender } = render( + , + ); + + const webViewInitial = getByTestId('mock-webview'); + act(() => { + webViewInitial.props.onLoadEnd(); + }); + + rerender( + , + ); + + const webViewAfterKeyChange = getByTestId('mock-webview'); + act(() => { + webViewAfterKeyChange.props.onLoadEnd(); + }); + + mockPostMessage.mockClear(); + + act(() => { + ref.current?.reset(); + }); + + const webViewAfterReset = getByTestId('mock-webview'); + act(() => { + webViewAfterReset.props.onLoadEnd(); + }); + + expect( + mockPostMessage.mock.calls.some((call) => { + try { + return JSON.parse(call[0] as string).type === 'SET_OHLCV_DATA'; + } catch { + return false; + } + }), + ).toBe(true); + }); + it('exposes addIndicator via ref', () => { const ref = React.createRef(); render();