diff --git a/src/disco/components/Addon.js b/src/disco/components/Addon.js index 928bad7c0ba..de78c980066 100644 --- a/src/disco/components/Addon.js +++ b/src/disco/components/Addon.js @@ -29,6 +29,7 @@ import { THEME_PREVIEW, THEME_RESET_PREVIEW, THEME_TYPE, + UNINSTALLING, UNINSTALLED, UNINSTALL_CATEGORY, } from 'disco/constants'; @@ -56,6 +57,7 @@ export class Addon extends React.Component { iconUrl: PropTypes.string, id: PropTypes.string.isRequired, installURL: PropTypes.string, + needsRestart: PropTypes.bool.isRequired, previewURL: PropTypes.string, name: PropTypes.string.isRequired, setCurrentStatus: PropTypes.func.isRequired, @@ -68,6 +70,7 @@ export class Addon extends React.Component { static defaultProps = { // Defaults themeAction to the imported func. themeAction, + needsRestart: false, } componentDidMount() { @@ -84,12 +87,18 @@ export class Addon extends React.Component { } getError() { - return this.props.status === ERROR ? (
+ return this.props.status === ERROR ? (

{this.errorMessage()}

Close
) : null; } + getRestart() { + return this.props.needsRestart ? (
+

{this.restartMessage()}

+
) : null; + } + getLogo() { const { iconUrl } = this.props; if (this.props.type === EXTENSION_TYPE) { @@ -140,6 +149,16 @@ export class Addon extends React.Component { } } + restartMessage() { + const { status, i18n } = this.props; + switch (status) { + case UNINSTALLING: + return i18n.gettext('This add-on will be uninstalled after you restart Firefox.'); + default: + return i18n.gettext('Please restart Firefox to use this add-on.'); + } + } + closeError = (e) => { e.preventDefault(); this.setCurrentStatus(); @@ -174,6 +193,7 @@ export class Addon extends React.Component { {this.getLogo()}
{this.getError()} + {this.getRestart()}

.error { - align-content: stretch; - align-items: center; - background: #c33c32 url('../img/warning.svg') no-repeat calc(100% - 40px) 50%; - bottom: 0; - color: #fff; - display: flex; - flex-direction: row; - left: 0; - padding: 15px 20px; - position: absolute; - right: 0; - top: 0; - z-index: 10; - - .message { - flex-grow: 1; - } - - .close { - bottom: 10px; - color: #fff; - display: block; - left: 15px; - padding: 5px; - position: absolute; - } - } .copy { padding: 30px 20px; @@ -120,4 +92,40 @@ $addon-padding: 20px; } } + .notification { + align-content: stretch; + align-items: center; + bottom: 0; + color: #fff; + display: flex; + flex-direction: row; + left: 0; + padding: 15px 20px; + position: absolute; + right: 0; + top: 0; + + &.error { + background: #c33c32 url('../img/warning.svg') no-repeat calc(100% - 40px) 50%; + z-index: 20; + } + + &.restart { + background: #70CF5B url('../img/restart.svg') no-repeat calc(100% - 40px) 50%; + z-index: 10; + } + + .message { + flex-grow: 1; + } + + .close { + bottom: 10px; + color: #fff; + display: block; + left: 15px; + padding: 5px; + position: absolute; + } + } } diff --git a/src/disco/img/restart.svg b/src/disco/img/restart.svg new file mode 100644 index 00000000000..9f19e162c3a --- /dev/null +++ b/src/disco/img/restart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/disco/reducers/installations.js b/src/disco/reducers/installations.js index 308e00d3ed5..6a12664ae3c 100644 --- a/src/disco/reducers/installations.js +++ b/src/disco/reducers/installations.js @@ -40,6 +40,7 @@ export default function installations(state = {}, { type, payload }) { url: payload.url, downloadProgress: 0, status: normalizeStatus(payload.status), + needsRestart: payload.needsRestart || false, }; } else if (type === START_DOWNLOAD) { addon.status = DOWNLOADING; diff --git a/tests/client/disco/components/TestAddon.js b/tests/client/disco/components/TestAddon.js index bf7da780f3a..dc2f3cff563 100644 --- a/tests/client/disco/components/TestAddon.js +++ b/tests/client/disco/components/TestAddon.js @@ -28,6 +28,7 @@ import { THEME_RESET_PREVIEW, THEME_TYPE, UNINSTALLED, + UNINSTALLING, UNINSTALL_CATEGORY, } from 'disco/constants'; import { getFakeAddonManagerWrapper, getFakeI18nInst } from 'tests/client/helpers'; @@ -63,7 +64,7 @@ describe('', () => { it('renders a default error overlay', () => { const data = {...result, status: ERROR, setCurrentStatus: sinon.stub()}; root = renderAddon(data); - const error = findDOMNode(root).querySelector('.error'); + const error = findDOMNode(root).querySelector('.notification.error'); assert.equal( error.querySelector('p').textContent, 'An unexpected error occurred.', @@ -77,7 +78,7 @@ describe('', () => { ...result, status: ERROR, error: INSTALL_FAILED, setCurrentStatus: sinon.stub(), }; root = renderAddon(data); - const error = findDOMNode(root).querySelector('.error'); + const error = findDOMNode(root).querySelector('.notification.error'); assert.equal( error.querySelector('p').textContent, 'Installation failed. Please try again.', @@ -91,7 +92,7 @@ describe('', () => { ...result, status: ERROR, error: DOWNLOAD_FAILED, setCurrentStatus: sinon.stub(), }; root = renderAddon(data); - const error = findDOMNode(root).querySelector('.error'); + const error = findDOMNode(root).querySelector('.notification.error'); assert.equal( error.querySelector('p').textContent, 'Download failed. Please check your connection.', @@ -101,7 +102,31 @@ describe('', () => { }); it('does not normally render an error', () => { - assert.notOk(findDOMNode(root).querySelector('.error')); + assert.notOk(findDOMNode(root).querySelector('.notification.error')); + }); + + it('renders a default restart notification', () => { + const data = { ...result, needsRestart: true }; + root = renderAddon(data); + const restart = findDOMNode(root).querySelector('.notification.restart'); + assert.equal( + restart.querySelector('p').textContent, + 'Please restart Firefox to use this add-on.', + 'restart message should be present'); + }); + + it('renders a uninstallation restart notification', () => { + const data = { ...result, needsRestart: true, status: UNINSTALLING }; + root = renderAddon(data); + const restart = findDOMNode(root).querySelector('.notification.restart'); + assert.equal( + restart.querySelector('p').textContent, + 'This add-on will be uninstalled after you restart Firefox.', + 'restart uninstallation message should be present'); + }); + + it('does not normally render a restart notification', () => { + assert.notOk(findDOMNode(root).querySelector('.notification.restart')); }); it('renders the heading', () => { diff --git a/tests/client/disco/reducers/test_installations.js b/tests/client/disco/reducers/test_installations.js index caf4619ad8b..6d1ef97a481 100644 --- a/tests/client/disco/reducers/test_installations.js +++ b/tests/client/disco/reducers/test_installations.js @@ -42,6 +42,29 @@ describe('installations reducer', () => { url: 'https://cdn.amo/download/my-addon.xpi', downloadProgress: 0, status: UNINSTALLED, + needsRestart: false, + }, + }); + }); + + it('passes down needsRestart=true', () => { + assert.deepEqual( + installations(undefined, { + type: INSTALL_STATE, + payload: { + guid: 'my-addon@me.com', + url: 'https://cdn.amo/download/my-addon.xpi', + status: UNINSTALLING, + needsRestart: true, + }, + }), + { + 'my-addon@me.com': { + guid: 'my-addon@me.com', + url: 'https://cdn.amo/download/my-addon.xpi', + downloadProgress: 0, + status: UNINSTALLING, + needsRestart: true, }, }); }); @@ -61,6 +84,7 @@ describe('installations reducer', () => { url: undefined, downloadProgress: 0, status: INSTALLED, + needsRestart: false, }, }); }); @@ -80,6 +104,7 @@ describe('installations reducer', () => { url: undefined, downloadProgress: 0, status: UNINSTALLED, + needsRestart: false, }, }); }); @@ -100,6 +125,7 @@ describe('installations reducer', () => { url: 'https://cdn.amo/download/an-addon.xpi', downloadProgress: 0, status: INSTALLED, + needsRestart: false, }, }); });