diff --git a/src/disco/addonManager.js b/src/disco/addonManager.js index 623bd09176f..06e60c23819 100644 --- a/src/disco/addonManager.js +++ b/src/disco/addonManager.js @@ -4,6 +4,7 @@ import { globalEvents, globalEventStatusMap, installEventList, + SET_ENABLE_NOT_AVAILABLE, } from 'disco/constants'; @@ -74,3 +75,14 @@ export function addChangeListeners(callback, mozAddonManager) { } return handleChangeEvent; } + +export function enable(guid, { _mozAddonManager = window.navigator.mozAddonManager } = {}) { + return getAddon(guid, { _mozAddonManager }) + .then((addon) => { + log.info(`Enable ${guid}`); + if (addon.setEnabled) { + return addon.setEnabled(true); + } + throw new Error(SET_ENABLE_NOT_AVAILABLE); + }); +} diff --git a/src/disco/components/Addon.js b/src/disco/components/Addon.js index 17462b74cb3..44cad892c7e 100644 --- a/src/disco/components/Addon.js +++ b/src/disco/components/Addon.js @@ -14,8 +14,7 @@ import log from 'core/logger'; import InstallButton from 'disco/components/InstallButton'; import { - validAddonTypes, - validInstallStates, + CLOSE_INFO, DISABLED, DOWNLOAD_FAILED, DOWNLOAD_PROGRESS, @@ -25,20 +24,22 @@ import { FATAL_ERROR, FATAL_INSTALL_ERROR, FATAL_UNINSTALL_ERROR, - CLOSE_INFO, INSTALL_CATEGORY, INSTALL_ERROR, INSTALL_FAILED, INSTALL_STATE, + SET_ENABLE_NOT_AVAILABLE, SHOW_INFO, START_DOWNLOAD, THEME_INSTALL, THEME_PREVIEW, THEME_RESET_PREVIEW, THEME_TYPE, - UNINSTALLING, UNINSTALLED, + UNINSTALLING, UNINSTALL_CATEGORY, + validAddonTypes, + validInstallStates, } from 'disco/constants'; import 'disco/css/Addon.scss'; @@ -286,6 +287,37 @@ export function mapDispatchToProps(dispatch, { _tracking = tracking, // eslint-disable-next-line no-param-reassign _dispatchEvent = _dispatchEvent || document.dispatchEvent; + function showInfo({ name, iconUrl, i18n }) { + if (_config.has('useUiTour') && _config.get('useUiTour')) { + _dispatchEvent(new CustomEvent('mozUITour', { + bubbles: true, + detail: { + action: 'showInfo', + data: { + target: 'appMenu', + icon: iconUrl, + title: i18n.gettext('Your add-on is ready'), + text: i18n.sprintf( + i18n.gettext('Now you can access %(name)s from the toolbar.'), + { name }), + buttons: [{ label: i18n.gettext('OK!'), callbackID: 'add-on-installed' }], + }, + }, + })); + } else { + dispatch({ + type: SHOW_INFO, + payload: { + addonName: name, + imageURL: iconUrl, + closeAction: () => { + dispatch({ type: CLOSE_INFO }); + }, + }, + }); + } + } + return { setCurrentStatus({ guid, installURL }) { const payload = { guid, url: installURL }; @@ -316,6 +348,25 @@ export function mapDispatchToProps(dispatch, { _tracking = tracking, }); }, + enable({ _showInfo = showInfo } = {}) { + const { guid, name, iconUrl, i18n } = ownProps; + return _addonManager.enable(guid) + .then(() => { + _showInfo({ name, iconUrl, i18n }); + }) + .catch((err) => { + if (err && err.message === SET_ENABLE_NOT_AVAILABLE) { + log.info(`addon.setEnabled not available. Unable to enable ${guid}`); + } else { + log.error(err); + dispatch({ + type: INSTALL_STATE, + payload: { guid, status: ERROR, error: FATAL_ERROR }, + }); + } + }); + }, + install() { const { guid, i18n, iconUrl, installURL, name } = ownProps; dispatch({ type: START_DOWNLOAD, payload: { guid } }); @@ -326,34 +377,7 @@ export function mapDispatchToProps(dispatch, { _tracking = tracking, category: INSTALL_CATEGORY, label: name, }); - if (_config.has('useUiTour') && _config.get('useUiTour')) { - _dispatchEvent(new CustomEvent('mozUITour', { - bubbles: true, - detail: { - action: 'showInfo', - data: { - target: 'appMenu', - icon: iconUrl, - title: i18n.gettext('Your add-on is ready'), - text: i18n.sprintf( - i18n.gettext('Now you can access %(name)s from the toolbar.'), - { name }), - buttons: [{ label: i18n.gettext('Ok'), callbackID: 'add-on-installed' }], - }, - }, - })); - } else { - dispatch({ - type: SHOW_INFO, - payload: { - addonName: name, - imageURL: iconUrl, - closeAction: () => { - dispatch({ type: CLOSE_INFO }); - }, - }, - }); - } + showInfo({ name, iconUrl, i18n }); }) .catch((err) => { log.error(err); diff --git a/src/disco/components/InstallButton.js b/src/disco/components/InstallButton.js index 6b8209a643c..34c73040f32 100644 --- a/src/disco/components/InstallButton.js +++ b/src/disco/components/InstallButton.js @@ -3,6 +3,8 @@ import translate from 'core/i18n/translate'; import { DOWNLOADING, + DISABLED, + ENABLED, INSTALLED, THEME_TYPE, UNINSTALLED, @@ -16,19 +18,20 @@ import 'disco/css/InstallButton.scss'; export class InstallButton extends React.Component { static propTypes = { - handleChange: PropTypes.func, + downloadProgress: PropTypes.number, + enable: PropTypes.func, guid: PropTypes.string.isRequired, + handleChange: PropTypes.func, + i18n: PropTypes.object.isRequired, install: PropTypes.func.isRequired, installTheme: PropTypes.func.isRequired, - i18n: PropTypes.object.isRequired, installURL: PropTypes.string, name: PropTypes.string.isRequired, - uninstall: PropTypes.func.isRequired, - url: PropTypes.string, - downloadProgress: PropTypes.number, slug: PropTypes.string.isRequired, status: PropTypes.oneOf(validStates), type: PropTypes.oneOf(validAddonTypes), + uninstall: PropTypes.func.isRequired, + url: PropTypes.string, } static defaultProps = { @@ -39,13 +42,15 @@ export class InstallButton extends React.Component { handleClick = (e) => { e.preventDefault(); const { - guid, install, installURL, name, status, installTheme, type, uninstall, + guid, enable, install, installURL, name, status, installTheme, type, uninstall, } = this.props; - if (type === THEME_TYPE && status === UNINSTALLED) { + if (type === THEME_TYPE && [UNINSTALLED, DISABLED].includes(status)) { installTheme(this.refs.themeData, guid, name); } else if (status === UNINSTALLED) { install(); - } else if (status === INSTALLED) { + } else if (status === DISABLED) { + enable(); + } else if ([INSTALLED, ENABLED].includes(status)) { uninstall({ guid, installURL, name, type }); } } @@ -57,7 +62,7 @@ export class InstallButton extends React.Component { throw new Error('Invalid add-on status'); } - const isInstalled = status === INSTALLED; + const isInstalled = [INSTALLED, ENABLED].includes(status); const isDisabled = status === UNKNOWN; const isDownloading = status === DOWNLOADING; const switchClasses = `switch ${status.toLowerCase()}`; diff --git a/src/disco/constants.js b/src/disco/constants.js index 30d994d1489..f4547ca38fb 100644 --- a/src/disco/constants.js +++ b/src/disco/constants.js @@ -111,3 +111,7 @@ export const globalEvents = Object.keys(globalEventStatusMap); export const SHOW_INFO = 'SHOW_INFO'; export const CLOSE_INFO = 'CLOSE_INFO'; + +// Error used to know that the setEnable method on addon is +// not available. +export const SET_ENABLE_NOT_AVAILABLE = 'SET_ENABLE_NOT_AVAILABLE'; diff --git a/src/disco/css/InstallButton.scss b/src/disco/css/InstallButton.scss index c347a3f202d..f2d1c5ce1d0 100644 --- a/src/disco/css/InstallButton.scss +++ b/src/disco/css/InstallButton.scss @@ -182,8 +182,12 @@ $installStripeColor2: #00c42e; } } - &.installed input + label::before { - background: $switchBackgroundOn url('../img/tick.svg') no-repeat 35% 50%; + // When add-on status is enabled installed is implied. + &.enabled, + &.installed { + input + label::before { + background: $switchBackgroundOn url('../img/tick.svg') no-repeat 35% 50%; + } } } diff --git a/src/disco/reducers/installations.js b/src/disco/reducers/installations.js index 80092ddfb18..b8c901a1886 100644 --- a/src/disco/reducers/installations.js +++ b/src/disco/reducers/installations.js @@ -1,8 +1,6 @@ import { - DISABLED, DOWNLOAD_PROGRESS, DOWNLOADING, - ENABLED, ERROR, INSTALLED, INSTALL_COMPLETE, @@ -15,17 +13,6 @@ import { } from 'disco/constants'; -function normalizeStatus(status) { - switch (status) { - case DISABLED: - return UNINSTALLED; - case ENABLED: - return INSTALLED; - default: - return status; - } -} - export default function installations(state = {}, { type, payload }) { if (!acceptedInstallTypes.includes(type)) { return state; @@ -40,7 +27,7 @@ export default function installations(state = {}, { type, payload }) { url: payload.url, error: payload.error, downloadProgress: 0, - status: normalizeStatus(payload.status), + status: payload.status, needsRestart: payload.needsRestart || false, }; } else if (type === START_DOWNLOAD) { diff --git a/tests/client/disco/TestAddonManager.js b/tests/client/disco/TestAddonManager.js index 482e6b9a51f..60d8777d859 100644 --- a/tests/client/disco/TestAddonManager.js +++ b/tests/client/disco/TestAddonManager.js @@ -3,6 +3,7 @@ import { unexpectedSuccess } from 'tests/client/helpers'; import { globalEventStatusMap, installEventList, + SET_ENABLE_NOT_AVAILABLE, } from 'disco/constants'; @@ -153,4 +154,26 @@ describe('addonManager', () => { handleChangeEvent({ type: 'whatevs' }); }, Error, /Unknown global event/)); }); + + describe('enable()', () => { + it('should call addon.setEnable()', () => { + fakeAddon = { + setEnabled: sinon.stub(), + }; + fakeMozAddonManager.getAddonByID.returns(Promise.resolve(fakeAddon)); + return addonManager.enable('whatever', { _mozAddonManager: fakeMozAddonManager }) + .then(() => { + assert.ok(fakeAddon.setEnabled.calledWith(true)); + }); + }); + + it('should throw if addon.setEnable does not exist', () => { + fakeAddon = {}; + fakeMozAddonManager.getAddonByID.returns(Promise.resolve(fakeAddon)); + return addonManager.enable('whatevs', { _mozAddonManager: fakeMozAddonManager }) + .then(unexpectedSuccess, (err) => { + assert.equal(err.message, SET_ENABLE_NOT_AVAILABLE); + }); + }); + }); }); diff --git a/tests/client/disco/components/TestAddon.js b/tests/client/disco/components/TestAddon.js index 4cc5dc498b9..8ef7ee386fe 100644 --- a/tests/client/disco/components/TestAddon.js +++ b/tests/client/disco/components/TestAddon.js @@ -27,6 +27,7 @@ import { INSTALL_CATEGORY, INSTALL_FAILED, INSTALL_STATE, + SET_ENABLE_NOT_AVAILABLE, SHOW_INFO, START_DOWNLOAD, THEME_INSTALL, @@ -526,6 +527,63 @@ describe('', () => { }); }); + describe('enable', () => { + const guid = '@enable'; + const name = 'whatever addon'; + const iconUrl = 'something.jpg'; + + it('calls addonManager.enable()', () => { + const fakeAddonManager = getFakeAddonManagerWrapper(); + const dispatch = sinon.spy(); + const i18n = getFakeI18nInst(); + const { enable } = mapDispatchToProps( + dispatch, + { name, iconUrl, guid, _addonManager: fakeAddonManager, i18n }); + const fakeShowInfo = sinon.stub(); + return enable({ _showInfo: fakeShowInfo }) + .then(() => { + assert.ok(fakeAddonManager.enable.calledWith(guid)); + assert.ok(fakeShowInfo.calledWith({ name, i18n, iconUrl })); + }); + }); + + it('dispatches a FATAL_ERROR', () => { + const fakeAddonManager = { + enable: sinon.stub().returns(Promise.reject(new Error('hai'))), + }; + const dispatch = sinon.spy(); + const i18n = getFakeI18nInst(); + const { enable } = mapDispatchToProps( + dispatch, + { name, iconUrl, guid, _addonManager: fakeAddonManager, i18n }); + return enable() + .then(() => { + assert.ok(dispatch.calledWith({ + type: INSTALL_STATE, + payload: { guid, status: ERROR, error: FATAL_ERROR }, + })); + }); + }); + + it('does not dispatch a FATAL_ERROR when setEnabled is missing', () => { + const fakeAddonManager = { + enable: sinon.stub().returns(Promise.reject(new Error(SET_ENABLE_NOT_AVAILABLE))), + }; + const dispatch = sinon.spy(); + const i18n = getFakeI18nInst(); + const { enable } = mapDispatchToProps( + dispatch, + { name, iconUrl, guid, _addonManager: fakeAddonManager, i18n }); + return enable() + .then(() => { + assert.notOk(dispatch.calledWith({ + type: INSTALL_STATE, + payload: { guid, status: ERROR, error: FATAL_ERROR }, + })); + }); + }); + }); + describe('install', () => { const guid = '@install'; const installURL = 'https://mysite.com/download.xpi'; diff --git a/tests/client/disco/reducers/test_installations.js b/tests/client/disco/reducers/test_installations.js index 80417f40afc..d8a84561dca 100644 --- a/tests/client/disco/reducers/test_installations.js +++ b/tests/client/disco/reducers/test_installations.js @@ -71,7 +71,7 @@ describe('installations reducer', () => { }); }); - it('treats ENABLED as INSTALLED in INSTALL_STATE', () => { + it('handles ENABLED status in INSTALL_STATE', () => { assert.deepEqual( installations(undefined, { type: 'INSTALL_STATE', @@ -86,13 +86,13 @@ describe('installations reducer', () => { error: undefined, guid: 'my-addon@me.com', needsRestart: false, - status: INSTALLED, + status: ENABLED, url: undefined, }, }); }); - it('treats DISABLED as UNINSTALLED in INSTALL_STATE', () => { + it('handles DISABLED status in INSTALL_STATE', () => { assert.deepEqual( installations(undefined, { type: 'INSTALL_STATE', @@ -107,7 +107,7 @@ describe('installations reducer', () => { error: undefined, guid: 'my-addon@me.com', needsRestart: false, - status: UNINSTALLED, + status: DISABLED, url: undefined, }, }); diff --git a/tests/client/helpers.js b/tests/client/helpers.js index 86bdb47fd06..206cc2f732e 100644 --- a/tests/client/helpers.js +++ b/tests/client/helpers.js @@ -22,10 +22,11 @@ const enabledExtension = Promise.resolve({ type: EXTENSION_TYPE, isActive: true, export function getFakeAddonManagerWrapper({ getAddon = enabledExtension } = {}) { return { + addChangeListeners: sinon.stub(), + enable: sinon.stub().returns(Promise.resolve()), getAddon: sinon.stub().returns(getAddon), install: sinon.stub().returns(Promise.resolve()), uninstall: sinon.stub().returns(Promise.resolve()), - addChangeListerners: sinon.stub(), }; }