Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/disco/addonManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
globalEvents,
globalEventStatusMap,
installEventList,
SET_ENABLE_NOT_AVAILABLE,
} from 'disco/constants';


Expand Down Expand Up @@ -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);
});
}
88 changes: 56 additions & 32 deletions src/disco/components/Addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -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.'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "s" in %(name)s is intentional, right? It seems off but maybe I don't get what name will be set to...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking through it seems not. 🤔

Copy link
Contributor Author

@muffinresearch muffinresearch Jun 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's sprintf format [1] - we're using sprintf via jed [2] for localization substitutions. The 2nd arg is on the line below.

[1] http://www.diveintojavascript.com/projects/javascript-sprintf
[2] https://slexaxton.github.io/Jed/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooohhhh okay yeah I should've realised.

Cool, all good, then. My r+wc is now just an r+ 😄

{ 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 };
Expand Down Expand Up @@ -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 } });
Expand All @@ -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);
Expand Down
23 changes: 14 additions & 9 deletions src/disco/components/InstallButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import translate from 'core/i18n/translate';

import {
DOWNLOADING,
DISABLED,
ENABLED,
INSTALLED,
THEME_TYPE,
UNINSTALLED,
Expand All @@ -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 = {
Expand All @@ -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 });
}
}
Expand All @@ -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()}`;
Expand Down
4 changes: 4 additions & 0 deletions src/disco/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
8 changes: 6 additions & 2 deletions src/disco/css/InstallButton.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
}
}
}

Expand Down
15 changes: 1 addition & 14 deletions src/disco/reducers/installations.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
DISABLED,
DOWNLOAD_PROGRESS,
DOWNLOADING,
ENABLED,
ERROR,
INSTALLED,
INSTALL_COMPLETE,
Expand All @@ -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;
Expand All @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions tests/client/disco/TestAddonManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { unexpectedSuccess } from 'tests/client/helpers';
import {
globalEventStatusMap,
installEventList,
SET_ENABLE_NOT_AVAILABLE,
} from 'disco/constants';


Expand Down Expand Up @@ -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);
});
});
});
});
58 changes: 58 additions & 0 deletions tests/client/disco/components/TestAddon.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
INSTALL_CATEGORY,
INSTALL_FAILED,
INSTALL_STATE,
SET_ENABLE_NOT_AVAILABLE,
SHOW_INFO,
START_DOWNLOAD,
THEME_INSTALL,
Expand Down Expand Up @@ -526,6 +527,63 @@ describe('<Addon />', () => {
});
});

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';
Expand Down
Loading