diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 887ab9e42..e1d91e249 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1210,6 +1210,9 @@ "ipfsGatewayDescription": { "message": "Enter the URL of the IPFS CID gateway to use for ENS content resolution." }, + "jsDeliver": { + "message": "jsDeliver" + }, "jsonFile": { "message": "JSON File", "description": "format for importing an account" @@ -1620,6 +1623,14 @@ "message": "\"$1\" will close this tab and direct back to $2", "description": "Return the user to the site that initiated onboarding" }, + "onboardingShowIncomingTransactionsDescription": { + "message": "Showing incoming transactions in your wallet relies on communication with $1. Etherscan will have access to your Ethereum address and your IP address. View $2.", + "description": "$1 is a clickable link with text defined by the 'etherscan' key. $2 is a clickable link with text defined by the 'privacyMsg' key." + }, + "onboardingUsePhishingDetectionDescription": { + "message": "Phishing detection alerts rely on communication with $1. jsDeliver will have access to your IP address. View $2.", + "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" + }, "onlyAddTrustedNetworks": { "message": "A malicious network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust." }, @@ -2020,6 +2031,12 @@ "separateEachWord": { "message": "Separate each word with a single space" }, + "setAdvancedPrivacySettings": { + "message": "Set advanced privacy settings" + }, + "setAdvancedPrivacySettingsDetails": { + "message": "MetaMask uses these trusted third-party services to enhance product usability and safety." + }, "settings": { "message": "Settings" }, @@ -2718,6 +2735,9 @@ "tryAgain": { "message": "Try again" }, + "turnOnTokenDetection": { + "message": "Turn on Token Detection" + }, "typePassword": { "message": "Type your MetaMask password" }, diff --git a/ui/pages/onboarding-flow/index.scss b/ui/pages/onboarding-flow/index.scss index 8d5a5cd0c..afcf7b2fa 100644 --- a/ui/pages/onboarding-flow/index.scss +++ b/ui/pages/onboarding-flow/index.scss @@ -2,6 +2,7 @@ @import 'new-account/index'; @import 'onboarding-app-header/index'; @import 'secure-your-wallet/index'; +@import 'privacy-settings/index'; .onboarding-flow { width: 100%; diff --git a/ui/pages/onboarding-flow/onboarding-flow.js b/ui/pages/onboarding-flow/onboarding-flow.js index 82c1b00e6..c00af0977 100644 --- a/ui/pages/onboarding-flow/onboarding-flow.js +++ b/ui/pages/onboarding-flow/onboarding-flow.js @@ -9,6 +9,7 @@ import { ONBOARDING_UNLOCK_ROUTE, DEFAULT_ROUTE, ONBOARDING_SECURE_YOUR_WALLET_ROUTE, + ONBOARDING_PRIVACY_SETTINGS_ROUTE, } from '../../helpers/constants/routes'; import { getCompletedOnboarding, @@ -26,6 +27,7 @@ import NewAccount from './new-account/new-account'; import ReviewRecoveryPhrase from './recovery-phrase/review-recovery-phrase'; import SecureYourWallet from './secure-your-wallet/secure-your-wallet'; import ConfirmRecoveryPhrase from './recovery-phrase/confirm-recovery-phrase'; +import PrivacySettings from './privacy-settings/privacy-settings'; export default function OnboardingFlow() { const [seedPhrase, setSeedPhrase] = useState(''); @@ -41,7 +43,7 @@ export default function OnboardingFlow() { // For ONBOARDING_V2 dev purposes, // Remove when ONBOARDING_V2 dev complete if (process.env.ONBOARDING_V2) { - history.push(ONBOARDING_SECURE_YOUR_WALLET_ROUTE); + history.push(ONBOARDING_PRIVACY_SETTINGS_ROUTE); return; } @@ -108,6 +110,10 @@ export default function OnboardingFlow() { )} /> + diff --git a/ui/pages/onboarding-flow/privacy-settings/index.scss b/ui/pages/onboarding-flow/privacy-settings/index.scss new file mode 100644 index 000000000..e5a6d059b --- /dev/null +++ b/ui/pages/onboarding-flow/privacy-settings/index.scss @@ -0,0 +1,50 @@ +.privacy-settings { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + &__header { + display: flex; + justify-content: center; + flex-direction: column; + text-align: center; + max-width: 500px; + margin: 24px; + } + + &__settings { + display: flex; + justify-content: center; + flex-direction: column; + text-align: center; + max-width: 620px; + margin-bottom: 20px; + + a { + color: $Blue-500; + + &:hover { + cursor: pointer; + color: $Blue-300; + } + } + } + + &__setting { + display: flex; + justify-content: center; + flex-direction: column; + text-align: left; + max-width: 430px; + + &__toggle { + margin-left: 42px; + } + } + + & button { + max-width: 50%; + padding: 15px; + } +} diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js new file mode 100644 index 000000000..e372e1130 --- /dev/null +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; + +import Button from '../../../components/ui/button'; +import Typography from '../../../components/ui/typography'; +import { + FONT_WEIGHT, + TYPOGRAPHY, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + setFeatureFlag, + setUsePhishDetect, + setUseTokenDetection, +} from '../../../store/actions'; +import { ONBOARDING_PIN_EXTENSION_ROUTE } from '../../../helpers/constants/routes'; +import { Setting } from './setting'; + +export default function PrivacySettings() { + const t = useI18nContext(); + const dispatch = useDispatch(); + const history = useHistory(); + const [usePhishingDetection, setUsePhishingDetection] = useState(true); + const [turnOnTokenDetection, setTurnOnTokenDetection] = useState(true); + const [showIncomingTransactions, setShowIncomingTransactions] = useState( + true, + ); + + const handleSubmit = () => { + dispatch( + setFeatureFlag('showIncomingTransactions', showIncomingTransactions), + ); + dispatch(setUsePhishDetect(usePhishingDetection)); + dispatch(setUseTokenDetection(turnOnTokenDetection)); + history.push(ONBOARDING_PIN_EXTENSION_ROUTE); + }; + + return ( + <> +
+
+ + {t('setAdvancedPrivacySettings')} + + + {t('setAdvancedPrivacySettingsDetails')} + +
+
+ + {t('etherscan')} + , + + {t('privacyMsg')} + , + ])} + /> + + {t('jsDeliver')} + , + + {t('privacyMsg')} + , + ])} + /> + +
+ +
+ + ); +} diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.stories.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.stories.js new file mode 100644 index 000000000..c0f775439 --- /dev/null +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.stories.js @@ -0,0 +1,15 @@ +import React from 'react'; +import PrivacySettings from './privacy-settings'; + +export default { + title: 'Onboarding - Privacy Settings', + id: __filename, +}; + +export const Base = () => { + return ( +
+ +
+ ); +}; diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js new file mode 100644 index 000000000..51c9df987 --- /dev/null +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.test.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import * as actions from '../../../store/actions'; +import { renderWithProvider } from '../../../../test/jest'; +import PrivacySettings from './privacy-settings'; + +describe('Privacy Settings Onboarding View', () => { + const mockStore = { + metamask: { + provider: { + type: 'test', + }, + }, + }; + + const store = configureMockStore([thunk])(mockStore); + const setFeatureFlagStub = jest.fn(); + const setUsePhishDetectStub = jest.fn(); + const setUseTokenDetectionStub = jest.fn(); + + actions._setBackgroundConnection({ + setFeatureFlag: setFeatureFlagStub, + setUsePhishDetect: setUsePhishDetectStub, + setUseTokenDetection: setUseTokenDetectionStub, + }); + + it('should update preferences', () => { + const { container, getByText } = renderWithProvider( + , + store, + ); + // All settings are initialized toggled to true + expect(setFeatureFlagStub).toHaveBeenCalledTimes(0); + expect(setUsePhishDetectStub).toHaveBeenCalledTimes(0); + expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(0); + const toggles = container.querySelectorAll('input[type=checkbox]'); + const submitButton = getByText('Done'); + + // toggle to false + fireEvent.click(toggles[0]); + fireEvent.click(toggles[1]); + fireEvent.click(toggles[2]); + fireEvent.click(submitButton); + + expect(setFeatureFlagStub).toHaveBeenCalledTimes(1); + expect(setUsePhishDetectStub).toHaveBeenCalledTimes(1); + expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(1); + expect(setFeatureFlagStub.mock.calls[0][1]).toStrictEqual(false); + expect(setUsePhishDetectStub.mock.calls[0][0]).toStrictEqual(false); + expect(setUseTokenDetectionStub.mock.calls[0][0]).toStrictEqual(false); + + // toggle back to true + fireEvent.click(toggles[0]); + fireEvent.click(toggles[1]); + fireEvent.click(toggles[2]); + fireEvent.click(submitButton); + expect(setFeatureFlagStub).toHaveBeenCalledTimes(2); + expect(setUsePhishDetectStub).toHaveBeenCalledTimes(2); + expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(2); + expect(setFeatureFlagStub.mock.calls[1][1]).toStrictEqual(true); + expect(setUsePhishDetectStub.mock.calls[1][0]).toStrictEqual(true); + expect(setUseTokenDetectionStub.mock.calls[1][0]).toStrictEqual(true); + }); +}); diff --git a/ui/pages/onboarding-flow/privacy-settings/setting.js b/ui/pages/onboarding-flow/privacy-settings/setting.js new file mode 100644 index 000000000..82ba1275c --- /dev/null +++ b/ui/pages/onboarding-flow/privacy-settings/setting.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Box from '../../../components/ui/box'; +import Typography from '../../../components/ui/typography'; +import ToggleButton from '../../../components/ui/toggle-button'; +import { + JUSTIFY_CONTENT, + TYPOGRAPHY, + FONT_WEIGHT, +} from '../../../helpers/constants/design-system'; + +export const Setting = ({ value, setValue, title, description }) => { + return ( + +
+ + {title} + + {description} +
+
+ setValue(!val)} /> +
+
+ ); +}; + +Setting.propTypes = { + value: PropTypes.bool, + setValue: PropTypes.func, + title: PropTypes.string, + description: PropTypes.string, +};