Skip to content

feat: Resimulate transactions for every 3 seconds on focused MM window #29878

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Mar 20, 2025
3 changes: 2 additions & 1 deletion app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3759,7 +3759,8 @@ export default class MetamaskController extends EventEmitter {
null,
this.getTransactionMetricsRequest(),
),

setTransactionActive:
txController.setTransactionActive.bind(txController),
// decryptMessageController
decryptMessage: this.decryptMessageController.decryptMessage.bind(
this.decryptMessageController,
Expand Down
40 changes: 40 additions & 0 deletions ui/hooks/useWindowFocus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useWindowFocus } from './useWindowFocus';

describe('useWindowFocus', () => {
it('return true if the window is focused initially', () => {
jest.spyOn(document, 'hasFocus').mockReturnValue(true);

const { result } = renderHook(() => useWindowFocus());

expect(result.current).toBe(true);
});

it('return false if the window is not focused initially', () => {
jest.spyOn(document, 'hasFocus').mockReturnValue(false);

const { result } = renderHook(() => useWindowFocus());

expect(result.current).toBe(false);
});

it('update to true when window gains focus', () => {
const { result } = renderHook(() => useWindowFocus());

act(() => {
window.dispatchEvent(new Event('focus'));
});

expect(result.current).toBe(true);
});

it('update to false when window loses focus', () => {
const { result } = renderHook(() => useWindowFocus());

act(() => {
window.dispatchEvent(new Event('blur'));
});

expect(result.current).toBe(false);
});
});
20 changes: 20 additions & 0 deletions ui/hooks/useWindowFocus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';

export const useWindowFocus = () => {
const [isFocused, setIsFocused] = useState(document.hasFocus());

useEffect(() => {
const onFocus = () => setIsFocused(true);
const onBlur = () => setIsFocused(false);

window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);

return () => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
};
}, []);

return isFocused;
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ jest.mock('../../../hooks/useAssetDetails', () => ({
}),
}));

jest.mock('../../../hooks/useTransactionFocusEffect', () => ({
useTransactionFocusEffect: jest.fn(),
}));

describe('Info', () => {
const mockedAssetDetails = jest.mocked(useAssetDetails);

Expand Down
2 changes: 2 additions & 0 deletions ui/pages/confirmations/components/confirm/info/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
import { useConfirmContext } from '../../../context/confirm';
import { SignatureRequestType } from '../../../types/confirm';
import { useSmartTransactionFeatureFlags } from '../../../hooks/useSmartTransactionFeatureFlags';
import { useTransactionFocusEffect } from '../../../hooks/useTransactionFocusEffect';
import ApproveInfo from './approve/approve';
import BaseTransactionInfo from './base-transaction-info/base-transaction-info';
import NativeTransferInfo from './native-transfer/native-transfer';
Expand All @@ -18,6 +19,7 @@ const Info = () => {

// TODO: Create TransactionInfo and SignatureInfo components.
useSmartTransactionFeatureFlags();
useTransactionFocusEffect();

const ConfirmationInfoComponentMap = useMemo(
() => ({
Expand Down
135 changes: 135 additions & 0 deletions ui/pages/confirmations/hooks/useTransactionFocusEffect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { TransactionType } from '@metamask/transaction-controller';
import { renderHook } from '@testing-library/react-hooks';
import { useDispatch } from 'react-redux';
import { setTransactionActive } from '../../../store/actions';
import { useWindowFocus } from '../../../hooks/useWindowFocus';
import { useConfirmContext } from '../context/confirm';
import { type Confirmation } from '../types/confirm';
import { useTransactionFocusEffect } from './useTransactionFocusEffect';

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn(),
}));

jest.mock('../context/confirm', () => ({
useConfirmContext: jest.fn(),
}));

jest.mock('../../../hooks/useWindowFocus', () => ({
useWindowFocus: jest.fn(),
}));

jest.mock('../../../store/actions', () => ({
setTransactionActive: jest.fn(),
}));

const mockConfirmation: Confirmation = {
id: '1',
type: TransactionType.simpleSend,
};

const confirmContextMock = {
currentConfirmation: mockConfirmation,
isScrollToBottomCompleted: false,
setIsScrollToBottomCompleted: jest.fn(),
};

describe('useTransactionFocusEffect', () => {
const dispatchMock = jest.fn();
const setTransactionActiveMock = setTransactionActive as jest.MockedFunction<
typeof setTransactionActive
>;
const useConfirmContextMock = useConfirmContext as jest.MockedFunction<
typeof useConfirmContext
>;
const useWindowFocusMock = useWindowFocus as jest.MockedFunction<
typeof useWindowFocus
>;
const useDispatchMock = useDispatch as jest.MockedFunction<
typeof useDispatch
>;

beforeEach(() => {
useDispatchMock.mockReturnValue(dispatchMock);
useWindowFocusMock.mockReturnValue(true);
useConfirmContextMock.mockReturnValue(confirmContextMock);

setTransactionActiveMock.mockClear();
dispatchMock.mockClear();
});

it('should focus the confirmation when window is focused and type is valid', () => {
renderHook(() => useTransactionFocusEffect());

expect(dispatchMock).toHaveBeenCalledWith(setTransactionActive('1', true));
});

it('should focus new confirmation if previous confirmation is different', () => {
const { rerender } = renderHook(() => useTransactionFocusEffect());

const simpleSendConfirmation = {
id: '2',
type: TransactionType.simpleSend,
};

useConfirmContextMock.mockReturnValue({
...confirmContextMock,
currentConfirmation: simpleSendConfirmation,
});

rerender();

expect(dispatchMock).toHaveBeenCalledWith(setTransactionActive('1', false));
expect(dispatchMock).toHaveBeenCalledWith(setTransactionActive('2', true));
});

it('should unfocus the confirmation when window is not focused', () => {
const { rerender } = renderHook(() => useTransactionFocusEffect());

useWindowFocusMock.mockReturnValue(false);

rerender();

expect(dispatchMock).toHaveBeenCalledWith(setTransactionActive('1', false));
});

describe('when confirmation type is not valid', () => {
it('should not focus transaction initially', () => {
const signatureConfirmation = {
id: '2',
type: TransactionType.signTypedData,
};

useConfirmContextMock.mockReturnValue({
...confirmContextMock,
currentConfirmation: signatureConfirmation,
});
renderHook(() => useTransactionFocusEffect());
expect(dispatchMock).not.toHaveBeenCalled();
});

it('should unfocus the previous transaction', () => {
const { rerender } = renderHook(() => useTransactionFocusEffect());

const signatureConfirmation = {
id: '2',
type: TransactionType.signTypedData,
};

useConfirmContextMock.mockReturnValue({
...confirmContextMock,
currentConfirmation: signatureConfirmation,
});

rerender();

expect(dispatchMock).toHaveBeenCalledWith(
setTransactionActive('1', false),
);
expect(dispatchMock).toHaveBeenCalledWith(
setTransactionActive('2', true),
);
});
});
});
63 changes: 63 additions & 0 deletions ui/pages/confirmations/hooks/useTransactionFocusEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { TransactionType } from '@metamask/transaction-controller';
import { setTransactionActive } from '../../../store/actions';
import { useWindowFocus } from '../../../hooks/useWindowFocus';
import { useConfirmContext } from '../context/confirm';

const FOCUSABLE_TYPES: Set<TransactionType> = new Set([
TransactionType.contractInteraction,
TransactionType.deployContract,
TransactionType.simpleSend,
TransactionType.smart,
TransactionType.tokenMethodTransfer,
TransactionType.tokenMethodTransferFrom,
TransactionType.tokenMethodSafeTransferFrom,
]);

export const useTransactionFocusEffect = () => {
const { currentConfirmation } = useConfirmContext();
const { id, type } = currentConfirmation ?? {};
const isWindowFocused = useWindowFocus();
const dispatch = useDispatch();
const [focusedConfirmationId, setFocusedConfirmationId] = useState<
string | null
>(null);

const setTransactionFocus = useCallback(
async (transactionId: string, isFocused: boolean) => {
await dispatch(setTransactionActive(transactionId, isFocused));
},
[dispatch],
);

useEffect(() => {
const isFocusable = FOCUSABLE_TYPES.has(type as TransactionType);

if (!isFocusable) {
// If the transaction type is not one of the types that should be focused,
// we need to unfocus the previous focused confirmation and reset the focused confirmation
if (focusedConfirmationId) {
setTransactionFocus(focusedConfirmationId, false);
setFocusedConfirmationId(null);
}
return;
}

if (isWindowFocused && focusedConfirmationId !== id) {
// If the window is focused and the focused confirmation is not the current one,
// we need to unfocus the previous focused confirmation and focus the current one
if (focusedConfirmationId) {
setTransactionFocus(focusedConfirmationId, false);
}
// Set the focused confirmation to the current one
setFocusedConfirmationId(id);
setTransactionFocus(id, true);
} else if (!isWindowFocused && focusedConfirmationId) {
// If the window is not focused and there is a focused confirmation,
// we need to unfocus the focused confirmation
setTransactionFocus(focusedConfirmationId, false);
setFocusedConfirmationId(null);
}
}, [focusedConfirmationId, id, isWindowFocused, setTransactionFocus, type]);
};
12 changes: 12 additions & 0 deletions ui/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6101,3 +6101,15 @@ export async function getCode(address: Hex, networkClientId: string) {
networkClientId,
]);
}

export function setTransactionActive(
transactionId: string,
isFocused: boolean,
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
return async () => {
await submitRequestToBackground('setTransactionActive', [
transactionId,
isFocused,
]);
};
}
Loading