import EventEmitter from 'events'; import { ObservableStore } from '@metamask/obs-store'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; import { MINUTE } from '../../../shared/constants/time'; export default class AppStateController extends EventEmitter { /** * @constructor * @param {Object} opts */ constructor(opts = {}) { const { addUnlockListener, isUnlocked, initState, onInactiveTimeout, showUnlockRequest, preferencesStore, qrHardwareStore, } = opts; super(); this.onInactiveTimeout = onInactiveTimeout || (() => undefined); this.store = new ObservableStore({ timeoutMinutes: 0, connectedStatusPopoverHasBeenShown: true, defaultHomeActiveTabName: null, browserEnvironment: {}, popupGasPollTokens: [], notificationGasPollTokens: [], fullScreenGasPollTokens: [], recoveryPhraseReminderHasBeenShown: false, recoveryPhraseReminderLastShown: new Date().getTime(), collectiblesDetectionNoticeDismissed: false, showTestnetMessageInDropdown: true, trezorModel: null, ...initState, qrHardware: {}, }); this.timer = null; this.isUnlocked = isUnlocked; this.waitingForUnlock = []; addUnlockListener(this.handleUnlock.bind(this)); this._showUnlockRequest = showUnlockRequest; preferencesStore.subscribe(({ preferences }) => { const currentState = this.store.getState(); if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) { this._setInactiveTimeout(preferences.autoLockTimeLimit); } }); qrHardwareStore.subscribe((state) => { this.store.updateState({ qrHardware: state }); }); const { preferences } = preferencesStore.getState(); this._setInactiveTimeout(preferences.autoLockTimeLimit); } /** * Get a Promise that resolves when the extension is unlocked. * This Promise will never reject. * * @param {boolean} shouldShowUnlockRequest - Whether the extension notification * popup should be opened. * @returns {Promise} A promise that resolves when the extension is * unlocked, or immediately if the extension is already unlocked. */ getUnlockPromise(shouldShowUnlockRequest) { return new Promise((resolve) => { if (this.isUnlocked()) { resolve(); } else { this.waitForUnlock(resolve, shouldShowUnlockRequest); } }); } /** * Adds a Promise's resolve function to the waitingForUnlock queue. * Also opens the extension popup if specified. * * @param {Promise.resolve} resolve - A Promise's resolve function that will * be called when the extension is unlocked. * @param {boolean} shouldShowUnlockRequest - Whether the extension notification * popup should be opened. */ waitForUnlock(resolve, shouldShowUnlockRequest) { this.waitingForUnlock.push({ resolve }); this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); if (shouldShowUnlockRequest) { this._showUnlockRequest(); } } /** * Drains the waitingForUnlock queue, resolving all the related Promises. */ handleUnlock() { if (this.waitingForUnlock.length > 0) { while (this.waitingForUnlock.length > 0) { this.waitingForUnlock.shift().resolve(); } this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); } } /** * Sets the default home tab * @param {string} [defaultHomeActiveTabName] - the tab name */ setDefaultHomeActiveTabName(defaultHomeActiveTabName) { this.store.updateState({ defaultHomeActiveTabName, }); } /** * Record that the user has seen the connected status info popover */ setConnectedStatusPopoverHasBeenShown() { this.store.updateState({ connectedStatusPopoverHasBeenShown: true, }); } /** * Record that the user has been shown the recovery phrase reminder * @returns {void} */ setRecoveryPhraseReminderHasBeenShown() { this.store.updateState({ recoveryPhraseReminderHasBeenShown: true, }); } /** * Record the timestamp of the last time the user has seen the recovery phrase reminder * @param {number} lastShown - timestamp when user was last shown the reminder * @returns {void} */ setRecoveryPhraseReminderLastShown(lastShown) { this.store.updateState({ recoveryPhraseReminderLastShown: lastShown, }); } /** * Sets the last active time to the current time * @returns {void} */ setLastActiveTime() { this._resetTimer(); } /** * Sets the inactive timeout for the app * @param {number} timeoutMinutes - the inactive timeout in minutes * @returns {void} * @private */ _setInactiveTimeout(timeoutMinutes) { this.store.updateState({ timeoutMinutes, }); this._resetTimer(); } /** * Resets the internal inactive timer * * If the {@code timeoutMinutes} state is falsy (i.e., zero) then a new * timer will not be created. * * @returns {void} * @private */ _resetTimer() { const { timeoutMinutes } = this.store.getState(); if (this.timer) { clearTimeout(this.timer); } if (!timeoutMinutes) { return; } this.timer = setTimeout( () => this.onInactiveTimeout(), timeoutMinutes * MINUTE, ); } /** * Sets the current browser and OS environment * @returns {void} */ setBrowserEnvironment(os, browser) { this.store.updateState({ browserEnvironment: { os, browser } }); } /** * Adds a pollingToken for a given environmentType * @returns {void} */ addPollingToken(pollingToken, pollingTokenType) { const prevState = this.store.getState()[pollingTokenType]; this.store.updateState({ [pollingTokenType]: [...prevState, pollingToken], }); } /** * removes a pollingToken for a given environmentType * @returns {void} */ removePollingToken(pollingToken, pollingTokenType) { const prevState = this.store.getState()[pollingTokenType]; this.store.updateState({ [pollingTokenType]: prevState.filter((token) => token !== pollingToken), }); } /** * clears all pollingTokens * @returns {void} */ clearPollingTokens() { this.store.updateState({ popupGasPollTokens: [], notificationGasPollTokens: [], fullScreenGasPollTokens: [], }); } /** * Sets whether the testnet dismissal link should be shown in the network dropdown * @returns {void} */ setShowTestnetMessageInDropdown(showTestnetMessageInDropdown) { this.store.updateState({ showTestnetMessageInDropdown }); } /** * Sets a property indicating the model of the user's Trezor hardware wallet * @returns {void} */ setTrezorModel(trezorModel) { this.store.updateState({ trezorModel }); } /** * A setter for the `collectiblesDetectionNoticeDismissed` property */ setCollectiblesDetectionNoticeDismissed( collectiblesDetectionNoticeDismissed, ) { this.store.updateState({ collectiblesDetectionNoticeDismissed, }); } }