[Bug]: Flashing on all bottom sheets in Send flow
Describe the bug
When clicking into parts of the confirmation screen bottom sheets, there is consistent flashing when clicking again.
Expected behavior
It shouldn't flash when opening bottom sheets through the send flow.
Screenshots/Recordings
https://github.com/user-attachments/assets/e55d2f7b-c70e-46dc-b578-508df0704e5b https://github.com/user-attachments/assets/e4513be4-6156-4144-9129-2d6aa1ace3ec
Steps to reproduce
- Do a send transaction
- Click on any pill where a bottom sheet propagates
- Click outside of the bottom sheet
- Notice flash
Error messages or log output
Detection stage
During release testing
Version
7.61
Build number
3242
Build type
None
Device
iPhone Max Pro 13
Operating system
iOS
Additional context
No response
Severity
No response
Cursor Analysis
⚠️ Note: This is an AI-generated analysis based on user-submitted issue content. Please verify suggestions before implementing.
Analysis
1. Problem
Bottom sheets flash when opening from pills in the Send flow confirmation screen, especially when reopening after closing. The sheet briefly appears in the wrong position before animating.
2. Root Cause
Two likely causes:
Cause 1: State not reset on screen reuse
BottomSheetDialoginitializescurrentYOffsettoscreenHeight(off-screen).- The opening animation runs only after layout measurement (
onLayout). - When React Navigation reuses a screen,
isMounted.currentstaystrue, so the opening animation may be skipped, causing a flash.
Cause 2: Initial render visibility
- The sheet renders with
currentYOffset = screenHeightbeforeonLayoutfires. - On screen reuse, it may briefly show the previous animated state before resetting.
3. Target Repo
This repo (MetaMask Mobile). Fix in:
app/component-library/components/BottomSheets/BottomSheet/foundation/BottomSheetDialog/BottomSheetDialog.tsx
4. Solutions
Solution 1: Reset state on unmount/focus (recommended)
- Reset
isMountedandcurrentYOffsetwhen the component unmounts or loses focus. - Use
useFocusEffectto handle screen focus/blur.
Pros:
- Handles screen reuse correctly
- Minimal changes
- Works with React Navigation lifecycle
Cons:
- Requires adding navigation focus handling
Solution 2: Hide sheet until layout is measured
- Start with
opacity: 0ordisplay: 'none'until the first layout measurement completes.
Pros:
- Prevents visual flash
- Simple change
Cons:
- May cause a brief blank state
- Doesn’t address root cause
Solution 3: Reset on navigation events
- Use navigation listeners to reset state when navigating away/back.
Pros:
- Explicit control over state
Cons:
- More complex
- Requires navigation prop access
Recommended Fix
Implement Solution 1: reset state on unmount and use focus effects.
// app/component-library/components/BottomSheets/BottomSheet/foundation/BottomSheetDialog/BottomSheetDialog.tsx
// Add import
import { useFocusEffect } from '@react-navigation/native';
// Inside BottomSheetDialog component, add:
// Reset state when component unmounts or loses focus
useFocusEffect(
useCallback(() => {
// Reset on mount/focus
isMounted.current = false;
currentYOffset.value = screenHeight;
return () => {
// Cleanup on unmount/blur
isMounted.current = false;
currentYOffset.value = screenHeight;
};
}, [screenHeight, currentYOffset])
);
// Also add cleanup in useEffect for unmount
useEffect(() => {
return () => {
isMounted.current = false;
currentYOffset.value = screenHeight;
};
}, [screenHeight, currentYOffset]);
Code Diff:
--- a/app/component-library/components/BottomSheets/BottomSheet/foundation/BottomSheetDialog/BottomSheetDialog.tsx
+++ b/app/component-library/components/BottomSheets/BottomSheet/foundation/BottomSheetDialog/BottomSheetDialog.tsx
@@ -1,6 +1,7 @@
/* eslint-disable react/prop-types */
// Third party dependencies.
+import { useFocusEffect } from '@react-navigation/native';
import React, {
forwardRef,
useCallback,
@@ -191,6 +192,20 @@ const BottomSheetDialog = forwardRef<
[onCloseDialog],
);
+ // Reset state when screen gains/loses focus to prevent flashing on screen reuse
+ useFocusEffect(
+ useCallback(() => {
+ // Reset on mount/focus
+ isMounted.current = false;
+ currentYOffset.value = screenHeight;
+
+ return () => {
+ // Cleanup on unmount/blur
+ isMounted.current = false;
+ currentYOffset.value = screenHeight;
+ };
+ }, [screenHeight, currentYOffset])
+ );
+
useEffect(
() =>
// Automatically handles animation when content changes
Note: useFocusEffect requires the component to be within a navigation context. If not available, use a useEffect cleanup instead:
useEffect(() => {
// Reset on mount
isMounted.current = false;
currentYOffset.value = screenHeight;
return () => {
// Reset on unmount
isMounted.current = false;
currentYOffset.value = screenHeight;
};
}, [screenHeight, currentYOffset]);
This ensures the bottom sheet resets to its initial state when the screen is reused, preventing the flash.
Automated analysis by Cursor CLI