Skip to content

Commit bae7faf

Browse files
chore(runway): cherry-pick fix: compliance modal appear once on asset page cp-7.74.0 (#29208)
- fix: compliance modal appear once on asset page cp-7.74.0 (#29201) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> On the token details (spot assets) page, tapping **Long** or **Short** when a wallet is OFAC-blocked would open the `AccessRestrictedModal` correctly the first time, but pressing either button again would do nothing — the modal could never be reopened without leaving and re-entering the screen. **Root cause:** `TokenDetailsActions` sets a `navigationLockRef` to `true` on every button tap to prevent rapid double-navigation. It is cleared by three paths: screen refocus, navigation state change, or an explicit `resetNavigationLockRef` callback passed in from the parent. When `useComplianceGate`'s `gate()` detects a blocked wallet it shows `AccessRestrictedModal` (rendered at the app root by `AccessRestrictedProvider`) and returns early — no navigation occurs, the screen never blurs, and the external reset was never called. The lock stayed `true` permanently, silently swallowing all subsequent Long/Short presses. **Fix:** Added `.finally(() => resetNavigationLockRef.current?.())` to both `handleLongPress` and `handleShortPress` in `AssetOverviewContent.tsx`. This mirrors what the geo-block path already does inside `closeEligibilityModal`. The call is a safe no-op when `handlePerpsAction` navigates away, since the focus/state listeners also clear the lock in that case. Additionally, `AccessRestrictedModal` was migrated from the legacy internal `BottomSheet` component to the `BottomSheet` exported by `@metamask/design-system-react-native`, which is the single source of truth for bottom sheets in the codebase. The `shouldNavigateBack={false}` prop was also removed — the design-system `BottomSheet` does not expose this prop and the modal is rendered at the app root so no back-navigation is possible regardless. ## **Changelog** CHANGELOG entry: Fixed an issue where the access-restricted compliance modal could not be reopened after being dismissed on the token details page. ## **Related issues** No issue: bug fix for observed regression on the compliance-gated Long/Short buttons on the token details screen. ## **Manual testing steps** ```gherkin Feature: Access-restricted compliance modal on the token details page Background: Given I am logged into MetaMask Mobile And my wallet address is flagged as OFAC-blocked (compliance check returns blocked) And I am on the token details page for a token that has a Perps market (e.g. ETH) Scenario: user can reopen the compliance modal after dismissing it Given I see the Long and Short action buttons When user taps the Long button Then the access-restricted bottom sheet modal appears When user dismisses the modal by tapping the close button Then the modal is dismissed When user taps the Long button again Then the access-restricted bottom sheet modal appears again Scenario: user can open the compliance modal via the Short button after a previous dismissal Given I see the Long and Short action buttons When user taps the Short button Then the access-restricted bottom sheet modal appears When user dismisses the modal by tapping the close button Then the modal is dismissed When user taps the Short button again Then the access-restricted bottom sheet modal appears again ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [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. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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] > **Low Risk** > Low risk bug fix that only adjusts button press flow and a modal implementation; main risk is unintended double-tap navigation if the lock is cleared too aggressively. > > **Overview** > Fixes an issue where `Long`/`Short` on the token details screen could only open the compliance access-restricted modal once by ensuring the `TokenDetailsActions` navigation lock is always released when `useComplianceGate().gate()` completes (including non-navigating paths). > > Migrates `AccessRestrictedModal` to use the design-system `BottomSheet` component and removes the unsupported `shouldNavigateBack` prop. Adds tests to assert repeated `Long`/`Short` presses re-invoke `gate()` when it resolves without navigating. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5a47cb7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> [0b88c75](0b88c75) Co-authored-by: Alejandro Garcia Anglada <aganglada@gmail.com>
1 parent a5e06f1 commit bae7faf

3 files changed

Lines changed: 58 additions & 3 deletions

File tree

app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
FontWeight,
88
ButtonBase,
99
ButtonBaseSize,
10+
BottomSheet,
1011
BottomSheetHeader,
1112
} from '@metamask/design-system-react-native';
12-
import BottomSheet from '../../../../component-library/components/BottomSheets/BottomSheet';
1313
import { strings } from '../../../../../locales/i18n';
1414
import { AccessRestrictedModalProps } from './AccessRestrictedModal.types';
1515
import { AccessRestrictedModalSelectorsIDs } from './AccessRestrictedModal.testIds';
@@ -23,7 +23,6 @@ const AccessRestrictedModal: React.FC<AccessRestrictedModalProps> = ({
2323

2424
return (
2525
<BottomSheet
26-
shouldNavigateBack={false}
2726
onClose={onClose}
2827
testID={AccessRestrictedModalSelectorsIDs.BOTTOM_SHEET}
2928
>

app/components/UI/TokenDetails/components/AssetOverviewContent.test.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,12 @@ jest.mock('@react-navigation/native', () => {
131131
};
132132
});
133133

134+
const mockGate = jest.fn(
135+
async (action: () => Promise<unknown>) => await action(),
136+
);
134137
jest.mock('../../Compliance', () => ({
135138
useComplianceGate: () => ({
136-
gate: (action: () => Promise<unknown>) => action(),
139+
gate: (action: () => Promise<unknown>) => mockGate(action),
137140
isBlocked: false,
138141
isComplianceEnabled: false,
139142
checkCompliance: jest.fn(),
@@ -325,6 +328,51 @@ describe('AssetOverviewContent', () => {
325328
expect(mockTrack).not.toHaveBeenCalled();
326329
});
327330

331+
it('releases the navigation lock when gate() settles without navigating so Long can be pressed again', async () => {
332+
// Simulate a blocked wallet: gate() shows the compliance modal and
333+
// returns without invoking the action. Without releasing the nav lock
334+
// in the finally(), the second press would be silently ignored.
335+
mockGate.mockImplementationOnce(async () => undefined);
336+
mockGate.mockImplementationOnce(async () => undefined);
337+
338+
const { getByTestId } = renderWithProvider(
339+
<AssetOverviewContent {...defaultProps} />,
340+
{ state: createState(true) },
341+
);
342+
343+
await act(async () => {
344+
fireEvent.press(getByTestId(TokenOverviewSelectorsIDs.LONG_BUTTON));
345+
});
346+
expect(mockGate).toHaveBeenCalledTimes(1);
347+
348+
await act(async () => {
349+
fireEvent.press(getByTestId(TokenOverviewSelectorsIDs.LONG_BUTTON));
350+
});
351+
expect(mockGate).toHaveBeenCalledTimes(2);
352+
expect(mockHandlePerpsAction).not.toHaveBeenCalled();
353+
});
354+
355+
it('releases the navigation lock when gate() settles without navigating so Short can be pressed again', async () => {
356+
mockGate.mockImplementationOnce(async () => undefined);
357+
mockGate.mockImplementationOnce(async () => undefined);
358+
359+
const { getByTestId } = renderWithProvider(
360+
<AssetOverviewContent {...defaultProps} />,
361+
{ state: createState(true) },
362+
);
363+
364+
await act(async () => {
365+
fireEvent.press(getByTestId(TokenOverviewSelectorsIDs.SHORT_BUTTON));
366+
});
367+
expect(mockGate).toHaveBeenCalledTimes(1);
368+
369+
await act(async () => {
370+
fireEvent.press(getByTestId(TokenOverviewSelectorsIDs.SHORT_BUTTON));
371+
});
372+
expect(mockGate).toHaveBeenCalledTimes(2);
373+
expect(mockHandlePerpsAction).not.toHaveBeenCalled();
374+
});
375+
328376
it('closes geo block modal when closeEligibilityModal is called', () => {
329377
const { getByTestId } = renderWithProvider(
330378
<AssetOverviewContent {...defaultProps} />,

app/components/UI/TokenDetails/components/AssetOverviewContent.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ const AssetOverviewContent: React.FC<AssetOverviewContentProps> = ({
276276
return;
277277
}
278278
handlePerpsAction?.('long');
279+
}).finally(() => {
280+
// Release the TokenDetailsActions nav lock whenever gate() settles
281+
// without navigating (compliance block modal or geo-block tooltip).
282+
// Safe no-op if handlePerpsAction navigated since the focus/state
283+
// listeners also clear the lock.
284+
resetNavigationLockRef.current?.();
279285
}),
280286
[gate, isEligible, track, handlePerpsAction],
281287
);
@@ -294,6 +300,8 @@ const AssetOverviewContent: React.FC<AssetOverviewContentProps> = ({
294300
return;
295301
}
296302
handlePerpsAction?.('short');
303+
}).finally(() => {
304+
resetNavigationLockRef.current?.();
297305
}),
298306
[gate, isEligible, track, handlePerpsAction],
299307
);

0 commit comments

Comments
 (0)