feat: add access restricted modal and compliance UI infrastructure#27694
Conversation
Introduce a general-purpose AccessRestrictedModal bottom sheet that informs users their wallet has been flagged during compliance screening and provides a contact support action. Includes context provider and hook for triggering the modal from anywhere in the component tree.
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
…iance Move scattered compliance components into a single feature directory following the same pattern as Earn, Bridge, and Perps.
aganglada
left a comment
There was a problem hiding this comment.
- Bugbot found some anti-patterns
- Sonar flagged deprecations on design system components
- Unit tests are mocking too much
- using useAccessRestrictedModal is another layer of the integration, I would integrate with the useComplianceGate directly
- Also wondering why do we need a context in this case
…and fix test anti-patterns Replace deprecated component-library imports (Text, BottomSheetHeader) with @metamask/design-system-react-native equivalents, swap Pressable for ButtonBase, use toBeOnTheScreen() over toBeTruthy(), reference testID constants instead of raw strings, and remove unnecessary BottomSheetHeader/useTailwind mocks.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #27694 +/- ##
==========================================
+ Coverage 82.34% 82.51% +0.16%
==========================================
Files 4787 4807 +20
Lines 123646 123959 +313
Branches 27511 27624 +113
==========================================
+ Hits 101822 102286 +464
+ Misses 14779 14608 -171
- Partials 7045 7065 +20 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…blocked useAccountGroupCompliance now calls useAccessRestrictedModal and automatically displays the modal when any address in the selected account group is blocked.
Integrate the AccessRestrictedProvider at the app root to enable wallet compliance checks and the access restricted modal globally.
…clears The useEffect in useAccountGroupCompliance only showed the modal when isBlocked became true but never hid it when switching to a non-blocked account group, leaving a stale restriction modal visible.
| const infuraProjectId = InfuraKey === 'null' ? '' : InfuraKey; | ||
|
|
||
| // Support | ||
| export const METAMASK_SUPPORT_URL = 'https://support.metamask.io'; |
There was a problem hiding this comment.
I think there is an env variable for this?
There was a problem hiding this comment.
I see it hardcoded like this but not an env variable
let supportUrl = 'https://support.metamask.io';
///: BEGIN:ONLY_INCLUDE_IF(beta)
supportUrl = 'https://intercom.help/internal-beta-testing/en/';
///: END:ONLY_INCLUDE_IF
| complianceGate.isBlocked, | ||
| showAccessRestrictedModal, | ||
| hideAccessRestrictedModal, | ||
| ]); |
There was a problem hiding this comment.
Unconditional hide call overrides other modal triggers
Low Severity
The useEffect in useAccountGroupCompliance unconditionally calls hideAccessRestrictedModal() in its else branch whenever complianceGate.isBlocked is false. Since useAccessRestrictedModal exposes showAccessRestrictedModal as a general-purpose API via context, any other caller that shows the modal (e.g., a per-flow compliance check) would have it immediately hidden if a mounted useAccountGroupCompliance instance has isBlocked = false. The else branch effectively makes this one hook the sole owner of modal visibility, conflicting with the flexible context API design.
| <ControllerEventToastBridge registrations={predictRegistrations} /> | ||
| <ProfilerManager /> | ||
| </WebSocketHealthToastProvider> | ||
| <AccessRestrictedProvider> |
There was a problem hiding this comment.
Is this rendered after onboarding?
There was a problem hiding this comment.
It is not shown unless anyone calls useAccountGroupCompliance
geositta
left a comment
There was a problem hiding this comment.
Approve with one non-blocking note.
In useWalletCompliance.ts addresses are passed to selectIsWalletBlocked and selectAreAnyWalletsBlocked without lowercasing them.
Probably worth confirming what casing the API returns and whether the controller should normalize before storing and comparing.
|
Hello @aganglada , what are your thoughts? |
abretonc7s
left a comment
There was a problem hiding this comment.
Review: PR #27694 — feat: add access restricted modal and compliance UI infrastructure
Tier: full | Tests: 24/24 pass | Device: validated on iOS simulator
1 blocking issue, 2 non-blocking, 1 minor. See inline comments.
Full review with live CDP device validation and video evidence.
| useEffect(() => { | ||
| if (complianceGate.isBlocked) { | ||
| showAccessRestrictedModal(); | ||
| } else { |
There was a problem hiding this comment.
[COMMENT] useAccountGroupCompliance is defined and fully wired to showAccessRestrictedModal, but it is not called from any rendered component in this PR or elsewhere in the codebase.
This appears to be intentional infrastructure-first scope — the PR title says "compliance UI infrastructure." The hook trigger is presumably deferred to a follow-up PR.
Question for the author: Is there a follow-up PR or ticket that adds the useAccountGroupCompliance() call (e.g., in App.tsx or WalletTabStackFlow)? If so, noting it in the PR description or linking the follow-up ticket would help reviewers understand the full picture.
|
|
||
| const handleContactSupport = useCallback(() => { | ||
| hideAccessRestrictedModal(); | ||
| navigation.navigate(Routes.WEBVIEW.MAIN, { |
There was a problem hiding this comment.
[NON-BLOCKING] navigation.navigate(Routes.WEBVIEW.MAIN, ...) may not work correctly from this context.
AccessRestrictedProvider lives in App.tsx outside AppFlow, so useNavigation() here returns the root navigator from NavigationProvider (which only has NavigationChildren as a direct screen). Routes.WEBVIEW.MAIN ('Webview') is registered inside OnboardingRootNav → AppFlow → NavigationChildren. All other components that navigate to 'Webview' (Perps, Ramp, Bridge, Stake) are mounted inside AppFlow's navigator hierarchy.
Please manually test that tapping "Contact support" actually opens the webview for a logged-in user on the Wallet screen. If it throws "NAVIGATE action not handled", consider moving the navigation call inside the app, or using NavigationService.navigation directly (which holds the root ref).
|
|
||
| expect(defaultProps.onContactSupport).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
[NON-BLOCKING] Missing test for the close (X) button calling onClose. The test covers onContactSupport but not onClose. The BottomSheet mock currently strips out onClose, so this interaction isn't exercised.
Consider adding a test case that exposes the onClose behavior, either by updating the mock to render the BottomSheetHeader close trigger, or by testing AccessRestrictedModal in integration with the real BottomSheet (using a wrapper test environment).
| TITLE: 'access-restricted-modal-title', | ||
| DESCRIPTION: 'access-restricted-modal-description', | ||
| CONTACT_SUPPORT_BUTTON: 'access-restricted-modal-contact-support', | ||
| } as const; |
There was a problem hiding this comment.
[MINOR] The close (X) button on BottomSheetHeader renders with the generic default testID "button-icon", making it non-specific for E2E targeting. BottomSheetHeader accepts closeButtonProps which can override testID:
closeButtonProps={{ testID: AccessRestrictedModalSelectorsIDs.CLOSE_BUTTON }}Would require adding CLOSE_BUTTON: 'access-restricted-modal-close' here.
Downgraded blocking issue to comment — this is an infrastructure-first PR. No blocking issues remain.
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection:
Selected tags rationale:
Not selected: More specialized tags (SmokeTrade, SmokeRamps, SmokePerps, etc.) are less likely to be directly affected since the compliance feature is new and feature-flagged. The core flows above provide sufficient coverage to verify the provider wrapping doesn't break existing functionality. Performance Test Selection: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
| url: METAMASK_SUPPORT_URL, | ||
| title: strings('access_restricted.contact_support'), | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Navigation to webview likely fails from root context
High Severity
handleContactSupport calls navigation.navigate(Routes.WEBVIEW.MAIN, ...) but AccessRestrictedProvider is rendered in App.tsx above AppFlow, outside the nested navigator tree where Routes.WEBVIEW.MAIN ('Webview') is registered (inside OnboardingRootNav). The useNavigation() hook here returns the root navigator, which may not be able to resolve 'Webview' depending on current navigation state. All other components that navigate to Routes.WEBVIEW.MAIN (Bridge, Ramp, etc.) are rendered inside AppFlow where the route is reachable. This means tapping "Contact support" on the compliance modal could fail silently or throw at runtime.
Additional Locations (1)
|
✅ E2E Fixture Validation — Schema is up to date |
|





Description
Adds OFAC compliance UI infrastructure under a new
app/components/UI/Compliance/feature directory, following the same co-location pattern asEarn,Bridge, andPerps.What changed:
AccessRestrictedModalBottomSheet component that informs users when their wallet address has been flagged during compliance screening, with a "Contact support" CTAAccessRestrictedContextwithAccessRestrictedProvideranduseAccessRestrictedModalhook for managing modal visibility from anywhere in the appuseWalletCompliance,useComplianceGate, anduseAccountGroupCompliancehooks into the Compliance feature directory (previously scattered underapp/components/hooks/)index.tsexporting all compliance UI primitivesMETAMASK_SUPPORT_URLconstant toapp/constants/urls.tsaccess_restrictedkeydocs/compliance.mdfile references to reflect the new directory structureFile structure:
Changelog
CHANGELOG entry: Added access restricted modal that notifies users when their wallet address has been flagged during compliance screening
Related issues
Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2558
Manual testing steps
Screenshots/Recordings
Before
N/A — new component
After
Pre-merge author checklist
Pre-merge reviewer checklist
Note
Medium Risk
Adds a globally mounted compliance modal and auto-show/hide side effects based on blocked-wallet status, which can affect navigation and user flows if triggered incorrectly. Risk is moderate due to new app-root provider wiring and behavioral changes tied to compliance selectors/feature flag.
Overview
Introduces a new Compliance UI feature module with an
AccessRestrictedModalbottom sheet (with test IDs + i18n strings) and anAccessRestrictedProvider/useAccessRestrictedModalcontext to control its visibility and route users to MetaMask support.Wires the provider at the app root (
App.tsx) and updatesuseAccountGroupComplianceto automatically show/hide the modal when the selected account group becomes blocked/unblocked. AddsMETAMASK_SUPPORT_URL, updates compliance docs paths, and expands unit tests to cover modal rendering, context behavior, and the new compliance→modal side effects.Written by Cursor Bugbot for commit 9ee060a. This will update automatically on new commits. Configure here.