mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-28 23:06:37 +01:00
Trigger unlock popup in appStateController using ApprovalController (#18386)
This commit is contained in:
parent
5d2c4c143a
commit
a3af0b53e3
@ -1,5 +1,7 @@
|
||||
import EventEmitter from 'events';
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import log from 'loglevel';
|
||||
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
|
||||
import { MINUTE } from '../../../shared/constants/time';
|
||||
import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms';
|
||||
@ -8,8 +10,11 @@ import { isBeta } from '../../../ui/helpers/utils/build-types';
|
||||
import {
|
||||
ENVIRONMENT_TYPE_BACKGROUND,
|
||||
POLLING_TOKEN_ENVIRONMENT_TYPES,
|
||||
ORIGIN_METAMASK,
|
||||
} from '../../../shared/constants/app';
|
||||
|
||||
const APPROVAL_REQUEST_TYPE = 'unlock';
|
||||
|
||||
export default class AppStateController extends EventEmitter {
|
||||
/**
|
||||
* @param {object} opts
|
||||
@ -20,9 +25,9 @@ export default class AppStateController extends EventEmitter {
|
||||
isUnlocked,
|
||||
initState,
|
||||
onInactiveTimeout,
|
||||
showUnlockRequest,
|
||||
preferencesStore,
|
||||
qrHardwareStore,
|
||||
messenger,
|
||||
} = opts;
|
||||
super();
|
||||
|
||||
@ -59,8 +64,6 @@ export default class AppStateController extends EventEmitter {
|
||||
this.waitingForUnlock = [];
|
||||
addUnlockListener(this.handleUnlock.bind(this));
|
||||
|
||||
this._showUnlockRequest = showUnlockRequest;
|
||||
|
||||
preferencesStore.subscribe(({ preferences }) => {
|
||||
const currentState = this.store.getState();
|
||||
if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) {
|
||||
@ -74,6 +77,9 @@ export default class AppStateController extends EventEmitter {
|
||||
|
||||
const { preferences } = preferencesStore.getState();
|
||||
this._setInactiveTimeout(preferences.autoLockTimeLimit);
|
||||
|
||||
this.messagingSystem = messenger;
|
||||
this._approvalRequestId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,7 +114,7 @@ export default class AppStateController extends EventEmitter {
|
||||
this.waitingForUnlock.push({ resolve });
|
||||
this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE);
|
||||
if (shouldShowUnlockRequest) {
|
||||
this._showUnlockRequest();
|
||||
this._requestApproval();
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,6 +128,8 @@ export default class AppStateController extends EventEmitter {
|
||||
}
|
||||
this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE);
|
||||
}
|
||||
|
||||
this._acceptApproval();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -369,4 +377,39 @@ export default class AppStateController extends EventEmitter {
|
||||
serviceWorkerLastActiveTime,
|
||||
});
|
||||
}
|
||||
|
||||
_requestApproval() {
|
||||
this._approvalRequestId = uuid();
|
||||
|
||||
this.messagingSystem
|
||||
.call(
|
||||
'ApprovalController:addRequest',
|
||||
{
|
||||
id: this._approvalRequestId,
|
||||
origin: ORIGIN_METAMASK,
|
||||
type: APPROVAL_REQUEST_TYPE,
|
||||
},
|
||||
true,
|
||||
)
|
||||
.catch(() => {
|
||||
// Intentionally ignored as promise not currently used
|
||||
});
|
||||
}
|
||||
|
||||
_acceptApproval() {
|
||||
if (!this._approvalRequestId) {
|
||||
log.error('Attempted to accept missing unlock approval request');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.messagingSystem.call(
|
||||
'ApprovalController:acceptRequest',
|
||||
this._approvalRequestId,
|
||||
);
|
||||
} catch (error) {
|
||||
log.error('Failed to accept transaction approval request', error);
|
||||
}
|
||||
|
||||
this._approvalRequestId = null;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,158 @@
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import log from 'loglevel';
|
||||
import { ORIGIN_METAMASK } from '../../../shared/constants/app';
|
||||
import AppStateController from './app-state';
|
||||
|
||||
jest.mock('loglevel');
|
||||
|
||||
let appStateController, mockStore;
|
||||
|
||||
describe('AppStateController', () => {
|
||||
mockStore = new ObservableStore();
|
||||
const createAppStateController = (initState = {}) => {
|
||||
return new AppStateController({
|
||||
addUnlockListener: jest.fn(),
|
||||
isUnlocked: jest.fn(() => true),
|
||||
initState,
|
||||
onInactiveTimeout: jest.fn(),
|
||||
showUnlockRequest: jest.fn(),
|
||||
preferencesStore: {
|
||||
subscribe: jest.fn(),
|
||||
getState: jest.fn(() => ({
|
||||
preferences: {
|
||||
autoLockTimeLimit: 0,
|
||||
},
|
||||
})),
|
||||
},
|
||||
qrHardwareStore: {
|
||||
subscribe: jest.fn(),
|
||||
},
|
||||
messenger: {
|
||||
call: jest.fn(() => ({
|
||||
catch: jest.fn(),
|
||||
})),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
appStateController = createAppStateController({ store: mockStore });
|
||||
});
|
||||
|
||||
describe('setOutdatedBrowserWarningLastShown', () => {
|
||||
it('should set the last shown time', () => {
|
||||
const appStateController = new AppStateController({
|
||||
it('sets the last shown time', () => {
|
||||
appStateController = createAppStateController();
|
||||
const date = new Date();
|
||||
|
||||
appStateController.setOutdatedBrowserWarningLastShown(date);
|
||||
|
||||
expect(
|
||||
appStateController.store.getState().outdatedBrowserWarningLastShown,
|
||||
).toStrictEqual(date);
|
||||
});
|
||||
|
||||
it('sets outdated browser warning last shown timestamp', () => {
|
||||
const lastShownTimestamp = Date.now();
|
||||
appStateController = createAppStateController();
|
||||
const updateStateSpy = jest.spyOn(
|
||||
appStateController.store,
|
||||
'updateState',
|
||||
);
|
||||
|
||||
appStateController.setOutdatedBrowserWarningLastShown(lastShownTimestamp);
|
||||
|
||||
expect(updateStateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateStateSpy).toHaveBeenCalledWith({
|
||||
outdatedBrowserWarningLastShown: lastShownTimestamp,
|
||||
});
|
||||
|
||||
updateStateSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUnlockPromise', () => {
|
||||
it('waits for unlock if the extension is locked', async () => {
|
||||
appStateController = createAppStateController();
|
||||
const isUnlockedMock = jest
|
||||
.spyOn(appStateController, 'isUnlocked')
|
||||
.mockReturnValue(false);
|
||||
const waitForUnlockSpy = jest.spyOn(appStateController, 'waitForUnlock');
|
||||
|
||||
appStateController.getUnlockPromise(true);
|
||||
expect(isUnlockedMock).toHaveBeenCalled();
|
||||
expect(waitForUnlockSpy).toHaveBeenCalledWith(expect.any(Function), true);
|
||||
});
|
||||
|
||||
it('resolves immediately if the extension is already unlocked', async () => {
|
||||
appStateController = createAppStateController();
|
||||
const isUnlockedMock = jest
|
||||
.spyOn(appStateController, 'isUnlocked')
|
||||
.mockReturnValue(true);
|
||||
|
||||
await expect(
|
||||
appStateController.getUnlockPromise(false),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(isUnlockedMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('waitForUnlock', () => {
|
||||
it('resolves immediately if already unlocked', async () => {
|
||||
const emitSpy = jest.spyOn(appStateController, 'emit');
|
||||
const resolveFn = jest.fn();
|
||||
appStateController.waitForUnlock(resolveFn, false);
|
||||
expect(emitSpy).toHaveBeenCalledWith('updateBadge');
|
||||
expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('creates approval request when waitForUnlock is called with shouldShowUnlockRequest as true', async () => {
|
||||
jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false);
|
||||
|
||||
const resolveFn = jest.fn();
|
||||
appStateController.waitForUnlock(resolveFn, true);
|
||||
|
||||
expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(1);
|
||||
expect(appStateController.messagingSystem.call).toHaveBeenCalledWith(
|
||||
'ApprovalController:addRequest',
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
origin: ORIGIN_METAMASK,
|
||||
type: 'unlock',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleUnlock', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('accepts approval request revolving all the related promises', async () => {
|
||||
const emitSpy = jest.spyOn(appStateController, 'emit');
|
||||
const resolveFn = jest.fn();
|
||||
appStateController.waitForUnlock(resolveFn, true);
|
||||
|
||||
appStateController.handleUnlock();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalled();
|
||||
expect(emitSpy).toHaveBeenCalledWith('updateBadge');
|
||||
expect(appStateController.messagingSystem.call).toHaveBeenCalled();
|
||||
expect(appStateController.messagingSystem.call).toHaveBeenCalledWith(
|
||||
'ApprovalController:acceptRequest',
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
it('logs if rejecting approval request throws', async () => {
|
||||
appStateController._approvalRequestId = 'mock-approval-request-id';
|
||||
appStateController = new AppStateController({
|
||||
addUnlockListener: jest.fn(),
|
||||
isUnlocked: jest.fn(() => true),
|
||||
initState: {},
|
||||
onInactiveTimeout: jest.fn(),
|
||||
showUnlockRequest: jest.fn(),
|
||||
preferencesStore: {
|
||||
@ -20,14 +166,184 @@ describe('AppStateController', () => {
|
||||
qrHardwareStore: {
|
||||
subscribe: jest.fn(),
|
||||
},
|
||||
messenger: {
|
||||
call: jest.fn(() => {
|
||||
throw new Error('mock error');
|
||||
}),
|
||||
},
|
||||
});
|
||||
const date = new Date();
|
||||
|
||||
appStateController.setOutdatedBrowserWarningLastShown(date);
|
||||
appStateController.handleUnlock();
|
||||
|
||||
expect(log.error).toHaveBeenCalledTimes(1);
|
||||
expect(log.error).toHaveBeenCalledWith(
|
||||
'Attempted to accept missing unlock approval request',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns without call messenger if no approval request in pending', async () => {
|
||||
const emitSpy = jest.spyOn(appStateController, 'emit');
|
||||
|
||||
appStateController.handleUnlock();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledTimes(0);
|
||||
expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(0);
|
||||
expect(log.error).toHaveBeenCalledTimes(1);
|
||||
expect(log.error).toHaveBeenCalledWith(
|
||||
'Attempted to accept missing unlock approval request',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDefaultHomeActiveTabName', () => {
|
||||
it('sets the default home tab name', () => {
|
||||
appStateController.setDefaultHomeActiveTabName('testTabName');
|
||||
expect(appStateController.store.getState().defaultHomeActiveTabName).toBe(
|
||||
'testTabName',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setConnectedStatusPopoverHasBeenShown', () => {
|
||||
it('sets connected status popover as shown', () => {
|
||||
appStateController.setConnectedStatusPopoverHasBeenShown();
|
||||
expect(
|
||||
appStateController.store.getState().connectedStatusPopoverHasBeenShown,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRecoveryPhraseReminderHasBeenShown', () => {
|
||||
it('sets recovery phrase reminder as shown', () => {
|
||||
appStateController.setRecoveryPhraseReminderHasBeenShown();
|
||||
expect(
|
||||
appStateController.store.getState().recoveryPhraseReminderHasBeenShown,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRecoveryPhraseReminderLastShown', () => {
|
||||
it('sets the last shown time of recovery phrase reminder', () => {
|
||||
const timestamp = Date.now();
|
||||
appStateController.setRecoveryPhraseReminderLastShown(timestamp);
|
||||
|
||||
expect(
|
||||
appStateController.store.getState().outdatedBrowserWarningLastShown,
|
||||
).toStrictEqual(date);
|
||||
appStateController.store.getState().recoveryPhraseReminderLastShown,
|
||||
).toBe(timestamp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLastActiveTime', () => {
|
||||
it('sets the last active time to the current time', () => {
|
||||
const spy = jest.spyOn(appStateController, '_resetTimer');
|
||||
appStateController.setLastActiveTime();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setBrowserEnvironment', () => {
|
||||
it('sets the current browser and OS environment', () => {
|
||||
appStateController.setBrowserEnvironment('Windows', 'Chrome');
|
||||
expect(
|
||||
appStateController.store.getState().browserEnvironment,
|
||||
).toStrictEqual({
|
||||
os: 'Windows',
|
||||
browser: 'Chrome',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addPollingToken', () => {
|
||||
it('adds a pollingToken for a given environmentType', () => {
|
||||
const pollingTokenType = 'popupGasPollTokens';
|
||||
appStateController.addPollingToken('token1', pollingTokenType);
|
||||
expect(appStateController.store.getState()[pollingTokenType]).toContain(
|
||||
'token1',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePollingToken', () => {
|
||||
it('removes a pollingToken for a given environmentType', () => {
|
||||
const pollingTokenType = 'popupGasPollTokens';
|
||||
appStateController.addPollingToken('token1', pollingTokenType);
|
||||
appStateController.removePollingToken('token1', pollingTokenType);
|
||||
expect(
|
||||
appStateController.store.getState()[pollingTokenType],
|
||||
).not.toContain('token1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearPollingTokens', () => {
|
||||
it('clears all pollingTokens', () => {
|
||||
appStateController.addPollingToken('token1', 'popupGasPollTokens');
|
||||
appStateController.addPollingToken('token2', 'notificationGasPollTokens');
|
||||
appStateController.addPollingToken('token3', 'fullScreenGasPollTokens');
|
||||
appStateController.clearPollingTokens();
|
||||
|
||||
expect(
|
||||
appStateController.store.getState().popupGasPollTokens,
|
||||
).toStrictEqual([]);
|
||||
expect(
|
||||
appStateController.store.getState().notificationGasPollTokens,
|
||||
).toStrictEqual([]);
|
||||
expect(
|
||||
appStateController.store.getState().fullScreenGasPollTokens,
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setShowTestnetMessageInDropdown', () => {
|
||||
it('sets whether the testnet dismissal link should be shown in the network dropdown', () => {
|
||||
appStateController.setShowTestnetMessageInDropdown(true);
|
||||
expect(
|
||||
appStateController.store.getState().showTestnetMessageInDropdown,
|
||||
).toBe(true);
|
||||
|
||||
appStateController.setShowTestnetMessageInDropdown(false);
|
||||
expect(
|
||||
appStateController.store.getState().showTestnetMessageInDropdown,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setShowBetaHeader', () => {
|
||||
it('sets whether the beta notification heading on the home page', () => {
|
||||
appStateController.setShowBetaHeader(true);
|
||||
expect(appStateController.store.getState().showBetaHeader).toBe(true);
|
||||
|
||||
appStateController.setShowBetaHeader(false);
|
||||
expect(appStateController.store.getState().showBetaHeader).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCurrentPopupId', () => {
|
||||
it('sets the currentPopupId in the appState', () => {
|
||||
const popupId = 'popup1';
|
||||
|
||||
appStateController.setCurrentPopupId(popupId);
|
||||
expect(appStateController.store.getState().currentPopupId).toBe(popupId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentPopupId', () => {
|
||||
it('retrieves the currentPopupId saved in the appState', () => {
|
||||
const popupId = 'popup1';
|
||||
|
||||
appStateController.setCurrentPopupId(popupId);
|
||||
expect(appStateController.getCurrentPopupId()).toBe(popupId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setFirstTimeUsedNetwork', () => {
|
||||
it('updates the array of the first time used networks', () => {
|
||||
const chainId = '0x1';
|
||||
|
||||
appStateController.setFirstTimeUsedNetwork(chainId);
|
||||
expect(appStateController.store.getState().usedNetworks[chainId]).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -520,9 +520,15 @@ export default class MetamaskController extends EventEmitter {
|
||||
isUnlocked: this.isUnlocked.bind(this),
|
||||
initState: initState.AppStateController,
|
||||
onInactiveTimeout: () => this.setLocked(),
|
||||
showUnlockRequest: opts.showUserConfirmation,
|
||||
preferencesStore: this.preferencesController.store,
|
||||
qrHardwareStore: this.qrHardwareKeyring.getMemStore(),
|
||||
messenger: this.controllerMessenger.getRestricted({
|
||||
name: 'AppStateController',
|
||||
allowedActions: [
|
||||
`${this.approvalController.name}:addRequest`,
|
||||
`${this.approvalController.name}:acceptRequest`,
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const currencyRateMessenger = this.controllerMessenger.getRestricted({
|
||||
|
Loading…
Reference in New Issue
Block a user