From aac092fd4df8750024c082b25dcbd0ae6ee618fd Mon Sep 17 00:00:00 2001 From: myarmolinsky Date: Tue, 18 Feb 2025 13:01:13 -0500 Subject: [PATCH] Add informational notice about how to disable analytics collection Change-type: minor --- .../source-selector/source-selector.tsx | 16 ++ .../target-selector/target-selector.tsx | 3 + lib/gui/app/pages/main/MainPage.tsx | 215 ++++++++++++------ 3 files changed, 160 insertions(+), 74 deletions(-) diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 9192e3df..82808f8d 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -308,6 +308,7 @@ const FlowSelector = styled( interface SourceSelectorProps { flashing: boolean; + hideAnalyticsAlert: () => void; } interface SourceSelectorState { @@ -359,6 +360,20 @@ export class SourceSelector extends React.Component< ipcRenderer.removeListener('select-image', this.onSelectImage); } + public componentDidUpdate( + _prevProps: Readonly, + prevState: Readonly, + ) { + if ( + (!prevState.showDriveSelector && this.state.showDriveSelector) || + (!prevState.showURLSelector && this.state.showURLSelector) || + (!prevState.showImageDetails && this.state.showImageDetails) || + (!prevState.imageSelectorOpen && this.state.imageSelectorOpen) + ) { + this.props.hideAnalyticsAlert(); + } + } + private async onSelectImage(_event: IpcRendererEvent, imagePath: string) { this.setState({ imageLoading: true }); await this.selectSource( @@ -382,6 +397,7 @@ export class SourceSelector extends React.Component< }); selectionState.deselectImage(); + this.props.hideAnalyticsAlert(); } private selectSource( diff --git a/lib/gui/app/components/target-selector/target-selector.tsx b/lib/gui/app/components/target-selector/target-selector.tsx index 1f369180..a79daed9 100644 --- a/lib/gui/app/components/target-selector/target-selector.tsx +++ b/lib/gui/app/components/target-selector/target-selector.tsx @@ -100,12 +100,14 @@ interface TargetSelectorProps { disabled: boolean; hasDrive: boolean; flashing: boolean; + hideAnalyticsAlert: () => void; } export const TargetSelector = ({ disabled, hasDrive, flashing, + hideAnalyticsAlert, }: TargetSelectorProps) => { // TODO: inject these from redux-connector const [{ driveListLabel, targets }, setStateSlice] = React.useState( @@ -137,6 +139,7 @@ export const TargetSelector = ({ tooltip={driveListLabel} openDriveSelector={() => { setShowTargetSelectorModal(true); + hideAnalyticsAlert(); }} reselectDrive={() => { analytics.logEvent('Reselect drive'); diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index 80e6bc20..f8c9aa28 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -15,12 +15,13 @@ */ import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/gear.svg'; +import CloseSvg from '@fortawesome/fontawesome-free/svgs/solid/x.svg'; import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-question.svg'; import * as path from 'path'; import prettyBytes from 'pretty-bytes'; import * as React from 'react'; -import { Flex } from 'rendition'; +import { Alert, Flex, Link } from 'rendition'; import styled from 'styled-components'; import FinishPage from '../../components/finish/finish'; @@ -35,6 +36,7 @@ import { observe } from '../../models/store'; import { open as openExternal } from '../../os/open-external/services/open-external'; import { IconButton as BaseIcon, + IconButton, ThemedProvider, } from '../../styled-components'; @@ -46,6 +48,7 @@ import { FlashStep } from './Flash'; import EtcherSvg from '../../../assets/etcher.svg'; import { SafeWebview } from '../../components/safe-webview/safe-webview'; +import { theme } from '../../theme'; const Icon = styled(BaseIcon)` margin-right: 20px; @@ -97,6 +100,8 @@ const StepBorder = styled.div<{ margin-left: ${(props) => (props.right ? '-120px' : undefined)}; `; +const ANALYTICS_ALERT_VISIBILITY_KEY = 'analytics_alert_visible'; + interface MainPageStateFromStore { isFlashing: boolean; hasImage: boolean; @@ -113,6 +118,7 @@ interface MainPageState { isWebviewShowing: boolean; hideSettings: boolean; featuredProjectURL?: string; + analyticsAlertIsVisible: boolean; } export class MainPage extends React.Component< @@ -125,6 +131,8 @@ export class MainPage extends React.Component< current: 'main', isWebviewShowing: false, hideSettings: true, + analyticsAlertIsVisible: + localStorage.getItem(ANALYTICS_ALERT_VISIBILITY_KEY) !== 'false', ...this.stateHelper(), }; } @@ -153,6 +161,13 @@ export class MainPage extends React.Component< return url.toString(); } + private hideAnalyticsAlert = () => { + if (this.state.analyticsAlertIsVisible) { + localStorage.setItem(ANALYTICS_ALERT_VISIBILITY_KEY, 'false'); + this.setState({ analyticsAlertIsVisible: false }); + } + }; + public async componentDidMount() { observe(() => { this.setState(this.stateHelper()); @@ -160,6 +175,17 @@ export class MainPage extends React.Component< this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() }); } + public componentDidUpdate( + _prevProps: object, + prevState: Readonly, + ) { + if (this.state.analyticsAlertIsVisible) { + if (prevState.hideSettings !== this.state.hideSettings) { + this.setState({ analyticsAlertIsVisible: false }); + } + } + } + private renderMain() { const state = flashState.getFlashState(); const shouldDriveStepBeDisabled = !this.state.hasImage; @@ -169,86 +195,127 @@ export class MainPage extends React.Component< !this.state.isFlashing || !this.state.isWebviewShowing; return ( - {notFlashingOrSplitView && ( - <> - - - - - - - - - - )} + + {notFlashingOrSplitView && ( + <> + + + + + + + + + + )} - {this.state.isFlashing && this.state.isWebviewShowing && ( - - + + + )} + {this.state.isFlashing && this.state.featuredProjectURL && ( + { + this.setState({ isWebviewShowing }); + }} + style={{ + position: 'absolute', + right: 0, + bottom: 0, + width: '63.8vw', + height: '100vh', }} /> - - )} - {this.state.isFlashing && this.state.featuredProjectURL && ( - { - this.setState({ isWebviewShowing }); - }} - style={{ - position: 'absolute', - right: 0, - bottom: 0, - width: '63.8vw', - height: '100vh', - }} - /> - )} + )} - this.setState({ current: 'success' })} - shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} - isFlashing={this.state.isFlashing} - step={state.type} - percentage={state.percentage} - position={state.position} - failed={state.failed} - speed={state.speed} - eta={state.eta} - style={{ zIndex: 1 }} - /> + this.setState({ current: 'success' })} + shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} + isFlashing={this.state.isFlashing} + step={state.type} + percentage={state.percentage} + position={state.position} + failed={state.failed} + speed={state.speed} + eta={state.eta} + style={{ zIndex: 1 }} + /> + + {this.state.analyticsAlertIsVisible && ( + + + +
+ Etcher collects a limited amount of anonymous data to help us + improve user experience. You can opt out in the{' '} + this.setState({ hideSettings: false })}> + settings + + . +
+
+ For more information about how we use this data, see our{' '} + { + e.stopPropagation(); + openExternal('https://www.balena.io/privacy-policy'); + }} + > + privacy policy + + . +
+
+ {/* TODO: can we use onDismiss instead? */} + + + +
+
+ )} ); }