Merge branch 'develop' into fix-telemetry-modal

This commit is contained in:
Christopher Willis-Ford 2021-01-26 12:39:58 -08:00 committed by GitHub
commit 3d0963fbd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 410 additions and 48 deletions

1
.gitattributes vendored
View file

@ -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

View file

@ -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;
});

View file

@ -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) {

39
src/renderer/about.css Normal file
View file

@ -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;
}

View file

@ -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 = () => (
<div
style={{
color: 'white',
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
fontWeight: 'bolder',
margin: 0,
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
>
<div className={styles.aboutBox}>
<div><img
alt={`${productName} icon`}
src={logo}
style={{
maxWidth: '10rem',
maxHeight: '10rem'
}}
className={styles.aboutLogo}
/></div>
<div style={{margin: '1.5rem'}}>
<div className={styles.aboutText}>
<h2>{productName}</h2>
<div>Version {version}</div>
<table style={{fontSize: 'x-small'}}>
Version {version}
<table className={styles.aboutDetails}><tbody>
{
['Electron', 'Chrome'].map(component => {
['Electron', 'Chrome', 'Node'].map(component => {
const componentVersion = process.versions[component.toLowerCase()];
return <tr key={component}><td>{component}</td><td>{componentVersion}</td></tr>;
})
}
</table>
</tbody></table>
</div>
</div>
);
const appTarget = document.getElementById('app');
ReactDOM.render(<AboutElement />, appTarget);
export default <AboutElement />;

View file

@ -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 (<WrappedComponent
canEditTitle
canModifyCloudData={false}
canSave={false}
isScratchDesktop
onClickLogo={this.handleClickLogo}
isTelemetryEnabled={this.state.telemetryDidOptIn}
showTelemetryModal={shouldShowTelemetryModal}
onClickAbout={[
{
title: 'About',
onClick: () => 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(<WrappedGui />, appTarget);
export default <WrappedGui />;

View file

@ -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);
});

14
src/renderer/privacy.css Normal file
View file

@ -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;
}

235
src/renderer/privacy.jsx Normal file
View file

@ -0,0 +1,235 @@
import React from 'react';
import styles from './privacy.css';
const PrivacyElement = () => (
<div className={styles.privacyBox}>
<h1>Privacy Policy</h1>
<i>The Scratch Privacy Policy was last updated: October 5, 2020</i>
<p>
The Scratch Foundation (&ldquo;Scratch&rdquo;, &ldquo;we&rdquo; or &ldquo;us&rdquo;) understands how
important privacy is to our community. We wrote this Privacy Policy to explain what Personal Information
(&ldquo;Information&rdquo;) we collect through our offline editor (the &ldquo;<a
href="https://scratch.mit.edu/download"
target="_blank"
rel="noopener noreferrer"
>Scratch App</a>&rdquo;), how we use, process, and share it, and what we&apos;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 <a
href="https://scratch.mit.edu/contact-us/"
target="_blank"
rel="noopener noreferrer"
>contact us</a> if you have any questions or concerns.
</p>
<h2>What Information Does Scratch Collect About Me?</h2>
<p>
For the purpose of this Privacy Policy, &ldquo;Information&rdquo; 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.
</p>
<h2>How Does Scratch Use My Information?</h2>
<p>We use this Information for the following purposes:</p>
<ul>
<li>
<b>Analytics and Improving the Scratch App</b> - We use the Information to analyze use of the Scratch
App and to enhance your learning experience on the Scratch App.
</li>
<li>
<b>Academic and Scientific Research</b> - 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 <a
href="https://scratch.mit.edu/research"
target="_blank"
rel="noopener noreferrer"
>Research page</a>.
</li>
<li>
<b>Legal</b> - We may use your Information to enforce our <a
href="https://scratch.mit.edu/terms_of_use"
target="_blank"
rel="noopener noreferrer"
>Terms of Use</a>, 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.
</li>
</ul>
<h2>What Are The Legal Grounds For Processing Your Information?</h2>
<p>
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 &ldquo;legal ground&rdquo; 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 partys legitimate interests to process your
Information when these interests are not overridden by your rights and interests.
</p>
<h2>How Does Scratch Share My Information?</h2>
<p>
We disclose information that we collect through the Scratch App to third parties in the following
circumstances:
</p>
<ul>
<li>
<b>Service Providers</b> - 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.
</li>
<li>
<b>Academic and Scientific Research</b> - 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.
</li>
<li>
<b>Merger</b> - 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&apos;s planned processing of your Information differs materially from that set forth in
this Privacy Policy.
</li>
<li>
<b>Legal</b> - 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.
</li>
</ul>
<h2>Children and Student Privacy</h2>
<p>
The Scratch Foundation is a 501(c)(3) nonprofit organization. As such, the Children&apos;s Online Privacy
Protection Act (COPPA) does not apply to Scratch. Nevertheless, Scratch takes children&apos;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.
</p>
<p>
Scratch does not collect information from a student&apos;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.
</p>
<h2>Your Data Protection Rights (EEA)</h2>
<p>
If you are located in the European Economic Area, the United Kingdom or Switzerland, you have certain
rights in relation to your Information:
</p>
<ul>
<li>
<b>Access, Correction and Data Portability</b> - 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.
</li>
<li>
<b>Objection</b> You may object to (this means &ldquo;ask us to stop&rdquo;) 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.
</li>
<li>
<b>Deletion</b> - 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 <a
href="mailto:help@scratch.mit.edu"
target="_blank"
rel="noopener noreferrer"
>help@scratch.mit.edu</a> for assistance.
</li>
<li>
<b>Restriction Of Processing</b> - 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.
</li>
<li>
<b>Withdrawal Of Consent</b> 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.
</li>
</ul>
<p>
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 <a
href="mailto:help@scratch.mit.edu"
target="_blank"
rel="noopener noreferrer"
>help@scratch.mit.edu</a> for assistance.
</p>
<h2>Data Retention</h2>
<p>
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.
</p>
<h2>How Does Scratch Protect My Information?</h2>
<p>
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.
</p>
<h2>International Data Transfer</h2>
<p>
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.
</p>
<h2>Notifications Of Changes To The Privacy Policy</h2>
<p>
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
&ldquo;Last Updated&rdquo; 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.
</p>
<h2>Contact Us</h2>
<p>
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 <a
href="mailto:help@scratch.mit.edu"
target="_blank"
rel="noopener noreferrer"
>help@scratch.mit.edu</a> or via mail at:
</p>
<div className="vcard">
<div className="org">Scratch Foundation</div>
<div className="fn">ATTN: Privacy Policy</div>
<div className="adr">
<div className="street-address">201 South Street</div>
<span className="locality">Boston</span>, <span className="region">MA</span> <span
className="postal-code"
>02111</span>
</div>
</div>
</div>
);
export default <PrivacyElement />;

View file

@ -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;