Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fe7877a
fix(perps): prevent Android crash in tutorial carousel Rive animation…
michalconsensys Jun 11, 2026
e207081
fix(money): direct mUSD Add routing & deposit screen padding (MUSD-86…
Kureev Jun 11, 2026
f9dff61
fix: display wallet name next to From/To labels in account picker, if…
jpuri Jun 11, 2026
3f60d87
feat: update World Cup event count in PredictionsSection and related …
PatrykLucka Jun 11, 2026
abe3d6b
chore(perps): bump @metamask/perps-controller to 8.1.0 (#31537)
abretonc7s Jun 11, 2026
7b4e98c
chore: replace BottomSheet with BottomSheetDialog in QuickBuy to remo…
zone-live Jun 11, 2026
8196fc8
fix: compliance switch account race (#31114)
geositta Jun 11, 2026
d144976
fix: error icon ignores color prop due to hardcoded svg fill (TSA-662…
xavier-brochard Jun 11, 2026
d4159cb
feat(perps): market detail - Magnifying glass shortcut to category-fi…
abretonc7s Jun 11, 2026
6d37964
chore: bump money-account-upgrade-controller (#31552)
MoMannn Jun 11, 2026
78d94b3
fix(perps): fix display limit for open positions on Perps home page (…
abretonc7s Jun 11, 2026
df2e7c5
feat(ci): add run-performance label to force PR performance tests (MM…
javiergarciavera Jun 11, 2026
4ee99d8
fix(card): money onboarding stepper displaying "Get card" instead of …
Brunonascdev Jun 11, 2026
0e1ed7d
fix(swaps): suppress bridge post-trade notifications (#31549)
bfullam Jun 11, 2026
2aca9f4
chore: bump @metamask/assets-controllers to 109.0.0 (#31508)
gabrieledm Jun 11, 2026
b470975
feat(perf): send BrowserStack profiling metrics (FPS, CPU, memory) to…
javiergarciavera Jun 11, 2026
fafd355
feat: Perps Watchlist empty state (#31242)
gambinish Jun 11, 2026
c101846
fix(locales): update overflow text for event count in English localiz…
PatrykLucka Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/guidelines/LABELING_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ Using any of these labels should be exceptional in case of CI friction and urgen

- **skip-smart-e2e-selection**: Bypasses the AI-powered Smart E2E Selection so that the full E2E test suite runs instead of an AI-picked subset. This label does **not** force E2E builds/tests to run on a PR that would otherwise skip them (e.g. docs-only changes). Whether E2E runs at all is determined by path filters, branch, and other skip labels — not this label.

### Force Performance Tests

- **run-performance-tests**: Forces the PR performance E2E workflow to run (all performance tests on Android low-profile devices), even when Smart E2E Selection would skip them (e.g. no performance-relevant changes detected, `skip-e2e`, or `pr-not-ready-for-e2e`). Adding or removing this label re-triggers CI. Not honored on fork PRs.

### Block merge if any is present

- **needs-qa**: The PR requires a full manual QA prior to being merged and added to a release.
Expand Down
21 changes: 14 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1001,22 +1001,29 @@ jobs:

run-performance-tests-pr:
name: 'Run Performance Tests (PR)'
# Only run on pull_request events unless AI explicitly found no relevant changes.
# Run when:
# - run-performance-tests label is on the PR (forced run, all tests), OR
# - smart-e2e-selection ran and AI selected performance tags.
# ai_performance_test_tags == '[]' → AI ran and found no perf-relevant changes: skip.
# ai_performance_test_tags == '' → conservative fallback (AI failed): run all tests.
# ai_performance_test_tags == '' → conservative fallback (AI failed) or run-performance-tests label: run all tests.
# ai_performance_test_tags == '[…]' → specific tags selected: run those tests.
if: >-
${{
github.event_name == 'pull_request' &&
!cancelled() &&
needs.smart-e2e-selection.result != 'skipped' &&
needs.smart-e2e-selection.outputs.ai_performance_test_tags != '[]'
(
needs.get_requirements.outputs.run_performance == 'true' ||
(
needs.smart-e2e-selection.result == 'success' &&
needs.smart-e2e-selection.outputs.ai_performance_test_tags != '[]'
)
)
}}
needs: [smart-e2e-selection]
needs: [get_requirements, smart-e2e-selection]
uses: ./.github/workflows/run-performance-e2e.yml
with:
performance_tags: ${{ needs.smart-e2e-selection.outputs.ai_performance_test_tags }}
performance_tags_reasoning: ${{ needs.smart-e2e-selection.outputs.ai_performance_test_reasoning }}
performance_tags: ${{ needs.get_requirements.outputs.run_performance != 'true' && needs.smart-e2e-selection.outputs.ai_performance_test_tags || '' }}
performance_tags_reasoning: ${{ needs.get_requirements.outputs.run_performance == 'true' && 'Forced run via run-performance-tests label on PR' || needs.smart-e2e-selection.outputs.ai_performance_test_reasoning }}
build_variant: 'e2e'
branch_name: ${{ github.head_ref }}
pr_number: ${{ github.event.pull_request.number }}
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/get-requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ on:
run_smart_e2e_selection:
description: 'Whether the smart-e2e-selection job should run. False for non-PR events, forks, or hard E2E skips.'
value: ${{ jobs.detect-changes.outputs.run_smart_e2e_selection }}
run_performance:
description: 'Whether performance E2E tests should be forced to run via the run-performance-tests PR label'
value: ${{ jobs.detect-changes.outputs.run_performance }}

jobs:
detect-changes:
Expand All @@ -40,6 +43,7 @@ jobs:
changed_files: ${{ steps.set-outputs.outputs.changed_files }}
block_merge_for_e2e_readiness: ${{ steps.set-outputs.outputs.block_merge }}
run_smart_e2e_selection: ${{ steps.set-outputs.outputs.run_smart_e2e_selection }}
run_performance: ${{ steps.set-outputs.outputs.run_performance }}
env:
# For a `pull_request` event, the head commit hash is `github.event.pull_request.head.sha`.
# For a `push` event, the head commit hash is `github.sha`.
Expand Down Expand Up @@ -85,6 +89,7 @@ jobs:
echo "SKIP_E2E=false"
echo "LABEL_BLOCKS_MERGE=false"
echo "SKIP_SMART_SELECTION=false"
echo "RUN_PERFORMANCE=false"
} >> "$GITHUB_OUTPUT"

LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name')
Expand All @@ -107,6 +112,11 @@ jobs:
echo "-> SKIP_SMART_SELECTION=true due to 'skip-smart-e2e-selection' label on PR"
fi

if echo "$LABELS" | grep -qx "run-performance-tests"; then
echo "RUN_PERFORMANCE=true" >> "$GITHUB_OUTPUT"
echo "-> RUN_PERFORMANCE=true due to 'run-performance-tests' label on PR"
fi

- name: Filter changed files
id: filter
if: steps.skip-merge-queue.outputs.up-to-date != 'true'
Expand All @@ -124,6 +134,7 @@ jobs:
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
SHOULD_SKIP_E2E: ${{ steps.skip-e2e-tag.outputs.SKIP == 'true' || steps.check-labels.outputs.SKIP_E2E == 'true' }}
LABEL_BLOCKS_MERGE: ${{ steps.check-labels.outputs.LABEL_BLOCKS_MERGE }}
RUN_PERFORMANCE_LABEL: ${{ steps.check-labels.outputs.RUN_PERFORMANCE }}
ALL_CHANGES_COUNT: ${{ steps.filter.outputs.all_changes_count }}
ALL_CHANGES_FILES: ${{ github.event_name == 'pull_request' && steps.filter.outputs.all_changes_files || '' }}
IGNORABLE_COUNT: ${{ steps.filter.outputs.e2e_ignorable_count }}
Expand Down Expand Up @@ -220,13 +231,21 @@ jobs:
echo "-> BLOCK_MERGE bypassed — ignorable-only changes, E2E_WORKFLOWS_COUNT=0"
fi

RUN_PERF=false
if [[ "$GITHUB_EVENT_NAME" == "pull_request" && \
"$IS_FORK" != "true" && \
"$RUN_PERFORMANCE_LABEL" == "true" ]]; then
RUN_PERF=true
fi

echo "$MSG"
{
echo "android_final=$ANDROID"
echo "ios_final=$IOS"
echo "e2e_needed=$E2E_NEEDED"
echo "run_smart_e2e_selection=$RUN_SMART"
echo "block_merge=$BLOCK_MERGE"
echo "run_performance=$RUN_PERF"
} >> "$GITHUB_OUTPUT"

{
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/rerun-ci-on-skipped-e2e-labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ jobs:
github.event.label.name == 'skip-e2e' ||
github.event.label.name == 'skip-e2e-flakiness-detection' ||
github.event.label.name == 'pr-not-ready-for-e2e' ||
github.event.label.name == 'force-builds')
github.event.label.name == 'force-builds' ||
github.event.label.name == 'run-performance-tests')
runs-on: ubuntu-latest
permissions:
actions: write
Expand Down
17 changes: 15 additions & 2 deletions app/__mocks__/@metamask/compliance-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
* Compliance status is populated exclusively via per-address checks.
*/

interface ComplianceServiceOptions {
messenger: unknown;
fetch: typeof fetch;
apiUrl?: string;
env?: 'production' | 'development';
}

export class ComplianceService {
readonly name = 'ComplianceService';

constructor(args: Record<string, unknown>) {
constructor(args: ComplianceServiceOptions) {
Object.assign(this, args);
}
}
Expand Down Expand Up @@ -53,8 +60,14 @@ function findWalletComplianceStatus(
walletComplianceStatusMap: Record<string, { blocked: boolean } | undefined>,
address: string,
) {
const exactMatch = walletComplianceStatusMap[address];
if (exactMatch !== undefined) {
return exactMatch;
}

// Non-EVM addresses: no case-insensitive fallback (mirrors real package behaviour).
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
return walletComplianceStatusMap[address];
return undefined;
}

return Object.entries(walletComplianceStatusMap).find(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions app/components/UI/Bridge/Views/BridgeView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ import {
ButtonVariants,
} from '../../../../../component-library/components/Buttons/Button/Button.types.ts';
import { useIsNetworkFeeUnavailable } from '../../hooks/useIsNetworkFeeUnavailable/index.ts';
import {
hidePostTradeNotificationSurface,
showPostTradeNotificationSurface,
} from '../../utils/postTradeNotifications';

const SCROLL_NEAR_BOTTOM_PX = 160;

Expand Down Expand Up @@ -222,6 +226,16 @@ const BridgeViewContent = ({ latestSourceBalance }: BridgeViewContentProps) => {

useBridgeViewOnFocus({ inputRef, keypadRef });

useFocusEffect(
useCallback(() => {
showPostTradeNotificationSurface();

return () => {
hidePostTradeNotificationSurface();
};
}, []),
);

// Scroll to top when navigating to the bridge view if requested
useFocusEffect(
useCallback(() => {
Expand Down
12 changes: 12 additions & 0 deletions app/components/UI/Bridge/components/PostTradeBottomSheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ import {
import styleSheet from './PostTradeBottomSheet.styles';
import { usePostTradeTxStatus } from './usePostTradeTxStatus';
import { useBridgeQuoteRequest } from '../../hooks/useBridgeQuoteRequest';
import {
hidePostTradeNotificationSurface,
showPostTradeNotificationSurface,
} from '../../utils/postTradeNotifications';

export const getTradeSubtitle = ({
sourceAmount,
Expand Down Expand Up @@ -128,6 +132,14 @@ export const PostTradeBottomSheet = () => {
transactionHash: params.transactionHash,
});

useEffect(() => {
showPostTradeNotificationSurface();

return () => {
hidePostTradeNotificationSurface();
};
}, []);

useEffect(() => {
const isTerminalStatus =
status === PostTradeStatus.Success || status === PostTradeStatus.Failed;
Expand Down
15 changes: 10 additions & 5 deletions app/components/UI/Bridge/hooks/useBridgeConfirm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
POST_TRADE_MODAL_VARIANTS,
} from '../../components/PostTradeBottomSheet/abTestConfig';
import Engine from '../../../../../core/Engine';
import { withPostTradeNotificationSuppression } from '../../utils/postTradeNotifications';

interface Params {
activeQuote: ReturnType<typeof useBridgeQuoteData>['activeQuote'] | null;
Expand Down Expand Up @@ -68,11 +69,15 @@ export const useBridgeConfirm = ({
try {
dispatch(setIsSubmittingTx(true));

const submittedTransaction = await submitBridgeTx({
quoteResponse: activeQuote,
location,
transactionActiveAbTests,
});
const submitTransaction = () =>
submitBridgeTx({
quoteResponse: activeQuote,
location,
transactionActiveAbTests,
});
const submittedTransaction = isPostTradeModalEnabled
? await withPostTradeNotificationSuppression(submitTransaction)
: await submitTransaction();
const transactionHash =
submittedTransaction &&
'hash' in submittedTransaction &&
Expand Down
147 changes: 147 additions & 0 deletions app/components/UI/Bridge/utils/postTradeNotifications.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
TransactionMeta,
TransactionType,
} from '@metamask/transaction-controller';
import type { NotificationSkipPredicate } from '../../../../core/notificationSkipPredicates';

const mockRegisterNotificationSkipPredicate = jest.fn();
const mockGetIsBridgeTransaction = jest.fn();

jest.mock('../../../../core/notificationSkipPredicates', () => ({
__esModule: true,
registerNotificationSkipPredicate: mockRegisterNotificationSkipPredicate,
}));

jest.mock('./transaction', () => ({
__esModule: true,
getIsBridgeTransaction: mockGetIsBridgeTransaction,
}));

const loadPostTradeNotifications = () => {
jest.resetModules();
mockRegisterNotificationSkipPredicate.mockReset();
mockGetIsBridgeTransaction.mockReset();

return jest.requireActual(
'./postTradeNotifications',
) as typeof import('./postTradeNotifications');
};

const getRegisteredPredicate = (): NotificationSkipPredicate => {
expect(mockRegisterNotificationSkipPredicate).toHaveBeenCalledTimes(1);
return mockRegisterNotificationSkipPredicate.mock
.calls[0][0] as NotificationSkipPredicate;
};

const transactionMeta = (
txMeta: Partial<TransactionMeta> = {},
): TransactionMeta =>
({
id: 'tx-1',
type: TransactionType.contractInteraction,
...txMeta,
}) as TransactionMeta;

describe('postTradeNotifications', () => {
it('registers once and suppresses tracked submissions only while a surface is visible', async () => {
const {
showPostTradeNotificationSurface,
hidePostTradeNotificationSurface,
withPostTradeNotificationSuppression,
} = loadPostTradeNotifications();

showPostTradeNotificationSurface();
showPostTradeNotificationSurface();

expect(mockRegisterNotificationSkipPredicate).toHaveBeenCalledTimes(1);

await expect(
withPostTradeNotificationSuppression(() =>
Promise.resolve({ id: 'submitted-tx' }),
),
).resolves.toEqual({ id: 'submitted-tx' });

const predicate = getRegisteredPredicate();

expect(predicate(transactionMeta({ id: 'submitted-tx' }))).toBe(true);
expect(predicate(transactionMeta({ id: 'untracked-tx' }))).toBe(false);

hidePostTradeNotificationSurface();
hidePostTradeNotificationSurface();
hidePostTradeNotificationSurface();

expect(predicate(transactionMeta({ id: 'submitted-tx' }))).toBe(false);
});

it('suppresses in-flight Bridge and batch transactions while a surface is visible', async () => {
const {
showPostTradeNotificationSurface,
withPostTradeNotificationSuppression,
} = loadPostTradeNotifications();
const bridgeTxMeta = transactionMeta({ id: 'bridge-tx' });
const batchTxMeta = transactionMeta({
id: 'batch-tx',
type: TransactionType.batch,
});

showPostTradeNotificationSurface();

await withPostTradeNotificationSuppression(async () => {
const predicate = getRegisteredPredicate();

mockGetIsBridgeTransaction.mockReturnValueOnce(true);
expect(predicate(bridgeTxMeta)).toBe(true);
expect(mockGetIsBridgeTransaction).toHaveBeenCalledWith(bridgeTxMeta);

mockGetIsBridgeTransaction.mockReturnValueOnce(false);
expect(predicate(batchTxMeta)).toBe(true);

return { id: 'submitted-batch-tx' };
});
});

it('does not suppress missing or unrelated transaction metadata', async () => {
const {
showPostTradeNotificationSurface,
withPostTradeNotificationSuppression,
} = loadPostTradeNotifications();

showPostTradeNotificationSurface();
mockGetIsBridgeTransaction.mockReturnValue(false);

await withPostTradeNotificationSuppression(async () => {
const predicate = getRegisteredPredicate();

expect(predicate(undefined)).toBe(false);
expect(predicate(transactionMeta({ id: 'unrelated-tx' }))).toBe(false);

return undefined;
});
});

it('clears pending submission state when submit fails', async () => {
const {
showPostTradeNotificationSurface,
withPostTradeNotificationSuppression,
} = loadPostTradeNotifications();
const bridgeTxMeta = transactionMeta({ id: 'bridge-tx' });
const error = new Error('submit failed');

showPostTradeNotificationSurface();
mockGetIsBridgeTransaction.mockReturnValue(true);

await expect(
withPostTradeNotificationSuppression(async () => {
const predicate = getRegisteredPredicate();

expect(predicate(bridgeTxMeta)).toBe(true);

throw error;
}),
).rejects.toThrow(error);

const predicate = getRegisteredPredicate();

expect(predicate(bridgeTxMeta)).toBe(false);
});
});
Loading
Loading