Skip to content

Commit

Permalink
feat(app): add ODD analytics setting screen (#13525)
Browse files Browse the repository at this point in the history
adds a robot settings privacy screen to change analytics settings and an opt in modal at the end of
unboxing flow to follow the welcome modal

closes RAUT-660
  • Loading branch information
brenthagen authored Sep 12, 2023
1 parent 6481d14 commit 9cd9b6b
Show file tree
Hide file tree
Showing 16 changed files with 439 additions and 72 deletions.
15 changes: 11 additions & 4 deletions app/src/assets/localization/en/app_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"cal_block": "Always use calibration block to calibrate",
"change_folder_button": "Change labware source folder",
"channel": "Channel",
"choose_what_data_to_share": "Choose what data to share with Opentrons.",
"clear_confirm": "Clear unavailable robots",
"clear_robots_button": "Clear unavailable robots list",
"clear_robots_description": "Clear the list of unavailable robots on the Devices page. This action cannot be undone.",
Expand Down Expand Up @@ -56,6 +57,10 @@
"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_description": "Automatically send us anonymous diagnostics and usage data. We only use this information to improve our products.",
"opt_in": "Opt in",
"opt_out": "Opt out",
"ot2_advanced_settings": "OT-2 Advanced Settings",
"override_path": "override path",
"override_path_to_python": "Override Path to Python",
Expand All @@ -75,9 +80,10 @@
"setup_connection": "Set up connection",
"share_app_analytics": "Share App Analytics with Opentrons",
"share_app_analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.",
"share_app_analytics_description_short": "Share anonymous app usage data with Opentrons.",
"share_app_analytics_short": "Share App Analytics",
"share_robot_analytics": "Share Robot Analytics with Opentrons",
"share_display_usage_description": "Data on how you interact with the touchscreen on Flex.",
"share_display_usage": "Share display usage",
"share_robot_logs_description": "Data on actions the robot does, like running protocols.",
"share_robot_logs": "Share robot logs",
"show_labware_offset_snippets": "Show Labware Offset data code snippets",
"show_labware_offset_snippets_description": "Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.",
"software_update_available": "Software Update Available",
Expand Down Expand Up @@ -107,5 +113,6 @@
"view_issue_tracker": "View Opentrons issue tracker",
"view_release_notes": "View full Opentrons release notes",
"view_software_update": "View software update",
"view_update": "View Update"
"view_update": "View Update",
"want_to_help_out": "Want to help out Opentrons?"
}
1 change: 0 additions & 1 deletion app/src/assets/localization/en/device_details.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
"firmware_update_failed": "Failed to update module firmware",
"firmware_update_installation_successful": "Installation successful",
"firmware_update_occurring": "Firmware update in progress...",
"got_it": "Got it",
"have_not_run": "No recent runs",
"have_not_run_description": "After you run some protocols, they will appear here.",
"heater": "Heater",
Expand Down
94 changes: 94 additions & 0 deletions app/src/organisms/RobotSettingsDashboard/Privacy.tsx
Original file line number Diff line number Diff line change
@@ -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/OnDeviceDisplay/RobotDashboard/AnalyticsOptInModal'
import { RobotSettingButton } from '../../pages/OnDeviceDisplay/RobotSettingsDashboard/RobotSettingButton'
import { OnOffToggle } from '../../pages/OnDeviceDisplay/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/OnDeviceDisplay/RobotSettingsDashboard'

interface PrivacyProps {
robotName: string
setCurrentOption: SetSettingOption
}

export function Privacy({
robotName,
setCurrentOption,
}: PrivacyProps): JSX.Element {
const { t } = useTranslation('app_settings')
const dispatch = useDispatch<Dispatch>()

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 (
<Flex flexDirection={DIRECTION_COLUMN}>
<ChildNavigation
header={t('app_settings:privacy')}
onClickBack={() => setCurrentOption(null)}
/>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing24}
paddingX={SPACING.spacing40}
marginTop="7.75rem"
>
<StyledText
fontSize={TYPOGRAPHY.fontSize28}
lineHeight={TYPOGRAPHY.lineHeight36}
fontWeight={TYPOGRAPHY.fontWeightRegular}
>
{t('opentrons_cares_about_privacy')}
</StyledText>
<Flex flexDirection={DIRECTION_COLUMN}>
<RobotSettingButton
settingName={t('share_robot_logs')}
settingInfo={t('share_robot_logs_description')}
dataTestId="RobotSettingButton_share_analytics"
rightElement={<OnOffToggle isOn={!isRobotAnalyticsDisabled} />}
onClick={() =>
dispatch(
updateSetting(
robotName,
ROBOT_ANALYTICS_SETTING_ID,
!isRobotAnalyticsDisabled
)
)
}
/>
<RobotSettingButton
settingName={t('share_display_usage')}
settingInfo={t('share_display_usage_description')}
dataTestId="RobotSettingButton_share_app_analytics"
rightElement={<OnOffToggle isOn={appAnalyticsOptedIn} />}
onClick={() => dispatch(toggleAnalyticsOptedIn())}
/>
</Flex>
</Flex>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from '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<typeof Privacy>) => {
return renderWithProviders(<Privacy {...props} />, {
i18nInstance: i18n,
})
}

describe('Privacy', () => {
let props: React.ComponentProps<typeof Privacy>
beforeEach(() => {
props = {
robotName: 'Otie',
setCurrentOption: jest.fn(),
}
mockGetRobotSettings.mockReturnValue([])
})

afterEach(() => {
jest.clearAllMocks()
})

it('should render text and buttons', () => {
const [{ getByText }] = render(props)
getByText('Privacy')
getByText(
'Opentrons cares about your privacy. We anonymize all data and only use it to improve our products.'
)
getByText('Share robot logs')
getByText('Data on actions the robot does, like running protocols.')
getByText('Share display usage')
getByText('Data on how you interact with the touchscreen on Flex.')
})

it('should toggle display usage sharing on click', () => {
const [{ getByText }] = render(props)

getByText('Share display usage').click()
expect(mockToggleAnalyticsOptedIn).toBeCalled()
})

it('should toggle robot logs sharing on click', () => {
const [{ getByText }] = render(props)

getByText('Share robot logs').click()
expect(mockUpdateSetting).toBeCalledWith(
'Otie',
'disableLogAggregation',
true
)
})
})
1 change: 1 addition & 0 deletions app/src/organisms/RobotSettingsDashboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'

import {
Flex,
COLORS,
DIRECTION_COLUMN,
DIRECTION_ROW,
SPACING,
} from '@opentrons/components'

import { SmallButton } from '../../../atoms/buttons'
import { StyledText } from '../../../atoms/text'
import { Modal } from '../../../molecules/Modal'
import { updateConfigValue } from '../../../redux/config'
import { getLocalRobot } from '../../../redux/discovery'
import { updateSetting } from '../../../redux/robot-settings'

import type { Dispatch } from '../../../redux/types'

export const ROBOT_ANALYTICS_SETTING_ID = 'disableLogAggregation'

interface AnalyticsOptInModalProps {
setShowAnalyticsOptInModal: (showAnalyticsOptInModal: boolean) => void
}

export function AnalyticsOptInModal({
setShowAnalyticsOptInModal,
}: AnalyticsOptInModalProps): JSX.Element {
const { t } = useTranslation(['app_settings', 'shared'])
const dispatch = useDispatch<Dispatch>()

const localRobot = useSelector(getLocalRobot)
const robotName = localRobot?.name != null ? localRobot.name : 'no name'

const handleCloseModal = (): void => {
dispatch(
updateConfigValue(
'onDeviceDisplaySettings.unfinishedUnboxingFlowRoute',
null
)
)
setShowAnalyticsOptInModal(false)
}

const handleOptIn = (): void => {
dispatch(updateSetting(robotName, ROBOT_ANALYTICS_SETTING_ID, false))
dispatch(updateConfigValue('analytics.optedIn', true))
handleCloseModal()
}

const handleOptOut = (): void => {
dispatch(updateSetting(robotName, ROBOT_ANALYTICS_SETTING_ID, true))
dispatch(updateConfigValue('analytics.optedIn', false))
handleCloseModal()
}

return (
<Modal modalSize="medium" header={{ title: t('want_to_help_out') }}>
<Flex flexDirection={DIRECTION_COLUMN}>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing12}
paddingBottom={SPACING.spacing32}
>
<StyledText as="p" color={COLORS.darkBlack90}>
{t('opt_in_description')}
</StyledText>
</Flex>
<Flex
flexDirection={DIRECTION_ROW}
gridGap={SPACING.spacing8}
width="100%"
>
<SmallButton
flex="1"
buttonText={t('opt_out')}
buttonType="secondary"
onClick={handleOptOut}
/>
<SmallButton
flex="1"
buttonText={t('opt_in')}
onClick={handleOptIn}
/>
</Flex>
</Flex>
</Modal>
)
}
17 changes: 5 additions & 12 deletions app/src/pages/OnDeviceDisplay/RobotDashboard/WelcomeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'

import {
Flex,
Expand All @@ -15,22 +14,21 @@ import { useCreateLiveCommandMutation } from '@opentrons/react-api-client'
import { StyledText } from '../../../atoms/text'
import { SmallButton } from '../../../atoms/buttons'
import { Modal } from '../../../molecules/Modal'
import { updateConfigValue } from '../../../redux/config'

import welcomeModalImage from '../../../assets/images/on-device-display/welcome_dashboard_modal.png'

import type { SetStatusBarCreateCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command/incidental'
import type { Dispatch } from '../../../redux/types'

interface WelcomeModalProps {
setShowAnalyticsOptInModal: (showAnalyticsOptInModal: boolean) => void
setShowWelcomeModal: (showWelcomeModal: boolean) => void
}

export function WelcomeModal({
setShowAnalyticsOptInModal,
setShowWelcomeModal,
}: WelcomeModalProps): JSX.Element {
const { t } = useTranslation('device_details')
const dispatch = useDispatch<Dispatch>()
const { t } = useTranslation(['device_details', 'shared'])

const { createLiveCommand } = useCreateLiveCommandMutation()
const animationCommand: SetStatusBarCreateCommand = {
Expand All @@ -48,13 +46,8 @@ export function WelcomeModal({
}

const handleCloseModal = (): void => {
dispatch(
updateConfigValue(
'onDeviceDisplaySettings.unfinishedUnboxingFlowRoute',
null
)
)
setShowWelcomeModal(false)
setShowAnalyticsOptInModal(true)
}

React.useEffect(startDiscoAnimation, [])
Expand Down Expand Up @@ -90,7 +83,7 @@ export function WelcomeModal({
{t('welcome_modal_description')}
</StyledText>
</Flex>
<SmallButton buttonText={t('got_it')} onClick={handleCloseModal} />
<SmallButton buttonText={t('shared:next')} onClick={handleCloseModal} />
</Flex>
</Modal>
)
Expand Down
Loading

0 comments on commit 9cd9b6b

Please sign in to comment.