From 93a94d82533863bd5f85c23e4cdc2fa6a319e6f8 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 6 Oct 2020 14:34:01 -0700 Subject: [PATCH 01/15] move About window styling into a CSS file --- .gitattributes | 1 + src/renderer/about.css | 26 ++++++++++++++++++++++++++ src/renderer/about.jsx | 23 +++++------------------ 3 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 src/renderer/about.css 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/renderer/about.css b/src/renderer/about.css new file mode 100644 index 0000000..b804c78 --- /dev/null +++ b/src/renderer/about.css @@ -0,0 +1,26 @@ +html, body { + color: white; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: bolder; +} + +.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; +} diff --git a/src/renderer/about.jsx b/src/renderer/about.jsx index 9b121bc..17ffef1 100644 --- a/src/renderer/about.jsx +++ b/src/renderer/about.jsx @@ -3,33 +3,20 @@ 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}
- +
{ ['Electron', 'Chrome'].map(component => { const componentVersion = process.versions[component.toLowerCase()]; From 8fc98e1c37096ddf27e441941fba9a55a7ef11ca Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 6 Oct 2020 14:34:01 -0700 Subject: [PATCH 02/15] add new window for privacy policy --- src/main/index.js | 16 +++++++++++ src/renderer/about.css | 1 + src/renderer/index.js | 3 ++ src/renderer/privacy.css | 14 ++++++++++ src/renderer/privacy.jsx | 59 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+) create mode 100644 src/renderer/privacy.css create mode 100644 src/renderer/privacy.jsx diff --git a/src/main/index.js b/src/main/index.js index 1688313..6362996 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -210,6 +210,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,6 +382,11 @@ 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', () => { diff --git a/src/renderer/about.css b/src/renderer/about.css index b804c78..68b55f4 100644 --- a/src/renderer/about.css +++ b/src/renderer/about.css @@ -1,4 +1,5 @@ html, body { + background-color: #4D97FF; color: white; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-weight: bolder; diff --git a/src/renderer/index.js b/src/renderer/index.js index 49915ad..abca150 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -9,4 +9,7 @@ case 'app': case 'about': import('./about.jsx'); // eslint-disable-line no-unused-expressions break; +case 'privacy': + import('./privacy.jsx'); + break; } 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..52e9901 --- /dev/null +++ b/src/renderer/privacy.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import styles from './privacy.css'; + +// TODO: localization? +const PrivacyElement = () => ( +
+

Privacy Policy

+

+ We understand how important privacy is to our community, especially children and their parents. We wrote + this privacy policy to explain what information we collect through the Scratch application (the + “App”), how we use it, and what we're doing to keep it safe. If you have any questions + regarding this privacy policy, you can contact us. +

+

What information does the App collect?

+

+ The Scratch Team is always looking to better understand how Scratch is used around the world. To help + support this effort, Scratch only collects anonymous usage information from the Scratch App. This + information does not include any Personal Information. For purposes of this Privacy Policy, “Personal + Information” means any information relating to an identified or identifiable individual. +

+

+ The anonymous usage information we collect includes data about what parts of the app you use and basic + information about your network that allows us to roughly estimate what part of the world you are located + in. We also may collect general information about the device that you are using, such as make, model, + operating system and screen resolution. We do not collect device identifiers, such as advertising IDs, MAC + addresses, or IP addresses. We do not associate any of this information with an identified or identifiable + individual. +

+

How does the Scratch Team use the information it collects?

+
    +
  • + We may use anonymous usage information in research studies intended to improve our understanding of how + people learn with Scratch. The results of this research are shared with educators and researchers + through conferences, journals, and other publications. +
  • +
  • + We analyze the information to understand and improve Scratch, such as determining which elements are + most used and how long users spend in the app. +
  • +
  • + We will never share anonymous usage data with any other person, company, or organization, except: +
      +
    • + As required to comply with our obligations under the law +
    • +
    • + For technical reasons, if we are required to transfer the data on our servers to another + location or organization +
    • +
    +
  • +
+
+); + +const appTarget = document.getElementById('app'); +ReactDOM.render(, appTarget); From 3b1dd4e008e822269e49379863fb72582a5bdedc Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 6 Oct 2020 14:34:01 -0700 Subject: [PATCH 03/15] open external links in system browser --- src/main/index.js | 7 ++++++- src/renderer/about.jsx | 1 - src/renderer/app.jsx | 8 +------- src/renderer/privacy.jsx | 7 +++++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 6362996..8977a3d 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,6 +193,11 @@ 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); diff --git a/src/renderer/about.jsx b/src/renderer/about.jsx index 17ffef1..b07c2e4 100644 --- a/src/renderer/about.jsx +++ b/src/renderer/about.jsx @@ -5,7 +5,6 @@ import {productName, version} from '../../package.json'; import logo from '../icon/ScratchDesktop.svg'; import styles from './about.css'; -// TODO: localization? const AboutElement = () => (
(

Privacy Policy

@@ -11,7 +10,11 @@ const PrivacyElement = () => ( We understand how important privacy is to our community, especially children and their parents. We wrote this privacy policy to explain what information we collect through the Scratch application (the “App”), how we use it, and what we're doing to keep it safe. If you have any questions - regarding this privacy policy, you can contact us. + regarding this privacy policy, you can contact us.

What information does the App collect?

From fb26baeac10683d01c4fdae565090839caf932fb Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 6 Oct 2020 14:34:03 -0700 Subject: [PATCH 04/15] app.jsx cleanup --- src/renderer/app.jsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index 4ed002e..2aa02ce 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -28,12 +28,8 @@ import ElectronStorageHelper from '../common/ElectronStorageHelper'; import styles from './app.css'; -// 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); @@ -117,6 +113,7 @@ const ScratchDesktopHOC = function (WrappedComponent) { return ( Date: Tue, 6 Oct 2020 14:34:03 -0700 Subject: [PATCH 05/15] add privacy policy link to 'about' dialog --- src/main/index.js | 7 +++++++ src/renderer/about.css | 12 ++++++++++++ src/renderer/about.jsx | 28 ++++++++++++++++++++++++---- src/renderer/app.jsx | 4 ++-- src/renderer/index.js | 13 +++++++++++-- 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 8977a3d..8b182ec 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -200,6 +200,9 @@ const createWindow = ({search = null, url = 'index.html', ...browserWindowOption const fullUrl = makeFullUrl(url, search); window.loadURL(fullUrl); + window.once('ready-to-show', () => { + webContents.send('ready-to-show'); + }); return window; }; @@ -398,6 +401,10 @@ 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 index 68b55f4..43f9ce7 100644 --- a/src/renderer/about.css +++ b/src/renderer/about.css @@ -5,6 +5,14 @@ html, body { 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; @@ -25,3 +33,7 @@ html, body { .aboutDetails { font-size: x-small; } + +.aboutFooter { + font-size: small; +} diff --git a/src/renderer/about.jsx b/src/renderer/about.jsx index b07c2e4..da1ff42 100644 --- a/src/renderer/about.jsx +++ b/src/renderer/about.jsx @@ -1,3 +1,4 @@ +import {ipcRenderer} from 'electron'; import React from 'react'; import ReactDOM from 'react-dom'; import {productName, version} from '../../package.json'; @@ -5,6 +6,16 @@ import {productName, version} from '../../package.json'; import logo from '../icon/ScratchDesktop.svg'; import styles from './about.css'; +// don't actually follow the link in the `href` attribute +// instead, tell the main process to open the privacy policy window +const showPrivacyPolicy = event => { + if (event) { + event.preventDefault(); + } + ipcRenderer.send('open-privacy-policy-window'); + return false; +}; + const AboutElement = () => (

( />

{productName}

-
Version {version}
-
+ Version {version} +
{ - ['Electron', 'Chrome'].map(component => { + ['Electron', 'Chrome', 'Node'].map(component => { const componentVersion = process.versions[component.toLowerCase()]; return ; }) } -
{component}{componentVersion}
+ +

+ View the Privacy Policy... +

); diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index 2aa02ce..85a2409 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -84,7 +84,7 @@ const ScratchDesktopHOC = function (WrappedComponent) { componentWillUnmount () { ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave); } - handleClickLogo () { + handleClickAbout () { ipcRenderer.send('open-about-window'); } handleProjectTelemetryEvent (event, metadata) { @@ -116,7 +116,7 @@ const ScratchDesktopHOC = function (WrappedComponent) { canSave={false} isScratchDesktop showTelemetryModal={shouldShowTelemetryModal} - onClickLogo={this.handleClickLogo} + onClickAbout={this.handleClickAbout} onProjectTelemetryEvent={this.handleProjectTelemetryEvent} onStorageInit={this.handleStorageInit} onTelemetryModalOptIn={this.handleTelemetryModalOptIn} diff --git a/src/renderer/index.js b/src/renderer/index.js index abca150..cc0d471 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -1,5 +1,14 @@ -// 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'; + +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'; switch (route) { From 73819d8eb7ed540c1101c5124b3537a092e1fdb3 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 6 Oct 2020 14:34:04 -0700 Subject: [PATCH 06/15] view built-in privacy policy from telemetry dialog --- src/renderer/about.jsx | 12 +----------- src/renderer/app.jsx | 2 ++ src/renderer/showPrivacyPolicy.js | 13 +++++++++++++ 3 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 src/renderer/showPrivacyPolicy.js diff --git a/src/renderer/about.jsx b/src/renderer/about.jsx index da1ff42..31a6884 100644 --- a/src/renderer/about.jsx +++ b/src/renderer/about.jsx @@ -1,20 +1,10 @@ -import {ipcRenderer} from 'electron'; 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'; - -// don't actually follow the link in the `href` attribute -// instead, tell the main process to open the privacy policy window -const showPrivacyPolicy = event => { - if (event) { - event.preventDefault(); - } - ipcRenderer.send('open-privacy-policy-window'); - return false; -}; +import showPrivacyPolicy from './showPrivacyPolicy'; const AboutElement = () => (
diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index 85a2409..a8923bd 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -26,6 +26,7 @@ import { import ElectronStorageHelper from '../common/ElectronStorageHelper'; +import showPrivacyPolicy from './showPrivacyPolicy'; import styles from './app.css'; const appTarget = document.getElementById('app'); @@ -118,6 +119,7 @@ const ScratchDesktopHOC = function (WrappedComponent) { showTelemetryModal={shouldShowTelemetryModal} onClickAbout={this.handleClickAbout} onProjectTelemetryEvent={this.handleProjectTelemetryEvent} + onShowPrivacyPolicy={showPrivacyPolicy} onStorageInit={this.handleStorageInit} onTelemetryModalOptIn={this.handleTelemetryModalOptIn} onTelemetryModalOptOut={this.handleTelemetryModalOptOut} 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; From 371bd60a7db615962e973813e9b6195a79fee9a5 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 6 Oct 2020 14:34:04 -0700 Subject: [PATCH 07/15] move ReactDOM.render() into index.js This also means we no longer need to disable eslint's "no-unused-expressions" rule for each route in index.js --- src/renderer/about.jsx | 4 +--- src/renderer/app.jsx | 3 +-- src/renderer/index.js | 15 ++++++++++++--- src/renderer/privacy.jsx | 4 +--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/renderer/about.jsx b/src/renderer/about.jsx index 31a6884..b4e652a 100644 --- a/src/renderer/about.jsx +++ b/src/renderer/about.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import {productName, version} from '../../package.json'; import logo from '../icon/ScratchDesktop.svg'; @@ -37,5 +36,4 @@ const AboutElement = () => (
); -const appTarget = document.getElementById('app'); -ReactDOM.render(, appTarget); +export default ; diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index a8923bd..8fff8cf 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -3,7 +3,6 @@ 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'; @@ -181,4 +180,4 @@ const WrappedGui = compose( ScratchDesktopHOC )(GUI); -ReactDOM.render(, appTarget); +export default ; diff --git a/src/renderer/index.js b/src/renderer/index.js index cc0d471..c0609b7 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -3,6 +3,8 @@ 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. @@ -11,14 +13,21 @@ ipcRenderer.on('ready-to-show', () => { }); 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': - import('./privacy.jsx'); + 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.jsx b/src/renderer/privacy.jsx index a433acc..4bfa309 100644 --- a/src/renderer/privacy.jsx +++ b/src/renderer/privacy.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import styles from './privacy.css'; @@ -58,5 +57,4 @@ const PrivacyElement = () => (
); -const appTarget = document.getElementById('app'); -ReactDOM.render(, appTarget); +export default ; From 10b2bae67084351a58b6699714cdb3d4255dbc9e Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Wed, 7 Oct 2020 18:17:54 -0700 Subject: [PATCH 08/15] update privacy policy to October 5, 2020 copy --- src/renderer/privacy.jsx | 243 +++++++++++++++++++++++++++++++++------ 1 file changed, 209 insertions(+), 34 deletions(-) diff --git a/src/renderer/privacy.jsx b/src/renderer/privacy.jsx index 4bfa309..ac7aaf0 100644 --- a/src/renderer/privacy.jsx +++ b/src/renderer/privacy.jsx @@ -5,55 +5,230 @@ import styles from './privacy.css'; const PrivacyElement = () => (

Privacy Policy

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

- We understand how important privacy is to our community, especially children and their parents. We wrote - this privacy policy to explain what information we collect through the Scratch application (the - “App”), how we use it, and what we're doing to keep it safe. If you have any questions - regarding this privacy policy, you can 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. + >contact us if you have any questions or concerns.

-

What information does the App collect?

+

What Information Does Scratch Collect About Me?

- The Scratch Team is always looking to better understand how Scratch is used around the world. To help - support this effort, Scratch only collects anonymous usage information from the Scratch App. This - information does not include any Personal Information. For purposes of this Privacy Policy, “Personal - Information” means any information relating to an identified or identifiable individual. + 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.

-

- The anonymous usage information we collect includes data about what parts of the app you use and basic - information about your network that allows us to roughly estimate what part of the world you are located - in. We also may collect general information about the device that you are using, such as make, model, - operating system and screen resolution. We do not collect device identifiers, such as advertising IDs, MAC - addresses, or IP addresses. We do not associate any of this information with an identified or identifiable - individual. -

-

How does the Scratch Team use the information it collects?

+

How Does Scratch Use My Information?

+

We use this Information for the following purposes:

  • - We may use anonymous usage information in research studies intended to improve our understanding of how - people learn with Scratch. The results of this research are shared with educators and researchers - through conferences, journals, and other publications. + 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.
  • - We analyze the information to understand and improve Scratch, such as determining which elements are - most used and how long users spend in the 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.
  • - We will never share anonymous usage data with any other person, company, or organization, except: -
      -
    • - As required to comply with our obligations under the law -
    • -
    • - For technical reasons, if we are required to transfer the data on our servers to another - location or organization -
    • -
    + 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 +
+
); From 4fe9e5b703b548cc72d590209001fe71ce2c3001 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Wed, 7 Oct 2020 18:23:50 -0700 Subject: [PATCH 09/15] fix lint: long lines --- src/renderer/about.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/about.jsx b/src/renderer/about.jsx index b4e652a..af57688 100644 --- a/src/renderer/about.jsx +++ b/src/renderer/about.jsx @@ -25,9 +25,9 @@ const AboutElement = () => (

View the Privacy Policy... From 25072c00463cfaf151334506c81e3548b935abf6 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 17 Nov 2020 15:02:47 -0800 Subject: [PATCH 10/15] pass about menu items to GUI --- src/main/ScratchDesktopTelemetry.js | 8 ++++++++ src/main/telemetry/TelemetryClient.js | 3 +++ src/renderer/app.jsx | 18 +++++++++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/ScratchDesktopTelemetry.js b/src/main/ScratchDesktopTelemetry.js index ec27205..1dbecf0 100644 --- a/src/main/ScratchDesktopTelemetry.js +++ b/src/main/ScratchDesktopTelemetry.js @@ -36,6 +36,9 @@ class ScratchDesktopTelemetry { set didOptIn (value) { this._telemetryClient.didOptIn = value; } + clearDidOptIn () { + this._telemetryClient.clearDidOptIn(); + } appWasOpened () { this._telemetryClient.addEvent('app::open', {...EVENT_TEMPLATE, ...APP_INFO}); @@ -95,6 +98,11 @@ ipcMain.on('getTelemetryDidOptIn', event => { }); ipcMain.on('setTelemetryDidOptIn', (event, arg) => { scratchDesktopTelemetrySingleton.didOptIn = arg; + event.returnValue = null; +}); +ipcMain.on('clearTelemetryDidOptIn', event => { + scratchDesktopTelemetrySingleton.clearDidOptIn(); + event.returnValue = null; }); ipcMain.on('projectDidLoad', (event, arg) => { scratchDesktopTelemetrySingleton.projectDidLoad(arg); diff --git a/src/main/telemetry/TelemetryClient.js b/src/main/telemetry/TelemetryClient.js index c38cb5b..fafda8b 100644 --- a/src/main/telemetry/TelemetryClient.js +++ b/src/main/telemetry/TelemetryClient.js @@ -185,6 +185,9 @@ class TelemetryClient { set didOptIn (value) { this._store.set('optIn', !!value); } + clearDidOptIn () { + this._store.delete('optIn'); + } /** * Semi-persistent unique ID for this client diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index 8fff8cf..9fa906e 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -116,7 +116,23 @@ const ScratchDesktopHOC = function (WrappedComponent) { canSave={false} isScratchDesktop showTelemetryModal={shouldShowTelemetryModal} - onClickAbout={this.handleClickAbout} + onClickAbout={[ + { + title: 'About', + onClick: () => this.handleClickAbout() + }, + { + title: 'Privacy Policy', + onClick: () => showPrivacyPolicy() + }, + { + title: 'Telemetry Settings', + onClick: () => { + // set to null (non-Boolean) to cause app to ask again + ipcRenderer.sendSync('clearTelemetryDidOptIn'); + } + } + ]} onProjectTelemetryEvent={this.handleProjectTelemetryEvent} onShowPrivacyPolicy={showPrivacyPolicy} onStorageInit={this.handleStorageInit} From afb29bfdefa4c811a50112f5a3758e663b5122b4 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Wed, 18 Nov 2020 15:34:01 -0800 Subject: [PATCH 11/15] use openTelemetryModal instead of showTelemetryModal --- src/renderer/app.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index 9fa906e..aaff2ae 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -20,7 +20,8 @@ import { } from 'scratch-gui/src/reducers/project-state'; import { openLoadingProject, - closeLoadingProject + closeLoadingProject, + openTelemetryModal } from 'scratch-gui/src/reducers/modals'; import ElectronStorageHelper from '../common/ElectronStorageHelper'; @@ -127,10 +128,7 @@ const ScratchDesktopHOC = function (WrappedComponent) { }, { title: 'Telemetry Settings', - onClick: () => { - // set to null (non-Boolean) to cause app to ask again - ipcRenderer.sendSync('clearTelemetryDidOptIn'); - } + onClick: () => this.props.onTelemetrySettingsClicked() } ]} onProjectTelemetryEvent={this.handleProjectTelemetryEvent} @@ -154,6 +152,7 @@ const ScratchDesktopHOC = function (WrappedComponent) { onLoadingCompleted: PropTypes.func, onLoadingStarted: PropTypes.func, onRequestNewProject: PropTypes.func, + onTelemetrySettingsClicked: PropTypes.func, vm: PropTypes.instanceOf(VM).isRequired }; const mapStateToProps = state => { @@ -182,7 +181,8 @@ const ScratchDesktopHOC = 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)(ScratchDesktopComponent); From d56a3c58101a92625bd21ab155aae855fdeda823 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Wed, 2 Dec 2020 14:03:57 -0800 Subject: [PATCH 12/15] keep telemetry modal state in sync with main process --- src/renderer/app.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index aaff2ae..5d728a5 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -99,15 +99,18 @@ const ScratchDesktopHOC = function (WrappedComponent) { } handleTelemetryModalOptIn () { ipcRenderer.send('setTelemetryDidOptIn', true); + this.forceUpdate(); } handleTelemetryModalOptOut () { ipcRenderer.send('setTelemetryDidOptIn', false); + this.forceUpdate(); } handleUpdateProjectTitle (newTitle) { this.setState({projectTitle: newTitle}); } render () { - const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean'); + const currentTelemetryState = ipcRenderer.sendSync('getTelemetryDidOptIn'); + const shouldShowTelemetryModal = (typeof currentTelemetryState !== 'boolean'); const childProps = omit(this.props, Object.keys(ScratchDesktopComponent.propTypes)); @@ -116,6 +119,7 @@ const ScratchDesktopHOC = function (WrappedComponent) { canModifyCloudData={false} canSave={false} isScratchDesktop + isTelemetryEnabled={currentTelemetryState} showTelemetryModal={shouldShowTelemetryModal} onClickAbout={[ { From 6e840825ccddf1f0d29ef441ff3306fee9643d5f Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Wed, 2 Dec 2020 14:24:58 -0800 Subject: [PATCH 13/15] apply feedback from design team --- src/renderer/about.jsx | 10 ---------- src/renderer/app.jsx | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/renderer/about.jsx b/src/renderer/about.jsx index af57688..204b829 100644 --- a/src/renderer/about.jsx +++ b/src/renderer/about.jsx @@ -3,7 +3,6 @@ import {productName, version} from '../../package.json'; import logo from '../icon/ScratchDesktop.svg'; import styles from './about.css'; -import showPrivacyPolicy from './showPrivacyPolicy'; const AboutElement = () => (

@@ -23,15 +22,6 @@ const AboutElement = () => ( }) } -

- View the Privacy Policy... -

); diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index 5d728a5..eb1a638 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -131,7 +131,7 @@ const ScratchDesktopHOC = function (WrappedComponent) { onClick: () => showPrivacyPolicy() }, { - title: 'Telemetry Settings', + title: 'Data Settings', onClick: () => this.props.onTelemetrySettingsClicked() } ]} From bcc9ff5c1e00b447ab4e7987e8e2e7d65482231a Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 15 Dec 2020 15:25:03 -0800 Subject: [PATCH 14/15] avoid IPC sendSync in render() --- src/main/ScratchDesktopTelemetry.js | 10 +++++----- src/renderer/app.jsx | 17 ++++++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/ScratchDesktopTelemetry.js b/src/main/ScratchDesktopTelemetry.js index 1dbecf0..802b8f9 100644 --- a/src/main/ScratchDesktopTelemetry.js +++ b/src/main/ScratchDesktopTelemetry.js @@ -93,16 +93,16 @@ 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; }); ipcMain.on('setTelemetryDidOptIn', (event, arg) => { scratchDesktopTelemetrySingleton.didOptIn = arg; - event.returnValue = null; -}); -ipcMain.on('clearTelemetryDidOptIn', event => { - scratchDesktopTelemetrySingleton.clearDidOptIn(); - event.returnValue = null; }); ipcMain.on('projectDidLoad', (event, arg) => { scratchDesktopTelemetrySingleton.projectDidLoad(arg); diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index eb1a638..56438c4 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -46,6 +46,10 @@ const ScratchDesktopHOC = 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); @@ -99,18 +103,21 @@ const ScratchDesktopHOC = function (WrappedComponent) { } handleTelemetryModalOptIn () { ipcRenderer.send('setTelemetryDidOptIn', true); - this.forceUpdate(); + ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { + this.setState({telemetryDidOptIn}); + }); } handleTelemetryModalOptOut () { ipcRenderer.send('setTelemetryDidOptIn', false); - this.forceUpdate(); + ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { + this.setState({telemetryDidOptIn}); + }); } handleUpdateProjectTitle (newTitle) { this.setState({projectTitle: newTitle}); } render () { - const currentTelemetryState = ipcRenderer.sendSync('getTelemetryDidOptIn'); - const shouldShowTelemetryModal = (typeof currentTelemetryState !== 'boolean'); + const shouldShowTelemetryModal = (typeof this.state.telemetryDidOptIn !== 'boolean'); const childProps = omit(this.props, Object.keys(ScratchDesktopComponent.propTypes)); @@ -119,7 +126,7 @@ const ScratchDesktopHOC = function (WrappedComponent) { canModifyCloudData={false} canSave={false} isScratchDesktop - isTelemetryEnabled={currentTelemetryState} + isTelemetryEnabled={this.state.telemetryDidOptIn} showTelemetryModal={shouldShowTelemetryModal} onClickAbout={[ { From 7f8d0d7084c0f960a4ad9da814b27cbdebeb57bc Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 15 Dec 2020 15:28:39 -0800 Subject: [PATCH 15/15] remove now-unused clearDidOptIn --- src/main/ScratchDesktopTelemetry.js | 3 --- src/main/telemetry/TelemetryClient.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/main/ScratchDesktopTelemetry.js b/src/main/ScratchDesktopTelemetry.js index 802b8f9..906a7d4 100644 --- a/src/main/ScratchDesktopTelemetry.js +++ b/src/main/ScratchDesktopTelemetry.js @@ -36,9 +36,6 @@ class ScratchDesktopTelemetry { set didOptIn (value) { this._telemetryClient.didOptIn = value; } - clearDidOptIn () { - this._telemetryClient.clearDidOptIn(); - } appWasOpened () { this._telemetryClient.addEvent('app::open', {...EVENT_TEMPLATE, ...APP_INFO}); diff --git a/src/main/telemetry/TelemetryClient.js b/src/main/telemetry/TelemetryClient.js index fafda8b..c38cb5b 100644 --- a/src/main/telemetry/TelemetryClient.js +++ b/src/main/telemetry/TelemetryClient.js @@ -185,9 +185,6 @@ class TelemetryClient { set didOptIn (value) { this._store.set('optIn', !!value); } - clearDidOptIn () { - this._store.delete('optIn'); - } /** * Semi-persistent unique ID for this client