import EventEmitter from 'events' import { ObservableStore } from '@metamask/obs-store' export default class AppStateController extends EventEmitter { /** * @constructor * @param {Object} opts */ constructor(opts = {}) { const { addUnlockListener, isUnlocked, initState, onInactiveTimeout, showUnlockRequest, preferencesStore, } = opts super() this.onInactiveTimeout = onInactiveTimeout || (() => undefined) this.store = new ObservableStore({ timeoutMinutes: 0, connectedStatusPopoverHasBeenShown: true, swapsWelcomeMessageHasBeenShown: false, defaultHomeActiveTabName: null, ...initState, }) 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) } }) 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('updateBadge') 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('updateBadge') } } /** * 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 seen the swap screen welcome message */ setSwapsWelcomeMessageHasBeenShown() { this.store.updateState({ swapsWelcomeMessageHasBeenShown: true, }) } /** * 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 * 60 * 1000, ) } }