Skip to content

feat: add access restricted modal and compliance UI infrastructure#27694

Merged
michalconsensys merged 12 commits into
mainfrom
feat/ofac/ui
Mar 25, 2026
Merged

feat: add access restricted modal and compliance UI infrastructure#27694
michalconsensys merged 12 commits into
mainfrom
feat/ofac/ui

Conversation

@michalconsensys
Copy link
Copy Markdown
Contributor

@michalconsensys michalconsensys commented Mar 19, 2026

Description

Adds OFAC compliance UI infrastructure under a new app/components/UI/Compliance/ feature directory, following the same co-location pattern as Earn, Bridge, and Perps.

What changed:

  • New AccessRestrictedModal BottomSheet component that informs users when their wallet address has been flagged during compliance screening, with a "Contact support" CTA
  • New AccessRestrictedContext with AccessRestrictedProvider and useAccessRestrictedModal hook for managing modal visibility from anywhere in the app
  • Moved useWalletCompliance, useComplianceGate, and useAccountGroupCompliance hooks into the Compliance feature directory (previously scattered under app/components/hooks/)
  • Barrel index.ts exporting all compliance UI primitives
  • Added METAMASK_SUPPORT_URL constant to app/constants/urls.ts
  • Added localization strings under access_restricted key
  • Added test IDs for E2E testing
  • Updated docs/compliance.md file references to reflect the new directory structure

File structure:

app/components/UI/Compliance/
├── AccessRestrictedModal/
│   ├── AccessRestrictedModal.tsx
│   ├── AccessRestrictedModal.types.ts
│   ├── AccessRestrictedModal.testIds.ts
│   └── index.ts
├── contexts/
│   └── AccessRestrictedContext.tsx
├── hooks/
│   ├── useWalletCompliance.ts
│   └── useWalletCompliance.test.ts
└── index.ts

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

Feature: Access restricted modal for compliance screening

  Scenario: user sees access restricted modal when wallet is flagged
    Given the user has a wallet address flagged during compliance screening
    And compliance is enabled via feature flag

    When the compliance screening triggers the access restricted modal
    Then a bottom sheet is displayed with the title "Access restricted"
    And a description explaining that some MetaMask services are unavailable
    And a "Contact support" button is visible

  Scenario: user taps contact support
    Given the access restricted modal is displayed

    When the user taps the "Contact support" button
    Then the modal closes
    And the user is navigated to the MetaMask support page in a webview

  Scenario: user dismisses the modal
    Given the access restricted modal is displayed

    When the user taps the close (X) button
    Then the modal closes
    And the user remains on the current screen

Screenshots/Recordings

Before

N/A — new component

After

Screenshot 2026-03-20 at 09 51 31

Pre-merge author checklist

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.

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 AccessRestrictedModal bottom sheet (with test IDs + i18n strings) and an AccessRestrictedProvider/useAccessRestrictedModal context to control its visibility and route users to MetaMask support.

Wires the provider at the app root (App.tsx) and updates useAccountGroupCompliance to automatically show/hide the modal when the selected account group becomes blocked/unblocked. Adds METAMASK_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.

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.
@michalconsensys michalconsensys self-assigned this Mar 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

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.

@metamaskbot metamaskbot added the team-perps Perps team label Mar 19, 2026
@github-actions github-actions Bot added risk-low Low testing needed · Low bug introduction risk size-M labels Mar 19, 2026
…iance

Move scattered compliance components into a single feature directory
following the same pattern as Earn, Bridge, and Perps.
@michalconsensys michalconsensys changed the title feat: add access restricted modal for compliance screening feat: add access restricted modal and compliance UI infrastructure Mar 20, 2026
@michalconsensys michalconsensys marked this pull request as ready for review March 23, 2026 14:13
Comment thread app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.test.tsx Outdated
Comment thread app/components/UI/Compliance/AccessRestrictedModal/AccessRestrictedModal.test.tsx Outdated
Copy link
Copy Markdown
Contributor

@aganglada aganglada left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 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.
@github-actions github-actions Bot added risk-low Low testing needed · Low bug introduction risk and removed risk-low Low testing needed · Low bug introduction risk labels Mar 24, 2026
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.51%. Comparing base (eb832ce) to head (de08373).
⚠️ Report is 168 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot added risk-low Low testing needed · Low bug introduction risk and removed risk-low Low testing needed · Low bug introduction risk labels Mar 24, 2026
@github-actions github-actions Bot added risk-low Low testing needed · Low bug introduction risk and removed risk-low Low testing needed · Low bug introduction risk labels Mar 24, 2026
…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.
@github-actions github-actions Bot added size-L and removed size-M risk-low Low testing needed · Low bug introduction risk labels Mar 24, 2026
@github-actions github-actions Bot added the risk-medium Moderate testing recommended · Possible bug introduction risk label Mar 24, 2026
Comment thread app/components/UI/Compliance/hooks/useWalletCompliance.ts Outdated
…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.
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Mar 24, 2026
Comment thread app/constants/urls.ts
const infuraProjectId = InfuraKey === 'null' ? '' : InfuraKey;

// Support
export const METAMASK_SUPPORT_URL = 'https://support.metamask.io';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is an env variable for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

<ControllerEventToastBridge registrations={predictRegistrations} />
<ProfilerManager />
</WebSocketHealthToastProvider>
<AccessRestrictedProvider>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this rendered after onboarding?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not shown unless anyone calls useAccountGroupCompliance

Copy link
Copy Markdown
Contributor

@geositta geositta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@michalconsensys
Copy link
Copy Markdown
Contributor Author

Hello @aganglada , what are your thoughts?

Copy link
Copy Markdown
Contributor

@abretonc7s abretonc7s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor

@abretonc7s abretonc7s Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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);
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@abretonc7s abretonc7s dismissed their stale review March 25, 2026 09:22

Downgraded blocking issue to comment — this is an infrastructure-first PR. No blocking issues remain.

@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Mar 25, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeAccounts, SmokeConfirmations, SmokeWalletPlatform, SmokeNetworkAbstractions
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 72%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR introduces a new compliance/sanctions checking system with the following key changes:

  1. App.tsx global wrapping: The AccessRestrictedProvider is now wrapping the entire app component tree. This is the most impactful change - any issue with this provider (context errors, navigation issues, rendering problems) could affect ALL app flows. However, the feature is gated by selectComplianceEnabled feature flag, which reduces the risk of breaking existing tests.

  2. New compliance hooks: useWalletCompliance, useComplianceGate, and useAccountGroupCompliance are new hooks that interact with ComplianceController and the account group selector. The useAccountGroupCompliance hook uses useNavigation() inside the context provider, which could cause navigation-related issues.

  3. AccessRestrictedModal: A new BottomSheet modal that can appear globally when a wallet is blocked. Uses shared BottomSheet component.

  4. Localization strings: New access_restricted.* keys added to en.json.

Selected tags rationale:

  • SmokeAccounts: The compliance system directly monitors account addresses and account groups. The useAccountGroupCompliance hook monitors the selected account group. Account switching, creation, and management flows are directly affected by this compliance check.
  • SmokeConfirmations: Transaction flows are the primary use case for compliance blocking. The compliance gate is designed to block transactions for sanctioned addresses. Any regression in the provider wrapping could affect confirmation flows.
  • SmokeWalletPlatform: Core wallet platform tests cover the main wallet view, account management, and navigation. The global provider wrapping affects the entire app including the main wallet screen.
  • SmokeNetworkAbstractions: Network management and multi-chain features interact with account groups and addresses that are checked by the compliance system.

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:
The changes introduce a new compliance provider wrapping the app root and compliance hooks. While the provider adds a React context layer to the entire app, it is lightweight (no heavy computation, no list rendering, no data fetching on mount) and feature-flagged. The compliance checks are async and on-demand. There is no significant impact on app launch time, account list rendering, login performance, or other measured performance metrics. No performance tests are warranted.

View GitHub Actions results

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

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'),
},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

@github-actions
Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
16 value mismatches detected (expected — fixture represents an existing user).
View details

@sonarqubecloud
Copy link
Copy Markdown

@michalconsensys michalconsensys added this pull request to the merge queue Mar 25, 2026
Merged via the queue into main with commit b2ebb6d Mar 25, 2026
108 of 110 checks passed
@michalconsensys michalconsensys deleted the feat/ofac/ui branch March 25, 2026 12:38
@github-actions github-actions Bot locked and limited conversation to collaborators Mar 25, 2026
@metamaskbot metamaskbot added the release-7.72.0 Issue or pull request that will be included in release 7.72.0 label Mar 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.72.0 Issue or pull request that will be included in release 7.72.0 risk-medium Moderate testing recommended · Possible bug introduction risk size-L team-perps Perps team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants