diff --git a/.gitattributes b/.gitattributes index c49fe81..4e6bb68 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,7 @@ # File types which we know are binary # Prefer LF for most file types +*.css text eol=lf *.htm text eol=lf *.html text eol=lf *.js text eol=lf diff --git a/src/main/ScratchDesktopTelemetry.js b/src/main/ScratchDesktopTelemetry.js index ec27205..906a7d4 100644 --- a/src/main/ScratchDesktopTelemetry.js +++ b/src/main/ScratchDesktopTelemetry.js @@ -90,6 +90,11 @@ class ScratchDesktopTelemetry { // make a singleton so it's easy to share across both Electron processes const scratchDesktopTelemetrySingleton = new ScratchDesktopTelemetry(); +// `handle` works with `invoke` +ipcMain.handle('getTelemetryDidOptIn', () => + scratchDesktopTelemetrySingleton.didOptIn +); +// `on` works with `sendSync` (and `send`) ipcMain.on('getTelemetryDidOptIn', event => { event.returnValue = scratchDesktopTelemetrySingleton.didOptIn; }); diff --git a/src/main/index.js b/src/main/index.js index 1688313..8b182ec 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,4 +1,4 @@ -import {BrowserWindow, Menu, app, dialog, ipcMain, systemPreferences} from 'electron'; +import {BrowserWindow, Menu, app, dialog, ipcMain, shell, systemPreferences} from 'electron'; import fs from 'fs-extra'; import path from 'path'; import {URL} from 'url'; @@ -193,8 +193,16 @@ const createWindow = ({search = null, url = 'index.html', ...browserWindowOption } }); + webContents.on('new-window', (event, newWindowUrl) => { + shell.openExternal(newWindowUrl); + event.preventDefault(); + }); + const fullUrl = makeFullUrl(url, search); window.loadURL(fullUrl); + window.once('ready-to-show', () => { + webContents.send('ready-to-show'); + }); return window; }; @@ -210,6 +218,17 @@ const createAboutWindow = () => { return window; }; +const createPrivacyWindow = () => { + const window = createWindow({ + width: _windows.main.width * 0.8, + height: _windows.main.height * 0.8, + parent: _windows.main, + search: 'route=privacy', + title: 'Scratch Desktop Privacy Policy' + }); + return window; +}; + const getIsProjectSave = downloadItem => { switch (downloadItem.getMimeType()) { case 'application/x.scratch.sb3': @@ -371,12 +390,21 @@ app.on('ready', () => { event.preventDefault(); _windows.about.hide(); }); + _windows.privacy = createPrivacyWindow(); + _windows.privacy.on('close', event => { + event.preventDefault(); + _windows.privacy.hide(); + }); }); ipcMain.on('open-about-window', () => { _windows.about.show(); }); +ipcMain.on('open-privacy-policy-window', () => { + _windows.privacy.show(); +}); + // start loading initial project data before the GUI needs it so the load seems faster const initialProjectDataPromise = (async () => { if (argv._.length === 0) { diff --git a/src/renderer/about.css b/src/renderer/about.css new file mode 100644 index 0000000..43f9ce7 --- /dev/null +++ b/src/renderer/about.css @@ -0,0 +1,39 @@ +html, body { + background-color: #4D97FF; + color: white; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: bolder; +} + +a:active, a:hover, a:link, a:visited { + color: currentColor; +} + +a:active, a:hover { + filter: brightness(0.9); +} + +.aboutBox { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.aboutLogo { + max-width: 10rem; + max-height: 10rem; +} + +.aboutText { + margin: 1.5rem; +} + +.aboutDetails { + font-size: x-small; +} + +.aboutFooter { + font-size: small; +} diff --git a/src/renderer/about.jsx b/src/renderer/about.jsx index 9b121bc..204b829 100644 --- a/src/renderer/about.jsx +++ b/src/renderer/about.jsx @@ -1,45 +1,29 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import {productName, version} from '../../package.json'; import logo from '../icon/ScratchDesktop.svg'; +import styles from './about.css'; -// TODO: localization? const AboutElement = () => ( -
+
{`${productName}
-
+

{productName}

-
Version {version}
- + Version {version} +
{ - ['Electron', 'Chrome'].map(component => { + ['Electron', 'Chrome', 'Node'].map(component => { const componentVersion = process.versions[component.toLowerCase()]; return ; }) } -
{component}{componentVersion}
+
); -const appTarget = document.getElementById('app'); -ReactDOM.render(, appTarget); +export default ; diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index b868198..7810fc5 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -1,9 +1,8 @@ -import {ipcRenderer, remote, shell} from 'electron'; +import {ipcRenderer, remote} from 'electron'; import bindAll from 'lodash.bindall'; import omit from 'lodash.omit'; import PropTypes from 'prop-types'; import React from 'react'; -import ReactDOM from 'react-dom'; import {connect} from 'react-redux'; import {compose} from 'redux'; import GUI from 'scratch-gui/src/index'; @@ -21,25 +20,17 @@ import { } from 'scratch-gui/src/reducers/project-state'; import { openLoadingProject, - closeLoadingProject + closeLoadingProject, + openTelemetryModal } from 'scratch-gui/src/reducers/modals'; import ElectronStorageHelper from '../common/ElectronStorageHelper'; +import showPrivacyPolicy from './showPrivacyPolicy'; import styles from './app.css'; -// override window.open so that it uses the OS's default browser, not an electron browser -window.open = function (url, target) { - if (target === '_blank') { - shell.openExternal(url); - } -}; -// Register "base" page view -// analytics.pageview('/'); - const appTarget = document.getElementById('app'); -appTarget.className = styles.app || 'app'; // TODO -document.body.appendChild(appTarget); +appTarget.className = styles.app || 'app'; GUI.setAppElement(appTarget); @@ -80,6 +71,10 @@ const ScratchDesktopGUIHOC = function (WrappedComponent) { 'handleTelemetryModalOptOut', 'handleUpdateProjectTitle' ]); + this.state = { + // use `sendSync` because this should be set before first render + telemetryDidOptIn: ipcRenderer.sendSync('getTelemetryDidOptIn') + }; this.props.onLoadingStarted(); ipcRenderer.invoke('get-initial-project-data').then(initialProjectData => { const hasInitialProject = initialProjectData && (initialProjectData.length > 0); @@ -119,7 +114,7 @@ const ScratchDesktopGUIHOC = function (WrappedComponent) { componentWillUnmount () { ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave); } - handleClickLogo () { + handleClickAbout () { ipcRenderer.send('open-about-window'); } handleProjectTelemetryEvent (event, metadata) { @@ -133,22 +128,47 @@ const ScratchDesktopGUIHOC = function (WrappedComponent) { } handleTelemetryModalOptIn () { ipcRenderer.send('setTelemetryDidOptIn', true); + ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { + this.setState({telemetryDidOptIn}); + }); } handleTelemetryModalOptOut () { ipcRenderer.send('setTelemetryDidOptIn', false); + ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { + this.setState({telemetryDidOptIn}); + }); } handleUpdateProjectTitle (newTitle) { this.setState({projectTitle: newTitle}); } render () { + const shouldShowTelemetryModal = (typeof this.state.telemetryDidOptIn !== 'boolean'); + const childProps = omit(this.props, Object.keys(ScratchDesktopGUIComponent.propTypes)); return ( this.handleClickAbout() + }, + { + title: 'Privacy Policy', + onClick: () => showPrivacyPolicy() + }, + { + title: 'Data Settings', + onClick: () => this.props.onTelemetrySettingsClicked() + } + ]} onProjectTelemetryEvent={this.handleProjectTelemetryEvent} + onShowPrivacyPolicy={showPrivacyPolicy} onStorageInit={this.handleStorageInit} onTelemetryModalOptIn={this.handleTelemetryModalOptIn} onTelemetryModalOptOut={this.handleTelemetryModalOptOut} @@ -168,6 +188,7 @@ const ScratchDesktopGUIHOC = function (WrappedComponent) { onLoadingCompleted: PropTypes.func, onLoadingStarted: PropTypes.func, onRequestNewProject: PropTypes.func, + onTelemetrySettingsClicked: PropTypes.func, // using PropTypes.instanceOf(VM) here will cause prop type warnings due to VM mismatch vm: GUIComponent.WrappedComponent.propTypes.vm }; @@ -197,7 +218,8 @@ const ScratchDesktopGUIHOC = function (WrappedComponent) { const canSaveToServer = false; return dispatch(onLoadedProject(loadingState, canSaveToServer, loadSuccess)); }, - onRequestNewProject: () => dispatch(requestNewProject(false)) + onRequestNewProject: () => dispatch(requestNewProject(false)), + onTelemetrySettingsClicked: () => dispatch(openTelemetryModal()) }); return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopGUIComponent); @@ -212,4 +234,4 @@ const WrappedGui = compose( ScratchDesktopGUIHOC )(GUI); -ReactDOM.render(, appTarget); +export default ; diff --git a/src/renderer/index.js b/src/renderer/index.js index 49915ad..c0609b7 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -1,12 +1,33 @@ -// this is an async import so that it doesn't block the first render -// index.html contains a loading/splash screen which will display while this import loads +// This file does async imports of the heavy JSX, especially app.jsx, to avoid blocking the first render. +// The main index.html just contains a loading/splash screen which will display while this import loads. + +import {ipcRenderer} from 'electron'; + +import ReactDOM from 'react-dom'; + +ipcRenderer.on('ready-to-show', () => { + // Start without any element in focus, otherwise the first link starts with focus and shows an orange box. + // We shouldn't disable that box or the focus behavior in case someone wants or needs to navigate that way. + // This seems like a hack... maybe there's some better way to do avoid any element starting with focus? + document.activeElement.blur(); +}); const route = new URLSearchParams(window.location.search).get('route') || 'app'; +let routeModulePromise; switch (route) { case 'app': - import('./app.jsx'); // eslint-disable-line no-unused-expressions + routeModulePromise = import('./app.jsx'); break; case 'about': - import('./about.jsx'); // eslint-disable-line no-unused-expressions + routeModulePromise = import('./about.jsx'); + break; +case 'privacy': + routeModulePromise = import('./privacy.jsx'); break; } + +routeModulePromise.then(routeModule => { + const appTarget = document.getElementById('app'); + const routeElement = routeModule.default; + ReactDOM.render(routeElement, appTarget); +}); diff --git a/src/renderer/privacy.css b/src/renderer/privacy.css new file mode 100644 index 0000000..a40fb4f --- /dev/null +++ b/src/renderer/privacy.css @@ -0,0 +1,14 @@ +html, body { + background-color: #4D97FF; + color: white; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: normal; + line-height: 150%; +} + +.privacyBox { + background-color: white; + color: #575e75; + margin: 3rem; + padding: 2rem 3rem; +} diff --git a/src/renderer/privacy.jsx b/src/renderer/privacy.jsx new file mode 100644 index 0000000..ac7aaf0 --- /dev/null +++ b/src/renderer/privacy.jsx @@ -0,0 +1,235 @@ +import React from 'react'; + +import styles from './privacy.css'; + +const PrivacyElement = () => ( +
+

Privacy Policy

+ The Scratch Privacy Policy was last updated: October 5, 2020 +

+ The Scratch Foundation (“Scratch”, “we” or “us”) understands how + important privacy is to our community. We wrote this Privacy Policy to explain what Personal Information + (“Information”) we collect through our offline editor (the “Scratch App”), how we use, process, and share it, and what we're doing to keep it safe. It + also tells you about your rights and choices with respect to your Personal Information, and how you can contact us if you have any questions or concerns. +

+

What Information Does Scratch Collect About Me?

+

+ For the purpose of this Privacy Policy, “Information” means any information relating to an + identified or identifiable individual. The Scratch App automatically collects and stores locally the + following Information through its telemetry system: the title of your project in text form, language + setting, time zone and events related to your use of the Scratch App (namely when the Scratch App was + opened and closed, if a project file has been loaded or saved, or if a new project is created). If you + choose to turn on the telemetry sharing feature, the Scratch App will transmit this information to Scratch. + Projects created in the Scratch App are not transmitted to or accessible by Scratch. +

+

How Does Scratch Use My Information?

+

We use this Information for the following purposes:

+
    +
  • + Analytics and Improving the Scratch App - We use the Information to analyze use of the Scratch + App and to enhance your learning experience on the Scratch App. +
  • +
  • + Academic and Scientific Research - We de-identify and aggregate Information for statistical + analysis in the context of scientific and academic research. For example, to help us understand how + people learn through the Scratch App and how we can enhance learning tools for young people. The + results of such research are shared with educators and researchers through conferences, journals, and + other academic or scientific publications. You can find out more on our Research page. +
  • +
  • + Legal - We may use your Information to enforce our Terms of Use, to defend our legal rights, and to comply with our legal obligations and internal + policies. We may do this by analyzing your use of the Scratch App. +
  • +
+

What Are The Legal Grounds For Processing Your Information?

+

+ If you are located in the European Economic Area, the United Kingdom or Switzerland, we only process your + Information based on a valid legal ground. A “legal ground” is a reason that justifies our use + of your Information. In this case, we or a third party have a legitimate interest in using your Information + (if you choose to allow the Scratch App to send the Scratch team your Information) to create, analyze and + share your aggregated or de-identified Information for research purposes, to analyze and enhance your + learning experience on the Scratch App and otherwise ensure and improve the safety, security, and + performance of the Scratch App. We only rely on our or a third party’s legitimate interests to process your + Information when these interests are not overridden by your rights and interests. +

+

How Does Scratch Share My Information?

+

+ We disclose information that we collect through the Scratch App to third parties in the following + circumstances: +

+
    +
  • + Service Providers - To third parties who provide services such as website hosting, data + analysis, Information technology and related infrastructure provisions, customer service, email + delivery, and other services. +
  • +
  • + Academic and Scientific Research - To research institutions, such as the Massachusetts Institute + of Technology (MIT), to learn about how our users learn through the Scratch App and develop new + learning tools. The results of this research or the statistical analysis may be shared through + conferences, journals, and other publications. +
  • +
  • + Merger - To a potential or actual acquirer, successor, or assignee as part of any + reorganization, merger, sale, joint venture, assignment, transfer, or other disposition of all or any + portion of our organization or assets. You will have the opportunity to opt out of any such transfer if + the new entity's planned processing of your Information differs materially from that set forth in + this Privacy Policy. +
  • +
  • + Legal - If required to do so by law or in the good faith belief that such action is appropriate: + (a) under applicable law, including laws outside your country of residence; (b) to comply with legal + process; (c) to respond to requests from public and government authorities, such as school, school + districts, and law enforcement, including public and government authorities outside your country of + residence; (d) to enforce our terms and conditions; (e) to protect our operations or those of any of + our affiliates; (f) to protect our rights, privacy, safety, or property, and/or that of our affiliates, + you, or others; and (g) to allow us to pursue available remedies or limit the damages that we may + sustain. +
  • +
+

Children and Student Privacy

+

+ The Scratch Foundation is a 501(c)(3) nonprofit organization. As such, the Children's Online Privacy + Protection Act (COPPA) does not apply to Scratch. Nevertheless, Scratch takes children's privacy + seriously. Scratch collects only minimal information from its users, and only uses and discloses + information to provide the services and for limited other purposes, such as research, as described in this + Privacy Policy. +

+

+ Scratch does not collect information from a student's education record, as defined by the Family + Educational Rights and Privacy Act (FERPA). Scratch does not disclose information of students to any third + parties except as described in this Privacy Policy. +

+

Your Data Protection Rights (EEA)

+

+ If you are located in the European Economic Area, the United Kingdom or Switzerland, you have certain + rights in relation to your Information: +

+
    +
  • + Access, Correction and Data Portability - You may ask for an overview of the Information we + process about you and to receive a copy of your Information. You also have the right to request to + correct incomplete, inaccurate or outdated Information. To the extent required by applicable law, you + may request us to provide your Information to another company. +
  • +
  • + Objection – You may object to (this means “ask us to stop”) any use of your + Information that is not (i) processed to comply with a legal obligation, (ii) necessary to do what is + provided in a contract between Scratch and you, or (iii) if we have a compelling reason to do so (such + as, to ensure safety and security in our online community). If you do object, we will work with you to + find a reasonable solution. +
  • +
  • + Deletion - You may also request the deletion of your Information, as permitted under applicable + law. This applies, for instance, where your Information is outdated or the processing is not necessary + or is unlawful; where you withdraw your consent to our processing based on such consent; or where you + have objected to our processing. In some situations, we may need to retain your Information due to + legal obligations or for litigation purposes. If you want to have all of your Information removed from + our servers, please contact help@scratch.mit.edu for assistance. +
  • +
  • + Restriction Of Processing - You may request that we restrict processing of your Information + while we are processing a request relating to (i) the accuracy of your Information, (ii) the lawfulness + of the processing of your Information, or (iii) our legitimate interests to process this Information. + You may also request that we restrict processing of your Information if you wish to use the Information + for litigation purposes. +
  • +
  • + Withdrawal Of Consent – Where we rely on consent for the processing of your Information, you + have the right to withdraw it at any time and free of charge. When you do so, this will not affect the + lawfulness of the processing before your consent withdrawal. +
  • +
+

+ In addition to the above-mentioned rights, you also have the right to lodge a complaint with a competent + supervisory authority subject to applicable law. However, there are exceptions and limitations to each of + these rights. We may, for example, refuse to act on a request if the request is manifestly unfounded or + excessive, or if the request is likely to adversely affect the rights and freedoms of others, prejudice the + execution or enforcement of the law, interfere with pending or future litigation, or infringe applicable + law. To submit a request to exercise your rights, please contact help@scratch.mit.edu for assistance. +

+

Data Retention

+

+ We take measures to delete your Information or keep it in a form that does not allow you to be identified + when this Information is no longer necessary for the purposes for which we process it, unless we are + required by law to keep this Information for a longer period. When determining the retention period, we + take into account various criteria, such as the type of services requested by or provided to you, the + nature and length of our relationship with you, possible re-enrollment with our services, the impact on the + services we provide to you if we delete some Information from or about you, mandatory retention periods + provided by law and the statute of limitations. +

+

How Does Scratch Protect My Information?

+

+ Scratch has in place administrative, physical, and technical procedures that are intended to protect the + Information we collect on the Scratch App against accidental or unlawful destruction, accidental loss, + unauthorized alteration, unauthorized disclosure or access, misuse, and any other unlawful form of + processing of the Information. However, as effective as these measures are, no security system is + impenetrable. We cannot completely guarantee the security of our databases, nor can we guarantee that the + Information you supply will not be intercepted while being transmitted to us over the Internet. +

+

International Data Transfer

+

+ We may transfer your Information to countries other than the country where you are located, including to + the U.S. (where our Scratch servers are located) or any other country in which we or our service providers + maintain facilities. If you are located in the European Economic Area, the United Kingdom or Switzerland, + or other regions with laws governing data collection and use that may differ from U.S. law, please note + that we may transfer your Information to a country and jurisdiction that does not have the same data + protection laws as your jurisdiction. We apply appropriate safeguards to the Information processed and + transferred on our behalf. Please contact us for more information on the safeguards used. +

+

Notifications Of Changes To The Privacy Policy

+

+ We review our Privacy Policy on a periodic basis, and we may modify our policies as appropriate. We will + notify you of any material changes. We encourage you to review our Privacy Policy on a regular basis. The + “Last Updated” date at the top of this page indicates when this Privacy Policy was last + revised. Your continued use of the Scratch App following these changes means that you accept the revised + Privacy Policy. +

+

Contact Us

+

+ The Scratch Foundation is the entity responsible for the processing of your Information. If you have any + questions about this Privacy Policy, or if you would like to exercise your rights to your Information, you + may contact us at help@scratch.mit.edu or via mail at: +

+
+
Scratch Foundation
+
ATTN: Privacy Policy
+
+
201 South Street
+ Boston, MA 02111 +
+
+
+); + +export default ; diff --git a/src/renderer/showPrivacyPolicy.js b/src/renderer/showPrivacyPolicy.js new file mode 100644 index 0000000..5f4371c --- /dev/null +++ b/src/renderer/showPrivacyPolicy.js @@ -0,0 +1,13 @@ +import {ipcRenderer} from 'electron'; + +const showPrivacyPolicy = event => { + if (event) { + // Probably a click on a link; don't actually follow the link in the `href` attribute. + event.preventDefault(); + } + // tell the main process to open the privacy policy window + ipcRenderer.send('open-privacy-policy-window'); + return false; +}; + +export default showPrivacyPolicy;