Skip to content

Commit 48891cd

Browse files
runway-github[bot]michalconsensyscursoragent
authored
chore(runway): cherry-pick fix(perps): improve connection toast (swipe dismiss, delay, styling) cp-7.64.0 (#25659)
- fix(perps): improve connection toast (swipe dismiss, delay, styling) cp-7.64.0 (#25569) ## **Description** This PR improves the Perps WebSocket connection toast (the banner that shows "Your connection is offline", "Connecting...", or "Connected" when the WebSocket state changes). ## **Changelog** CHANGELOG entry: Added swipe-to-dismiss and 1 second delay for the Perps connection banner; improved toast styling with default/muted backgrounds and highest z-index. ## **Related issues** Fixes: #25570 Jira issue: https://consensyssoftware.atlassian.net/browse/TAT-2453 ## **Manual testing steps** ```gherkin Feature: Perps connection toast Scenario: user sees and dismisses offline banner Given user is on a screen where Perps WebSocket is connected When connection drops and 1 second passes Then the "Your connection is offline" banner appears at the top And user can swipe the banner left or right to dismiss it And after dismissing, the banner does not show again until connection is restored and drops again Scenario: banner does not flicker on quick reconnect Given user is on a screen where Perps WebSocket is connected When connection drops and reconnects within 1 second Then the offline banner does not appear ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** See here https://consensyssoftware.atlassian.net/browse/TAT-2453 ### **After** <!-- [screenshots/recordings] --> <img width="1206" height="2622" alt="Simulator Screenshot - iPhone 17 Pro - 2026-02-03 at 11 39 17" src="https://github.com/user-attachments/assets/8656ba68-5dd3-4584-bae0-765aa60e1f51" /> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes toast display timing/state transitions (new 1s delay and user-dismiss suppression) and adds gesture-driven dismissal, which could affect when users see connection status banners. > > **Overview** > **Improves Perps WebSocket connection toast UX and behavior.** The toast can now be swipe-dismissed left/right; dismissing sets a `userDismissed` flag so repeated `Disconnected` banners are suppressed until reconnection, while `Connecting`/`Connected` can still show and clear the suppression. > > **Reduces banner flicker and refreshes styling.** `useWebSocketHealthToast` now delays `Disconnected`/`Connecting` toasts by 1s (and cancels the timer on reconnect/unmount), while `Connected` still shows immediately; the toast UI adds a wrapper/inner muted background and updates tests to reflect the new delay and animation/timer behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d73b504. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> [9642300](9642300) Co-authored-by: Michal Szorad <michal.szorad@consensys.net> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a1313ef commit 48891cd

7 files changed

Lines changed: 373 additions & 82 deletions

app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,49 @@ describe('PerpsWebSocketHealthToast.context', () => {
160160
expect(result.current.state.reconnectionAttempt).toBe(2);
161161
expect(result.current.state.isVisible).toBe(false);
162162
});
163+
164+
it('when hide({ userDismissed: true }), subsequent Disconnected is suppressed but Connecting and Connected show again', () => {
165+
const { result } = renderHook(() => useWebSocketHealthToastContext(), {
166+
wrapper,
167+
});
168+
169+
act(() => {
170+
result.current.show(WebSocketConnectionState.Disconnected, 1);
171+
});
172+
expect(result.current.state.isVisible).toBe(true);
173+
174+
act(() => {
175+
result.current.hide({ userDismissed: true });
176+
});
177+
expect(result.current.state.isVisible).toBe(false);
178+
179+
// Showing Disconnected again should not show (user dismissed offline)
180+
act(() => {
181+
result.current.show(WebSocketConnectionState.Disconnected, 2);
182+
});
183+
expect(result.current.state.isVisible).toBe(false);
184+
185+
// Showing Connecting should show (user sees reconnection progress)
186+
act(() => {
187+
result.current.show(WebSocketConnectionState.Connecting, 3);
188+
});
189+
expect(result.current.state.isVisible).toBe(true);
190+
191+
// Showing Connected shows "online" toast and clears userDismissed
192+
act(() => {
193+
result.current.show(WebSocketConnectionState.Connected, 0);
194+
});
195+
expect(result.current.state.isVisible).toBe(true);
196+
197+
act(() => {
198+
result.current.hide();
199+
});
200+
// Next Disconnected will show again (userDismissed was cleared)
201+
act(() => {
202+
result.current.show(WebSocketConnectionState.Disconnected, 4);
203+
});
204+
expect(result.current.state.isVisible).toBe(true);
205+
});
163206
});
164207

165208
describe('setOnRetry()', () => {

app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.context.tsx

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import React, { createContext, useContext, useState, useCallback } from 'react';
1+
import React, {
2+
createContext,
3+
useContext,
4+
useState,
5+
useCallback,
6+
useMemo,
7+
} from 'react';
28
import { WebSocketConnectionState } from '../../controllers/types';
39

410
/** No-op function for context defaults */
@@ -13,6 +19,12 @@ export interface WebSocketHealthToastState {
1319
reconnectionAttempt: number;
1420
}
1521

22+
/** Options for hiding the toast (e.g. user swipe dismiss) */
23+
export interface WebSocketHealthToastHideOptions {
24+
/** When true, toast will not be shown again until connection is restored (Connected state) */
25+
userDismissed?: boolean;
26+
}
27+
1628
/**
1729
* Context params for controlling the WebSocket health toast.
1830
*/
@@ -22,7 +34,7 @@ export interface WebSocketHealthToastContextParams {
2234
connectionState: WebSocketConnectionState,
2335
reconnectionAttempt?: number,
2436
) => void;
25-
hide: () => void;
37+
hide: (options?: WebSocketHealthToastHideOptions) => void;
2638
onRetry?: () => void;
2739
setOnRetry: (callback: () => void) => void;
2840
}
@@ -50,33 +62,73 @@ export const WebSocketHealthToastProvider: React.FC<{
5062
children: React.ReactNode;
5163
}> = ({ children }) => {
5264
const [state, setState] = useState<WebSocketHealthToastState>(defaultState);
53-
const [onRetry, setOnRetryCallback] = useState<(() => void) | undefined>(
54-
undefined,
55-
);
65+
const [userDismissed, setUserDismissed] = useState(false);
66+
const [onRetryCallback, setOnRetryCallback] = useState<
67+
(() => void) | undefined
68+
>(undefined);
5669

5770
const show = useCallback(
5871
(connectionState: WebSocketConnectionState, reconnectionAttempt = 0) => {
72+
const isConnected =
73+
connectionState === WebSocketConnectionState.Connected;
74+
const isConnecting =
75+
connectionState === WebSocketConnectionState.Connecting;
76+
77+
// When connection is restored, always show "online" toast and clear dismiss state
78+
// (handled first so we never skip due to stale userDismissed closure)
79+
if (isConnected) {
80+
setUserDismissed(false);
81+
setState({
82+
isVisible: true,
83+
connectionState,
84+
reconnectionAttempt,
85+
});
86+
return;
87+
}
88+
89+
// When reconnecting, clear userDismissed so "connecting" and later "online" toasts can show
90+
if (isConnecting) {
91+
setUserDismissed(false);
92+
}
93+
94+
// Don't show Disconnected if user previously dismissed (until connection is restoring/restored).
95+
// Connecting is always shown so user sees progress after having dismissed "offline".
96+
if (userDismissed && !isConnecting) {
97+
return;
98+
}
5999
setState({
60100
isVisible: true,
61101
connectionState,
62102
reconnectionAttempt,
63103
});
64104
},
65-
[],
105+
[userDismissed],
66106
);
67107

68-
const hide = useCallback(() => {
108+
const hide = useCallback((options?: WebSocketHealthToastHideOptions) => {
109+
if (options?.userDismissed) {
110+
setUserDismissed(true);
111+
}
69112
setState((prev) => ({ ...prev, isVisible: false }));
70113
}, []);
71114

72115
const setOnRetry = useCallback((callback: () => void) => {
73116
setOnRetryCallback(() => callback);
74117
}, []);
75118

119+
const contextValue = useMemo(
120+
() => ({
121+
state,
122+
show,
123+
hide,
124+
onRetry: onRetryCallback,
125+
setOnRetry,
126+
}),
127+
[state, show, hide, onRetryCallback, setOnRetry],
128+
);
129+
76130
return (
77-
<WebSocketHealthToastContext.Provider
78-
value={{ state, show, hide, onRetry, setOnRetry }}
79-
>
131+
<WebSocketHealthToastContext.Provider value={contextValue}>
80132
{children}
81133
</WebSocketHealthToastContext.Provider>
82134
);

app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.styles.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,12 @@ const styleSheet = (params: { theme: Theme }) => {
1313
right: 12,
1414
zIndex: 9999,
1515
},
16-
// Inner toast content
17-
toast: {
18-
flexDirection: 'row',
19-
alignItems: 'center',
20-
gap: 12,
21-
paddingVertical: 12,
22-
paddingHorizontal: 16,
16+
// Wrapper with default background (close wrap: same edges, radius)
17+
toastWrapper: {
2318
borderRadius: 12,
2419
backgroundColor: colors.background.default,
20+
padding: 2,
21+
overflow: 'hidden',
2522
// Shadow for elevation
2623
shadowColor: colors.shadow.default,
2724
shadowOffset: {
@@ -32,6 +29,16 @@ const styleSheet = (params: { theme: Theme }) => {
3229
shadowRadius: 8,
3330
elevation: 8,
3431
},
32+
// Inner toast content (muted background)
33+
toast: {
34+
flexDirection: 'row',
35+
alignItems: 'center',
36+
gap: 12,
37+
paddingVertical: 12,
38+
paddingHorizontal: 16,
39+
borderRadius: 10,
40+
backgroundColor: colors.background.muted,
41+
},
3542
// Icon container
3643
iconContainer: {
3744
width: 32,

app/components/UI/Perps/components/PerpsWebSocketHealthToast/PerpsWebSocketHealthToast.test.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import React from 'react';
6-
import { render, fireEvent, waitFor } from '@testing-library/react-native';
6+
import { render, fireEvent, waitFor, act } from '@testing-library/react-native';
77
import PerpsWebSocketHealthToast from './PerpsWebSocketHealthToast';
88
import { WebSocketConnectionState } from '../../controllers/types';
99
import { PerpsWebSocketHealthToastSelectorsIDs } from '../../Perps.testIds';
@@ -248,8 +248,10 @@ describe('PerpsWebSocketHealthToast', () => {
248248

249249
render(<PerpsWebSocketHealthToast />);
250250

251-
// Fast-forward time
252-
jest.advanceTimersByTime(3000);
251+
// Fast-forward time (wrap in act so Animated callbacks flush)
252+
await act(async () => {
253+
jest.advanceTimersByTime(3000);
254+
});
253255

254256
expect(mockHide).toHaveBeenCalled();
255257
});

0 commit comments

Comments
 (0)