diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx
index 3eef5b1a3..14b7c7805 100644
--- a/src/components/gui/gui.jsx
+++ b/src/components/gui/gui.jsx
@@ -31,6 +31,7 @@ import Cards from '../../containers/cards.jsx';
import Alerts from '../../containers/alerts.jsx';
import DragLayer from '../../containers/drag-layer.jsx';
import ConnectionModal from '../../containers/connection-modal.jsx';
+import TelemetryModal from '../telemetry-modal/telemetry-modal.jsx';
import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants';
import {resolveStageSize} from '../../lib/screen-utils';
@@ -100,6 +101,7 @@ const GUIComponent = props => {
onExtensionButtonClick,
onRequestCloseBackdropLibrary,
onRequestCloseCostumeLibrary,
+ onRequestCloseTelemetryModal,
onSeeCommunity,
onShare,
previewInfoVisible,
@@ -107,6 +109,8 @@ const GUIComponent = props => {
soundsTabVisible,
stageSizeMode,
targetIsStage,
+ telemetryModalHandlers,
+ telemetryModalVisible,
tipsLibraryVisible,
vm,
...componentProps
@@ -151,6 +155,12 @@ const GUIComponent = props => {
{previewInfoVisible ? (
) : null}
+ {telemetryModalVisible ? (
+
+ ) : null}
{loading ? (
) : null}
@@ -379,6 +389,7 @@ GUIComponent.propTypes = {
onOpenRegistration: PropTypes.func,
onRequestCloseBackdropLibrary: PropTypes.func,
onRequestCloseCostumeLibrary: PropTypes.func,
+ onRequestCloseTelemetryModal: PropTypes.func,
onSeeCommunity: PropTypes.func,
onShare: PropTypes.func,
onTabSelect: PropTypes.func,
@@ -390,6 +401,12 @@ GUIComponent.propTypes = {
soundsTabVisible: PropTypes.bool,
stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)),
targetIsStage: PropTypes.bool,
+ telemetryModalHandlers: PropTypes.shape({
+ onCancel: PropTypes.func,
+ onOptIn: PropTypes.func.isRequired,
+ onOptOut: PropTypes.func.isRequired
+ }),
+ telemetryModalVisible: PropTypes.bool,
tipsLibraryVisible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
diff --git a/src/components/telemetry-modal/telemetry-modal-header.png b/src/components/telemetry-modal/telemetry-modal-header.png
new file mode 100644
index 000000000..b297b108e
Binary files /dev/null and b/src/components/telemetry-modal/telemetry-modal-header.png differ
diff --git a/src/components/telemetry-modal/telemetry-modal.css b/src/components/telemetry-modal/telemetry-modal.css
new file mode 100644
index 000000000..18b8d1ed8
--- /dev/null
+++ b/src/components/telemetry-modal/telemetry-modal.css
@@ -0,0 +1,82 @@
+@import "../../css/colors.css";
+@import "../../css/units.css";
+@import "../../css/typography.css";
+@import "../../css/z-index.css";
+
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: $z-index-modal;
+ background-color: $ui-modal-overlay;
+}
+
+.modal-content {
+ margin: 100px auto;
+ outline: none;
+ border: .25rem solid $ui-white-transparent;
+ padding: 0;
+ border-radius: $space;
+ user-select: none;
+ width: 640px;
+
+ color: $text-primary;
+ overflow: hidden;
+}
+
+.illustration {
+ width: 100%;
+ height: 128px;
+ background-color: $motion-primary;
+ background-image: url('./telemetry-modal-header.png');
+ background-size: cover;
+}
+
+.body {
+ background: $ui-white;
+ padding: 1.5rem 2.25rem;
+ text-align: left;
+}
+
+.privacy-policy-link {
+ color: $motion-primary;
+ text-decoration: none;
+}
+
+/* Confirmation buttons at the bottom of the modal */
+.button-row {
+ margin: 1.5rem 0;
+ font-weight: bolder;
+ text-align: right;
+ display: flex;
+ justify-content: center;
+}
+
+.button-row button {
+ border: 1px solid $motion-primary;
+ border-radius: 0.25rem;
+ padding: 0.5rem 1.5rem;
+ background: white;
+ font-weight: bold;
+ font-size: .875rem;
+ cursor: pointer;
+}
+
+.button-row button.opt-in {
+ background: $motion-primary;
+ color: white;
+}
+
+.button-row button.opt-out {
+ color: $motion-primary;
+}
+
+[dir="ltr"] .button-row button + button {
+ margin-left: 0.5rem;
+}
+
+[dir="rtl"] .button-row button + button {
+ margin-right: 0.5rem;
+}
diff --git a/src/components/telemetry-modal/telemetry-modal.jsx b/src/components/telemetry-modal/telemetry-modal.jsx
new file mode 100644
index 000000000..b9e805aaf
--- /dev/null
+++ b/src/components/telemetry-modal/telemetry-modal.jsx
@@ -0,0 +1,136 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
+import ReactModal from 'react-modal';
+
+import Box from '../box/box.jsx';
+
+import styles from './telemetry-modal.css';
+
+const messages = defineMessages({
+ label: {
+ id: 'gui.telemetryOptIn.label',
+ defaultMessage: 'Report statistics to improve Scratch',
+ description: 'Scratch 3.0 telemetry modal label - for accessibility'
+ },
+ bodyText1: {
+ defaultMessage: 'The Scratch Team is always looking to better understand how Scratch is used around the ' +
+ 'world. To help support this effort, you can allow Scratch to automatically send usage information to ' +
+ 'the Scratch Team.',
+ description: 'First paragraph of body text for telemetry opt-in modal',
+ id: 'gui.telemetryOptIn.body1'
+ },
+ bodyText2: {
+ defaultMessage: 'The information we collect includes language selection, blocks usage, and some events like ' +
+ 'saving, loading, and uploading a project. We DO NOT collect any personal information. Please see our ' +
+ '{privacyPolicyLink} for more information.',
+ description: 'First paragraph of body text for telemetry opt-in modal',
+ id: 'gui.telemetryOptIn.body2'
+ },
+ privacyPolicyLink: {
+ defaultMessage: 'Privacy Policy',
+ description: 'Link to the Scratch privacy policy',
+ id: 'gui.telemetryOptIn.privacyPolicyLink'
+ },
+ noButton: {
+ defaultMessage: 'No, thanks',
+ description: 'Text for telemetry modal opt-out button',
+ id: 'gui.telemetryOptIn.buttonTextNo'
+ },
+ noTooltip: {
+ defaultMessage: 'Disable telemetry',
+ description: 'Tooltip for telemetry modal opt-out button',
+ id: 'gui.telemetryOptIn.buttonTooltipNo'
+ },
+ yesButton: {
+ defaultMessage: "Yes, I'd like to help improve Scratch",
+ description: 'Text for telemetry modal opt-in button',
+ id: 'gui.telemetryOptIn.buttonTextYes'
+ },
+ yesTooltip: {
+ defaultMessage: 'Enable telemetry',
+ description: 'Tooltip for telemetry modal opt-in button',
+ id: 'gui.telemetryOptIn.buttonTooltipYes'
+ }
+});
+
+class TelemetryModal extends React.PureComponent {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleCancel',
+ 'handleOptIn',
+ 'handleOptOut'
+ ]);
+ }
+ handleCancel () {
+ this.props.onRequestClose();
+ if (this.props.onCancel) {
+ this.props.onCancel();
+ }
+ }
+ handleOptIn () {
+ this.props.onRequestClose();
+ this.props.onOptIn();
+ }
+ handleOptOut () {
+ this.props.onRequestClose();
+ this.props.onOptOut();
+ }
+ render () {
+ return (
+
+
+
+
+
+
+
+ )
+ }}
+ />
+
+
+
+
+
+
+ );
+ }
+}
+
+TelemetryModal.propTypes = {
+ intl: intlShape.isRequired,
+ isRtl: PropTypes.bool,
+ onCancel: PropTypes.func,
+ onOptIn: PropTypes.func.isRequired,
+ onOptOut: PropTypes.func.isRequired,
+ onRequestClose: PropTypes.func
+};
+
+export default injectIntl(TelemetryModal);
diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx
index 025a73966..4011ba271 100644
--- a/src/containers/gui.jsx
+++ b/src/containers/gui.jsx
@@ -7,7 +7,6 @@ import VM from 'scratch-vm';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
-import {openExtensionLibrary} from '../reducers/modals';
import {
getIsError,
getIsShowingProject
@@ -22,7 +21,9 @@ import {
import {
closeCostumeLibrary,
- closeBackdropLibrary
+ closeBackdropLibrary,
+ closeTelemetryModal,
+ openExtensionLibrary
} from '../reducers/modals';
import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
@@ -128,6 +129,7 @@ GUI.propTypes = {
projectHost: PropTypes.string,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
projectTitle: PropTypes.string,
+ telemetryModalVisible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
@@ -157,11 +159,12 @@ const mapStateToProps = state => {
loadingStateVisible: state.scratchGui.modals.loadingProject,
previewInfoVisible: state.scratchGui.modals.previewInfo,
projectId: state.scratchGui.projectState.projectId,
+ soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX,
targetIsStage: (
state.scratchGui.targets.stage &&
state.scratchGui.targets.stage.id === state.scratchGui.targets.editingTarget
),
- soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX,
+ telemetryModalVisible: state.scratchGui.modals.telemetryModal,
tipsLibraryVisible: state.scratchGui.modals.tipsLibrary,
vm: state.scratchGui.vm
};
@@ -174,6 +177,7 @@ const mapDispatchToProps = dispatch => ({
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()),
onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
+ onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal()),
onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title))
});
diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx
index 4156cd094..eacf2eab6 100644
--- a/src/lib/app-state-hoc.jsx
+++ b/src/lib/app-state-hoc.jsx
@@ -51,7 +51,8 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
guiMiddleware,
initFullScreen,
initPlayer,
- initPreviewInfo
+ initPreviewInfo,
+ initTelemetryModal
} = guiRedux;
const {ScratchPaintReducer} = require('scratch-paint');
@@ -63,6 +64,8 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
if (props.isPlayerOnly) {
initializedGui = initPlayer(initializedGui);
}
+ } else if (props.showTelemetryModal) {
+ initializedGui = initTelemetryModal(initializedGui);
} else if (props.showPreviewInfo) {
initializedGui = initPreviewInfo(initializedGui);
}
@@ -98,6 +101,7 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
isFullScreen, // eslint-disable-line no-unused-vars
isPlayerOnly, // eslint-disable-line no-unused-vars
showPreviewInfo, // eslint-disable-line no-unused-vars
+ showTelemetryModal, // eslint-disable-line no-unused-vars
...componentProps
} = this.props;
return (
diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx
index 3ae35e4d3..ab67cd2c1 100644
--- a/src/playground/render-gui.jsx
+++ b/src/playground/render-gui.jsx
@@ -6,11 +6,24 @@ import AppStateHOC from '../lib/app-state-hoc.jsx';
import GUI from '../containers/gui.jsx';
import HashParserHOC from '../lib/hash-parser-hoc.jsx';
import TitledHOC from '../lib/titled-hoc.jsx';
+import log from '../lib/log.js';
const onClickLogo = () => {
window.location = 'https://scratch.mit.edu';
};
+const handleTelemetryModalCancel = () => {
+ log('User canceled telemetry modal');
+};
+
+const handleTelemetryModalOptIn = () => {
+ log('User opted into telemetry');
+};
+
+const handleTelemetryModalOptOut = () => {
+ log('User opted out of telemetry');
+};
+
/*
* Render the GUI playground. This is a separate function because importing anything
* that instantiates the VM causes unsupported browsers to crash
@@ -55,7 +68,13 @@ export default appTarget => {
simulateScratchDesktop ?
: