diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 13a7091a03bc..745e60244e44 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1291,6 +1291,18 @@ "dataBackupSeemsCorrupt": { "message": "Can not restore your data. The file appears to be corrupt." }, + "dataCollectionForMarketing": { + "message": "Data collection for marketing" + }, + "dataCollectionForMarketingDescription": { + "message": "We'll use MetaMetrics to learn how you interact with our marketing communications. We may share relevant news (like product features and other materials)." + }, + "dataCollectionWarningPopoverButton": { + "message": "Okay" + }, + "dataCollectionWarningPopoverDescription": { + "message": "You turned off data collection for our marketing purposes. This only applies to this device. If you use MetaMask on other devices, make sure to opt out there as well." + }, "dataHex": { "message": "Hex" }, @@ -3321,6 +3333,37 @@ "on": { "message": "On" }, + "onboardedMetametricsAccept": { + "message": "I agree" + }, + "onboardedMetametricsDisagree": { + "message": "No thanks" + }, + "onboardedMetametricsKey1": { + "message": "Latest developments" + }, + "onboardedMetametricsKey2": { + "message": "Product features" + }, + "onboardedMetametricsKey3": { + "message": "Other relevant promotional materials" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "In addition to $1, we'd like to use data to understand how you interact with marketing communications.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "This helps us personalize what we share with you, like:" + }, + "onboardedMetametricsParagraph3": { + "message": "Remember, we never sell the data you provide and you can opt out any time." + }, + "onboardedMetametricsTitle": { + "message": "Help us enhance your experience" + }, "onboarding": { "message": "Onboarding" }, @@ -3437,6 +3480,9 @@ "onboardingMetametricsTitle": { "message": "Help us improve MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "We’ll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features)." + }, "onboardingPinExtensionBillboardAccess": { "message": "Full access" }, diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index c15208435702..e178d75c2754 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -154,6 +154,7 @@ export default class MetaMetricsController { this.store = new ObservableStore({ participateInMetaMetrics: null, metaMetricsId: null, + dataCollectionForMarketing: null, eventsBeforeMetricsOptIn: [], traits: {}, previousUserTraits: {}, @@ -471,6 +472,12 @@ export default class MetaMetricsController { return metaMetricsId; } + setDataCollectionForMarketing(dataCollectionForMarketing) { + const { metaMetricsId } = this.state; + this.store.updateState({ dataCollectionForMarketing }); + return metaMetricsId; + } + get state() { return this.store.getState(); } @@ -824,6 +831,10 @@ export default class MetaMetricsController { metamaskState.securityAlertsEnabled ? ['blockaid'] : [], [MetaMetricsUserTrait.PetnameAddressCount]: this._getPetnameAddressCount(metamaskState), + [MetaMetricsUserTrait.IsMetricsOptedIn]: + metamaskState.participateInMetaMetrics, + [MetaMetricsUserTrait.HasMarketingConsent]: + metamaskState.dataCollectionForMarketing, }; if (!previousUserTraits) { diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 179df5ba8668..94843c2e9a98 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -169,6 +169,7 @@ export const SENTRY_BACKGROUND_STATE = { previousUserTraits: false, segmentApiCalls: false, traits: false, + dataCollectionForMarketing: false, }, NameController: { names: false, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index fdfddfd3689b..b6a520fd8d1b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3067,6 +3067,10 @@ export default class MetamaskController extends EventEmitter { metaMetricsController.setParticipateInMetaMetrics.bind( metaMetricsController, ), + setDataCollectionForMarketing: + metaMetricsController.setDataCollectionForMarketing.bind( + metaMetricsController, + ), setCurrentLocale: preferencesController.setCurrentLocale.bind( preferencesController, ), diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 0cec97a26f31..b7110c67c3e6 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -383,6 +383,14 @@ export type MetaMetricsUserTraits = { }; export enum MetaMetricsUserTrait { + /** + * Identifies if the user has opted in for MetaMetrics + */ + IsMetricsOptedIn = 'is_metrics_opted_in', + /** + * Identifies is the user has given marketing consent + */ + HasMarketingConsent = 'has_marketing_consent', /** * Identified when the user adds or modifies addresses in the address book. */ @@ -505,6 +513,7 @@ export enum MetaMetricsEventName { AccountRenamed = 'Account Renamed', ActivityDetailsOpened = 'Activity Details Opened', ActivityDetailsClosed = 'Activity Details Closed', + AnalyticsPreferenceSelected = 'Analytics Preference Selected', AppInstalled = 'App Installed', AppUnlocked = 'App Unlocked', AppUnlockedFailed = 'App Unlocked Failed', diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 1c6a2d77433b..98413671caca 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -139,6 +139,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { fragments: {}, metaMetricsId: null, participateInMetaMetrics: false, + dataCollectionForMarketing: false, traits: {}, }, NetworkController: { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index d7f940573fe8..e6a62e19d994 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -103,6 +103,7 @@ "traits": "object", "previousUserTraits": "object", "fragments": "object", + "dataCollectionForMarketing": "boolean", "segmentApiCalls": "object" }, "MetamaskNotificationsController": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 91ab85dc3fb0..11d99c406b31 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -40,6 +40,7 @@ "knownMethodData": "object", "use4ByteResolution": true, "participateInMetaMetrics": true, + "dataCollectionForMarketing": "boolean", "nextNonce": null, "currencyRates": { "ETH": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index c191ad287761..60379a1d0811 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -69,6 +69,7 @@ "fragments": "object", "metaMetricsId": "fake-metrics-id", "participateInMetaMetrics": true, + "dataCollectionForMarketing": "boolean", "traits": "object" }, "NetworkController": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 84ca7297d46c..48392c6b4db3 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -69,6 +69,7 @@ "fragments": "object", "metaMetricsId": "fake-metrics-id", "participateInMetaMetrics": true, + "dataCollectionForMarketing": "boolean", "traits": "object" }, "NetworkController": { diff --git a/ui/components/ui/popover/index.scss b/ui/components/ui/popover/index.scss index c657ef2f3764..a935508e7476 100644 --- a/ui/components/ui/popover/index.scss +++ b/ui/components/ui/popover/index.scss @@ -28,6 +28,10 @@ &__title--center { flex: 1; } + + &__title-wrap { + white-space: normal; + } } &-bg { diff --git a/ui/components/ui/popover/popover.component.js b/ui/components/ui/popover/popover.component.js index 261883c5666c..385f2896bb15 100644 --- a/ui/components/ui/popover/popover.component.js +++ b/ui/components/ui/popover/popover.component.js @@ -75,6 +75,7 @@ const Popover = ({ showScrollDown, onScrollDownButtonClick, centerTitle, + wrapTitle, headerProps = defaultHeaderProps, contentProps = defaultContentProps, footerProps = defaultFooterProps, @@ -106,6 +107,7 @@ const Popover = ({ ) : null} { + const { t } = this.context; + const { setDataCollectionForMarketing } = this.props; + + const handleClose = () => { + setDataCollectionForMarketing(false); + this.context.trackEvent({ + category: MetaMetricsEventCategory.Home, + event: MetaMetricsEventName.AnalyticsPreferenceSelected, + properties: { + has_marketing_consent: false, + location: 'marketing_consent_modal', + }, + }); + }; + + const handleConsent = (consent) => { + setDataCollectionForMarketing(consent); + this.context.trackEvent({ + category: MetaMetricsEventCategory.Home, + event: MetaMetricsEventName.AnalyticsPreferenceSelected, + properties: { + has_marketing_consent: consent, + location: 'marketing_consent_modal', + }, + }); + }; + + return ( + + + + + {t('onboardedMetametricsTitle')} + + + + + {t('onboardedMetametricsParagraph1', [ + + {t('onboardedMetametricsLink')} + , + ])} + + {t('onboardedMetametricsParagraph2')} +
    +
  • {t('onboardedMetametricsKey1')}
  • +
  • {t('onboardedMetametricsKey2')}
  • +
  • {t('onboardedMetametricsKey3')}
  • +
+ {t('onboardedMetametricsParagraph3')} +
+
+ + + + + + +
+
+ ); + }; + renderPopover = () => { const { setConnectedStatusPopoverHasBeenShown } = this.props; const { t } = this.context; @@ -809,6 +917,8 @@ export default class Home extends PureComponent { useExternalServices, setBasicFunctionalityModalOpen, forgottenPassword, + participateInMetaMetrics, + dataCollectionForMarketing, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) connectedStatusPopoverHasBeenShown, isPopup, @@ -871,6 +981,10 @@ export default class Home extends PureComponent { exact />
+ {dataCollectionForMarketing === null && + participateInMetaMetrics === true + ? this.renderOnboardingPopover() + : null} { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) } diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index f5aaa584a4b5..91f37b9aff03 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -71,6 +71,7 @@ import { setNewTokensImported, setActiveNetwork, setNewTokensImportedError, + setDataCollectionForMarketing, setShowTokenAutodetectModal, setShowTokenAutodetectModalOnUpgrade, } from '../../store/actions'; @@ -103,6 +104,8 @@ const mapStateToProps = (state) => { connectedStatusPopoverHasBeenShown, defaultHomeActiveTabName, swapsState, + dataCollectionForMarketing, + participateInMetaMetrics, firstTimeFlowType, completedOnboarding, } = metamask; @@ -166,9 +169,11 @@ const mapStateToProps = (state) => { shouldShowSeedPhraseReminder: getShouldShowSeedPhraseReminder(state), isPopup, isNotification, + dataCollectionForMarketing, selectedAddress, firstPermissionsRequestId, totalUnapprovedCount, + participateInMetaMetrics, hasApprovalFlows: getApprovalFlows(state)?.length > 0, connectedStatusPopoverHasBeenShown, defaultHomeActiveTabName, @@ -220,6 +225,8 @@ const mapDispatchToProps = (dispatch) => { ///: END:ONLY_INCLUDE_IF return { + setDataCollectionForMarketing: (val) => + dispatch(setDataCollectionForMarketing(val)), closeNotificationPopup: () => closeNotificationPopup(), setConnectedStatusPopoverHasBeenShown: () => dispatch(setConnectedStatusPopoverHasBeenShown()), diff --git a/ui/pages/home/index.scss b/ui/pages/home/index.scss index 471a3b35d7fa..03b4cd5d7cf9 100644 --- a/ui/pages/home/index.scss +++ b/ui/pages/home/index.scss @@ -1,6 +1,14 @@ @use "design-system"; .home { + &__onboarding_list { + list-style: initial; + margin-inline-start: 20px; + display: flex; + flex-direction: column; + gap: 10px; + } + &__container { display: flex; min-height: 100%; diff --git a/ui/pages/onboarding-flow/metametrics/__snapshots__/metametrics.test.js.snap b/ui/pages/onboarding-flow/metametrics/__snapshots__/metametrics.test.js.snap index 8425fda35aa4..f669f81f59d7 100644 --- a/ui/pages/onboarding-flow/metametrics/__snapshots__/metametrics.test.js.snap +++ b/ui/pages/onboarding-flow/metametrics/__snapshots__/metametrics.test.js.snap @@ -150,3 +150,154 @@ exports[`Onboarding Metametrics Component should match snapshot 1`] = `
`; + +exports[`Onboarding Metametrics Component should match snapshot after new policy date 1`] = ` +
+
+

+ Help us improve MetaMask +

+

+ MetaMask would like to gather usage data to better understand how our users interact with MetaMask. This data will be used to provide the service, which includes improving the service based on your use. +

+

+ MetaMask will... +

+
    +
  • + + Always allow you to opt-out via Settings +
  • +
  • + + Send anonymized click and pageview events +
  • +
  • +
    + + + + + + Never + + collect information we don’t need to provide the service (such as keys, addresses, transaction hashes, or balances) + + +
    +
  • +
  • +
    + + + + + + Never + + collect your full IP address* + + +
    +
  • +
  • +
    + + + + + + Never + + sell data. Ever! + + +
    + +
  • +
+
+ This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. +
+
+ + + * When you use Infura as your default RPC provider in MetaMask, Infura will collect your IP address and your Ethereum wallet address when you send a transaction. We don’t store this information in a way that allows our systems to associate those two pieces of data. For more information on how MetaMask and Infura interact from a data collection perspective, see our update + + here + + . For more information on our privacy practices in general, see our + + Privacy Policy here + + . + + +
+
+ + +
+
+
+`; diff --git a/ui/pages/onboarding-flow/metametrics/metametrics.js b/ui/pages/onboarding-flow/metametrics/metametrics.js index 60cc82975ae2..f874c99ca5e0 100644 --- a/ui/pages/onboarding-flow/metametrics/metametrics.js +++ b/ui/pages/onboarding-flow/metametrics/metametrics.js @@ -11,8 +11,13 @@ import { } from '../../../helpers/constants/design-system'; import Button from '../../../components/ui/button'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { setParticipateInMetaMetrics } from '../../../store/actions'; import { + setParticipateInMetaMetrics, + setDataCollectionForMarketing, +} from '../../../store/actions'; +import { + getParticipateInMetaMetrics, + getDataCollectionForMarketing, getFirstTimeFlowType, getFirstTimeFlowTypeRouteAfterMetaMetricsOptIn, } from '../../../selectors'; @@ -25,6 +30,7 @@ import { import { MetaMetricsContext } from '../../../contexts/metametrics'; import { + Checkbox, Icon, IconName, IconSize, @@ -45,9 +51,16 @@ export default function OnboardingMetametrics() { const nextRoute = useSelector(getFirstTimeFlowTypeRouteAfterMetaMetricsOptIn); const firstTimeFlowType = useSelector(getFirstTimeFlowType); + const dataCollectionForMarketing = useSelector(getDataCollectionForMarketing); + const participateInMetaMetrics = useSelector(getParticipateInMetaMetrics); + const trackEvent = useContext(MetaMetricsContext); const onConfirm = async () => { + if (dataCollectionForMarketing === null) { + await dispatch(setDataCollectionForMarketing(false)); + } + const [, metaMetricsId] = await dispatch(setParticipateInMetaMetrics(true)); try { trackEvent( @@ -67,6 +80,23 @@ export default function OnboardingMetametrics() { flushImmediately: true, }, ); + + if (participateInMetaMetrics) { + trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.AppInstalled, + }); + + trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.AnalyticsPreferenceSelected, + properties: { + is_metrics_opted_in: true, + has_marketing_consent: Boolean(dataCollectionForMarketing), + location: 'onboarding_metametrics', + }, + }); + } } finally { history.push(nextRoute); } @@ -74,6 +104,7 @@ export default function OnboardingMetametrics() { const onCancel = async () => { await dispatch(setParticipateInMetaMetrics(false)); + await dispatch(setDataCollectionForMarketing(false)); history.push(nextRoute); }; @@ -319,6 +350,15 @@ export default function OnboardingMetametrics() { {' '} + + dispatch(setDataCollectionForMarketing(!dataCollectionForMarketing)) + } + label={t('onboardingMetametricsUseDataCheckbox')} + paddingBottom={3} + /> ({ setParticipateInMetaMetrics: jest .fn() .mockReturnValue(jest.fn((val) => Promise.resolve([val]))), + setDataCollectionForMarketing: jest + .fn() + .mockReturnValue(jest.fn((val) => Promise.resolve([val]))), })); describe('Onboarding Metametrics Component', () => { @@ -58,6 +64,20 @@ describe('Onboarding Metametrics Component', () => { expect(container).toMatchSnapshot(); }); + it('should match snapshot after new policy date', () => { + // TODO: merge this with the previous test once this date is reached + jest.useFakeTimers().setSystemTime(new Date('2024-06-05')); + + const { container } = renderWithProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); + + jest.useRealTimers(); + }); + it('should set setParticipateInMetaMetrics to true when clicking agree', async () => { const { queryByText } = renderWithProvider( , @@ -94,6 +114,24 @@ describe('Onboarding Metametrics Component', () => { }); }); + it('should set setDataCollectionForMarketing to false when clicking cancel', async () => { + const { queryByText } = renderWithProvider( + , + mockStore, + ); + + const confirmCancel = queryByText(onboardingMetametricsDisagree.message); + + fireEvent.click(confirmCancel); + + await waitFor(() => { + expect(setDataCollectionForMarketing).toHaveBeenCalledWith(false); + expect(mockPushHistory).toHaveBeenCalledWith( + ONBOARDING_CREATE_PASSWORD_ROUTE, + ); + }); + }); + it('should render the Onboarding component when the current date is after the new privacy policy date', () => { jest.useFakeTimers().setSystemTime(new Date('2099-11-11')); const { queryByTestId } = renderWithProvider( diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index 648afd6bc71e..9a5c48d2f5b9 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -1748,6 +1748,74 @@ exports[`Security Tab should match snapshot 1`] = ` +
+
+ + Data collection for marketing + +
+ + We'll use MetaMetrics to learn how you interact with our marketing communications. We may share relevant news (like product features and other materials). + +
+
+
+
diff --git a/ui/pages/settings/security-tab/metametrics-toggle/metametrics-toggle.test.tsx b/ui/pages/settings/security-tab/metametrics-toggle/metametrics-toggle.test.tsx index 04a851797097..5e93bc9feaab 100644 --- a/ui/pages/settings/security-tab/metametrics-toggle/metametrics-toggle.test.tsx +++ b/ui/pages/settings/security-tab/metametrics-toggle/metametrics-toggle.test.tsx @@ -46,7 +46,11 @@ describe('MetametricsToggle', () => { mockUseSelectorReturnValue = false; const { getByTestId } = render( - + {}} + /> , ); expect(getByTestId('profileSyncToggle')).toBeInTheDocument(); @@ -56,7 +60,11 @@ describe('MetametricsToggle', () => { mockUseSelectorReturnValue = false; const { getByTestId } = render( - + {}} + /> , ); fireEvent.click(getByTestId('toggleButton')); @@ -67,7 +75,11 @@ describe('MetametricsToggle', () => { mockUseSelectorReturnValue = true; const { getByTestId } = render( - + {}} + /> , ); fireEvent.click(getByTestId('toggleButton')); diff --git a/ui/pages/settings/security-tab/metametrics-toggle/metametrics-toggle.tsx b/ui/pages/settings/security-tab/metametrics-toggle/metametrics-toggle.tsx index 60c208df8ccd..661dd7f974b4 100644 --- a/ui/pages/settings/security-tab/metametrics-toggle/metametrics-toggle.tsx +++ b/ui/pages/settings/security-tab/metametrics-toggle/metametrics-toggle.tsx @@ -22,7 +22,13 @@ import { TextVariant, } from '../../../../helpers/constants/design-system'; -const MetametricsToggle = () => { +const MetametricsToggle = ({ + dataCollectionForMarketing, + setDataCollectionForMarketing, +}: { + dataCollectionForMarketing: boolean; + setDataCollectionForMarketing: (value: boolean) => void; +}) => { const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); const { enableMetametrics, error: enableMetametricsError } = @@ -46,6 +52,16 @@ const MetametricsToggle = () => { participateInMetaMetrics, }, }); + + trackEvent({ + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.AnalyticsPreferenceSelected, + properties: { + is_metrics_opted_in: false, + has_marketing_consent: false, + location: 'Settings', + }, + }); } else { await enableMetametrics(); trackEvent({ @@ -57,6 +73,10 @@ const MetametricsToggle = () => { }, }); } + + if (dataCollectionForMarketing) { + setDataCollectionForMarketing(false); + } }; return ( diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js index fc2db3b0f93d..b20435e4b258 100644 --- a/ui/pages/settings/security-tab/security-tab.component.js +++ b/ui/pages/settings/security-tab/security-tab.component.js @@ -25,17 +25,23 @@ import SRPQuiz from '../../../components/app/srp-quiz-modal/SRPQuiz'; import { Button, BUTTON_SIZES, + Icon, + IconSize, + IconName, Box, Text, } from '../../../components/component-library'; import TextField from '../../../components/ui/text-field'; import ToggleButton from '../../../components/ui/toggle-button'; +import Popover from '../../../components/ui/popover'; import { Display, + BlockSize, FlexDirection, JustifyContent, TextColor, TextVariant, + IconColor, } from '../../../helpers/constants/design-system'; import { ADD_POPULAR_CUSTOM_NETWORK } from '../../../helpers/constants/routes'; import { @@ -60,6 +66,10 @@ export default class SecurityTab extends PureComponent { setOpenSeaEnabled: PropTypes.func, useNftDetection: PropTypes.bool, setUseNftDetection: PropTypes.func, + dataCollectionForMarketing: PropTypes.bool, + setDataCollectionForMarketing: PropTypes.func.isRequired, + participateInMetaMetrics: PropTypes.bool.isRequired, + setParticipateInMetaMetrics: PropTypes.func.isRequired, incomingTransactionsPreferences: PropTypes.object.isRequired, allNetworks: PropTypes.array.isRequired, setIncomingTransactionsPreferences: PropTypes.func.isRequired, @@ -98,6 +108,7 @@ export default class SecurityTab extends PureComponent { ipfsGateway: this.props.ipfsGateway || IPFS_DEFAULT_GATEWAY_URL, ipfsGatewayError: '', srpQuizModalVisible: false, + showDataCollectionDisclaimer: false, ipfsToggle: this.props.ipfsGateway.length > 0, }; @@ -114,9 +125,17 @@ export default class SecurityTab extends PureComponent { return React.createRef(); }); - componentDidUpdate() { + componentDidUpdate(prevProps) { const { t } = this.context; handleSettingsRefs(t, t('securityAndPrivacy'), this.settingsRefs); + + if ( + prevProps.dataCollectionForMarketing === true && + this.props.participateInMetaMetrics === true && + this.props.dataCollectionForMarketing === false + ) { + this.setState({ showDataCollectionDisclaimer: true }); + } } componentDidMount() { @@ -325,6 +344,61 @@ export default class SecurityTab extends PureComponent { ); } + renderDataCollectionForMarketing() { + const { t } = this.context; + const { + dataCollectionForMarketing, + participateInMetaMetrics, + setDataCollectionForMarketing, + setParticipateInMetaMetrics, + } = this.props; + + return ( + +
+ {t('dataCollectionForMarketing')} +
+ {t('dataCollectionForMarketingDescription')} +
+
+ +
+ { + setDataCollectionForMarketing(!value); + if (participateInMetaMetrics) { + this.context.trackEvent({ + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.AnalyticsPreferenceSelected, + properties: { + is_metrics_opted_in: true, + has_marketing_consent: false, + location: 'Settings', + }, + }); + } else { + setParticipateInMetaMetrics(true); + } + }} + offLabel={t('off')} + onLabel={t('on')} + /> +
+
+ ); + } + renderChooseYourNetworkButton() { const { t } = this.context; @@ -998,12 +1072,60 @@ export default class SecurityTab extends PureComponent { ); } + renderDataCollectionWarning = () => { + const { t } = this.context; + + return ( + this.setState({ showDataCollectionDisclaimer: false })} + title={ + + } + footer={ + + } + > + + {t('dataCollectionWarningPopoverDescription')} + + + ); + }; + render() { - const { warning, petnamesEnabled } = this.props; + const { + warning, + petnamesEnabled, + dataCollectionForMarketing, + setDataCollectionForMarketing, + } = this.props; + const { showDataCollectionDisclaimer } = this.state; return (
{this.renderUseExternalServices()} + {showDataCollectionDisclaimer + ? this.renderDataCollectionWarning() + : null} {warning &&
{warning}
} @@ -1085,7 +1207,11 @@ export default class SecurityTab extends PureComponent { {this.context.t('metrics')}
- + + {this.renderDataCollectionForMarketing()}
); diff --git a/ui/pages/settings/security-tab/security-tab.container.js b/ui/pages/settings/security-tab/security-tab.container.js index f411e1ceb1f2..98a2c7c9c3f5 100644 --- a/ui/pages/settings/security-tab/security-tab.container.js +++ b/ui/pages/settings/security-tab/security-tab.container.js @@ -6,6 +6,7 @@ import { setIpfsGateway, setIsIpfsGatewayEnabled, setParticipateInMetaMetrics, + setDataCollectionForMarketing, setUseCurrencyRateCheck, setUseMultiAccountBalanceChecker, setUsePhishDetect, @@ -43,6 +44,7 @@ const mapStateToProps = (state) => { const { incomingTransactionsPreferences, participateInMetaMetrics, + dataCollectionForMarketing, usePhishDetect, useTokenDetection, ipfsGateway, @@ -64,6 +66,7 @@ const mapStateToProps = (state) => { incomingTransactionsPreferences, allNetworks, participateInMetaMetrics, + dataCollectionForMarketing, usePhishDetect, useTokenDetection, ipfsGateway, @@ -90,6 +93,8 @@ const mapDispatchToProps = (dispatch) => { dispatch(setIncomingTransactionsPreferences(chainId, value)), setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)), + setDataCollectionForMarketing: (val) => + dispatch(setDataCollectionForMarketing(val)), setUsePhishDetect: (val) => dispatch(setUsePhishDetect(val)), setUseCurrencyRateCheck: (val) => dispatch(setUseCurrencyRateCheck(val)), setUseTokenDetection: (val) => dispatch(setUseTokenDetection(val)), diff --git a/ui/selectors/metametrics.js b/ui/selectors/metametrics.js index 377181a3e6a8..c623e378c003 100644 --- a/ui/selectors/metametrics.js +++ b/ui/selectors/metametrics.js @@ -2,6 +2,12 @@ import { createSelector } from 'reselect'; export const selectFragments = (state) => state.metamask.fragments; +export const getDataCollectionForMarketing = (state) => + state.metamask.dataCollectionForMarketing; + +export const getParticipateInMetaMetrics = (state) => + Boolean(state.metamask.participateInMetaMetrics); + export const selectFragmentBySuccessEvent = createSelector( selectFragments, (_, fragmentOptions) => fragmentOptions, diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 2029dbd7f94e..85343608c936 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -80,6 +80,8 @@ export const DEPRECATED_NETWORK_POPOVER_CLOSE = export const UPDATE_CUSTOM_NONCE = 'UPDATE_CUSTOM_NONCE'; export const SET_PARTICIPATE_IN_METAMETRICS = 'SET_PARTICIPATE_IN_METAMETRICS'; +export const SET_DATA_COLLECTION_FOR_MARKETING = + 'SET_DATA_COLLECTION_FOR_MARKETING'; // locale export const SET_CURRENT_LOCALE = 'SET_CURRENT_LOCALE'; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 30a20b842333..641f9d05bc0d 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3282,6 +3282,26 @@ export function setParticipateInMetaMetrics( }; } +export function setDataCollectionForMarketing( + dataCollectionPreference: boolean, +): ThunkAction< + Promise<[boolean, string]>, + MetaMaskReduxState, + unknown, + AnyAction +> { + return async (dispatch: MetaMaskReduxDispatch) => { + log.debug(`background.setDataCollectionForMarketing`); + await submitRequestToBackground('setDataCollectionForMarketing', [ + dataCollectionPreference, + ]); + dispatch({ + type: actionConstants.SET_DATA_COLLECTION_FOR_MARKETING, + value: dataCollectionPreference, + }); + }; +} + export function setUseBlockie( val: boolean, ): ThunkAction {