diff --git a/src/lib/sentry.js b/src/lib/sentry.js new file mode 100644 index 000000000..b16cd03a9 --- /dev/null +++ b/src/lib/sentry.js @@ -0,0 +1,18 @@ +const initSentry = () => { + // initialize Sentry instance, making sure it hasn't been initialized already + if (!window.Sentry && `${process.env.SENTRY_DSN}` !== '') { + const Sentry = require('@sentry/browser'); + + Sentry.init({ + dsn: `${process.env.SENTRY_DSN}`, + // Do not collect global onerror, only collect specifically from React error boundaries. + // TryCatch plugin also includes errors from setTimeouts (i.e. the VM) + integrations: integrations => integrations.filter(i => + !(i.name === 'GlobalHandlers' || i.name === 'TryCatch')) + }); + + window.Sentry = Sentry; // Allow GUI access to Sentry via window + } +}; + +module.exports = initSentry; diff --git a/src/routes.json b/src/routes.json index 4af0ff35b..d4e473daf 100644 --- a/src/routes.json +++ b/src/routes.json @@ -177,12 +177,20 @@ }, { "name": "projects", - "pattern": "^/projects(/editor|(/\\d+(/editor|/fullscreen|/embed)?)?)?/?(\\?.*)?$", + "pattern": "^/projects(/editor|(/\\d+(/editor|/fullscreen)?)?)?/?(\\?.*)?$", "routeAlias": "/projects/?$", "view": "preview/preview", "title": "Scratch Project", "dynamicMetaTags": true }, + { + "name": "embed", + "pattern": "^/projects/\\d+/embed/?(\\?.*)?$", + "routeAlias": "/projects/?$", + "view": "preview/embed", + "title": "Scratch Project", + "dynamicMetaTags": true + }, { "name": "parents", "pattern": "^/parents/?(\\?.*)?$", diff --git a/src/views/preview/embed-view.jsx b/src/views/preview/embed-view.jsx new file mode 100644 index 000000000..be3184d5b --- /dev/null +++ b/src/views/preview/embed-view.jsx @@ -0,0 +1,97 @@ +// embed view + +const React = require('react'); +const PropTypes = require('prop-types'); +const connect = require('react-redux').connect; +const injectIntl = require('react-intl').injectIntl; + +const ErrorBoundary = require('../../components/errorboundary/errorboundary.jsx'); +const projectShape = require('./projectshape.jsx').projectShape; +const NotAvailable = require('../../components/not-available/not-available.jsx'); +const Meta = require('./meta.jsx'); + +const previewActions = require('../../redux/preview.js'); + +const GUI = require('scratch-gui'); +const IntlGUI = injectIntl(GUI.default); + +const initSentry = require('../../lib/sentry.js'); +initSentry(); + +class EmbedView extends React.Component { + constructor (props) { + super(props); + const pathname = window.location.pathname.toLowerCase(); + const parts = pathname.split('/').filter(Boolean); + this.state = { + extensions: [], + invalidProject: parts.length === 1, + projectId: parts[1] + }; + } + componentDidMount () { + this.props.getProjectInfo(this.state.projectId); + } + render () { + if (this.props.projectNotAvailable || this.state.invalidProject) { + return ( + +
+ +
+
+ ); + } + + return ( + + + + + ); + } +} + +EmbedView.propTypes = { + assetHost: PropTypes.string.isRequired, + getProjectInfo: PropTypes.func.isRequired, + projectHost: PropTypes.string.isRequired, + projectInfo: projectShape, + projectNotAvailable: PropTypes.bool +}; + +EmbedView.defaultProps = { + assetHost: process.env.ASSET_HOST, + projectHost: process.env.PROJECT_HOST +}; + +const mapStateToProps = state => ({ + projectInfo: state.preview.projectInfo, + projectNotAvailable: state.preview.projectNotAvailable +}); + +const mapDispatchToProps = dispatch => ({ + getProjectInfo: (id, token) => { + dispatch(previewActions.getProjectInfo(id, token)); + } +}); + +module.exports.View = connect( + mapStateToProps, + mapDispatchToProps +)(EmbedView); + +GUI.setAppElement(document.getElementById('app')); +module.exports.initGuiState = GUI.initEmbedded; +module.exports.guiReducers = GUI.guiReducers; +module.exports.guiInitialState = GUI.guiInitialState; +module.exports.guiMiddleware = GUI.guiMiddleware; +module.exports.initLocale = GUI.initLocale; +module.exports.localesInitialState = GUI.localesInitialState; diff --git a/src/views/preview/embed.jsx b/src/views/preview/embed.jsx new file mode 100644 index 000000000..f458948be --- /dev/null +++ b/src/views/preview/embed.jsx @@ -0,0 +1,30 @@ +const React = require('react'); +const ErrorBoundary = require('../../components/errorboundary/errorboundary.jsx'); +const render = require('../../lib/render.jsx'); + +// Require this even though we don't use it because, without it, webpack runs out of memory... +const Page = require('../../components/page/www/page.jsx'); // eslint-disable-line no-unused-vars + +const previewActions = require('../../redux/preview.js'); + +const isSupportedBrowser = require('../../lib/supported-browser').default; +const UnsupportedBrowser = require('./unsupported-browser.jsx'); + +if (isSupportedBrowser()) { + const EmbedView = require('./embed-view.jsx'); + render( + , + document.getElementById('app'), + { + preview: previewActions.previewReducer, + ...EmbedView.guiReducers + }, + { + locales: EmbedView.initLocale(EmbedView.localesInitialState, window._locale), + scratchGui: EmbedView.initGuiState(EmbedView.guiInitialState) + }, + EmbedView.guiMiddleware + ); +} else { + render(, document.getElementById('app')); +} diff --git a/src/views/preview/project-view.jsx b/src/views/preview/project-view.jsx index f31353bc1..c41ec84ee 100644 --- a/src/views/preview/project-view.jsx +++ b/src/views/preview/project-view.jsx @@ -35,17 +35,8 @@ const IntlGUI = injectIntl(GUI.default); const localStorageAvailable = 'localStorage' in window && window.localStorage !== null; -const Sentry = require('@sentry/browser'); -if (`${process.env.SENTRY_DSN}` !== '') { - Sentry.init({ - dsn: `${process.env.SENTRY_DSN}`, - // Do not collect global onerror, only collect specifically from React error boundaries. - // TryCatch plugin also includes errors from setTimeouts (i.e. the VM) - integrations: integrations => integrations.filter(i => - !(i.name === 'GlobalHandlers' || i.name === 'TryCatch')) - }); - window.Sentry = Sentry; // Allow GUI access to Sentry via window -} +const initSentry = require('../../lib/sentry.js'); +initSentry(); class Preview extends React.Component { constructor (props) { @@ -258,7 +249,7 @@ class Preview extends React.Component { // this is a project.json as an object // validate expects a string or buffer as input // TODO not sure if we need to check that it also isn't a data view - input = JSON.stringify(input); + input = JSON.stringify(input); // NOTE: what is the point of doing this?? } parser(projectAsset.data, false, (err, projectData) => { if (err) { @@ -1092,16 +1083,13 @@ module.exports.initGuiState = guiInitialState => { const parts = pathname.split('/').filter(Boolean); // parts[0]: 'projects' // parts[1]: either :id or 'editor' - // parts[2]: undefined if no :id, otherwise either 'editor', 'fullscreen' or 'embed' + // parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen' if (parts.indexOf('editor') === -1) { guiInitialState = GUI.initPlayer(guiInitialState); } if (parts.indexOf('fullscreen') !== -1) { guiInitialState = GUI.initFullScreen(guiInitialState); } - if (parts.indexOf('embed') !== -1) { - guiInitialState = GUI.initEmbedded(guiInitialState); - } return guiInitialState; };