From 0ca4f6b530237f5168a0d0fc3da6b2537e21ca9c Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 21 Feb 2024 17:08:14 -0500 Subject: [PATCH] Revert "feat(app): remove privacy setting tabs (#14423)" This reverts commit c4660ae37bfb5da9a22abdd0db91dbf31a317418. --- app/src/App/types.ts | 7 +- .../assets/localization/en/app_settings.json | 2 + .../localization/en/device_settings.json | 1 + app/src/molecules/NavTab/NavTab.stories.tsx | 1 + .../RobotSettings/RobotSettingsPrivacy.tsx | 71 ++++++++++++++ .../RobotSettingsDashboard/Privacy.tsx | 94 +++++++++++++++++++ .../__tests__/Privacy.test.tsx | 71 ++++++++++++++ .../organisms/RobotSettingsDashboard/index.ts | 1 + app/src/pages/AppSettings/PrivacySettings.tsx | 56 +++++++++++ .../AppSettings/__test__/AppSettings.test.tsx | 7 ++ .../__test__/PrivacySettings.test.tsx | 32 +++++++ app/src/pages/AppSettings/index.tsx | 3 + .../__tests__/RobotSettings.test.tsx | 17 ++++ app/src/pages/Devices/RobotSettings/index.tsx | 11 ++- .../RobotSettingsList.tsx | 7 ++ .../__tests__/RobotSettingsDashboard.test.tsx | 13 +++ .../pages/RobotSettingsDashboard/index.tsx | 6 ++ 17 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 app/src/organisms/Devices/RobotSettings/RobotSettingsPrivacy.tsx create mode 100644 app/src/organisms/RobotSettingsDashboard/Privacy.tsx create mode 100644 app/src/organisms/RobotSettingsDashboard/__tests__/Privacy.test.tsx create mode 100644 app/src/pages/AppSettings/PrivacySettings.tsx create mode 100644 app/src/pages/AppSettings/__test__/PrivacySettings.test.tsx diff --git a/app/src/App/types.ts b/app/src/App/types.ts index 935f7d43d97..ad81d57e452 100644 --- a/app/src/App/types.ts +++ b/app/src/App/types.ts @@ -23,8 +23,13 @@ export type RobotSettingsTab = | 'networking' | 'advanced' | 'feature-flags' + | 'privacy' -export type AppSettingsTab = 'general' | 'advanced' | 'feature-flags' +export type AppSettingsTab = + | 'general' + | 'privacy' + | 'advanced' + | 'feature-flags' export type ProtocolRunDetailsTab = 'setup' | 'module-controls' | 'run-preview' diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 33bdc6df0ed..b8d7bdc4c2c 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -57,6 +57,7 @@ "opentrons_app_update_available": "Opentrons App Update Available", "opentrons_app_update_available_variation": "An Opentrons App update is available.", "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", + "opentrons_cares_about_privacy": "Opentrons cares about your privacy. We anonymize all data and only use it to improve our products.", "opt_in": "Opt in", "opt_in_description": "Automatically send us anonymous diagnostics and usage data. We only use this information to improve our products.", "opt_out": "Opt out", @@ -66,6 +67,7 @@ "prevent_robot_caching": "Prevent Robot Caching", "prevent_robot_caching_description": "The app will immediately clear unavailable robots and will not remember unavailable robots while this is enabled. On networks with many robots, preventing caching may improve network performance at the expense of slower and less reliable robot discovery on app launch.", "previous_releases": "View previous Opentrons releases", + "privacy": "Privacy", "problem_during_update": "This update is taking longer than usual.", "prompt": "Always show the prompt to choose calibration block or trash bin", "receive_alert": "Receive an alert when an Opentrons software update is available.", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 81b73df22e2..8bab2a1671a 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -206,6 +206,7 @@ "pipette_offset_calibration_recommended": "Pipette Offset calibration recommended", "pipette_offset_calibrations_history": "See all Pipette Offset Calibration history", "pipette_offset_calibrations_title": "Pipette Offset Calibrations", + "privacy": "Privacy", "problem_during_update": "This update is taking longer than usual.", "proceed_without_updating": "Proceed without update", "protocol_run_history": "Protocol run History", diff --git a/app/src/molecules/NavTab/NavTab.stories.tsx b/app/src/molecules/NavTab/NavTab.stories.tsx index 5f1a21b92ba..88fcc0dc2e6 100644 --- a/app/src/molecules/NavTab/NavTab.stories.tsx +++ b/app/src/molecules/NavTab/NavTab.stories.tsx @@ -24,6 +24,7 @@ const Template: Story> = args => ( > + diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsPrivacy.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsPrivacy.tsx new file mode 100644 index 00000000000..267171774d9 --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsPrivacy.tsx @@ -0,0 +1,71 @@ +import * as React from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { useTranslation } from 'react-i18next' + +import { getRobotSettings, fetchSettings } from '../../../redux/robot-settings' + +import type { State, Dispatch } from '../../../redux/types' +import type { + RobotSettings, + RobotSettingsField, +} from '../../../redux/robot-settings/types' +import { SettingToggle } from './SettingToggle' + +interface RobotSettingsPrivacyProps { + robotName: string +} + +const PRIVACY_SETTINGS = ['disableLogAggregation'] + +const INFO_BY_SETTING_ID: { + [id: string]: { + titleKey: string + descriptionKey: string + invert: boolean + } +} = { + disableLogAggregation: { + titleKey: 'share_logs_with_opentrons', + descriptionKey: 'share_logs_with_opentrons_description', + invert: true, + }, +} + +export function RobotSettingsPrivacy({ + robotName, +}: RobotSettingsPrivacyProps): JSX.Element { + const { t } = useTranslation('device_settings') + const settings = useSelector((state: State) => + getRobotSettings(state, robotName) + ) + const privacySettings = settings.filter(({ id }) => + PRIVACY_SETTINGS.includes(id) + ) + const translatedPrivacySettings: Array< + RobotSettingsField & { invert: boolean } + > = privacySettings.map(s => { + const { titleKey, descriptionKey, invert } = INFO_BY_SETTING_ID[s.id] + return s.id in INFO_BY_SETTING_ID + ? { + ...s, + title: t(titleKey), + description: t(descriptionKey), + invert, + } + : { ...s, invert: false } + }) + + const dispatch = useDispatch() + + React.useEffect(() => { + dispatch(fetchSettings(robotName)) + }, [dispatch, robotName]) + + return ( + <> + {translatedPrivacySettings.map(field => ( + + ))} + + ) +} diff --git a/app/src/organisms/RobotSettingsDashboard/Privacy.tsx b/app/src/organisms/RobotSettingsDashboard/Privacy.tsx new file mode 100644 index 00000000000..b3b056d0b26 --- /dev/null +++ b/app/src/organisms/RobotSettingsDashboard/Privacy.tsx @@ -0,0 +1,94 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' + +import { + Flex, + SPACING, + DIRECTION_COLUMN, + TYPOGRAPHY, +} from '@opentrons/components' + +import { StyledText } from '../../atoms/text' +import { ChildNavigation } from '../../organisms/ChildNavigation' +import { ROBOT_ANALYTICS_SETTING_ID } from '../../pages/RobotDashboard/AnalyticsOptInModal' +import { RobotSettingButton } from '../../pages/RobotSettingsDashboard/RobotSettingButton' +import { OnOffToggle } from '../../pages/RobotSettingsDashboard/RobotSettingsList' +import { + getAnalyticsOptedIn, + toggleAnalyticsOptedIn, +} from '../../redux/analytics' +import { getRobotSettings, updateSetting } from '../../redux/robot-settings' + +import type { Dispatch, State } from '../../redux/types' +import type { SetSettingOption } from '../../pages/RobotSettingsDashboard' + +interface PrivacyProps { + robotName: string + setCurrentOption: SetSettingOption +} + +export function Privacy({ + robotName, + setCurrentOption, +}: PrivacyProps): JSX.Element { + const { t } = useTranslation('app_settings') + const dispatch = useDispatch() + + const allRobotSettings = useSelector((state: State) => + getRobotSettings(state, robotName) + ) + + const appAnalyticsOptedIn = useSelector(getAnalyticsOptedIn) + + const isRobotAnalyticsDisabled = + allRobotSettings.find(({ id }) => id === ROBOT_ANALYTICS_SETTING_ID) + ?.value ?? false + + return ( + + setCurrentOption(null)} + /> + + + {t('opentrons_cares_about_privacy')} + + + } + onClick={() => + dispatch( + updateSetting( + robotName, + ROBOT_ANALYTICS_SETTING_ID, + !isRobotAnalyticsDisabled + ) + ) + } + /> + } + onClick={() => dispatch(toggleAnalyticsOptedIn())} + /> + + + + ) +} diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/Privacy.test.tsx b/app/src/organisms/RobotSettingsDashboard/__tests__/Privacy.test.tsx new file mode 100644 index 00000000000..a2943526a76 --- /dev/null +++ b/app/src/organisms/RobotSettingsDashboard/__tests__/Privacy.test.tsx @@ -0,0 +1,71 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '@opentrons/components' + +import { i18n } from '../../../i18n' +import { toggleAnalyticsOptedIn } from '../../../redux/analytics' +import { getRobotSettings, updateSetting } from '../../../redux/robot-settings' + +import { Privacy } from '../Privacy' + +jest.mock('../../../redux/analytics') +jest.mock('../../../redux/robot-settings') + +const mockGetRobotSettings = getRobotSettings as jest.MockedFunction< + typeof getRobotSettings +> +const mockUpdateSetting = updateSetting as jest.MockedFunction< + typeof updateSetting +> +const mockToggleAnalyticsOptedIn = toggleAnalyticsOptedIn as jest.MockedFunction< + typeof toggleAnalyticsOptedIn +> + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('Privacy', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + robotName: 'Otie', + setCurrentOption: jest.fn(), + } + mockGetRobotSettings.mockReturnValue([]) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should render text and buttons', () => { + render(props) + screen.getByText('Privacy') + screen.getByText( + 'Opentrons cares about your privacy. We anonymize all data and only use it to improve our products.' + ) + screen.getByText('Share robot logs') + screen.getByText('Data on actions the robot does, like running protocols.') + screen.getByText('Share display usage') + screen.getByText('Data on how you interact with the touchscreen on Flex.') + }) + + it('should toggle display usage sharing on click', () => { + render(props) + fireEvent.click(screen.getByText('Share display usage')) + expect(mockToggleAnalyticsOptedIn).toBeCalled() + }) + + it('should toggle robot logs sharing on click', () => { + render(props) + fireEvent.click(screen.getByText('Share robot logs')) + expect(mockUpdateSetting).toBeCalledWith( + 'Otie', + 'disableLogAggregation', + true + ) + }) +}) diff --git a/app/src/organisms/RobotSettingsDashboard/index.ts b/app/src/organisms/RobotSettingsDashboard/index.ts index 8dc444ff75c..ba05950ff24 100644 --- a/app/src/organisms/RobotSettingsDashboard/index.ts +++ b/app/src/organisms/RobotSettingsDashboard/index.ts @@ -5,6 +5,7 @@ export * from './NetworkSettings/RobotSettingsSetWifiCred' export * from './NetworkSettings/RobotSettingsWifi' export * from './NetworkSettings/RobotSettingsWifiConnect' export * from './NetworkSettings' +export * from './Privacy' export * from './RobotName' export * from './RobotSystemVersion' export * from './TextSize' diff --git a/app/src/pages/AppSettings/PrivacySettings.tsx b/app/src/pages/AppSettings/PrivacySettings.tsx new file mode 100644 index 00000000000..ed8869c5efb --- /dev/null +++ b/app/src/pages/AppSettings/PrivacySettings.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector, useDispatch } from 'react-redux' + +import { + Flex, + Box, + SIZE_2, + TYPOGRAPHY, + JUSTIFY_SPACE_BETWEEN, + SPACING, +} from '@opentrons/components' + +import { + toggleAnalyticsOptedIn, + getAnalyticsOptedIn, +} from '../../redux/analytics' +import { ToggleButton } from '../../atoms/buttons' +import { StyledText } from '../../atoms/text' + +import type { Dispatch, State } from '../../redux/types' + +export function PrivacySettings(): JSX.Element { + const { t } = useTranslation('app_settings') + const dispatch = useDispatch() + const analyticsOptedIn = useSelector((s: State) => getAnalyticsOptedIn(s)) + + return ( + + + + {t('share_app_analytics')} + + + {t('share_app_analytics_description')} + + + dispatch(toggleAnalyticsOptedIn())} + id="PrivacySettings_analytics" + /> + + ) +} diff --git a/app/src/pages/AppSettings/__test__/AppSettings.test.tsx b/app/src/pages/AppSettings/__test__/AppSettings.test.tsx index f465164bcbe..4434c199c66 100644 --- a/app/src/pages/AppSettings/__test__/AppSettings.test.tsx +++ b/app/src/pages/AppSettings/__test__/AppSettings.test.tsx @@ -7,12 +7,14 @@ import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../i18n' import * as Config from '../../../redux/config' import { GeneralSettings } from '../GeneralSettings' +import { PrivacySettings } from '../PrivacySettings' import { AdvancedSettings } from '../AdvancedSettings' import { FeatureFlags } from '../../../organisms/AppSettings/FeatureFlags' import { AppSettings } from '..' jest.mock('../../../redux/config') jest.mock('../GeneralSettings') +jest.mock('../PrivacySettings') jest.mock('../AdvancedSettings') jest.mock('../../../organisms/AppSettings/FeatureFlags') @@ -22,6 +24,9 @@ const getDevtoolsEnabled = Config.getDevtoolsEnabled as jest.MockedFunction< const mockGeneralSettings = GeneralSettings as jest.MockedFunction< typeof GeneralSettings > +const mockPrivacySettings = PrivacySettings as jest.MockedFunction< + typeof PrivacySettings +> const mockAdvancedSettings = AdvancedSettings as jest.MockedFunction< typeof AdvancedSettings > @@ -45,6 +50,7 @@ describe('AppSettingsHeader', () => { beforeEach(() => { getDevtoolsEnabled.mockReturnValue(false) mockGeneralSettings.mockReturnValue(
Mock General Settings
) + mockPrivacySettings.mockReturnValue(
Mock Privacy Settings
) mockAdvancedSettings.mockReturnValue(
Mock Advanced Settings
) mockFeatureFlags.mockReturnValue(
Mock Feature Flags
) }) @@ -56,6 +62,7 @@ describe('AppSettingsHeader', () => { const [{ getByText }] = render('/app-settings/general') getByText('App Settings') getByText('General') + getByText('Privacy') getByText('Advanced') }) it('does not render feature flags link if dev tools disabled', () => { diff --git a/app/src/pages/AppSettings/__test__/PrivacySettings.test.tsx b/app/src/pages/AppSettings/__test__/PrivacySettings.test.tsx new file mode 100644 index 00000000000..662212c1b02 --- /dev/null +++ b/app/src/pages/AppSettings/__test__/PrivacySettings.test.tsx @@ -0,0 +1,32 @@ +import * as React from 'react' +import { MemoryRouter } from 'react-router-dom' + +import { renderWithProviders } from '@opentrons/components' + +import { i18n } from '../../../i18n' +import { PrivacySettings } from '../PrivacySettings' + +jest.mock('../../../redux/analytics') +jest.mock('../../../redux/config') + +const render = (): ReturnType => { + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + ) +} + +describe('PrivacySettings', () => { + it('renders correct title, body text, and toggle', () => { + const [{ getByText, getByRole }] = render() + getByText('Share App Analytics with Opentrons') + getByText( + 'Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.' + ) + getByRole('switch', { name: 'analytics_opt_in' }) + }) +}) diff --git a/app/src/pages/AppSettings/index.tsx b/app/src/pages/AppSettings/index.tsx index fe374381b04..cceff371bf4 100644 --- a/app/src/pages/AppSettings/index.tsx +++ b/app/src/pages/AppSettings/index.tsx @@ -16,6 +16,7 @@ import { import * as Config from '../../redux/config' import { GeneralSettings } from './GeneralSettings' +import { PrivacySettings } from './PrivacySettings' import { AdvancedSettings } from './AdvancedSettings' import { FeatureFlags } from '../../organisms/AppSettings/FeatureFlags' import { NavTab } from '../../molecules/NavTab' @@ -33,6 +34,7 @@ export function AppSettings(): JSX.Element { [K in AppSettingsTab]: JSX.Element } = { general: , + privacy: , advanced: , 'feature-flags': , } @@ -64,6 +66,7 @@ export function AppSettings(): JSX.Element { gridGap={SPACING.spacing20} > + {devToolsOn && ( +const mockRobotSettingsPrivacy = RobotSettingsPrivacy as jest.MockedFunction< + typeof RobotSettingsPrivacy +> const mockUseRobot = useRobot as jest.MockedFunction const mockGetRobotUpdateSession = getRobotUpdateSession as jest.MockedFunction< @@ -68,6 +73,9 @@ describe('RobotSettings', () => { mockRobotSettingsAdvanced.mockReturnValue(
Mock RobotSettingsAdvanced
) + mockRobotSettingsPrivacy.mockReturnValue( +
Mock RobotSettingsPrivacy
+ ) }) afterEach(() => { jest.resetAllMocks() @@ -162,4 +170,13 @@ describe('RobotSettings', () => { fireEvent.click(AdvancedTab) screen.getByText('Mock RobotSettingsAdvanced') }) + + it('renders privacy content when the privacy tab is clicked', () => { + render('/devices/otie/robot-settings/calibration') + + const PrivacyTab = screen.getByText('Privacy') + expect(screen.queryByText('Mock RobotSettingsPrivacy')).toBeFalsy() + fireEvent.click(PrivacyTab) + screen.getByText('Mock RobotSettingsPrivacy') + }) }) diff --git a/app/src/pages/Devices/RobotSettings/index.tsx b/app/src/pages/Devices/RobotSettings/index.tsx index e8e01df97ef..ea2e8cabaaa 100644 --- a/app/src/pages/Devices/RobotSettings/index.tsx +++ b/app/src/pages/Devices/RobotSettings/index.tsx @@ -33,6 +33,7 @@ import { RobotSettingsCalibration } from '../../../organisms/RobotSettingsCalibr import { RobotSettingsAdvanced } from '../../../organisms/Devices/RobotSettings/RobotSettingsAdvanced' import { RobotSettingsNetworking } from '../../../organisms/Devices/RobotSettings/RobotSettingsNetworking' import { RobotSettingsFeatureFlags } from '../../../organisms/Devices/RobotSettings/RobotSettingsFeatureFlags' +import { RobotSettingsPrivacy } from '../../../organisms/Devices/RobotSettings/RobotSettingsPrivacy' import { ReachableBanner } from '../../../organisms/Devices/ReachableBanner' import type { DesktopRouteParams, RobotSettingsTab } from '../../../App/types' @@ -42,6 +43,7 @@ export function RobotSettings(): JSX.Element | null { const { robotName, robotSettingsTab } = useParams() const robot = useRobot(robotName) const isCalibrationDisabled = robot?.status !== CONNECTABLE + const isPrivacyDisabled = robot?.status === UNREACHABLE const isNetworkingDisabled = robot?.status === UNREACHABLE const [showRobotBusyBanner, setShowRobotBusyBanner] = React.useState( false @@ -74,6 +76,7 @@ export function RobotSettings(): JSX.Element | null { /> ), 'feature-flags': , + privacy: , } const devToolsOn = useSelector(getDevtoolsEnabled) @@ -90,7 +93,8 @@ export function RobotSettings(): JSX.Element | null { robotSettingsTab === 'calibration' && isCalibrationDisabled const cannotViewFeatureFlags = robotSettingsTab === 'feature-flags' && !devToolsOn - if (cannotViewCalibration || cannotViewFeatureFlags) { + const cannotViewPrivacy = robotSettingsTab === 'privacy' && isPrivacyDisabled + if (cannotViewCalibration || cannotViewFeatureFlags || cannotViewPrivacy) { return } @@ -140,6 +144,11 @@ export function RobotSettings(): JSX.Element | null { tabName={t('networking')} disabled={isNetworkingDisabled} /> + setCurrentOption('TouchscreenBrightness')} iconName="brightness" /> + setCurrentOption('Privacy')} + iconName="privacy" + /> const mockDeviceReset = DeviceReset as jest.MockedFunction +const mockPrivacy = Privacy as jest.MockedFunction const mockRobotSystemVersion = RobotSystemVersion as jest.MockedFunction< typeof RobotSystemVersion > @@ -98,6 +101,7 @@ describe('RobotSettingsDashboard', () => { mockTouchScreenSleep.mockReturnValue(
Mock Touchscreen Sleep
) mockNetworkSettings.mockReturnValue(
Mock Network Settings
) mockDeviceReset.mockReturnValue(
Mock Device Reset
) + mockPrivacy.mockReturnValue(
Mock Privacy
) mockRobotSystemVersion.mockReturnValue(
Mock Robot System Version
) mockGetRobotSettings.mockReturnValue([ { @@ -134,6 +138,8 @@ describe('RobotSettingsDashboard', () => { getByText('Control the strip of color lights on the front of the robot.') getByText('Touchscreen Sleep') getByText('Touchscreen Brightness') + getByText('Privacy') + getByText('Choose what data to share with Opentrons.') getByText('Device Reset') getByText('Update Channel') getByText('Apply Labware Offsets') @@ -195,6 +201,13 @@ describe('RobotSettingsDashboard', () => { getByText('Mock Touchscreen Brightness') }) + it('should render component when tapping privacy', () => { + const [{ getByText }] = render() + const button = getByText('Privacy') + fireEvent.click(button) + getByText('Mock Privacy') + }) + it('should render component when tapping device rest', () => { const [{ getByText }] = render() const button = getByText('Device Reset') diff --git a/app/src/pages/RobotSettingsDashboard/index.tsx b/app/src/pages/RobotSettingsDashboard/index.tsx index 47a09f69c61..29473a807b3 100644 --- a/app/src/pages/RobotSettingsDashboard/index.tsx +++ b/app/src/pages/RobotSettingsDashboard/index.tsx @@ -9,6 +9,7 @@ import { TouchscreenBrightness, TouchScreenSleep, NetworkSettings, + Privacy, RobotName, RobotSettingsJoinOtherNetwork, RobotSettingsSelectAuthenticationType, @@ -46,6 +47,7 @@ export type SettingOption = | 'TouchscreenSleep' | 'TouchscreenBrightness' | 'TextSize' + | 'Privacy' | 'DeviceReset' | 'UpdateChannel' | 'EthernetConnectionDetails' @@ -148,6 +150,10 @@ export function RobotSettingsDashboard(): JSX.Element { return case 'TouchscreenBrightness': return + case 'Privacy': + return ( + + ) // TODO(bh, 2023-6-9): TextSize does not appear to be active in the app yet // case 'TextSize': // return