diff --git a/src/disco/constants.js b/src/disco/constants.js
index 214535513f4..c810d4d3109 100644
--- a/src/disco/constants.js
+++ b/src/disco/constants.js
@@ -18,7 +18,7 @@ export const validInstallStates = [
// Add-on types.
export const EXTENSION_TYPE = 'extension';
-export const THEME_TYPE = 'theme';
+export const THEME_TYPE = 'persona';
// These types are not used.
// export const DICT_TYPE = 'dictionary';
// export const SEARCH_TYPE = 'search';
diff --git a/src/disco/containers/DiscoPane.js b/src/disco/containers/DiscoPane.js
index 23f28f5bd41..6d3f95ef03a 100644
--- a/src/disco/containers/DiscoPane.js
+++ b/src/disco/containers/DiscoPane.js
@@ -5,6 +5,10 @@ import { connect } from 'react-redux';
import { asyncConnect } from 'redux-async-connect';
import { camelCaseProps } from 'core/utils';
+import { getDiscoveryAddons } from 'disco/api';
+import { discoResults } from 'disco/actions';
+import { loadEntities } from 'core/actions';
+
import Addon from 'disco/components/Addon';
import translate from 'core/i18n/translate';
@@ -13,7 +17,7 @@ import videoMp4 from 'disco/video/AddOns.mp4';
import videoWebm from 'disco/video/AddOns.webm';
-class DiscoPane extends React.Component {
+export class DiscoPane extends React.Component {
static propTypes = {
i18n: PropTypes.object.isRequired,
results: PropTypes.arrayOf(PropTypes.object),
@@ -72,15 +76,26 @@ class DiscoPane extends React.Component {
}
}
-function loadDataIfNeeded() {
- /* istanbul ignore next */
- return Promise.resolve();
+function loadedAddons(state) {
+ return state.discoResults.map((result) => ({...result, ...state.addons[result.addon]}));
+}
+
+export function loadDataIfNeeded({ store: { dispatch, getState }}) {
+ const state = getState();
+ const addons = loadedAddons(state);
+ if (addons.length > 0) {
+ return Promise.resolve();
+ }
+ return getDiscoveryAddons({api: state.api})
+ .then(({ entities, result }) => {
+ dispatch(loadEntities(entities));
+ dispatch(discoResults(result.results.map((r) => entities.discoResults[r])));
+ });
}
-function mapStateToProps(state) {
- const { addons } = state;
+export function mapStateToProps(state) {
return {
- results: [addons['japanese-tattoo'], addons['awesome-screenshot-capture-']],
+ results: loadedAddons(state),
};
}
diff --git a/src/disco/css/Addon.scss b/src/disco/css/Addon.scss
index 57d9c23238d..d6dcb2469d8 100644
--- a/src/disco/css/Addon.scss
+++ b/src/disco/css/Addon.scss
@@ -11,6 +11,16 @@ $addon-padding: 20px;
line-height: 1.5;
margin-top: 20px;
+ blockquote {
+ margin: 0;
+ }
+
+ .editorial-description {
+ color: $secondary-font-color;
+ font-size: 12px;
+ margin: 0.5em 0;
+ }
+
&.theme {
display: block;
@@ -49,6 +59,19 @@ $addon-padding: 20px;
}
}
+ .heading {
+ color: $primary-font-color;
+ font-size: 18px;
+ font-weight: medium;
+ margin: 0;
+
+ span {
+ color: $secondary-font-color;
+ font-size: 14px;
+ font-weight: normal;
+ }
+ }
+
.content {
display: flex;
align-items: center;
@@ -88,25 +111,6 @@ $addon-padding: 20px;
padding: 30px 20px;
flex-grow: 1;
- .heading {
- color: $primary-font-color;
- font-size: 18px;
- font-weight: medium;
- margin: 0;
- }
-
- .sub-heading {
- color: $secondary-font-color;
- font-size: 14px;
- font-weight: normal;
- }
-
- .editorial-description {
- color: $secondary-font-color;
- font-size: 12px;
- margin: 0.5em 0;
- }
-
// Remove the bottom margin of the last element.
& :last-child {
margin-bottom: 0;
diff --git a/src/disco/fakeData.js b/src/disco/fakeData.js
deleted file mode 100644
index 20adc6a5b77..00000000000
--- a/src/disco/fakeData.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import config from 'config';
-
-const amoCDN = config.get('amoCDN');
-const amoHost = config.get('apiHost');
-
-export default {
- addons: {
- 'japanese-tattoo': {
- editorial_description: 'Hover over the image to preview',
- heading: 'Japanese Tattoo by MaDonna',
- id: '18781',
- guid: '18781@personas.mozilla.org',
- name: 'Japanese Tattoo',
- slug: 'japanese-tattoo',
- sub_heading: null,
- type: 'theme',
- url: `${amoHost}/en-US/firefox/addon/japanese-tattoo/`,
- installURL: 'https://addons.mozilla.org/en-US/firefox/addon/10900/',
- themeURL: `${amoCDN}/user-media/addons/18781/preview_large.jpg?1239806327`,
- headerURL: `${amoCDN}/user-media/addons/18781/personare.jpg?1239806327`,
- footerURL: `${amoCDN}/user-media/addons/18781/persona2re.jpg?1239806327`,
- textcolor: '#000000',
- accentcolor: '#ffffff',
- },
- 'awesome-screenshot-capture-': {
- editorial_description: '
“The best. Very easy to use.” — meetdak',
- heading: 'Take screenshots',
- sub_heading: 'with Awesome Screenshot Plus',
- id: 727,
- guid: 'jid0-GXjLLfbCoAx0LcltEdFrEkQdQPI@jetpack',
- imageURL: `${amoCDN}/user-media/addon_icons/287/287841-64.png?modified=1353989650`,
- installURL: 'https://addons.mozilla.org/firefox/downloads/latest/287841/' +
- 'addon-287841-latest.xpi?src=dp-btn-primary',
- name: 'Awesome Screenshot Plus - Capture, Annotate & More',
- slug: 'awesome-screenshot-capture-',
- type: 'extension',
- url: `${amoHost}/en-US/firefox/addon/awesome-screenshot-capture-/`,
- },
- },
-};
diff --git a/src/disco/reducers/discoResults.js b/src/disco/reducers/discoResults.js
new file mode 100644
index 00000000000..4fa2aa18434
--- /dev/null
+++ b/src/disco/reducers/discoResults.js
@@ -0,0 +1,6 @@
+export default function discoResults(state = [], { type, payload }) {
+ if (type === 'DISCO_RESULTS') {
+ return payload.results;
+ }
+ return state;
+}
diff --git a/src/disco/store.js b/src/disco/store.js
index 369425023db..23bb9032a52 100644
--- a/src/disco/store.js
+++ b/src/disco/store.js
@@ -3,14 +3,13 @@ import { reducer as reduxAsyncConnect } from 'redux-async-connect';
import { middleware } from 'core/store';
import addons from 'core/reducers/addons';
-import fakeData from 'disco/fakeData';
+import discoResults from 'disco/reducers/discoResults';
import installations from 'disco/reducers/installations';
export default function createStore(initialState = {}) {
- const state = {...initialState, ...fakeData};
return _createStore(
- combineReducers({addons, installations, reduxAsyncConnect}),
- state,
+ combineReducers({addons, discoResults, installations, reduxAsyncConnect}),
+ initialState,
middleware(),
);
}
diff --git a/src/search/actions/index.js b/src/search/actions/index.js
index 1a96c5e3d27..5b0c348ec81 100644
--- a/src/search/actions/index.js
+++ b/src/search/actions/index.js
@@ -18,19 +18,3 @@ export function searchFail({ page, query }) {
payload: { page, query },
};
}
-
-export function loadEntities(entities) {
- return {
- type: 'ENTITIES_LOADED',
- payload: {entities},
- };
-}
-
-export function setCurrentUser(username) {
- return {
- type: 'SET_CURRENT_USER',
- payload: {
- username,
- },
- };
-}
diff --git a/src/search/containers/AddonPage/index.js b/src/search/containers/AddonPage/index.js
index 33deb146f31..02287ddceba 100644
--- a/src/search/containers/AddonPage/index.js
+++ b/src/search/containers/AddonPage/index.js
@@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { asyncConnect } from 'redux-async-connect';
import { fetchAddon } from 'core/api';
-import { loadEntities } from 'search/actions';
+import { loadEntities } from 'core/actions';
import { gettext as _ } from 'core/utils';
import NotFound from 'core/components/NotFound';
diff --git a/src/search/containers/UserPage/index.js b/src/search/containers/UserPage/index.js
index d5541933f51..a9a730c01d8 100644
--- a/src/search/containers/UserPage/index.js
+++ b/src/search/containers/UserPage/index.js
@@ -2,8 +2,8 @@ import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { asyncConnect } from 'redux-async-connect';
+import { loadEntities, setCurrentUser } from 'core/actions';
import { fetchProfile } from 'core/api';
-import { loadEntities, setCurrentUser } from 'search/actions';
import './styles.scss';
diff --git a/tests/client/core/actions/test_index.js b/tests/client/core/actions/test_index.js
index d5025fb4174..4dcd530f989 100644
--- a/tests/client/core/actions/test_index.js
+++ b/tests/client/core/actions/test_index.js
@@ -7,3 +7,16 @@ describe('core actions setJWT', () => {
{type: 'SET_JWT', payload: {token: 'my.amo.token'}});
});
});
+
+describe('ENTITIES_LOADED', () => {
+ const entities = sinon.stub();
+ const action = actions.loadEntities(entities);
+
+ it('sets the type', () => {
+ assert.equal(action.type, 'ENTITIES_LOADED');
+ });
+
+ it('sets the payload', () => {
+ assert.deepEqual(action.payload, {entities});
+ });
+});
diff --git a/tests/client/core/reducers/test_addons.js b/tests/client/core/reducers/test_addons.js
index d46fdb20192..fadcf4a4976 100644
--- a/tests/client/core/reducers/test_addons.js
+++ b/tests/client/core/reducers/test_addons.js
@@ -23,4 +23,47 @@ describe('addon reducer', () => {
});
assert.deepEqual(state, {foo: {slug: 'foo'}, bar: {slug: 'bar'}, baz: {slug: 'baz'}});
});
+
+ it('pulls down the install URL from the file', () => {
+ const fileOne = {url: 'https://a.m.o/download.xpi'};
+ const fileTwo = {file: 'data'};
+ const addon = {
+ slug: 'installable',
+ current_version: {
+ files: [fileOne, fileTwo],
+ },
+ };
+ assert.deepEqual(
+ addons(undefined, {payload: {entities: {addons: {installable: addon}}}}),
+ {
+ installable: {
+ ...addon,
+ installURL: 'https://a.m.o/download.xpi',
+ },
+ });
+ });
+
+ it('flattens theme data', () => {
+ const state = addons(originalState, {
+ payload: {
+ entities: {
+ addons: {
+ baz: {slug: 'baz', id: 42, theme_data: {theme_thing: 'some-data'}},
+ },
+ },
+ },
+ });
+ assert.deepEqual(
+ state,
+ {
+ foo: {slug: 'foo'},
+ bar: {slug: 'bar'},
+ baz: {
+ id: 42,
+ slug: 'baz',
+ theme_thing: 'some-data',
+ guid: '42@personas.mozilla.org',
+ },
+ });
+ });
});
diff --git a/tests/client/disco/components/TestAddon.js b/tests/client/disco/components/TestAddon.js
index 925f29649f4..083a2b003a6 100644
--- a/tests/client/disco/components/TestAddon.js
+++ b/tests/client/disco/components/TestAddon.js
@@ -8,7 +8,7 @@ import { findDOMNode } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import Addon from 'disco/components/Addon';
-import { ERROR, THEME_PREVIEW, THEME_RESET_PREVIEW } from 'disco/constants';
+import { ERROR, THEME_PREVIEW, THEME_RESET_PREVIEW, THEME_TYPE } from 'disco/constants';
import { stubAddonManager, getFakeI18nInst } from 'tests/client/helpers';
import I18nProvider from 'core/i18n/Provider';
@@ -17,8 +17,7 @@ const result = {
type: 'extension',
heading: 'test-heading',
slug: 'test-slug',
- subHeading: 'test-sub-heading',
- editorialDescription: 'test-editorial-description',
+ description: 'test-editorial-description',
};
const store = createStore((s) => s, {installations: {}, addons: {}});
@@ -65,17 +64,26 @@ describe('
', () => {
});
it('renders the editorial description', () => {
- assert.equal(root.refs['editorial-description'].textContent, 'test-editorial-description');
+ assert.equal(root.refs.editorialDescription.textContent, 'test-editorial-description');
});
- it('renders the sub-heading', () => {
- assert.equal(root.refs['sub-heading'].textContent, 'test-sub-heading');
+ it('purifies the heading', () => {
+ root = renderAddon({
+ ...result,
+ heading: '
Hey! This is an add-on',
+ });
+ assert.include(root.refs.heading.innerHTML, 'Hey! This is
an add-on');
});
- it("doesn't render the subheading when not present", () => {
- const data = {...result, subHeading: undefined};
- root = renderAddon(data);
- assert.notEqual(root.refs.heading.textContent, 'test-sub-heading');
+ it('purifies the editorial description', () => {
+ root = renderAddon({
+ ...result,
+ description: '
This is an add-on!
' +
+ '
Reviewed by a person',
+ });
+ assert.equal(
+ root.refs.editorialDescription.innerHTML,
+ '
This is an add-on!
Reviewed by
a person');
});
it('does render a logo for an extension', () => {
@@ -100,7 +108,7 @@ describe('
', () => {
let root;
beforeEach(() => {
- const data = {...result, type: 'theme'};
+ const data = {...result, type: THEME_TYPE};
root = renderAddon(data);
});
@@ -121,7 +129,7 @@ describe('
', () => {
beforeEach(() => {
themeAction = sinon.stub();
- const data = {...result, type: 'theme', themeAction};
+ const data = {...result, type: THEME_TYPE, themeAction};
root = renderAddon(data);
themeImage = findDOMNode(root).querySelector('.theme-image');
});
diff --git a/tests/client/disco/containers/TestDiscoPane.js b/tests/client/disco/containers/TestDiscoPane.js
index 407d8017699..2d7824ad373 100644
--- a/tests/client/disco/containers/TestDiscoPane.js
+++ b/tests/client/disco/containers/TestDiscoPane.js
@@ -2,11 +2,18 @@ import React from 'react';
import { Simulate, renderIntoDocument } from 'react-addons-test-utils';
import { findDOMNode } from 'react-dom';
import { Provider } from 'react-redux';
+import { discoResults } from 'disco/actions';
+import * as discoApi from 'disco/api';
import createStore from 'disco/store';
-import DiscoPane from 'disco/containers/DiscoPane';
+import { EXTENSION_TYPE } from 'disco/constants';
+import * as helpers from 'disco/containers/DiscoPane';
import { stubAddonManager, getFakeI18nInst } from 'tests/client/helpers';
+import { loadEntities } from 'core/actions';
import I18nProvider from 'core/i18n/Provider';
+// Use DiscoPane that isn't wrapped in asyncConnect.
+const { DiscoPane } = helpers;
+
describe('AddonPage', () => {
beforeEach(() => {
@@ -14,23 +21,25 @@ describe('AddonPage', () => {
});
function render() {
+ const store = createStore({
+ addons: {foo: {type: EXTENSION_TYPE}},
+ discoResults: [{addon: 'foo'}],
+ });
+ const results = [{addon: 'foo', type: EXTENSION_TYPE}];
+ const i18n = getFakeI18nInst();
+ // We need the providers for i18n and since InstallButton will pull data from the store.
return findDOMNode(renderIntoDocument(
-
-
-
+
+
+
));
}
describe('rendered fields', () => {
- let root;
-
- beforeEach(() => {
- root = render();
- });
-
it('renders an addon', () => {
+ const root = render();
assert.ok(root.querySelector('.addon'));
});
});
@@ -49,4 +58,53 @@ describe('AddonPage', () => {
assert.notOk(root.querySelector('.show-video'));
});
});
+
+ describe('loadDataIfNeeded', () => {
+ it('does nothing if there are loaded results', () => {
+ const store = {
+ getState() {
+ return {addons: {foo: {}}, discoResults: [{addon: 'foo'}]};
+ },
+ };
+ const getAddons = sinon.stub(discoApi, 'getDiscoveryAddons');
+ return helpers.loadDataIfNeeded({store})
+ .then(() => assert.notOk(getAddons.called));
+ });
+
+ it('loads the addons if there are none', () => {
+ const api = {the: 'config'};
+ const dispatch = sinon.spy();
+ const store = {
+ dispatch,
+ getState() {
+ return {addons: {}, api, discoResults: []};
+ },
+ };
+ const entities = {addons: {foo: {slug: 'foo'}}, discoResults: {foo: {addon: 'foo'}}};
+ const result = {results: ['foo']};
+ const getAddons = sinon.stub(discoApi, 'getDiscoveryAddons')
+ .returns(Promise.resolve({entities, result}));
+ return helpers.loadDataIfNeeded({store})
+ .then(() => {
+ assert.ok(getAddons.calledWith({api}));
+ assert.ok(dispatch.calledWith(loadEntities(entities)));
+ assert.ok(dispatch.calledWith(discoResults([{addon: 'foo'}])));
+ });
+ });
+ });
+
+ describe('mapStateToProps', () => {
+ it('only sets results', () => {
+ const props = helpers.mapStateToProps({discoResults: []});
+ assert.deepEqual(Object.keys(props), ['results']);
+ });
+
+ it('sets the results', () => {
+ const props = helpers.mapStateToProps({
+ addons: {one: {slug: 'one'}, two: {slug: 'two'}},
+ discoResults: [{addon: 'two'}],
+ });
+ assert.deepEqual(props.results, [{slug: 'two', addon: 'two'}]);
+ });
+ });
});
diff --git a/tests/client/disco/containers/TestInstallButton.js b/tests/client/disco/containers/TestInstallButton.js
index b085899590b..d882caf3eac 100644
--- a/tests/client/disco/containers/TestInstallButton.js
+++ b/tests/client/disco/containers/TestInstallButton.js
@@ -216,7 +216,7 @@ describe('', () => {
});
it('sets the status to INSTALLED when an installed theme is found', () => {
- stubAddonManager({getAddon: Promise.resolve({type: 'theme', isEnabled: true})});
+ stubAddonManager({getAddon: Promise.resolve({type: THEME_TYPE, isEnabled: true})});
const dispatch = sinon.spy();
const guid = '@foo';
const slug = 'foo';
@@ -232,7 +232,7 @@ describe('', () => {
});
it('sets the status to UNINSTALLED when an uninstalled theme is found', () => {
- stubAddonManager({getAddon: Promise.resolve({type: 'theme', isEnabled: false})});
+ stubAddonManager({getAddon: Promise.resolve({type: THEME_TYPE, isEnabled: false})});
const dispatch = sinon.spy();
const guid = '@foo';
const slug = 'foo';
diff --git a/tests/client/disco/reducers/testDiscoResults.js b/tests/client/disco/reducers/testDiscoResults.js
new file mode 100644
index 00000000000..570c01dd290
--- /dev/null
+++ b/tests/client/disco/reducers/testDiscoResults.js
@@ -0,0 +1,14 @@
+import discoResults from 'disco/reducers/discoResults';
+
+describe('discoResults reducer', () => {
+ it('defaults to an empty array', () => {
+ assert.deepEqual(discoResults(undefined, {type: 'UNRELATED'}), []);
+ });
+
+ it('setst the state to the results', () => {
+ const results = ['foo', 'bar'];
+ assert.strictEqual(
+ discoResults(['baz'], {type: 'DISCO_RESULTS', payload: {results}}),
+ results);
+ });
+});
diff --git a/tests/client/disco/test_api.js b/tests/client/disco/test_api.js
new file mode 100644
index 00000000000..e13674dc112
--- /dev/null
+++ b/tests/client/disco/test_api.js
@@ -0,0 +1,36 @@
+import { arrayOf, normalize } from 'normalizr';
+
+import { discoResult, getDiscoveryAddons } from 'disco/api';
+import * as coreApi from 'core/api';
+
+describe('disco api', () => {
+ describe('getDiscoveryAddons', () => {
+ it('calls the API', () => {
+ const callApi = sinon.stub(coreApi, 'callApi');
+ const api = {some: 'apiconfig'};
+ getDiscoveryAddons({api});
+ assert.ok(callApi.calledWith({
+ endpoint: 'discovery',
+ schema: {results: arrayOf(discoResult)},
+ params: {lang: 'en-US'},
+ state: api,
+ }));
+ });
+ });
+
+ describe('discoResult', () => {
+ it("uses the addon's slug as an id", () => {
+ const normalized = normalize({addon: {slug: 'foo'}}, discoResult);
+ assert.deepEqual(
+ normalized,
+ {
+ entities: {
+ addons: {foo: {slug: 'foo'}},
+ discoResults: {foo: {addon: 'foo'}},
+ },
+ result: 'foo',
+ },
+ sinon.format(normalized.entities));
+ });
+ });
+});
diff --git a/tests/client/init.js b/tests/client/init.js
index 833752c1bd2..0297b34afd8 100644
--- a/tests/client/init.js
+++ b/tests/client/init.js
@@ -1,6 +1,7 @@
const realSinon = sinon;
window.sinon = realSinon.sandbox.create();
window.sinon.createStubInstance = realSinon.createStubInstance;
+window.sinon.format = realSinon.format;
afterEach(() => {
window.sinon.restore();
diff --git a/tests/client/search/TestUserPage.js b/tests/client/search/TestUserPage.js
index be5a06a6dae..5f0ee0950cf 100644
--- a/tests/client/search/TestUserPage.js
+++ b/tests/client/search/TestUserPage.js
@@ -2,8 +2,8 @@ import React from 'react';
import { renderIntoDocument } from 'react-addons-test-utils';
import { findDOMNode } from 'react-dom';
+import { loadEntities, setCurrentUser } from 'core/actions';
import * as api from 'core/api';
-import { loadEntities, setCurrentUser } from 'search/actions';
import { UserPage, mapStateToProps, loadProfileIfNeeded } from 'search/containers/UserPage';
describe('', () => {
diff --git a/tests/client/search/actions/test_index.js b/tests/client/search/actions/test_index.js
index 044b16622a3..21b32c92356 100644
--- a/tests/client/search/actions/test_index.js
+++ b/tests/client/search/actions/test_index.js
@@ -37,16 +37,3 @@ describe('SEARCH_FAILED', () => {
assert.deepEqual(action.payload, {page: 25, query: 'foo'});
});
});
-
-describe('ENTITIES_LOADED', () => {
- const entities = sinon.stub();
- const action = actions.loadEntities(entities);
-
- it('sets the type', () => {
- assert.equal(action.type, 'ENTITIES_LOADED');
- });
-
- it('sets the payload', () => {
- assert.deepEqual(action.payload, {entities});
- });
-});
diff --git a/tests/client/search/containers/TestAddonPage.js b/tests/client/search/containers/TestAddonPage.js
index 547197f22d1..d036f3880f5 100644
--- a/tests/client/search/containers/TestAddonPage.js
+++ b/tests/client/search/containers/TestAddonPage.js
@@ -4,8 +4,8 @@ import { findDOMNode } from 'react-dom';
import { Provider } from 'react-redux';
import AddonPage, { findAddon, loadAddonIfNeeded } from 'search/containers/AddonPage';
import createStore from 'search/store';
+import * as actions from 'core/actions';
import * as api from 'core/api';
-import * as actions from 'search/actions';
import { stubAddonManager, unexpectedSuccess } from 'tests/client/helpers';
describe('AddonPage', () => {
diff --git a/webpack.dev.config.babel.js b/webpack.dev.config.babel.js
index 8f1b5daf195..6cbd9802010 100644
--- a/webpack.dev.config.babel.js
+++ b/webpack.dev.config.babel.js
@@ -93,6 +93,8 @@ export default Object.assign({}, webpackConfig, {
new webpack.NormalModuleReplacementPlugin(/config$/, 'core/client/config.js'),
// Substitutes client only config.
new webpack.NormalModuleReplacementPlugin(/core\/logger$/, 'core/client/logger.js'),
+ // Use the browser's window for window.
+ new webpack.NormalModuleReplacementPlugin(/core\/window/, 'core/browserWindow.js'),
new webpack.HotModuleReplacementPlugin(),
new webpack.IgnorePlugin(/webpack-stats\.json$/),
webpackIsomorphicToolsPlugin.development(),
diff --git a/webpack.prod.config.babel.js b/webpack.prod.config.babel.js
index 0504f7a29fb..a2a8718a86a 100644
--- a/webpack.prod.config.babel.js
+++ b/webpack.prod.config.babel.js
@@ -67,6 +67,8 @@ export default {
new webpack.NormalModuleReplacementPlugin(/config$/, 'core/client/config.js'),
// Substitutes client only config.
new webpack.NormalModuleReplacementPlugin(/core\/logger$/, 'core/client/logger.js'),
+ // Use the browser's window for window.
+ new webpack.NormalModuleReplacementPlugin(/core\/window/, 'core/browserWindow.js'),
// This allow us to exclude locales for other apps being built.
new webpack.ContextReplacementPlugin(
/locale$/,