diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index d28f82b3b36..95a2abd1afd 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -27,6 +27,7 @@ import { ConnectViaWifi } from '../pages/ConnectViaWifi' import { EmergencyStop } from '../pages/EmergencyStop' import { NameRobot } from '../pages/NameRobot' import { NetworkSetupMenu } from '../pages/NetworkSetupMenu' +import { PrivacyPolicy } from '../pages/PrivacyPolicy' import { ProtocolSetup } from '../pages/ProtocolSetup' import { RobotDashboard } from '../pages/RobotDashboard' import { RobotSettingsDashboard } from '../pages/RobotSettingsDashboard' @@ -195,6 +196,12 @@ export const onDeviceDisplayRoutes: RouteProps[] = [ name: 'Emergency Stop', path: '/emergency-stop', }, + { + Component: PrivacyPolicy, + exact: true, + name: 'Privacy Policy', + path: '/privacy-policy', + }, { Component: DeckConfigurationEditor, exact: true, diff --git a/app/src/assets/images/on-device-display/privacy_policy_qrcode.png b/app/src/assets/images/on-device-display/privacy_policy_qrcode.png new file mode 100644 index 00000000000..3de0cbe2158 Binary files /dev/null and b/app/src/assets/images/on-device-display/privacy_policy_qrcode.png differ diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 70da075cbc2..7223c34972e 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -3,7 +3,9 @@ "about_calibration_description": "For the robot to move accurately and precisely, you need to calibrate it. Positional calibration happens in three parts: deck calibration, pipette offset calibration and tip length calibration.", "about_calibration_description_ot3": "For the robot to move accurately and precisely, you need to calibrate it. Pipette and gripper calibration is an automated process that uses a calibration probe or pin.After calibration is complete, you can save the calibration data to your computer as a JSON file.", "about_calibration_title": "About Calibration", + "acknowledge_privacy_policy": "Acknowledge Privacy Policy", "advanced": "Advanced", + "agree": "I agree", "alpha_description": "Warning: alpha releases are feature-complete but may contain significant bugs.", "alternative_security_types": "Alternative security types", "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", @@ -206,6 +208,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_policy_description": "By proceeding you are agreeing to share robot usage data. Opentrons uses this data to improve our products and services.To read more about our data collection policies, visit our Privacy Policy.", "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/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx b/app/src/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx index b8a7123603f..be5fe3530b7 100644 --- a/app/src/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx +++ b/app/src/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx @@ -34,7 +34,7 @@ export function ConfirmRobotName({ } return ( <> - + - + - + { render() screen.getByTestId('StepMeter_StepMeterContainer') const bar = screen.getByTestId('StepMeter_StepMeterBar') - expect(bar).toHaveStyle('width: 33.33333333333333%') + expect(bar).toHaveStyle('width: 20%') }) it('should render Searching for networks', () => { diff --git a/app/src/pages/ConnectViaWifi/index.tsx b/app/src/pages/ConnectViaWifi/index.tsx index fb3fcc98077..97792806512 100644 --- a/app/src/pages/ConnectViaWifi/index.tsx +++ b/app/src/pages/ConnectViaWifi/index.tsx @@ -110,7 +110,7 @@ export function ConnectViaWifi(): JSX.Element { return ( <> - + disengaged @@ -38,7 +45,7 @@ export function EmergencyStop(): JSX.Element { return ( <> - + history.push('/robot-settings/rename-robot')} + onClick={() => { + seenOptIn && optedIn + ? history.push('/robot-settings/rename-robot') + : history.push('/privacy-policy') + }} /> diff --git a/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx b/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx index b07def9bebb..e58ffcd56d7 100644 --- a/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx +++ b/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx @@ -150,11 +150,4 @@ describe('NameRobot', () => { screen.getByText('Enter up to 17 characters (letters and numbers only)') screen.getByText('Confirm') }) - - it('should call a mock function when tapping back button', () => { - mockuseIsUnboxingFlowOngoing.mockReturnValue(false) - render() - fireEvent.click(screen.getByTestId('name_back_button')) - expect(mockPush).toHaveBeenCalledWith('/robot-settings') - }) }) diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/NameRobot/index.tsx index 2b65755b8fc..a8d4a0ebdba 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/NameRobot/index.tsx @@ -19,7 +19,6 @@ import { COLORS, TYPOGRAPHY, Icon, - Btn, } from '@opentrons/components' import { useUpdateRobotNameMutation } from '@opentrons/react-api-client' @@ -153,7 +152,7 @@ export function NameRobot(): JSX.Element { ) : ( <> {isUnboxingFlowOngoing ? ( - + ) : null} - - { - if (isUnboxingFlowOngoing) { - history.push('/emergency-stop') - } else { - history.push('/robot-settings') - } - }} - > - - - + {isUnboxingFlowOngoing diff --git a/app/src/pages/NetworkSetupMenu/index.tsx b/app/src/pages/NetworkSetupMenu/index.tsx index fe245bf22f5..6d668131862 100644 --- a/app/src/pages/NetworkSetupMenu/index.tsx +++ b/app/src/pages/NetworkSetupMenu/index.tsx @@ -44,7 +44,7 @@ export function NetworkSetupMenu(): JSX.Element { return ( <> - + () + const isUnboxingFlowOngoing = useIsUnboxingFlowOngoing() + const seenOptedIn = useSelector(getAnalyticsOptInSeen) + const optedIn = useSelector(getAnalyticsOptedIn) + + const handleAgree = (): void => { + dispatch(setAnalyticsOptInSeen()) + dispatch(toggleAnalyticsOptedIn()) + } + + if (seenOptedIn && optedIn) { + if (isUnboxingFlowOngoing) { + history.push('/robot-settings/rename-robot') + } else { + history.push('/dashboard') + } + } + + return ( + <> + {isUnboxingFlowOngoing ? ( + + ) : null} + + + + {t('acknowledge_privacy_policy')} + + + + + }} + /> + + {PRIVACY_POLICY_URL} + + + + {IMG_ALT} + + + + + + + + ) +} diff --git a/app/src/pages/RobotDashboard/AnalyticsOptInModal.tsx b/app/src/pages/RobotDashboard/AnalyticsOptInModal.tsx deleted file mode 100644 index ae3d6a112f5..00000000000 --- a/app/src/pages/RobotDashboard/AnalyticsOptInModal.tsx +++ /dev/null @@ -1,91 +0,0 @@ -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() - - 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 ( - - - - - {t('opt_in_description')} - - - - - - - - - ) -} diff --git a/app/src/pages/RobotDashboard/WelcomeModal.tsx b/app/src/pages/RobotDashboard/WelcomeModal.tsx index 23777645a81..32ba09d7ec0 100644 --- a/app/src/pages/RobotDashboard/WelcomeModal.tsx +++ b/app/src/pages/RobotDashboard/WelcomeModal.tsx @@ -1,5 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' +import { updateConfigValue } from '../../redux/config' import { COLORS, @@ -18,19 +20,19 @@ import { Modal } from '../../molecules/Modal' import welcomeModalImage from '../../assets/images/on-device-display/welcome_dashboard_modal.png' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data' +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', 'shared']) const { createLiveCommand } = useCreateLiveCommandMutation() + const dispatch = useDispatch() const animationCommand: SetStatusBarCreateCommand = { commandType: 'setStatusBar', params: { animation: 'disco' }, @@ -46,8 +48,13 @@ export function WelcomeModal({ } const handleCloseModal = (): void => { + dispatch( + updateConfigValue( + 'onDeviceDisplaySettings.unfinishedUnboxingFlowRoute', + null + ) + ) setShowWelcomeModal(false) - setShowAnalyticsOptInModal(true) } React.useEffect(startDiscoAnimation, []) diff --git a/app/src/pages/RobotDashboard/__tests__/AnalyticsOptInModal.test.tsx b/app/src/pages/RobotDashboard/__tests__/AnalyticsOptInModal.test.tsx deleted file mode 100644 index 09e521b43da..00000000000 --- a/app/src/pages/RobotDashboard/__tests__/AnalyticsOptInModal.test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from 'react' -import { fireEvent } from '@testing-library/react' - -import { renderWithProviders } from '@opentrons/components' - -import { i18n } from '../../../i18n' -import { updateConfigValue } from '../../../redux/config' -import { getLocalRobot } from '../../../redux/discovery' -import { updateSetting } from '../../../redux/robot-settings' -import { AnalyticsOptInModal } from '../AnalyticsOptInModal' - -import type { DiscoveredRobot } from '../../../redux/discovery/types' - -jest.mock('../../../redux/config') -jest.mock('../../../redux/discovery') -jest.mock('../../../redux/robot-settings') - -const mockUpdateConfigValue = updateConfigValue as jest.MockedFunction< - typeof updateConfigValue -> -const mockGetLocalRobot = getLocalRobot as jest.MockedFunction< - typeof getLocalRobot -> -const mockUpdateSetting = updateSetting as jest.MockedFunction< - typeof updateSetting -> - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - }) -} - -describe('AnalyticsOptInModal', () => { - let props: React.ComponentProps - - beforeEach(() => { - props = { - setShowAnalyticsOptInModal: jest.fn(), - } - mockGetLocalRobot.mockReturnValue({ name: 'Otie' } as DiscoveredRobot) - }) - - it('should render text and button', () => { - const [{ getByText }] = render(props) - - getByText('Want to help out Opentrons?') - getByText( - 'Automatically send us anonymous diagnostics and usage data. We only use this information to improve our products.' - ) - getByText('Opt out') - getByText('Opt in') - }) - - it('should call a mock function when tapping opt out button', () => { - const [{ getByText }] = render(props) - fireEvent.click(getByText('Opt out')) - - expect(mockUpdateConfigValue).toHaveBeenCalledWith( - 'analytics.optedIn', - false - ) - expect(mockUpdateSetting).toHaveBeenCalledWith( - 'Otie', - 'disableLogAggregation', - true - ) - expect(props.setShowAnalyticsOptInModal).toHaveBeenCalled() - }) - - it('should call a mock function when tapping out in button', () => { - const [{ getByText }] = render(props) - fireEvent.click(getByText('Opt in')) - - expect(mockUpdateConfigValue).toHaveBeenCalledWith( - 'analytics.optedIn', - true - ) - expect(mockUpdateSetting).toHaveBeenCalledWith( - 'Otie', - 'disableLogAggregation', - true - ) - expect(props.setShowAnalyticsOptInModal).toHaveBeenCalled() - }) -}) diff --git a/app/src/pages/RobotDashboard/__tests__/WelcomeModal.test.tsx b/app/src/pages/RobotDashboard/__tests__/WelcomeModal.test.tsx index 77ca462c490..a035398aa18 100644 --- a/app/src/pages/RobotDashboard/__tests__/WelcomeModal.test.tsx +++ b/app/src/pages/RobotDashboard/__tests__/WelcomeModal.test.tsx @@ -33,7 +33,6 @@ describe('WelcomeModal', () => { mockCreateLiveCommand = jest.fn() mockCreateLiveCommand.mockResolvedValue(null) props = { - setShowAnalyticsOptInModal: jest.fn(), setShowWelcomeModal: mockFunc, } mockUseCreateLiveCommandMutation.mockReturnValue({ @@ -66,6 +65,5 @@ describe('WelcomeModal', () => { const [{ getByText }] = render(props) fireEvent.click(getByText('Next')) expect(props.setShowWelcomeModal).toHaveBeenCalled() - expect(props.setShowAnalyticsOptInModal).toHaveBeenCalled() }) }) diff --git a/app/src/pages/RobotDashboard/index.tsx b/app/src/pages/RobotDashboard/index.tsx index 3913147db07..346129757c8 100644 --- a/app/src/pages/RobotDashboard/index.tsx +++ b/app/src/pages/RobotDashboard/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' import { COLORS, @@ -18,7 +19,10 @@ import { RecentRunProtocolCarousel, } from '../../organisms/OnDeviceDisplay/RobotDashboard' import { getOnDeviceDisplaySettings } from '../../redux/config' -import { AnalyticsOptInModal } from './AnalyticsOptInModal' +import { + getAnalyticsOptInSeen, + getAnalyticsOptedIn, +} from '../../redux/analytics' import { WelcomeModal } from './WelcomeModal' import { RunData } from '@opentrons/api-client' import { ServerInitializing } from '../../organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing' @@ -39,10 +43,14 @@ export function RobotDashboard(): JSX.Element { const [showWelcomeModal, setShowWelcomeModal] = React.useState( unfinishedUnboxingFlowRoute !== null ) - const [ - showAnalyticsOptInModal, - setShowAnalyticsOptInModal, - ] = React.useState(false) + + const seen = useSelector(getAnalyticsOptInSeen) + const hasOptedIn = useSelector(getAnalyticsOptedIn) + const history = useHistory() + + if (!seen || !hasOptedIn) { + history.push('/privacy-policy') + } const recentRunsOfUniqueProtocols = (allRunsQueryData?.data ?? []) .reverse() // newest runs first @@ -89,15 +97,7 @@ export function RobotDashboard(): JSX.Element { gridGap={SPACING.spacing16} > {showWelcomeModal ? ( - - ) : null} - {showAnalyticsOptInModal ? ( - + ) : null} {contents}