From 0d1136ad99dec1428f251f1071b42a2985e3d7b9 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 11 Nov 2020 11:10:57 -0500 Subject: [PATCH 001/113] Work towards implementing stub of new intro hub --- app/ghostery-browser-hub/AppView/Constants.js | 16 +++ app/ghostery-browser-hub/AppView/Reducer.js | 34 +++++++ app/ghostery-browser-hub/createStore.js | 48 +++++++++ app/ghostery-browser-hub/index.jsx | 35 +++++++ app/scss/hub_ghostery_browser.scss | 99 +++++++++++++++++++ app/templates/panel.html | 11 ++- webpack.config.js | 6 +- 7 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 app/ghostery-browser-hub/AppView/Constants.js create mode 100644 app/ghostery-browser-hub/AppView/Reducer.js create mode 100644 app/ghostery-browser-hub/createStore.js create mode 100644 app/ghostery-browser-hub/index.jsx create mode 100644 app/scss/hub_ghostery_browser.scss diff --git a/app/ghostery-browser-hub/AppView/Constants.js b/app/ghostery-browser-hub/AppView/Constants.js new file mode 100644 index 000000000..881b15c30 --- /dev/null +++ b/app/ghostery-browser-hub/AppView/Constants.js @@ -0,0 +1,16 @@ +/** + * App View Constants + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +// App View +export const SET_TOAST = 'SET_TOAST'; +export const SEND_PING = 'SEND_PING'; diff --git a/app/ghostery-browser-hub/AppView/Reducer.js b/app/ghostery-browser-hub/AppView/Reducer.js new file mode 100644 index 000000000..0d4ed2019 --- /dev/null +++ b/app/ghostery-browser-hub/AppView/Reducer.js @@ -0,0 +1,34 @@ +/** + * Reducer used in the Ghostery Browser Hub App View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { SET_TOAST } from './Constants'; + +const initialState = {}; + +function GhosteryBrowserHubAppViewReducer(state = initialState, action) { + switch (action.type) { + case SET_TOAST: { + const { toastMessage, toastClass } = action.data; + return { + ...state, + app: { + toastMessage, + toastClass + } + }; + } + default: return state; + } +} + +export default GhosteryBrowserHubAppViewReducer; diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js new file mode 100644 index 000000000..c1438c0a5 --- /dev/null +++ b/app/ghostery-browser-hub/createStore.js @@ -0,0 +1,48 @@ +/** + * Ghostery Browser Hub React Store Init + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + * + * @namespace GhosteryBrowserHubReactStore + */ + +import { + applyMiddleware, + compose, + combineReducers, + createStore +} from 'redux'; +import thunk from 'redux-thunk'; + +import { reducer as app } from './Views/AppView'; +import { reducer as main } from './Views/MainView'; +import account from '../Account/AccountReducer'; +import settings from '../panel/reducers/settings'; + +const reducer = combineReducers({ + app, + account, + settings, +}); + +/** + * Build store using combined reducers and middleware + * @return {Object} + * @memberof HubReactStore + */ +export default function() { + return createStore( + reducer, + compose( + applyMiddleware(thunk), + window.devToolsExtension ? window.devToolsExtension() : f => f + ), + ); +} diff --git a/app/ghostery-browser-hub/index.jsx b/app/ghostery-browser-hub/index.jsx new file mode 100644 index 000000000..4a3a0afa1 --- /dev/null +++ b/app/ghostery-browser-hub/index.jsx @@ -0,0 +1,35 @@ +/** + * Ghostery Ghostery-Browser-Specific Hub React App Init + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + * + * @namespace HubGhosteryBrowserComponents + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import createStore from './createStore'; + +// Containers +import HubGhosteryBrowserAppView from './Views/AppView'; +import HubGhosteryBrowserMainView from './Views/MainView'; + +const store = createStore(); + +ReactDOM.render( + ( + + + + + + ), document.getElementById('root'), +); diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss new file mode 100644 index 000000000..2f71aa4f9 --- /dev/null +++ b/app/scss/hub_ghostery_browser.scss @@ -0,0 +1,99 @@ +/** + * Ghostery-Browser-Specific Hub Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +// Import Global Partials +@import './partials/colors'; + +html, body, #root { + height: 100%; + width: 100%; + margin: 0; +} + +.App { + position: fixed; + z-index: 0; +} +.App__mainContent { + margin-left: 230px; + overflow-y: auto; + // Very small windows/phone screens in Firefox slightly overflows on the x-axis + overflow-x: hidden; +} +@media only screen and (max-width: 740px) { + .App__mainContent { + margin-left: 54px; + } + .android-relative {position: relative;} +} + +// Foundation Overrides +.button { + font-size: 14px; + font-weight: 700; + line-height: 1.3; + text-transform: uppercase; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12); + &.primary { + &:not(.hollow):hover { + background-color: $dark-ghosty-blue; + } + &.hollow:hover, &.hollow:focus { + border-color: $dark-ghosty-blue; + color: $dark-ghosty-blue; + } + } +} + +// Helper Classes +.clickable { cursor: pointer; } +.full-height { height: 100%; } +.full-width { width: 100%; } +.display-inline { display: inline-block; } +.flex-shrink-none { flex-shrink: 0; } + +@import 'settings_hub'; // Import Foundation +@import './partials/_hub_mixins'; +@import './partials/_hub_svgs'; +@import './partials/_shared_components_svgs'; +@import './partials/_fonts'; +@import './partials/_loader'; + +// Imports from ../hub directory +@import '../hub/Views/SideNavigationView/SideNavigationView.scss'; +@import '../hub/Views/HomeView/HomeView.scss'; +@import '../hub/Views/SetupView/SetupView.scss'; +@import '../hub/Views/SetupViews/SetupHeader/SetupHeader.scss'; +@import '../hub/Views/SetupViews/SetupBlockingView/SetupBlockingView.scss'; +@import '../hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdown.scss'; +@import '../hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteView.scss'; +@import '../hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebView.scss'; +@import '../hub/Views/SetupViews/SetupDoneView/SetupDoneView.scss'; +@import '../hub/Views/TutorialView/TutorialView.scss'; +@import '../hub/Views/PlusView/PlusView.scss'; +@import '../hub/Views/RewardsView/RewardsView.scss'; +@import '../hub/Views/ProductsView/ProductsView.scss'; +@import '../hub/Views/SignedInView/SignedInView.scss'; +@import '../hub/Views/LogInView/LogInView.scss'; +@import '../hub/Views/CreateAccountView/CreateAccountView.scss'; +@import '../hub/Views/UpgradePlanView/UpgradePlanView.scss'; + +// Imports from ../shared-components directory +@import '../shared-components/ExitButton/ExitButton.scss'; +@import '../shared-components/Modal/Modal.scss'; +@import '../shared-components/ModalContent/PremiumPromoModalContent/PremiumPromoModalContent.scss'; +@import '../shared-components/SteppedNavigation/SteppedNavigation.scss'; +@import '../shared-components/ToastMessage/ToastMessage.scss'; +@import '../shared-components/ToggleCheckbox/ToggleCheckbox.scss'; +@import '../shared-components/ToggleSwitch/ToggleSwitch.scss'; +@import '../shared-components/ForgotPassword/ForgotPassword.scss'; diff --git a/app/templates/panel.html b/app/templates/panel.html index 41698a8e6..e3c65674a 100644 --- a/app/templates/panel.html +++ b/app/templates/panel.html @@ -22,6 +22,15 @@
- + diff --git a/webpack.config.js b/webpack.config.js index fb73dc38c..9562b78cf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,6 +25,7 @@ const SHARED_COMP_DIR = path.resolve(__dirname, 'app/shared-components'); const PANEL_DIR = path.resolve(__dirname, 'app/panel'); const PANEL_ANDROID_DIR = path.resolve(__dirname, 'app/panel-android'); const HUB_DIR = path.resolve(__dirname, 'app/hub'); +const HUB_GHOSTERY_BROWSER_DIR = path.resolve(__dirname, 'app/ghostery-browser-hub'); const LICENSES_DIR = path.resolve(__dirname, 'app/licenses'); const REWARDS_DIR = path.resolve(__dirname, 'app/rewards'); const SASS_DIR = path.resolve(__dirname, 'app/scss'); @@ -52,6 +53,7 @@ module.exports = { content_script_bundle: [`${CONTENT_SCRIPTS_DIR}/content_script_bundle.js`], ghostery_dot_com: [`${CONTENT_SCRIPTS_DIR}/ghostery_dot_com.js`], hub_react: [`${HUB_DIR}/index.jsx`], + hub_ghostery_browser_react: [`${HUB_GHOSTERY_BROWSER_DIR}/index.jsx`], licenses_react: [`${LICENSES_DIR}/Licenses.jsx`, `${LICENSES_DIR}/License.jsx`], notifications: [`${CONTENT_SCRIPTS_DIR}/notifications.js`], page_performance: [`${CONTENT_SCRIPTS_DIR}/page_performance.js`], @@ -64,6 +66,7 @@ module.exports = { foundation_hub: [`${SASS_DIR}/vendor/foundation_hub.scss`], ghostery_dot_com_css: [`${SASS_DIR}/ghostery_dot_com.scss`], hub: [`${SASS_DIR}/hub.scss`], + hub_ghostery_browser: [`${HUB_GHOSTERY_BROWSER_DIR}/hub_ghostery_browser.scss`], licenses: [`${SASS_DIR}/licenses.scss`], panel: [`${SASS_DIR}/panel.scss`], panel_android: [`${SASS_DIR}/panel_android.scss`], @@ -91,6 +94,7 @@ module.exports = { `${RM} ./dist/foundation_hub.js`, `${RM} ./dist/ghostery_dot_com_css.js`, `${RM} ./dist/hub.js`, + `${RM} ./dist/hub_ghostery_browser.js`, `${RM} ./dist/licenses.js`, `${RM} ./dist/panel.js`, `${RM} ./dist/panel_android.js`, @@ -119,7 +123,7 @@ module.exports = { } }, { test: /\.(js|jsx)$/, - include: [SHARED_COMP_DIR, PANEL_ANDROID_DIR, PANEL_DIR, HUB_DIR, LICENSES_DIR, CONTENT_SCRIPTS_DIR, REWARDS_DIR], + include: [SHARED_COMP_DIR, PANEL_ANDROID_DIR, PANEL_DIR, HUB_DIR, HUB_GHOSTERY_BROWSER_DIR, LICENSES_DIR, CONTENT_SCRIPTS_DIR, REWARDS_DIR], exclude: /node_modules/, use: [ { From d295be0f83eeae24ce2400753686a515ef7bd8f5 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 11 Nov 2020 12:36:02 -0500 Subject: [PATCH 002/113] Implement AppView for Ghostery Browser Hub --- .../Views/AppView/AppView.jsx | 65 +++++++++++++++++++ .../Views/AppView/AppViewActions.js | 37 +++++++++++ .../AppView/AppViewConstants.js} | 8 ++- .../AppView/AppViewReducer.js} | 8 ++- .../Views/AppView/index.js | 29 +++++++++ app/ghostery-browser-hub/index.jsx | 12 ++-- 6 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/AppView/AppView.jsx create mode 100644 app/ghostery-browser-hub/Views/AppView/AppViewActions.js rename app/ghostery-browser-hub/{AppView/Constants.js => Views/AppView/AppViewConstants.js} (65%) rename app/ghostery-browser-hub/{AppView/Reducer.js => Views/AppView/AppViewReducer.js} (76%) create mode 100644 app/ghostery-browser-hub/Views/AppView/index.js diff --git a/app/ghostery-browser-hub/Views/AppView/AppView.jsx b/app/ghostery-browser-hub/Views/AppView/AppView.jsx new file mode 100644 index 000000000..b46702b52 --- /dev/null +++ b/app/ghostery-browser-hub/Views/AppView/AppView.jsx @@ -0,0 +1,65 @@ +/** + * Ghostery Hub App View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { ToastMessage } from '../../../shared-components'; + +/** + * A functional React component that implements the App View for the Ghostery Browser Hub + * @extends Component + * @memberof GhosteryHubViews + */ +const AppView = (props) => { + const { app, children } = props; + + /** + * Handle clicking to exit the Toast Message. + */ + const exitToast = () => { + const { actions } = this.props; + actions.setToast({ + toastMessage: '', + toastClass: '', + }); + }; + + return ( +
+
+ + {children} +
+
+ ); +}; + +AppView.propTypes = { + actions: PropTypes.shape({ + setToast: PropTypes.func.isRequired, + }).isRequired, + app: PropTypes.shape({ + toastMessage: PropTypes.string, + toastClass: PropTypes.string, + }), +}; + +// Default props used in the App +AppView.defaultProps = { + app: { + toastMessage: '', + toastClass: '', + }, +}; + +export default AppView; diff --git a/app/ghostery-browser-hub/Views/AppView/AppViewActions.js b/app/ghostery-browser-hub/Views/AppView/AppViewActions.js new file mode 100644 index 000000000..7a2a65041 --- /dev/null +++ b/app/ghostery-browser-hub/Views/AppView/AppViewActions.js @@ -0,0 +1,37 @@ +/** + * Ghostery Browser Hub App View Action creators + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +// TODO I think we can just reuse the hub AppViewActions + +import { log, sendMessageInPromise } from '../../../hub/utils'; +import { SET_TOAST, SEND_PING } from './AppViewConstants'; + +export function setToast(data) { + return { + type: SET_TOAST, + data, + }; +} + +export function sendPing(actionData) { + return function(dispatch) { + return sendMessageInPromise(SEND_PING, actionData).then((data) => { + dispatch({ + type: SEND_PING, + data, + }); + }).catch((err) => { + log('appView Action sendPing Error', err); + }); + }; +} diff --git a/app/ghostery-browser-hub/AppView/Constants.js b/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js similarity index 65% rename from app/ghostery-browser-hub/AppView/Constants.js rename to app/ghostery-browser-hub/Views/AppView/AppViewConstants.js index 881b15c30..3bce0f948 100644 --- a/app/ghostery-browser-hub/AppView/Constants.js +++ b/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js @@ -1,16 +1,18 @@ /** - * App View Constants + * Ghostery Browser Hub App View Constants * * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -// App View +// TODO I think we can just use the Hub AppViewConstants + +// Ghostery Browser Hub App View export const SET_TOAST = 'SET_TOAST'; export const SEND_PING = 'SEND_PING'; diff --git a/app/ghostery-browser-hub/AppView/Reducer.js b/app/ghostery-browser-hub/Views/AppView/AppViewReducer.js similarity index 76% rename from app/ghostery-browser-hub/AppView/Reducer.js rename to app/ghostery-browser-hub/Views/AppView/AppViewReducer.js index 0d4ed2019..b068f8f78 100644 --- a/app/ghostery-browser-hub/AppView/Reducer.js +++ b/app/ghostery-browser-hub/Views/AppView/AppViewReducer.js @@ -11,11 +11,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { SET_TOAST } from './Constants'; +// TODO I think we can just use the Hub AppViewReducer + +import { SET_TOAST } from './AppViewConstants'; const initialState = {}; -function GhosteryBrowserHubAppViewReducer(state = initialState, action) { +function AppViewReducer(state = initialState, action) { switch (action.type) { case SET_TOAST: { const { toastMessage, toastClass } = action.data; @@ -31,4 +33,4 @@ function GhosteryBrowserHubAppViewReducer(state = initialState, action) { } } -export default GhosteryBrowserHubAppViewReducer; +export default AppViewReducer; diff --git a/app/ghostery-browser-hub/Views/AppView/index.js b/app/ghostery-browser-hub/Views/AppView/index.js new file mode 100644 index 000000000..ad47f3aa2 --- /dev/null +++ b/app/ghostery-browser-hub/Views/AppView/index.js @@ -0,0 +1,29 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub App View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import View from './AppView'; +import { setToast } from './AppViewActions'; +import Reducer from './AppViewReducer'; + +const mapStateToProps = state => ({ ...state.app }); + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ setToast }, dispatch), +}); + +export const reducer = Reducer; + +export default connect(mapStateToProps, mapDispatchToProps)(View); diff --git a/app/ghostery-browser-hub/index.jsx b/app/ghostery-browser-hub/index.jsx index 4a3a0afa1..813c325d8 100644 --- a/app/ghostery-browser-hub/index.jsx +++ b/app/ghostery-browser-hub/index.jsx @@ -10,7 +10,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0 * - * @namespace HubGhosteryBrowserComponents + * @namespace GhosteryBrowserHubComponents */ import React from 'react'; @@ -19,17 +19,17 @@ import { Provider } from 'react-redux'; import createStore from './createStore'; // Containers -import HubGhosteryBrowserAppView from './Views/AppView'; -import HubGhosteryBrowserMainView from './Views/MainView'; +import AppView from './Views/AppView'; +import MainView from './Views/MainView'; const store = createStore(); ReactDOM.render( ( - - - + + + ), document.getElementById('root'), ); From 2afb6009441611d8083b32e4f47a54ab5b13a8d9 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Fri, 13 Nov 2020 10:16:41 -0500 Subject: [PATCH 003/113] Progress towards intro hub skeleton --- .../Views/AppView/AppView.jsx | 2 +- .../OnboardingView/OnboardingViewConstants.js | 38 +++++ .../OnboardingView/OnboardingViewReducer.js | 140 ++++++++++++++++++ .../Views/OnboardingView/index.js | 91 ++++++++++++ app/ghostery-browser-hub/createStore.js | 2 +- app/ghostery-browser-hub/index.jsx | 4 +- .../__tests__/HomeViewActions.test.js | 2 +- .../SetupView/__tests__/SetupView.test.jsx | 6 +- .../__tests__/SetupViewActions.test.js | 2 +- .../__tests__/SetupViewContainer.test.jsx | 14 +- .../__tests__/SetupViewReducer.test.js | 2 +- .../__tests__/SetupAntiSuiteView.test.jsx | 2 +- .../SetupAntiSuiteViewActions.test.js | 2 +- .../__tests__/SetupBlockingDropdown.test.jsx | 2 +- .../__tests__/SetupBlockingView.test.jsx | 2 +- .../SetupBlockingViewActions.test.js | 2 +- .../__tests__/SetupDoneView.test.jsx | 2 +- .../__tests__/SetupDoneViewActions.test.js | 2 +- .../__tests__/SetupHeader.test.jsx | 2 +- .../__tests__/SetupHumanWebView.test.jsx | 2 +- .../SetupHumanWebViewActions.test.js | 2 +- 21 files changed, 296 insertions(+), 27 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingView/index.js diff --git a/app/ghostery-browser-hub/Views/AppView/AppView.jsx b/app/ghostery-browser-hub/Views/AppView/AppView.jsx index b46702b52..439d3f462 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppView.jsx +++ b/app/ghostery-browser-hub/Views/AppView/AppView.jsx @@ -27,7 +27,7 @@ const AppView = (props) => { * Handle clicking to exit the Toast Message. */ const exitToast = () => { - const { actions } = this.props; + const { actions } = props; actions.setToast({ toastMessage: '', toastClass: '', diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js new file mode 100644 index 000000000..48f0db769 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js @@ -0,0 +1,38 @@ +/** + * Onboarding Flow Constants + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +// Setup View +export const GET_SETUP_SHOW_WARNING_OVERRIDE = 'GET_SETUP_SHOW_WARNING_OVERRIDE'; +export const SET_SETUP_SHOW_WARNING_OVERRIDE = 'SET_SETUP_SHOW_WARNING_OVERRIDE'; +export const INIT_SETUP_PROPS = 'INIT_SETUP_PROPS'; +export const SET_SETUP_STEP = 'SET_SETUP_STEP'; +export const SET_SETUP_NAVIGATION = 'SET_SETUP_NAVIGATION'; + +// Setup Blocking +export const SET_TRACKER_BLOCKING_POLICY = 'SET_TRACKER_BLOCKING_POLICY'; +export const BLOCKING_POLICY_RECOMMENDED = 'BLOCKING_POLICY_RECOMMENDED'; +export const BLOCKING_POLICY_NOTHING = 'BLOCKING_POLICY_NOTHING'; +export const BLOCKING_POLICY_EVERYTHING = 'BLOCKING_POLICY_EVERYTHING'; +export const BLOCKING_POLICY_CUSTOM = 'BLOCKING_POLICY_CUSTOM'; + +// Setup Anti-Suite +export const SET_ANTI_TRACKING = 'SET_ANTI_TRACKING'; +export const SET_AD_BLOCK = 'SET_AD_BLOCK'; +export const SET_SMART_BLOCK = 'SET_SMART_BLOCK'; +export const SET_GHOSTERY_REWARDS = 'SET_GHOSTERY_REWARDS'; + +// Setup Human Web +export const SET_HUMAN_WEB = 'SET_HUMAN_WEB'; + +// Setup Done +export const SET_SETUP_COMPLETE = 'SET_SETUP_COMPLETE'; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js new file mode 100644 index 000000000..060791045 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js @@ -0,0 +1,140 @@ +/** + * Reducer used throughout the Onboarding View's flow + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { + SET_AD_BLOCK, + SET_TRACKER_BLOCKING_POLICY, + SET_ANTI_TRACKING, + + GET_SETUP_SHOW_WARNING_OVERRIDE, + SET_SETUP_SHOW_WARNING_OVERRIDE, + INIT_SETUP_PROPS, + SET_SETUP_NAVIGATION, + SET_SMART_BLOCK, + SET_GHOSTERY_REWARDS, + SET_HUMAN_WEB +} from './OnboardingViewConstants'; + +const initialState = {}; + +function SetupViewReducer(state = initialState, action) { + switch (action.type) { + // Setup View + case GET_SETUP_SHOW_WARNING_OVERRIDE: // Same as SET_SETUP_SHOW_WARNING_OVERRIDE + case SET_SETUP_SHOW_WARNING_OVERRIDE: { + const { setup_show_warning_override } = action.data; + return { ...state, setup: { ...state.setup, setup_show_warning_override } }; + } + case INIT_SETUP_PROPS: { + const { + navigation, + setup_show_warning_override, + blockingPolicy, + enable_anti_tracking, + enable_ad_block, + enable_smart_block, + enable_ghostery_rewards, + enable_human_web, + } = action.data; + const { + activeIndex, + hrefPrev, + hrefNext, + hrefDone, + textPrev, + textNext, + textDone, + } = navigation; + return { + ...state, + setup: { + navigation: { + activeIndex, + hrefPrev, + hrefNext, + hrefDone, + textPrev, + textNext, + textDone, + }, + setup_show_warning_override, + blockingPolicy, + enable_anti_tracking, + enable_ad_block, + enable_smart_block, + enable_ghostery_rewards, + enable_human_web, + } + }; + } + case SET_SETUP_NAVIGATION: { + const { + activeIndex, + hrefPrev, + hrefNext, + hrefDone, + textPrev, + textNext, + textDone, + } = action.data; + return { + ...state, + setup: { + ...state.setup, + navigation: { + activeIndex, + hrefPrev, + hrefNext, + hrefDone, + textPrev, + textNext, + textDone, + } + } + }; + } + + // Block Settings View + case SET_AD_BLOCK: { + const { enable_ad_block } = action.data; + return { ...state, setup: { ...state.setup, enable_ad_block } }; + } + case SET_TRACKER_BLOCKING_POLICY: { + const { blockingPolicy } = action.data; + return { ...state, setup: { ...state.setup, blockingPolicy } }; + } + case SET_ANTI_TRACKING: { + const { enable_anti_tracking } = action.data; + return { ...state, setup: { ...state.setup, enable_anti_tracking } }; + } + + case SET_SMART_BLOCK: { + const { enable_smart_block } = action.data; + return { ...state, setup: { ...state.setup, enable_smart_block } }; + } + case SET_GHOSTERY_REWARDS: { + const { enable_ghostery_rewards } = action.data; + return { ...state, setup: { ...state.setup, enable_ghostery_rewards } }; + } + + // Setup Human Web View + case SET_HUMAN_WEB: { + const { enable_human_web } = action.data; + return { ...state, setup: { ...state.setup, enable_human_web } }; + } + + default: return state; + } +} + +export default SetupViewReducer; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/index.js b/app/ghostery-browser-hub/Views/OnboardingView/index.js new file mode 100644 index 000000000..31b1cd587 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingView/index.js @@ -0,0 +1,91 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Onboarding View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { withRouter } from 'react-router-dom'; + +import OnboardingViewContainer from './OnboardingViewContainer'; +import OnboardingViewReducer from './OnboardingViewReducer'; +import * as MainViewActions from './MainViewActions'; +import setBlockingPolicy from '../OnboardingViews/BlockingView/BlockingViewActions'; +import { + setAntiTracking, + setAdBlock, + setSmartBlocking, + setGhosteryRewards +} from '../OnboardingViews/AntiSuiteView/AntiSuiteViewActions'; +import setHumanWeb from '../OnboardingViews/SetupHumanWebView/SetupHumanWebViewActions'; +import setSetupComplete from '../OnboardingViews/SetupDoneView/SetupDoneViewActions'; + +/** + * Map redux store state properties to the component's own properties. + * @param {Object} state entire Redux store's state + * @return {function} this function returns a plain object, which will be merged into the component's props + * @memberof HubContainers + */ +const mapStateToProps = state => ({ ...state.setup, ...state.account }); + +/** + * Bind the component's action creators using Redux's bindActionCreators. + * @param {function} dispatch redux store method which dispatches actions + * @return {function} to be used as an argument in redux connect call + * @memberof SetupContainers + */ +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ + ...SetupViewActions, + setBlockingPolicy, + setAntiTracking, + setAdBlock, + setSmartBlocking, + setGhosteryRewards, + setHumanWeb, + setSetupComplete + }, dispatch), +}); + +export const reducer = MainViewReducer; + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MainViewContainer)); + +import LogInViewContainer from './LogInViewContainer'; +import { login, getUser, getUserSettings } from '../../../Account/AccountActions'; +import { getTheme } from '../../../panel/actions/PanelActions'; +import { setToast } from '../AppView/AppViewActions'; + +/** + * Map redux store state properties to the component's own properties. + * @param {Object} state entire Redux store's state + * @return {function} this function returns a plain object, which will be merged into the component's props + * @memberof HubContainers + */ +const mapStateToProps = state => ({ ...state.account }); + +/** + * Bind the component's action creators using Redux's bindActionCreators. + * @param {function} dispatch redux store method which dispatches actions + * @return {function} to be used as an argument in redux connect call + * @memberof SetupContainers + */ +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ + setToast, + login, + getUser, + getUserSettings, + getTheme + }, dispatch), +}); + + diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index c1438c0a5..c1c84e652 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -22,7 +22,7 @@ import { import thunk from 'redux-thunk'; import { reducer as app } from './Views/AppView'; -import { reducer as main } from './Views/MainView'; +import { reducer as main } from './Views/OnboardingView'; import account from '../Account/AccountReducer'; import settings from '../panel/reducers/settings'; diff --git a/app/ghostery-browser-hub/index.jsx b/app/ghostery-browser-hub/index.jsx index 813c325d8..c713cc85a 100644 --- a/app/ghostery-browser-hub/index.jsx +++ b/app/ghostery-browser-hub/index.jsx @@ -20,7 +20,7 @@ import createStore from './createStore'; // Containers import AppView from './Views/AppView'; -import MainView from './Views/MainView'; +import OnboardingView from './Views/OnboardingView'; const store = createStore(); @@ -28,7 +28,7 @@ ReactDOM.render( ( - + ), document.getElementById('root'), diff --git a/app/hub/Views/HomeView/__tests__/HomeViewActions.test.js b/app/hub/Views/HomeView/__tests__/HomeViewActions.test.js index bc8b85eb4..17a7431da 100644 --- a/app/hub/Views/HomeView/__tests__/HomeViewActions.test.js +++ b/app/hub/Views/HomeView/__tests__/HomeViewActions.test.js @@ -35,7 +35,7 @@ utils.sendMessageInPromise = jest.fn((name, message) => new Promise((resolve, re } })); -describe('app/hub/Views/SetupView/ actions', () => { +describe('app/hub/Views/OnboardingView/ actions', () => { test('getHomeProps action should return correctly', () => { const initialState = {}; const store = mockStore(initialState); diff --git a/app/hub/Views/SetupView/__tests__/SetupView.test.jsx b/app/hub/Views/SetupView/__tests__/SetupView.test.jsx index 8321e856e..8ac8874ba 100644 --- a/app/hub/Views/SetupView/__tests__/SetupView.test.jsx +++ b/app/hub/Views/SetupView/__tests__/SetupView.test.jsx @@ -19,8 +19,8 @@ import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router'; // Mock Necessary Imports -jest.mock('../../SetupViews/SetupHeader', () => props =>
Mock Setup Header
); -jest.mock('../../SetupViews/SetupNavigation', () => props =>
Mock Setup Navigation
); +jest.mock('../../OnboardingViews/SetupHeader', () => props =>
Mock Setup Header
); +jest.mock('../../OnboardingViews/SetupNavigation', () => props =>
Mock Setup Navigation
); // Import Components import SetupView from '../SetupView'; @@ -33,7 +33,7 @@ const ExampleComponent = props => (
example component
); -describe('app/hub/Views/SetupView component', () => { +describe('app/hub/Views/OnboardingView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('setup view is rendered correctly on first route', () => { const initialState = { diff --git a/app/hub/Views/SetupView/__tests__/SetupViewActions.test.js b/app/hub/Views/SetupView/__tests__/SetupViewActions.test.js index 12f374c77..f3921cc2e 100644 --- a/app/hub/Views/SetupView/__tests__/SetupViewActions.test.js +++ b/app/hub/Views/SetupView/__tests__/SetupViewActions.test.js @@ -45,7 +45,7 @@ utils.sendMessageInPromise = jest.fn((name, message) => new Promise((resolve, re } })); -describe('app/hub/Views/SetupView/ actions', () => { +describe('app/hub/Views/OnboardingView/ actions', () => { test('getSetupShowWarningOverride action should return correctly', () => { const initialState = {}; const store = mockStore(initialState); diff --git a/app/hub/Views/SetupView/__tests__/SetupViewContainer.test.jsx b/app/hub/Views/SetupView/__tests__/SetupViewContainer.test.jsx index 163b4ec80..e1b8e5d96 100644 --- a/app/hub/Views/SetupView/__tests__/SetupViewContainer.test.jsx +++ b/app/hub/Views/SetupView/__tests__/SetupViewContainer.test.jsx @@ -18,12 +18,12 @@ import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router'; // Mock Necessary Imports -jest.mock('../../SetupViews/SetupBlockingView', () => props =>
Mock Setup Blocking View
); -jest.mock('../../SetupViews/SetupBlockingDropdown', () => props =>
Mock Setup Blocking Dropdown
); -jest.mock('../../SetupViews/SetupAntiSuiteView', () => props =>
Mock Setup Anti-Suite View
); -jest.mock('../../SetupViews/SetupHumanWebView', () => props =>
Mock Setup Human Web View
); -jest.mock('../../SetupViews/SetupDoneView', () => props =>
Mock Setup Done View
); -jest.mock('../../SetupViews/SetupNavigation', () => props =>
Mock Setup Navigation
); +jest.mock('../../OnboardingViews/SetupBlockingView', () => props =>
Mock Setup Blocking View
); +jest.mock('../../OnboardingViews/SetupBlockingDropdown', () => props =>
Mock Setup Blocking Dropdown
); +jest.mock('../../OnboardingViews/SetupAntiSuiteView', () => props =>
Mock Setup Anti-Suite View
); +jest.mock('../../OnboardingViews/SetupHumanWebView', () => props =>
Mock Setup Human Web View
); +jest.mock('../../OnboardingViews/SetupDoneView', () => props =>
Mock Setup Done View
); +jest.mock('../../OnboardingViews/SetupNavigation', () => props =>
Mock Setup Navigation
); // Import Components import SetupViewContainer from '../SetupViewContainer'; @@ -50,7 +50,7 @@ const actions = { setSetupComplete: () => {}, }; -describe('app/hub/Views/SetupView container', () => { +describe('app/hub/Views/OnboardingView container', () => { describe('Snapshot tests with react-test-renderer', () => { test('setup view container is rendered correctly on the Blocking step', () => { const paths = ['/setup/1', '/setup/1/custom', '/setup/2', '/setup/3', '/setup/4']; diff --git a/app/hub/Views/SetupView/__tests__/SetupViewReducer.test.js b/app/hub/Views/SetupView/__tests__/SetupViewReducer.test.js index 865ec3a7b..8fd93a7d9 100644 --- a/app/hub/Views/SetupView/__tests__/SetupViewReducer.test.js +++ b/app/hub/Views/SetupView/__tests__/SetupViewReducer.test.js @@ -50,7 +50,7 @@ const initialState = Immutable({ }, }); -describe('app/hub/Views/SetupView reducer', () => { +describe('app/hub/Views/OnboardingView reducer', () => { test('initial state is correct', () => { expect(SetupViewReducer(undefined, {})).toEqual({}); }); diff --git a/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/SetupAntiSuiteView.test.jsx b/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/SetupAntiSuiteView.test.jsx index 0099d4fd5..b3bf3ae9f 100644 --- a/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/SetupAntiSuiteView.test.jsx +++ b/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/SetupAntiSuiteView.test.jsx @@ -16,7 +16,7 @@ import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; import SetupAntiSuiteView from '../SetupAntiSuiteView'; -describe('app/hub/Views/SetupViews/SetupAntiSuiteView component', () => { +describe('app/hub/Views/OnboardingViews/SetupAntiSuiteView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('setup anti-suite view is rendered correctly', () => { const initialState = { diff --git a/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/SetupAntiSuiteViewActions.test.js b/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/SetupAntiSuiteViewActions.test.js index 2fe71a39d..4bf75b808 100644 --- a/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/SetupAntiSuiteViewActions.test.js +++ b/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/SetupAntiSuiteViewActions.test.js @@ -48,7 +48,7 @@ utils.sendMessageInPromise = jest.fn((name, message) => new Promise((resolve, re } })); -describe('app/hub/Views/SetupViews/SetupAntiSuiteView actions', () => { +describe('app/hub/Views/OnboardingViews/SetupAntiSuiteView actions', () => { test('setAntiTracking action should return correctly', () => { const initialState = {}; const store = mockStore(initialState); diff --git a/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/SetupBlockingDropdown.test.jsx b/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/SetupBlockingDropdown.test.jsx index ecd35cde4..1214f2e17 100644 --- a/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/SetupBlockingDropdown.test.jsx +++ b/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/SetupBlockingDropdown.test.jsx @@ -20,7 +20,7 @@ import { ToastMessage } from '../../../../../shared-components'; // Mock Necessary Imports jest.mock('../../../../../panel/components/Settings/GlobalBlocking', () => props =>
Mock Global Blocking
); -describe('app/hub/Views/SetupViews/SetupBlockingDropdown component', () => { +describe('app/hub/Views/OnboardingViews/SetupBlockingDropdown component', () => { describe('Snapshot tests with react-test-renderer', () => { test('setup blocking dropdown component is rendered correctly', () => { const initialState = { diff --git a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingView.test.jsx b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingView.test.jsx index fa76c3e6f..5daa97fd4 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingView.test.jsx +++ b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingView.test.jsx @@ -16,7 +16,7 @@ import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; import SetupBlockingView from '../SetupBlockingView'; -describe('app/hub/Views/SetupViews/SetupBlockingView component', () => { +describe('app/hub/Views/OnboardingViews/SetupBlockingView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('setup blocking view is rendered correctly', () => { const initialState = { diff --git a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingViewActions.test.js b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingViewActions.test.js index cfa80c6f0..238b35d84 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingViewActions.test.js +++ b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingViewActions.test.js @@ -31,7 +31,7 @@ utils.sendMessageInPromise = jest.fn((name, message) => new Promise((resolve, re } })); -describe('app/hub/Views/SetupViews/SetupBlockingView actions', () => { +describe('app/hub/Views/OnboardingViews/SetupBlockingView actions', () => { test('setBlockingPolicy action should return correctly', () => { const initialState = {}; const store = mockStore(initialState); diff --git a/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneView.test.jsx b/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneView.test.jsx index b23bc9cc9..826d47ea4 100644 --- a/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneView.test.jsx +++ b/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneView.test.jsx @@ -17,7 +17,7 @@ import { shallow } from 'enzyme'; import { MemoryRouter } from 'react-router'; import SetupDoneView from '../SetupDoneView'; -describe('app/hub/Views/SetupViews/SetupDoneView component', () => { +describe('app/hub/Views/OnboardingViews/SetupDoneView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('setup human web view is rendered correctly', () => { const initialState = { diff --git a/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneViewActions.test.js b/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneViewActions.test.js index 1e8a99a74..e51a5ebe6 100644 --- a/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneViewActions.test.js +++ b/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneViewActions.test.js @@ -31,7 +31,7 @@ utils.sendMessageInPromise = jest.fn((name, message) => new Promise((resolve, re } })); -describe('app/hub/Views/SetupViews/SetupDoneView actions', () => { +describe('app/hub/Views/OnboardingViews/SetupDoneView actions', () => { test('setSetupComplete action should return correctly', () => { const initialState = {}; const store = mockStore(initialState); diff --git a/app/hub/Views/SetupViews/SetupHeader/__tests__/SetupHeader.test.jsx b/app/hub/Views/SetupViews/SetupHeader/__tests__/SetupHeader.test.jsx index e1fe0bbcb..a973feec7 100644 --- a/app/hub/Views/SetupViews/SetupHeader/__tests__/SetupHeader.test.jsx +++ b/app/hub/Views/SetupViews/SetupHeader/__tests__/SetupHeader.test.jsx @@ -16,7 +16,7 @@ import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; import SetupHeader from '../SetupHeader'; -describe('app/hub/Views/SetupViews/SetupHeader component', () => { +describe('app/hub/Views/OnboardingViews/SetupHeader component', () => { describe('Snapshot tests with react-test-renderer', () => { test('setup header is rendered correctly', () => { const initialState = { diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebView.test.jsx b/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebView.test.jsx index 2a9b48bc3..804477b95 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebView.test.jsx +++ b/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebView.test.jsx @@ -16,7 +16,7 @@ import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; import SetupHumanWebView from '../SetupHumanWebView'; -describe('app/hub/Views/SetupViews/SetupHumanWebView component', () => { +describe('app/hub/Views/OnboardingViews/SetupHumanWebView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('setup human web view is rendered correctly', () => { const initialState = { diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebViewActions.test.js b/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebViewActions.test.js index 3b21c84be..acb223581 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebViewActions.test.js +++ b/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebViewActions.test.js @@ -31,7 +31,7 @@ utils.sendMessageInPromise = jest.fn((name, message) => new Promise((resolve, re } })); -describe('app/hub/Views/SetupViews/SetupHumanWebView actions', () => { +describe('app/hub/Views/OnboardingViews/SetupHumanWebView actions', () => { test('setHumanWeb action should return correctly', () => { const initialState = {}; const store = mockStore(initialState); From 3bb394a88e91aba93691d0086520c39087d75780 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Sun, 29 Nov 2020 18:50:38 -0500 Subject: [PATCH 004/113] Add stubs for the various onboarding views. Add top level onboarding container and view. --- _locales/en/messages.json | 15 ++ .../Views/AppView/AppViewConstants.js | 1 - .../Views/OnboardingView/OnboardingView.jsx | 73 +++++++ .../OnboardingViewContainer.jsx | 202 ++++++++++++++++++ .../Views/OnboardingView/index.js | 32 +-- .../Step1_LoginView/LoginView.jsx | 18 ++ .../OnboardingViews/Step1_LoginView/index.js | 16 ++ .../BlockSettingsView.jsx | 18 ++ .../Step2_BlockSettingsView/index.js | 16 ++ .../ChooseDefaultSearchView.jsx | 18 ++ .../Step3_ChooseDefaultSearchView/index.js | 16 ++ .../Step4_ChoosePlanView/ChoosePlanView.jsx | 18 ++ .../Step4_ChoosePlanView/index.js | 16 ++ app/ghostery-browser-hub/createStore.js | 3 +- app/ghostery-browser-hub/index.jsx | 2 +- 15 files changed, 431 insertions(+), 33 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/index.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 041d0b141..77ceba8f9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1734,6 +1734,21 @@ "hub_create_account_toast_error": { "message": "That email address is already in use. Please choose another." }, + "ghostery_browser_hub_onboarding_page_title": { + "message": "Ghostery Browser Hub - Welcome" + }, + "ghostery_browser_hub_onboarding_header_title_login": { + "message": "Ghostery Browser Hub - Login" + }, + "ghostery_browser_hub_onboarding_header_title_block_settings": { + "message": "Ghostery Browser Hub - Block Settings" + }, + "ghostery_browser_hub_onboarding_header_title_search_choice": { + "message": "Ghostery Browser Hub - Search Choice" + }, + "ghostery_browser_hub_onboarding_header_title_plan_choices": { + "message": "Ghostery Browser Hub - Plan Choices" + }, "enable_when_paused": { "message": "To use this function, Resume Ghostery." }, diff --git a/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js b/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js index 3bce0f948..761bc73d7 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js +++ b/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js @@ -15,4 +15,3 @@ // Ghostery Browser Hub App View export const SET_TOAST = 'SET_TOAST'; -export const SEND_PING = 'SEND_PING'; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx new file mode 100644 index 000000000..3d478ae4c --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx @@ -0,0 +1,73 @@ +/** + * Ghostery Browser Hub Onboarding View Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Route } from 'react-router-dom'; + +// Components +import SetupNavigation from '../SetupViews/SetupNavigation'; +import SetupHeader from '../SetupViews/SetupHeader'; + +/** + * A Functional React component for rendering the Onboarding View + * @return {JSX} JSX for rendering the Onboarding View of the Ghostery Browser Hub app + * @memberof GhosteryBrowserHubViews + */ +const OnboardingView = (props) => { + const { sendMountActions, steps } = props; + + return ( +
+
+ {steps.map(step => ( + ( +
+ + +
+ )} + /> + ))} + {extraRoutes.map(route => ( + } + /> + ))} +
+ + +
+ ); +}; + +// PropTypes ensure we pass required props of the correct type +OnboardingView.propTypes = { + steps: PropTypes.arrayOf(PropTypes.shape({ + index: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + bodyComponent: PropTypes.shape.isRequired, + headerProps: PropTypes.shape({ + title: PropTypes.string.isRequired, + titleImage: PropTypes.string.isRequired, + }).isRequired, + })).isRequired, + sendMountActions: PropTypes.bool.isRequired, +}; + +export default OnboardingView; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx new file mode 100644 index 000000000..21552cdbc --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -0,0 +1,202 @@ +/** + * Ghostery Browser Hub Onboarding View Container + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import OnboardingView from './OnboardingView'; +import { BLOCKING_POLICY_RECOMMENDED } from './OnboardingViewConstants'; + +// Component Views +import LoginView from '../OnboardingViews/Step1_LoginView'; +import BlockSettingsView from '../OnboardingViews/Step2_BlockSettingsView'; +import ChooseDefaultSearchView from '../OnboardingViews/Step3_ChooseDefaultSearchView'; +import ChoosePlanView from '../OnboardingViews/Step4_ChoosePlanView'; + +/** + * @class Implement the Onboarding View for the Ghostery Browser Hub + * @extends Component + * @memberof GhosteryBrowserHubContainers + */ +class OnboardingViewContainer extends Component { + constructor(props) { + super(props); + this.state = { + sendMountActions: false, + }; + + const { history } = this.props; + if (!props.preventRedirect) { + history.push('/onboarding/1'); + } + + // TODO verify what document title we should use + const title = t('ghostery_browser_hub_onboarding_page_title'); + window.document.title = title; + + // TODO modify this as needed + const { actions, setup } = this.props; + actions.setSetupStep({ setup_step: 7 }); + actions.initSetupProps(setup); + + // TODO modify this as needed + const { origin, pathname, hash } = window.location; + window.history.pushState({}, '', `${origin}${pathname}${hash}`); + this._setDefaultSettings(); + } + + /** + * Function to persist the default settings to background + */ + // TODO modify this as needed + _setDefaultSettings() { + this.setState({ sendMountActions: true }); + const { actions } = this.props; + actions.setSetupStep({ setup_step: 8 }); + actions.setBlockingPolicy({ blockingPolicy: BLOCKING_POLICY_RECOMMENDED }); + actions.setAntiTracking({ enable_anti_tracking: true }); + actions.setAdBlock({ enable_ad_block: true }); + actions.setSmartBlocking({ enable_smart_block: true }); + actions.setHumanWeb({ enable_human_web: true }); + } + + /** + * React's required render function. Returns JSX + * @return {JSX} JSX for rendering the Onboarding View of the Ghostery Browser Hub app + */ + render() { + const { sendMountActions } = this.state; + // TODO set the correct title strings and titleImages + const steps = [ + { + index: 1, + path: '/onboarding/1', + bodyComponent: LoginView, + headerProps: { + title: t('ghostery_browser_hub_onboarding_header_title_login'), + titleImage: '/app/images/hub/setup/ghosty-block-all.svg', + }, + }, + { + index: 2, + path: '/onboarding/2', + bodyComponent: BlockSettingsView, + headerProps: { + title: t('ghostery_browser_hub_onboarding_header_title_block_settings'), + titleImage: '/app/images/hub/setup/ghosty-shield-stop-lightbulb.svg', + }, + }, + { + index: 3, + path: '/onboarding/3', + bodyComponent: ChooseDefaultSearchView, + headerProps: { + title: t('ghostery_browser_hub_onboarding_header_title_search_choice'), + titleImage: '/app/images/hub/setup/ghosty-human-web.svg', + }, + }, + { + index: 4, + path: '/onboarding/4', + bodyComponent: ChoosePlanView, + headerProps: { + title: t('ghostery_browser_hub_onboarding_header_title_plan_choices'), + titleImage: '/app/images/hub/setup/ghosty-check-wrench.svg', + }, + }, + ]; + + return ( +
+ +
+ ); + } +} + +// PropTypes ensure we pass required props of the correct type +// Note: isRequired is not needed when a prop has a default value +OnboardingViewContainer.propTypes = { + preventRedirect: PropTypes.bool, + setup: PropTypes.shape({ + navigation: PropTypes.shape({ + activeIndex: PropTypes.number, + hrefPrev: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), + hrefNext: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), + hrefDone: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), + textPrev: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), + textNext: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), + textDone: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), + }), + setup_show_warning_override: PropTypes.bool, + blockingPolicy: PropTypes.string, + enable_anti_tracking: PropTypes.bool, + enable_ad_block: PropTypes.bool, + enable_smart_block: PropTypes.bool, + enable_human_web: PropTypes.bool, + }), + actions: PropTypes.shape({ + getSetupShowWarningOverride: PropTypes.func.isRequired, + setSetupShowWarningOverride: PropTypes.func.isRequired, + initSetupProps: PropTypes.func.isRequired, + setSetupStep: PropTypes.func.isRequired, + setSetupNavigation: PropTypes.func.isRequired, + setBlockingPolicy: PropTypes.func.isRequired, + setAntiTracking: PropTypes.func.isRequired, + setAdBlock: PropTypes.func.isRequired, + setSmartBlocking: PropTypes.func.isRequired, + setHumanWeb: PropTypes.func.isRequired, + setSetupComplete: PropTypes.func.isRequired, + }).isRequired, +}; + +// Default props used throughout the Onboarding flow +OnboardingViewContainer.defaultProps = { + preventRedirect: false, + setup: { + navigation: { + activeIndex: 0, + hrefPrev: false, + hrefNext: false, + hrefDone: false, + textPrev: false, + textNext: false, + textDone: false, + }, + setup_show_warning_override: true, + blockingPolicy: BLOCKING_POLICY_RECOMMENDED, + enable_anti_tracking: true, + enable_ad_block: true, + enable_smart_block: true, + enable_human_web: true, + }, +}; + +export default OnboardingViewContainer; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/index.js b/app/ghostery-browser-hub/Views/OnboardingView/index.js index 31b1cd587..208024fb9 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingView/index.js @@ -55,37 +55,9 @@ const mapDispatchToProps = dispatch => ({ }, dispatch), }); -export const reducer = MainViewReducer; +export const reducer = OnboardingViewReducer; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MainViewContainer)); +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(OnboardingViewContainer)); -import LogInViewContainer from './LogInViewContainer'; -import { login, getUser, getUserSettings } from '../../../Account/AccountActions'; -import { getTheme } from '../../../panel/actions/PanelActions'; -import { setToast } from '../AppView/AppViewActions'; - -/** - * Map redux store state properties to the component's own properties. - * @param {Object} state entire Redux store's state - * @return {function} this function returns a plain object, which will be merged into the component's props - * @memberof HubContainers - */ -const mapStateToProps = state => ({ ...state.account }); - -/** - * Bind the component's action creators using Redux's bindActionCreators. - * @param {function} dispatch redux store method which dispatches actions - * @return {function} to be used as an argument in redux connect call - * @memberof SetupContainers - */ -const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators({ - setToast, - login, - getUser, - getUserSettings, - getTheme - }, dispatch), -}); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx new file mode 100644 index 000000000..76cf70f84 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx @@ -0,0 +1,18 @@ +/** + * Ghostery Browser Hub Login View Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; + +const LoginView = () => 'Step 1: Login View'; + +export default LoginView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/index.js new file mode 100644 index 000000000..d059cb767 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Onboarding Login View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import LoginView from './LoginView'; + +export default LoginView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx new file mode 100644 index 000000000..f56f4eb1f --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -0,0 +1,18 @@ +/** + * Ghostery Browser Hub Block Settings View Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; + +const BlockSettingsView = () => 'Step 2: Block Settings View'; + +export default BlockSettingsView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js new file mode 100644 index 000000000..ec251f216 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Onboarding Block Settings View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import BlockSettingsView from './BlockSettingsView'; + +export default BlockSettingsView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx new file mode 100644 index 000000000..412e5c035 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -0,0 +1,18 @@ +/** + * Ghostery Browser Hub Choose Default Search View Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; + +const ChooseDefaultSearchView = () => 'Step 3: Choose Default Search View'; + +export default ChooseDefaultSearchView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js new file mode 100644 index 000000000..1b5f2141f --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Choose Default Search View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import ChooseDefaultSearchView from './ChooseDefaultSearchView'; + +export default ChooseDefaultSearchView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx new file mode 100644 index 000000000..4debf0afa --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -0,0 +1,18 @@ +/** + * Ghostery Browser Hub Choose Plan View Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; + +const ChoosePlanView = () => 'Step 4: Choose Plan View'; + +export default ChoosePlanView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.js new file mode 100644 index 000000000..72ed11332 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Choose Plan View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import ChoosePlanView from './ChoosePlanView'; + +export default ChoosePlanView; diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index c1c84e652..23415177a 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -22,12 +22,13 @@ import { import thunk from 'redux-thunk'; import { reducer as app } from './Views/AppView'; -import { reducer as main } from './Views/OnboardingView'; +import { reducer as onboarding } from './Views/OnboardingView'; import account from '../Account/AccountReducer'; import settings from '../panel/reducers/settings'; const reducer = combineReducers({ app, + onboarding, account, settings, }); diff --git a/app/ghostery-browser-hub/index.jsx b/app/ghostery-browser-hub/index.jsx index c713cc85a..d9c998c9e 100644 --- a/app/ghostery-browser-hub/index.jsx +++ b/app/ghostery-browser-hub/index.jsx @@ -18,7 +18,7 @@ import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import createStore from './createStore'; -// Containers +// Views import AppView from './Views/AppView'; import OnboardingView from './Views/OnboardingView'; From f59a5dde0f23d8a591fd9bcd8a64d4ed039b2430 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 2 Dec 2020 11:05:04 -0500 Subject: [PATCH 005/113] Continued work on fleshing out the ghostery browser hub react app --- .../Views/AppView/AppView.jsx | 2 +- .../Views/AppView/AppViewConstants.js | 1 + .../Views/OnboardingView/OnboardingView.jsx | 18 ---- .../OnboardingView/OnboardingViewActions.js | 46 ++++++++ .../OnboardingView/OnboardingViewConstants.js | 16 +-- .../OnboardingViewContainer.jsx | 18 ---- .../OnboardingView/OnboardingViewReducer.js | 34 ++---- .../Views/OnboardingView/index.js | 13 +-- .../Step1_LoginView/LoginView.jsx | 2 +- .../BlockSettingsView.jsx | 2 +- .../ChooseDefaultSearchView.jsx | 2 +- .../Step4_ChoosePlanView/ChoosePlanView.jsx | 2 +- app/ghostery-browser-hub/createStore.js | 31 +----- .../actions/AntiSuiteViewActions.js | 58 ++++++++++ .../actions/SetupDoneViewActions.js | 28 +++++ app/shared-hub/constants/SetupConstants.js | 34 ++++++ app/shared-hub/reducers/SetupReducer.js | 102 ++++++++++++++++++ app/shared-hub/utils/index.js | 52 +++++++++ app/templates/hub.html | 13 ++- app/templates/panel.html | 11 +- 20 files changed, 355 insertions(+), 130 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewActions.js create mode 100644 app/shared-hub/actions/AntiSuiteViewActions.js create mode 100644 app/shared-hub/actions/SetupDoneViewActions.js create mode 100644 app/shared-hub/constants/SetupConstants.js create mode 100644 app/shared-hub/reducers/SetupReducer.js create mode 100644 app/shared-hub/utils/index.js diff --git a/app/ghostery-browser-hub/Views/AppView/AppView.jsx b/app/ghostery-browser-hub/Views/AppView/AppView.jsx index 439d3f462..e0c65d84e 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppView.jsx +++ b/app/ghostery-browser-hub/Views/AppView/AppView.jsx @@ -11,7 +11,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { ToastMessage } from '../../../shared-components'; diff --git a/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js b/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js index 761bc73d7..3bce0f948 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js +++ b/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js @@ -15,3 +15,4 @@ // Ghostery Browser Hub App View export const SET_TOAST = 'SET_TOAST'; +export const SEND_PING = 'SEND_PING'; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx index 3d478ae4c..4bad14783 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx @@ -15,10 +15,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Route } from 'react-router-dom'; -// Components -import SetupNavigation from '../SetupViews/SetupNavigation'; -import SetupHeader from '../SetupViews/SetupHeader'; - /** * A Functional React component for rendering the Onboarding View * @return {JSX} JSX for rendering the Onboarding View of the Ghostery Browser Hub app @@ -36,22 +32,12 @@ const OnboardingView = (props) => { path={step.path} render={() => (
-
)} /> ))} - {extraRoutes.map(route => ( - } - /> - ))} - - ); }; @@ -62,10 +48,6 @@ OnboardingView.propTypes = { index: PropTypes.number.isRequired, path: PropTypes.string.isRequired, bodyComponent: PropTypes.shape.isRequired, - headerProps: PropTypes.shape({ - title: PropTypes.string.isRequired, - titleImage: PropTypes.string.isRequired, - }).isRequired, })).isRequired, sendMountActions: PropTypes.bool.isRequired, }; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewActions.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewActions.js new file mode 100644 index 000000000..f1dc452b0 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewActions.js @@ -0,0 +1,46 @@ +/** + * Ghostery Browser Hub Onboarding View Action creators + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { log, sendMessageInPromise } from '../../../hub/utils'; +import { + INIT_ONBOARDING_PROPS, + SET_ONBOARDING_STEP, + SET_ONBOARDING_NAVIGATION +} from './OnboardingViewConstants'; + +export function initOnboardingProps(data) { + return { + type: INIT_ONBOARDING_PROPS, + data, + }; +} + +export function setOnboardingStep(actionData) { + return function(dispatch) { + return sendMessageInPromise(SET_ONBOARDING_STEP, actionData).then((data) => { + dispatch({ + type: SET_ONBOARDING_STEP, + data, + }); + }).catch((err) => { + log('onboardingView Action setOnboardingStep Error', err); + }); + }; +} + +export function setOnboardingNavigation(data) { + return { + type: SET_ONBOARDING_NAVIGATION, + data, + }; +} diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js index 48f0db769..67e6937b1 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js @@ -11,28 +11,20 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -// Setup View -export const GET_SETUP_SHOW_WARNING_OVERRIDE = 'GET_SETUP_SHOW_WARNING_OVERRIDE'; -export const SET_SETUP_SHOW_WARNING_OVERRIDE = 'SET_SETUP_SHOW_WARNING_OVERRIDE'; -export const INIT_SETUP_PROPS = 'INIT_SETUP_PROPS'; -export const SET_SETUP_STEP = 'SET_SETUP_STEP'; -export const SET_SETUP_NAVIGATION = 'SET_SETUP_NAVIGATION'; +export const INIT_ONBOARDING_PROPS = 'INIT_ONBOARDING_PROPS'; +export const SET_ONBOARDING_STEP = 'SET_ONBOARDING_STEP'; +export const SET_ONBOARDING_NAVIGATION = 'SET_ONBOARDING_NAVIGATION'; -// Setup Blocking export const SET_TRACKER_BLOCKING_POLICY = 'SET_TRACKER_BLOCKING_POLICY'; export const BLOCKING_POLICY_RECOMMENDED = 'BLOCKING_POLICY_RECOMMENDED'; export const BLOCKING_POLICY_NOTHING = 'BLOCKING_POLICY_NOTHING'; export const BLOCKING_POLICY_EVERYTHING = 'BLOCKING_POLICY_EVERYTHING'; export const BLOCKING_POLICY_CUSTOM = 'BLOCKING_POLICY_CUSTOM'; -// Setup Anti-Suite export const SET_ANTI_TRACKING = 'SET_ANTI_TRACKING'; export const SET_AD_BLOCK = 'SET_AD_BLOCK'; export const SET_SMART_BLOCK = 'SET_SMART_BLOCK'; -export const SET_GHOSTERY_REWARDS = 'SET_GHOSTERY_REWARDS'; -// Setup Human Web export const SET_HUMAN_WEB = 'SET_HUMAN_WEB'; -// Setup Done -export const SET_SETUP_COMPLETE = 'SET_SETUP_COMPLETE'; +export const SET_ONBOARDING_COMPLETE = 'SET_ONBOARDING_COMPLETE'; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index 21552cdbc..c22767819 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -66,7 +66,6 @@ class OnboardingViewContainer extends Component { actions.setAntiTracking({ enable_anti_tracking: true }); actions.setAdBlock({ enable_ad_block: true }); actions.setSmartBlocking({ enable_smart_block: true }); - actions.setHumanWeb({ enable_human_web: true }); } /** @@ -75,43 +74,26 @@ class OnboardingViewContainer extends Component { */ render() { const { sendMountActions } = this.state; - // TODO set the correct title strings and titleImages const steps = [ { index: 1, path: '/onboarding/1', bodyComponent: LoginView, - headerProps: { - title: t('ghostery_browser_hub_onboarding_header_title_login'), - titleImage: '/app/images/hub/setup/ghosty-block-all.svg', - }, }, { index: 2, path: '/onboarding/2', bodyComponent: BlockSettingsView, - headerProps: { - title: t('ghostery_browser_hub_onboarding_header_title_block_settings'), - titleImage: '/app/images/hub/setup/ghosty-shield-stop-lightbulb.svg', - }, }, { index: 3, path: '/onboarding/3', bodyComponent: ChooseDefaultSearchView, - headerProps: { - title: t('ghostery_browser_hub_onboarding_header_title_search_choice'), - titleImage: '/app/images/hub/setup/ghosty-human-web.svg', - }, }, { index: 4, path: '/onboarding/4', bodyComponent: ChoosePlanView, - headerProps: { - title: t('ghostery_browser_hub_onboarding_header_title_plan_choices'), - titleImage: '/app/images/hub/setup/ghosty-check-wrench.svg', - }, }, ]; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js index 060791045..8a4ab678b 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js @@ -15,27 +15,17 @@ import { SET_AD_BLOCK, SET_TRACKER_BLOCKING_POLICY, SET_ANTI_TRACKING, - - GET_SETUP_SHOW_WARNING_OVERRIDE, - SET_SETUP_SHOW_WARNING_OVERRIDE, - INIT_SETUP_PROPS, - SET_SETUP_NAVIGATION, + INIT_ONBOARDING_PROPS, + SET_ONBOARDING_NAVIGATION, SET_SMART_BLOCK, - SET_GHOSTERY_REWARDS, SET_HUMAN_WEB } from './OnboardingViewConstants'; const initialState = {}; -function SetupViewReducer(state = initialState, action) { +function OnboardingViewReducer(state = initialState, action) { switch (action.type) { - // Setup View - case GET_SETUP_SHOW_WARNING_OVERRIDE: // Same as SET_SETUP_SHOW_WARNING_OVERRIDE - case SET_SETUP_SHOW_WARNING_OVERRIDE: { - const { setup_show_warning_override } = action.data; - return { ...state, setup: { ...state.setup, setup_show_warning_override } }; - } - case INIT_SETUP_PROPS: { + case INIT_ONBOARDING_PROPS: { const { navigation, setup_show_warning_override, @@ -77,7 +67,7 @@ function SetupViewReducer(state = initialState, action) { } }; } - case SET_SETUP_NAVIGATION: { + case SET_ONBOARDING_NAVIGATION: { const { activeIndex, hrefPrev, @@ -104,7 +94,6 @@ function SetupViewReducer(state = initialState, action) { }; } - // Block Settings View case SET_AD_BLOCK: { const { enable_ad_block } = action.data; return { ...state, setup: { ...state.setup, enable_ad_block } }; @@ -117,24 +106,13 @@ function SetupViewReducer(state = initialState, action) { const { enable_anti_tracking } = action.data; return { ...state, setup: { ...state.setup, enable_anti_tracking } }; } - case SET_SMART_BLOCK: { const { enable_smart_block } = action.data; return { ...state, setup: { ...state.setup, enable_smart_block } }; } - case SET_GHOSTERY_REWARDS: { - const { enable_ghostery_rewards } = action.data; - return { ...state, setup: { ...state.setup, enable_ghostery_rewards } }; - } - - // Setup Human Web View - case SET_HUMAN_WEB: { - const { enable_human_web } = action.data; - return { ...state, setup: { ...state.setup, enable_human_web } }; - } default: return state; } } -export default SetupViewReducer; +export default OnboardingViewReducer; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/index.js b/app/ghostery-browser-hub/Views/OnboardingView/index.js index 208024fb9..e4c2d374f 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingView/index.js @@ -17,15 +17,12 @@ import { withRouter } from 'react-router-dom'; import OnboardingViewContainer from './OnboardingViewContainer'; import OnboardingViewReducer from './OnboardingViewReducer'; -import * as MainViewActions from './MainViewActions'; -import setBlockingPolicy from '../OnboardingViews/BlockingView/BlockingViewActions'; +import * as OnboardingViewActions from './OnboardingViewActions'; import { - setAntiTracking, setAdBlock, + setAntiTracking, setSmartBlocking, - setGhosteryRewards -} from '../OnboardingViews/AntiSuiteView/AntiSuiteViewActions'; -import setHumanWeb from '../OnboardingViews/SetupHumanWebView/SetupHumanWebViewActions'; +} from '../../../shared-hub/actions/AntiSuiteViewActions'; import setSetupComplete from '../OnboardingViews/SetupDoneView/SetupDoneViewActions'; /** @@ -44,13 +41,11 @@ const mapStateToProps = state => ({ ...state.setup, ...state.account }); */ const mapDispatchToProps = dispatch => ({ actions: bindActionCreators({ - ...SetupViewActions, + ...OnboardingViewActions, setBlockingPolicy, setAntiTracking, setAdBlock, setSmartBlocking, - setGhosteryRewards, - setHumanWeb, setSetupComplete }, dispatch), }); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx index 76cf70f84..29806db95 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx @@ -13,6 +13,6 @@ import React from 'react'; -const LoginView = () => 'Step 1: Login View'; +const LoginView = () =>

Step 1: Login View

; export default LoginView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index f56f4eb1f..e3e30de9f 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -13,6 +13,6 @@ import React from 'react'; -const BlockSettingsView = () => 'Step 2: Block Settings View'; +const BlockSettingsView = () =>

Step 2: Block Settings View

; export default BlockSettingsView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 412e5c035..dd0f23058 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -13,6 +13,6 @@ import React from 'react'; -const ChooseDefaultSearchView = () => 'Step 3: Choose Default Search View'; +const ChooseDefaultSearchView = () =>

Step 3: Choose Default Search View

; export default ChooseDefaultSearchView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 4debf0afa..abaddba4d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -13,6 +13,6 @@ import React from 'react'; -const ChoosePlanView = () => 'Step 4: Choose Plan View'; +const ChoosePlanView = () =>

Step 4: Choose Plan View

; export default ChoosePlanView; diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index 23415177a..ddb5ff023 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -13,37 +13,10 @@ * @namespace GhosteryBrowserHubReactStore */ -import { - applyMiddleware, - compose, - combineReducers, - createStore -} from 'redux'; -import thunk from 'redux-thunk'; - +import createStoreFactory from '../shared-hub/utils/index'; import { reducer as app } from './Views/AppView'; import { reducer as onboarding } from './Views/OnboardingView'; import account from '../Account/AccountReducer'; import settings from '../panel/reducers/settings'; -const reducer = combineReducers({ - app, - onboarding, - account, - settings, -}); - -/** - * Build store using combined reducers and middleware - * @return {Object} - * @memberof HubReactStore - */ -export default function() { - return createStore( - reducer, - compose( - applyMiddleware(thunk), - window.devToolsExtension ? window.devToolsExtension() : f => f - ), - ); -} +export default () => createStoreFactory({app, onboarding, account, settings}); diff --git a/app/shared-hub/actions/AntiSuiteViewActions.js b/app/shared-hub/actions/AntiSuiteViewActions.js new file mode 100644 index 000000000..5b3bf9ea0 --- /dev/null +++ b/app/shared-hub/actions/AntiSuiteViewActions.js @@ -0,0 +1,58 @@ +/** + * Anti Suite Action Creators used by Hub's Setup and Ghostery Browser Hub's Onboarding + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { log, sendMessageInPromise } from '../utils'; +import { + SET_ANTI_TRACKING, + SET_AD_BLOCK, + SET_SMART_BLOCK +} from '../constants/SetupConstants'; + +export function setAntiTracking(actionData) { + return function(dispatch) { + return sendMessageInPromise(SET_ANTI_TRACKING, actionData).then((data) => { + dispatch({ + type: SET_ANTI_TRACKING, + data, + }); + }).catch((err) => { + log('setAntiTracking setup action error', err); + }); + }; +} + +export function setAdBlock(actionData) { + return function(dispatch) { + return sendMessageInPromise(SET_AD_BLOCK, actionData).then((data) => { + dispatch({ + type: SET_AD_BLOCK, + data, + }); + }).catch((err) => { + log('setAdBlock setup action error', err); + }); + }; +} + +export function setSmartBlocking(actionData) { + return function(dispatch) { + return sendMessageInPromise(SET_SMART_BLOCK, actionData).then((data) => { + dispatch({ + type: SET_SMART_BLOCK, + data, + }); + }).catch((err) => { + log('setSmartBlocking setup action error', err); + }); + }; +} diff --git a/app/shared-hub/actions/SetupDoneViewActions.js b/app/shared-hub/actions/SetupDoneViewActions.js new file mode 100644 index 000000000..20d283720 --- /dev/null +++ b/app/shared-hub/actions/SetupDoneViewActions.js @@ -0,0 +1,28 @@ +/** + * Setup Done View Action creators + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { log, sendMessageInPromise } from '../utils'; +import { SET_SETUP_COMPLETE } from '../../SetupView/SetupViewConstants'; + +export default function setSetupComplete(actionData) { + return function(dispatch) { + return sendMessageInPromise(SET_SETUP_COMPLETE, actionData).then((data) => { + dispatch({ + type: SET_SETUP_COMPLETE, + data, + }); + }).catch((err) => { + log('setupDone Action setSetupComplete Error', err); + }); + }; +} diff --git a/app/shared-hub/constants/SetupConstants.js b/app/shared-hub/constants/SetupConstants.js new file mode 100644 index 000000000..3c3558195 --- /dev/null +++ b/app/shared-hub/constants/SetupConstants.js @@ -0,0 +1,34 @@ +/** + * Setup Constants shared by the standard and Ghostery Browser Hubs + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +// Setup View in Hub / Onboarding View in Ghostery Browser Hub +export const INIT_SETUP_PROPS = 'INIT_SETUP_PROPS'; +export const SET_SETUP_STEP = 'SET_SETUP_STEP'; + +// Setup Blocking in Hub / Block Settings in Ghostery Browser Hub +export const SET_BLOCKING_POLICY = 'SET_BLOCKING_POLICY'; +export const BLOCKING_POLICY_RECOMMENDED = 'BLOCKING_POLICY_RECOMMENDED'; +export const BLOCKING_POLICY_NOTHING = 'BLOCKING_POLICY_NOTHING'; +export const BLOCKING_POLICY_EVERYTHING = 'BLOCKING_POLICY_EVERYTHING'; +export const BLOCKING_POLICY_CUSTOM = 'BLOCKING_POLICY_CUSTOM'; + +// Setup Anti-Suite in Hub / Block Settings in Ghostery Browser Hub +export const SET_ANTI_TRACKING = 'SET_ANTI_TRACKING'; +export const SET_AD_BLOCK = 'SET_AD_BLOCK'; +export const SET_SMART_BLOCK = 'SET_SMART_BLOCK'; + +// Setup Human Web in Hub +export const SET_HUMAN_WEB = 'SET_HUMAN_WEB'; + +// Setup Done +export const SET_SETUP_COMPLETE = 'SET_SETUP_COMPLETE'; diff --git a/app/shared-hub/reducers/SetupReducer.js b/app/shared-hub/reducers/SetupReducer.js new file mode 100644 index 000000000..de88bf26a --- /dev/null +++ b/app/shared-hub/reducers/SetupReducer.js @@ -0,0 +1,102 @@ +/** + * Reducer used throughout the Setup/Onboarding flow in the regular and Ghostery Browser Hubs + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { + INIT_SETUP_PROPS, + SET_BLOCKING_POLICY, + SET_ANTI_TRACKING, + SET_AD_BLOCK, + SET_SMART_BLOCK, +} from '../constants/SetupConstants'; + +const initialState = {}; + +function SetupReducer(state = initialState, action) { + switch (action.type) { + case INIT_SHARED_SETUP_PROPS: { + const { + blockingPolicy, + enable_anti_tracking, + enable_ad_block, + enable_smart_block, + } = action.data; + return { + ...state, + setup: { + setup_show_warning_override, + blockingPolicy, + enable_anti_tracking, + enable_ad_block, + enable_smart_block, + enable_human_web, + } + }; + } + case SET_SETUP_NAVIGATION: { + const { + activeIndex, + hrefPrev, + hrefNext, + hrefDone, + textPrev, + textNext, + textDone, + } = action.data; + return { + ...state, + setup: { + ...state.setup, + navigation: { + activeIndex, + hrefPrev, + hrefNext, + hrefDone, + textPrev, + textNext, + textDone, + } + } + }; + } + + // Setup Blocking View + case SET_BLOCKING_POLICY: { + const { blockingPolicy } = action.data; + return { ...state, setup: { ...state.setup, blockingPolicy } }; + } + + // Setup Anti-Suite View + case SET_ANTI_TRACKING: { + const { enable_anti_tracking } = action.data; + return { ...state, setup: { ...state.setup, enable_anti_tracking } }; + } + case SET_AD_BLOCK: { + const { enable_ad_block } = action.data; + return { ...state, setup: { ...state.setup, enable_ad_block } }; + } + case SET_SMART_BLOCK: { + const { enable_smart_block } = action.data; + return { ...state, setup: { ...state.setup, enable_smart_block } }; + } + + // Setup Human Web View + case SET_HUMAN_WEB: { + const { enable_human_web } = action.data; + return { ...state, setup: { ...state.setup, enable_human_web } }; + } + + default: return state; + } +} + +export default SetupReducer; diff --git a/app/shared-hub/utils/index.js b/app/shared-hub/utils/index.js new file mode 100644 index 000000000..af32ca3bb --- /dev/null +++ b/app/shared-hub/utils/index.js @@ -0,0 +1,52 @@ +/** + * React Utility Imports + * Import files from app to prevent duplicate code + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +// Imports utilities from elsewhere in the codebase to reduce duplicate code +import { + applyMiddleware, + compose, + combineReducers, + createStore +} from 'redux'; +import thunk from 'redux-thunk'; + +import { log } from '../../../src/utils/common'; +import { sendMessage as importedSM, sendMessageInPromise as importedSMIP } from '../../panel/utils/msg'; + +const sendMessageInPromise = function(name, message) { + return importedSMIP(name, message, 'ghostery-hub'); +}; + +const sendMessage = function(name, message) { + return importedSM(name, message, 'ghostery-hub'); +}; + +/** + * Build store using combined reducers and middleware + * @return {Object} + * @memberof HubReactStore + */ +const createStoreFactory = function(reducers) { + const reducer = combineReducers(reducers); + + return createStore( + reducer, + compose( + applyMiddleware(thunk), + window.devToolsExtension ? window.devToolsExtension() : f => f + ), + ); +}; + +export { createStoreFactory, log, sendMessage, sendMessageInPromise }; diff --git a/app/templates/hub.html b/app/templates/hub.html index 25a9137b6..7f10239b2 100644 --- a/app/templates/hub.html +++ b/app/templates/hub.html @@ -21,6 +21,17 @@
- + diff --git a/app/templates/panel.html b/app/templates/panel.html index e3c65674a..41698a8e6 100644 --- a/app/templates/panel.html +++ b/app/templates/panel.html @@ -22,15 +22,6 @@
- + From 078a137e4c91c767cc58226df4ba281bcc981bbe Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 2 Dec 2020 14:21:43 -0500 Subject: [PATCH 006/113] Factor store creation and Redux HOC construction to utility functions --- .../Views/AppView/AppViewActions.js | 20 +------ .../Views/AppView/AppViewConstants.js | 7 ++- .../Views/AppView/AppViewReducer.js | 4 +- .../Views/AppView/index.js | 19 ++----- app/ghostery-browser-hub/createStore.js | 14 ++++- app/shared-hub/actions/MetricsActions.js | 15 +++++ app/shared-hub/constants/MetricsConstants.js | 16 ++++++ app/shared-hub/utils/index.js | 56 ++++++++++++++++--- 8 files changed, 104 insertions(+), 47 deletions(-) create mode 100644 app/shared-hub/actions/MetricsActions.js create mode 100644 app/shared-hub/constants/MetricsConstants.js diff --git a/app/ghostery-browser-hub/Views/AppView/AppViewActions.js b/app/ghostery-browser-hub/Views/AppView/AppViewActions.js index 7a2a65041..587fd23dd 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppViewActions.js +++ b/app/ghostery-browser-hub/Views/AppView/AppViewActions.js @@ -11,27 +11,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -// TODO I think we can just reuse the hub AppViewActions +import { SET_TOAST } from './AppViewConstants'; -import { log, sendMessageInPromise } from '../../../hub/utils'; -import { SET_TOAST, SEND_PING } from './AppViewConstants'; - -export function setToast(data) { +export default function setToast(data) { return { type: SET_TOAST, data, }; } - -export function sendPing(actionData) { - return function(dispatch) { - return sendMessageInPromise(SEND_PING, actionData).then((data) => { - dispatch({ - type: SEND_PING, - data, - }); - }).catch((err) => { - log('appView Action sendPing Error', err); - }); - }; -} diff --git a/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js b/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js index 3bce0f948..8da48868d 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js +++ b/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js @@ -11,8 +11,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -// TODO I think we can just use the Hub AppViewConstants +// TODO consider whether this should also be factored out // Ghostery Browser Hub App View -export const SET_TOAST = 'SET_TOAST'; -export const SEND_PING = 'SEND_PING'; +const SET_TOAST = 'SET_TOAST'; + +export default SET_TOAST; diff --git a/app/ghostery-browser-hub/Views/AppView/AppViewReducer.js b/app/ghostery-browser-hub/Views/AppView/AppViewReducer.js index b068f8f78..1aafaa4e2 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppViewReducer.js +++ b/app/ghostery-browser-hub/Views/AppView/AppViewReducer.js @@ -11,9 +11,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -// TODO I think we can just use the Hub AppViewReducer +// TODO consider whether to factor out the reducer case implementations to shared-hub -import { SET_TOAST } from './AppViewConstants'; +import SET_TOAST from './AppViewConstants'; const initialState = {}; diff --git a/app/ghostery-browser-hub/Views/AppView/index.js b/app/ghostery-browser-hub/Views/AppView/index.js index ad47f3aa2..b9a57a9a5 100644 --- a/app/ghostery-browser-hub/Views/AppView/index.js +++ b/app/ghostery-browser-hub/Views/AppView/index.js @@ -11,19 +11,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; +import { buildReduxHOC } from '../../../shared-hub/utils'; -import View from './AppView'; -import { setToast } from './AppViewActions'; -import Reducer from './AppViewReducer'; +import AppView from './AppView'; +import setToast from './AppViewActions'; +import AppViewReducer from './AppViewReducer'; -const mapStateToProps = state => ({ ...state.app }); +export const reducer = AppViewReducer; -const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators({ setToast }, dispatch), -}); - -export const reducer = Reducer; - -export default connect(mapStateToProps, mapDispatchToProps)(View); +export default buildReduxHOC(['app'], { setToast }, AppView); diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index ddb5ff023..f65178f27 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -13,10 +13,20 @@ * @namespace GhosteryBrowserHubReactStore */ -import createStoreFactory from '../shared-hub/utils/index'; +import thunk from 'redux-thunk'; + +import { createStoreFactory } from '../shared-hub/utils/index'; + import { reducer as app } from './Views/AppView'; import { reducer as onboarding } from './Views/OnboardingView'; import account from '../Account/AccountReducer'; import settings from '../panel/reducers/settings'; -export default () => createStoreFactory({app, onboarding, account, settings}); +const reducers = { + app, + onboarding, + account, + settings +}; + +export default () => createStoreFactory(reducers, [thunk]); diff --git a/app/shared-hub/actions/MetricsActions.js b/app/shared-hub/actions/MetricsActions.js new file mode 100644 index 000000000..b99d74ba1 --- /dev/null +++ b/app/shared-hub/actions/MetricsActions.js @@ -0,0 +1,15 @@ +import { log, sendMessageInPromise } from '../utils'; +import SEND_PING from '../constants/MetricsConstants'; + +export default function sendPing(actionData) { + return function(dispatch) { + return sendMessageInPromise(SEND_PING, actionData).then((data) => { + dispatch({ + type: SEND_PING, + data, + }); + }).catch((err) => { + log('sendPing Action Error', err); + }); + }; +} diff --git a/app/shared-hub/constants/MetricsConstants.js b/app/shared-hub/constants/MetricsConstants.js new file mode 100644 index 000000000..9c53ddb8d --- /dev/null +++ b/app/shared-hub/constants/MetricsConstants.js @@ -0,0 +1,16 @@ +/** + * Shared Hub Metrics Constants + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +const SEND_PING = 'SEND_PING'; + +export default SEND_PING; diff --git a/app/shared-hub/utils/index.js b/app/shared-hub/utils/index.js index af32ca3bb..1d3caeb9a 100644 --- a/app/shared-hub/utils/index.js +++ b/app/shared-hub/utils/index.js @@ -12,15 +12,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -// Imports utilities from elsewhere in the codebase to reduce duplicate code import { applyMiddleware, compose, combineReducers, - createStore + createStore, + bindActionCreators } from 'redux'; -import thunk from 'redux-thunk'; +import { connect } from 'react-redux'; +// Imports utilities from elsewhere in the codebase to reduce duplicate code import { log } from '../../../src/utils/common'; import { sendMessage as importedSM, sendMessageInPromise as importedSMIP } from '../../panel/utils/msg'; @@ -33,20 +34,57 @@ const sendMessage = function(name, message) { }; /** - * Build store using combined reducers and middleware - * @return {Object} - * @memberof HubReactStore + * @since 8.5.5 + * + * Build store using provided reducers and middlewares. + * + * @param {Object} reducers The reducers to combine. + * @param {Array} [middlewares] (Optional) The middlewares to apply, in the order they should be applied. + * @return {Object} The store created by calling redux's createStore with the provided reducers and middlewares. + * @memberof Utils */ -const createStoreFactory = function(reducers) { +const createStoreFactory = function(reducers, middlewares) { const reducer = combineReducers(reducers); return createStore( reducer, compose( - applyMiddleware(thunk), + applyMiddleware(...middlewares), window.devToolsExtension ? window.devToolsExtension() : f => f ), ); }; -export { createStoreFactory, log, sendMessage, sendMessageInPromise }; +/** + * @since 8.5.5 + * + * Uses react-redux's connect to wrap the provided base component in a HOC, + * with the provided state slices mapped to props and the provided action creators dispatch mapped to props.actions. + * + * @param {Array|null} stateKeys The slices of the Redux state store you want to map to props. Pass null if none. + * @param {Object|null} actionCreators The action creators you want to dispatch map to props.actions. Pass null if none. + * @param {*} baseComponent The React component you want to wrap. + * + * @returns {*} The created HOC. + */ +function buildReduxHOC(stateKeys, actionCreators, baseComponent) { + const mapStateToProps = (stateKeys === null) + ? null + : state => stateKeys.reduce((acc, key) => ({ ...acc, ...state[key] }), {}); + + const mapDispatchToProps = (actionCreators === null) + ? null + : dispatch => ({ + actions: bindActionCreators(actionCreators, dispatch) + }); + + return connect(mapStateToProps, mapDispatchToProps)(baseComponent); +} + +export { + buildReduxHOC, + createStoreFactory, + log, + sendMessage, + sendMessageInPromise +}; From 388f8f8eba561bc83033b027c606b64adf6d0827 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 2 Dec 2020 16:37:03 -0500 Subject: [PATCH 007/113] Factor out and clean up toast and anti suite reducers for the Hubs --- .../Views/AppView/index.js | 7 +-- .../OnboardingViewContainer.jsx | 6 +- .../OnboardingView/OnboardingViewReducer.js | 17 ------ .../Views/OnboardingView/index.js | 38 ++++-------- .../Step0_WelcomeView/WelcomeView.jsx | 18 ++++++ .../Step0_WelcomeView/index.js | 16 +++++ .../Step5_SuccessView/SuccessView.jsx | 18 ++++++ .../Step5_SuccessView/index.js | 16 +++++ app/ghostery-browser-hub/createStore.js | 6 +- app/shared-hub/actions/AntiSuiteActions.js | 44 ++++++++++++++ .../actions/AntiSuiteViewActions.js | 58 ------------------- .../actions/ToastActions.js} | 4 +- .../constants/AntiSuiteConstants.js | 16 +++++ app/shared-hub/constants/SetupConstants.js | 5 -- .../constants/ToastConstants.js} | 5 +- app/shared-hub/reducers/AntiSuiteReducer.js | 41 +++++++++++++ .../reducers/ToastReducer.js} | 12 ++-- app/shared-hub/utils/index.js | 1 + 18 files changed, 198 insertions(+), 130 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js create mode 100644 app/shared-hub/actions/AntiSuiteActions.js delete mode 100644 app/shared-hub/actions/AntiSuiteViewActions.js rename app/{ghostery-browser-hub/Views/AppView/AppViewActions.js => shared-hub/actions/ToastActions.js} (81%) create mode 100644 app/shared-hub/constants/AntiSuiteConstants.js rename app/{ghostery-browser-hub/Views/AppView/AppViewConstants.js => shared-hub/constants/ToastConstants.js} (74%) create mode 100644 app/shared-hub/reducers/AntiSuiteReducer.js rename app/{ghostery-browser-hub/Views/AppView/AppViewReducer.js => shared-hub/reducers/ToastReducer.js} (66%) diff --git a/app/ghostery-browser-hub/Views/AppView/index.js b/app/ghostery-browser-hub/Views/AppView/index.js index b9a57a9a5..b43f3675f 100644 --- a/app/ghostery-browser-hub/Views/AppView/index.js +++ b/app/ghostery-browser-hub/Views/AppView/index.js @@ -14,9 +14,6 @@ import { buildReduxHOC } from '../../../shared-hub/utils'; import AppView from './AppView'; -import setToast from './AppViewActions'; -import AppViewReducer from './AppViewReducer'; +import setToast from '../../../shared-hub/actions/ToastActions'; -export const reducer = AppViewReducer; - -export default buildReduxHOC(['app'], { setToast }, AppView); +export default buildReduxHOC(['toast'], { setToast }, AppView); diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index c22767819..3718397df 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -63,9 +63,9 @@ class OnboardingViewContainer extends Component { const { actions } = this.props; actions.setSetupStep({ setup_step: 8 }); actions.setBlockingPolicy({ blockingPolicy: BLOCKING_POLICY_RECOMMENDED }); - actions.setAntiTracking({ enable_anti_tracking: true }); - actions.setAdBlock({ enable_ad_block: true }); - actions.setSmartBlocking({ enable_smart_block: true }); + actions.setAntiTracking({ enable_anti_tracking: true }); // covered + actions.setAdBlock({ enable_ad_block: true }); // covered + actions.setSmartBlocking({ enable_smart_block: true }); // covered } /** diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js index 8a4ab678b..50f64e0cc 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js @@ -12,13 +12,9 @@ */ import { - SET_AD_BLOCK, SET_TRACKER_BLOCKING_POLICY, - SET_ANTI_TRACKING, INIT_ONBOARDING_PROPS, SET_ONBOARDING_NAVIGATION, - SET_SMART_BLOCK, - SET_HUMAN_WEB } from './OnboardingViewConstants'; const initialState = {}; @@ -93,23 +89,10 @@ function OnboardingViewReducer(state = initialState, action) { } }; } - - case SET_AD_BLOCK: { - const { enable_ad_block } = action.data; - return { ...state, setup: { ...state.setup, enable_ad_block } }; - } case SET_TRACKER_BLOCKING_POLICY: { const { blockingPolicy } = action.data; return { ...state, setup: { ...state.setup, blockingPolicy } }; } - case SET_ANTI_TRACKING: { - const { enable_anti_tracking } = action.data; - return { ...state, setup: { ...state.setup, enable_anti_tracking } }; - } - case SET_SMART_BLOCK: { - const { enable_smart_block } = action.data; - return { ...state, setup: { ...state.setup, enable_smart_block } }; - } default: return state; } diff --git a/app/ghostery-browser-hub/Views/OnboardingView/index.js b/app/ghostery-browser-hub/Views/OnboardingView/index.js index e4c2d374f..ba3535793 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingView/index.js @@ -11,10 +11,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import { withRouter } from 'react-router-dom'; +import { buildReduxHOC } from '../../../shared-hub/utils'; + import OnboardingViewContainer from './OnboardingViewContainer'; import OnboardingViewReducer from './OnboardingViewReducer'; import * as OnboardingViewActions from './OnboardingViewActions'; @@ -22,37 +22,21 @@ import { setAdBlock, setAntiTracking, setSmartBlocking, -} from '../../../shared-hub/actions/AntiSuiteViewActions'; +} from '../../../shared-hub/actions/AntiSuiteActions'; + import setSetupComplete from '../OnboardingViews/SetupDoneView/SetupDoneViewActions'; -/** - * Map redux store state properties to the component's own properties. - * @param {Object} state entire Redux store's state - * @return {function} this function returns a plain object, which will be merged into the component's props - * @memberof HubContainers - */ -const mapStateToProps = state => ({ ...state.setup, ...state.account }); +export const reducer = OnboardingViewReducer; -/** - * Bind the component's action creators using Redux's bindActionCreators. - * @param {function} dispatch redux store method which dispatches actions - * @return {function} to be used as an argument in redux connect call - * @memberof SetupContainers - */ -const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators({ +export default withRouter(buildReduxHOC( + ['setup', 'account'], + { ...OnboardingViewActions, setBlockingPolicy, setAntiTracking, setAdBlock, setSmartBlocking, setSetupComplete - }, dispatch), -}); - -export const reducer = OnboardingViewReducer; - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(OnboardingViewContainer)); - - - + }, + OnboardingViewContainer +)); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx new file mode 100644 index 000000000..eb3b3766a --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx @@ -0,0 +1,18 @@ +/** + * Ghostery Browser Hub Welcome View Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; + +const WelcomeView = () =>

Step 5: Welcome View

; + +export default WelcomeView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js new file mode 100644 index 000000000..1ae8bd4ca --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Welcome View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import WelcomeView from './WelcomeView'; + +export default WelcomesView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx new file mode 100644 index 000000000..2ac283a17 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx @@ -0,0 +1,18 @@ +/** + * Ghostery Browser Hub Success View Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; + +const SuccessView = () =>

Step 5: Success View

; + +export default SuccessView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js new file mode 100644 index 000000000..cdcea8832 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Success View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import SuccessView from './SuccessView'; + +export default SuccessView; diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index f65178f27..1202b4c3a 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -17,13 +17,15 @@ import thunk from 'redux-thunk'; import { createStoreFactory } from '../shared-hub/utils/index'; -import { reducer as app } from './Views/AppView'; +import toast from '../shared-hub/reducers/ToastReducer'; +import antiSuite from '../shared-hub/reducers/AntiSuiteReducer'; import { reducer as onboarding } from './Views/OnboardingView'; import account from '../Account/AccountReducer'; import settings from '../panel/reducers/settings'; const reducers = { - app, + toast, + antiSuite, onboarding, account, settings diff --git a/app/shared-hub/actions/AntiSuiteActions.js b/app/shared-hub/actions/AntiSuiteActions.js new file mode 100644 index 000000000..be2002c43 --- /dev/null +++ b/app/shared-hub/actions/AntiSuiteActions.js @@ -0,0 +1,44 @@ +/** + * Anti Suite Action Creators for the Hubs to use + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { log, sendMessageInPromise } from '../utils'; +import { + SET_ANTI_TRACKING, + SET_AD_BLOCK, + SET_SMART_BLOCK +} from '../constants/AntiSuiteConstants'; + +function _smipFactory(action, actionData) { + return function(dispatch) { + return sendMessageInPromise(action, actionData).then((data) => { + dispatch({ + type: action, + data, + }); + }).catch((err) => { + log(`${action} action creator error`, err); + }); + }; +} + +export function setAntiTracking(actionData) { + return _smipFactory(SET_ANTI_TRACKING, actionData); +} + +export function setAdBlock(actionData) { + return _smipFactory(SET_AD_BLOCK, actionData); +} + +export function setSmartBlocking(actionData) { + return _smipFactory(SET_SMART_BLOCK, actionData); +} diff --git a/app/shared-hub/actions/AntiSuiteViewActions.js b/app/shared-hub/actions/AntiSuiteViewActions.js deleted file mode 100644 index 5b3bf9ea0..000000000 --- a/app/shared-hub/actions/AntiSuiteViewActions.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Anti Suite Action Creators used by Hub's Setup and Ghostery Browser Hub's Onboarding - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import { log, sendMessageInPromise } from '../utils'; -import { - SET_ANTI_TRACKING, - SET_AD_BLOCK, - SET_SMART_BLOCK -} from '../constants/SetupConstants'; - -export function setAntiTracking(actionData) { - return function(dispatch) { - return sendMessageInPromise(SET_ANTI_TRACKING, actionData).then((data) => { - dispatch({ - type: SET_ANTI_TRACKING, - data, - }); - }).catch((err) => { - log('setAntiTracking setup action error', err); - }); - }; -} - -export function setAdBlock(actionData) { - return function(dispatch) { - return sendMessageInPromise(SET_AD_BLOCK, actionData).then((data) => { - dispatch({ - type: SET_AD_BLOCK, - data, - }); - }).catch((err) => { - log('setAdBlock setup action error', err); - }); - }; -} - -export function setSmartBlocking(actionData) { - return function(dispatch) { - return sendMessageInPromise(SET_SMART_BLOCK, actionData).then((data) => { - dispatch({ - type: SET_SMART_BLOCK, - data, - }); - }).catch((err) => { - log('setSmartBlocking setup action error', err); - }); - }; -} diff --git a/app/ghostery-browser-hub/Views/AppView/AppViewActions.js b/app/shared-hub/actions/ToastActions.js similarity index 81% rename from app/ghostery-browser-hub/Views/AppView/AppViewActions.js rename to app/shared-hub/actions/ToastActions.js index 587fd23dd..ed38ccb93 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppViewActions.js +++ b/app/shared-hub/actions/ToastActions.js @@ -1,5 +1,5 @@ /** - * Ghostery Browser Hub App View Action creators + * Toast action creators * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -11,7 +11,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { SET_TOAST } from './AppViewConstants'; +import SET_TOAST from '../constants/ToastConstants'; export default function setToast(data) { return { diff --git a/app/shared-hub/constants/AntiSuiteConstants.js b/app/shared-hub/constants/AntiSuiteConstants.js new file mode 100644 index 000000000..23803a225 --- /dev/null +++ b/app/shared-hub/constants/AntiSuiteConstants.js @@ -0,0 +1,16 @@ +/** + * Anti Suite Constants for use by both Hubs + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +export const SET_ANTI_TRACKING = 'SET_ANTI_TRACKING'; +export const SET_AD_BLOCK = 'SET_AD_BLOCK'; +export const SET_SMART_BLOCK = 'SET_SMART_BLOCK'; diff --git a/app/shared-hub/constants/SetupConstants.js b/app/shared-hub/constants/SetupConstants.js index 3c3558195..a5f8e078a 100644 --- a/app/shared-hub/constants/SetupConstants.js +++ b/app/shared-hub/constants/SetupConstants.js @@ -22,11 +22,6 @@ export const BLOCKING_POLICY_NOTHING = 'BLOCKING_POLICY_NOTHING'; export const BLOCKING_POLICY_EVERYTHING = 'BLOCKING_POLICY_EVERYTHING'; export const BLOCKING_POLICY_CUSTOM = 'BLOCKING_POLICY_CUSTOM'; -// Setup Anti-Suite in Hub / Block Settings in Ghostery Browser Hub -export const SET_ANTI_TRACKING = 'SET_ANTI_TRACKING'; -export const SET_AD_BLOCK = 'SET_AD_BLOCK'; -export const SET_SMART_BLOCK = 'SET_SMART_BLOCK'; - // Setup Human Web in Hub export const SET_HUMAN_WEB = 'SET_HUMAN_WEB'; diff --git a/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js b/app/shared-hub/constants/ToastConstants.js similarity index 74% rename from app/ghostery-browser-hub/Views/AppView/AppViewConstants.js rename to app/shared-hub/constants/ToastConstants.js index 8da48868d..92b33ee86 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppViewConstants.js +++ b/app/shared-hub/constants/ToastConstants.js @@ -1,5 +1,5 @@ /** - * Ghostery Browser Hub App View Constants + * Toast Constants Used By The Hubs * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -11,9 +11,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -// TODO consider whether this should also be factored out - -// Ghostery Browser Hub App View const SET_TOAST = 'SET_TOAST'; export default SET_TOAST; diff --git a/app/shared-hub/reducers/AntiSuiteReducer.js b/app/shared-hub/reducers/AntiSuiteReducer.js new file mode 100644 index 000000000..6e4de92a2 --- /dev/null +++ b/app/shared-hub/reducers/AntiSuiteReducer.js @@ -0,0 +1,41 @@ +/** + * Reducer used throughout the Onboarding View's flow + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { + SET_AD_BLOCK, + SET_ANTI_TRACKING, + SET_SMART_BLOCK, +} from '../constants/AntiSuiteConstants'; + +const initialState = {}; + +function AntiSuiteReducer(state = initialState, action) { + switch (action.type) { + case SET_AD_BLOCK: { + const { enable_ad_block } = action.data; + return { ...state, anti_suite: { ...state.anti_suite, enable_ad_block } }; + } + case SET_ANTI_TRACKING: { + const { enable_anti_tracking } = action.data; + return { ...state, anti_suite: { ...state.anti_suite, enable_anti_tracking } }; + } + case SET_SMART_BLOCK: { + const { enable_smart_block } = action.data; + return { ...state, anti_suite: { ...state.anti_suite, enable_smart_block } }; + } + + default: return state; + } +} + +export default AntiSuiteReducer; diff --git a/app/ghostery-browser-hub/Views/AppView/AppViewReducer.js b/app/shared-hub/reducers/ToastReducer.js similarity index 66% rename from app/ghostery-browser-hub/Views/AppView/AppViewReducer.js rename to app/shared-hub/reducers/ToastReducer.js index 1aafaa4e2..b73ab656f 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppViewReducer.js +++ b/app/shared-hub/reducers/ToastReducer.js @@ -1,5 +1,5 @@ /** - * Reducer used in the Ghostery Browser Hub App View + * Toast reducer used by the Hubs * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -11,19 +11,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -// TODO consider whether to factor out the reducer case implementations to shared-hub - -import SET_TOAST from './AppViewConstants'; +import SET_TOAST from '../constants/ToastConstants'; const initialState = {}; -function AppViewReducer(state = initialState, action) { +function ToastReducer(state = initialState, action) { switch (action.type) { case SET_TOAST: { const { toastMessage, toastClass } = action.data; return { ...state, - app: { + toast: { toastMessage, toastClass } @@ -33,4 +31,4 @@ function AppViewReducer(state = initialState, action) { } } -export default AppViewReducer; +export default ToastReducer; diff --git a/app/shared-hub/utils/index.js b/app/shared-hub/utils/index.js index 1d3caeb9a..4b703e590 100644 --- a/app/shared-hub/utils/index.js +++ b/app/shared-hub/utils/index.js @@ -66,6 +66,7 @@ const createStoreFactory = function(reducers, middlewares) { * @param {*} baseComponent The React component you want to wrap. * * @returns {*} The created HOC. + * */ function buildReduxHOC(stateKeys, actionCreators, baseComponent) { const mapStateToProps = (stateKeys === null) From 5b957e383e341dcf11d258116e7c1c353608773c Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 2 Dec 2020 22:18:07 -0500 Subject: [PATCH 008/113] Factor setup lifecycle actions and constants out to shared-hub dir. Name, document, and move makeDeferredDispatcher. Use it in all the actions files in shared-hub. --- .../OnboardingView/OnboardingViewActions.js | 46 -------- .../OnboardingViewContainer.jsx | 7 ++ .../Views/OnboardingView/index.js | 12 ++- app/ghostery-browser-hub/createStore.js | 4 +- app/shared-hub/actions/AntiSuiteActions.js | 30 ++---- app/shared-hub/actions/MetricsActions.js | 18 +--- .../actions/SetupDoneViewActions.js | 28 ----- .../actions/SetupLifecycleActions.js | 40 +++++++ app/shared-hub/constants/SetupConstants.js | 29 ----- .../constants/SetupLifecycleConstants.js | 17 +++ app/shared-hub/reducers/AntiSuiteReducer.js | 2 +- app/shared-hub/reducers/SetupReducer.js | 102 ------------------ app/shared-hub/utils/index.js | 40 +++++-- 13 files changed, 122 insertions(+), 253 deletions(-) delete mode 100644 app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewActions.js delete mode 100644 app/shared-hub/actions/SetupDoneViewActions.js create mode 100644 app/shared-hub/actions/SetupLifecycleActions.js delete mode 100644 app/shared-hub/constants/SetupConstants.js create mode 100644 app/shared-hub/constants/SetupLifecycleConstants.js delete mode 100644 app/shared-hub/reducers/SetupReducer.js diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewActions.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewActions.js deleted file mode 100644 index f1dc452b0..000000000 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewActions.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Ghostery Browser Hub Onboarding View Action creators - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import { log, sendMessageInPromise } from '../../../hub/utils'; -import { - INIT_ONBOARDING_PROPS, - SET_ONBOARDING_STEP, - SET_ONBOARDING_NAVIGATION -} from './OnboardingViewConstants'; - -export function initOnboardingProps(data) { - return { - type: INIT_ONBOARDING_PROPS, - data, - }; -} - -export function setOnboardingStep(actionData) { - return function(dispatch) { - return sendMessageInPromise(SET_ONBOARDING_STEP, actionData).then((data) => { - dispatch({ - type: SET_ONBOARDING_STEP, - data, - }); - }).catch((err) => { - log('onboardingView Action setOnboardingStep Error', err); - }); - }; -} - -export function setOnboardingNavigation(data) { - return { - type: SET_ONBOARDING_NAVIGATION, - data, - }; -} diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index 3718397df..838f495a0 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -17,10 +17,12 @@ import OnboardingView from './OnboardingView'; import { BLOCKING_POLICY_RECOMMENDED } from './OnboardingViewConstants'; // Component Views +import WelcomeView from '../OnboardingViews/Step0_WelcomeView'; import LoginView from '../OnboardingViews/Step1_LoginView'; import BlockSettingsView from '../OnboardingViews/Step2_BlockSettingsView'; import ChooseDefaultSearchView from '../OnboardingViews/Step3_ChooseDefaultSearchView'; import ChoosePlanView from '../OnboardingViews/Step4_ChoosePlanView'; +import SuccessView from '../OnboardingViews/Step5_SuccessView'; /** * @class Implement the Onboarding View for the Ghostery Browser Hub @@ -75,6 +77,11 @@ class OnboardingViewContainer extends Component { render() { const { sendMountActions } = this.state; const steps = [ + { + index: 0, + path: '/onboarding/0', + bodyComponent: WelcomeView, + } { index: 1, path: '/onboarding/1', diff --git a/app/ghostery-browser-hub/Views/OnboardingView/index.js b/app/ghostery-browser-hub/Views/OnboardingView/index.js index ba3535793..fba49112f 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingView/index.js @@ -17,13 +17,17 @@ import { buildReduxHOC } from '../../../shared-hub/utils'; import OnboardingViewContainer from './OnboardingViewContainer'; import OnboardingViewReducer from './OnboardingViewReducer'; -import * as OnboardingViewActions from './OnboardingViewActions'; +import { + initSetupProps, + setSetupStep, + setSetupNavigation, +} from '../../../shared-hub/actions/SetupLifecycleActions'; import { setAdBlock, setAntiTracking, setSmartBlocking, } from '../../../shared-hub/actions/AntiSuiteActions'; - +import setBlockingPolicy from '../../../shared-hub/actions/BlockingPolicyActions'; import setSetupComplete from '../OnboardingViews/SetupDoneView/SetupDoneViewActions'; export const reducer = OnboardingViewReducer; @@ -31,7 +35,9 @@ export const reducer = OnboardingViewReducer; export default withRouter(buildReduxHOC( ['setup', 'account'], { - ...OnboardingViewActions, + initSetupProps, + setSetupStep, + setSetupNavigation, setBlockingPolicy, setAntiTracking, setAdBlock, diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index 1202b4c3a..8b8913d6d 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -15,7 +15,7 @@ import thunk from 'redux-thunk'; -import { createStoreFactory } from '../shared-hub/utils/index'; +import { makeStoreCreator } from '../shared-hub/utils/index'; import toast from '../shared-hub/reducers/ToastReducer'; import antiSuite from '../shared-hub/reducers/AntiSuiteReducer'; @@ -31,4 +31,4 @@ const reducers = { settings }; -export default () => createStoreFactory(reducers, [thunk]); +export default makeStoreCreator(reducers, [thunk]); diff --git a/app/shared-hub/actions/AntiSuiteActions.js b/app/shared-hub/actions/AntiSuiteActions.js index be2002c43..7b7a2a77c 100644 --- a/app/shared-hub/actions/AntiSuiteActions.js +++ b/app/shared-hub/actions/AntiSuiteActions.js @@ -11,34 +11,18 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { log, sendMessageInPromise } from '../utils'; +import { makeDeferredDispatcher } from '../utils'; import { SET_ANTI_TRACKING, SET_AD_BLOCK, SET_SMART_BLOCK } from '../constants/AntiSuiteConstants'; -function _smipFactory(action, actionData) { - return function(dispatch) { - return sendMessageInPromise(action, actionData).then((data) => { - dispatch({ - type: action, - data, - }); - }).catch((err) => { - log(`${action} action creator error`, err); - }); - }; -} +export const setAntiTracking = + actionData => makeDeferredDispatcher(SET_ANTI_TRACKING, actionData); -export function setAntiTracking(actionData) { - return _smipFactory(SET_ANTI_TRACKING, actionData); -} +export const setAdBlock = + actionData => makeDeferredDispatcher(SET_AD_BLOCK, actionData); -export function setAdBlock(actionData) { - return _smipFactory(SET_AD_BLOCK, actionData); -} - -export function setSmartBlocking(actionData) { - return _smipFactory(SET_SMART_BLOCK, actionData); -} +export const setSmartBlocking = + actionData => makeDeferredDispatcher(SET_SMART_BLOCK, actionData); diff --git a/app/shared-hub/actions/MetricsActions.js b/app/shared-hub/actions/MetricsActions.js index b99d74ba1..c2925045c 100644 --- a/app/shared-hub/actions/MetricsActions.js +++ b/app/shared-hub/actions/MetricsActions.js @@ -1,15 +1,7 @@ -import { log, sendMessageInPromise } from '../utils'; +import { makeDeferredDispatcher } from '../utils'; import SEND_PING from '../constants/MetricsConstants'; -export default function sendPing(actionData) { - return function(dispatch) { - return sendMessageInPromise(SEND_PING, actionData).then((data) => { - dispatch({ - type: SEND_PING, - data, - }); - }).catch((err) => { - log('sendPing Action Error', err); - }); - }; -} +const sendPing = + actionData => makeDeferredDispatcher(SEND_PING, actionData); + +export default sendPing; diff --git a/app/shared-hub/actions/SetupDoneViewActions.js b/app/shared-hub/actions/SetupDoneViewActions.js deleted file mode 100644 index 20d283720..000000000 --- a/app/shared-hub/actions/SetupDoneViewActions.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Setup Done View Action creators - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2019 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import { log, sendMessageInPromise } from '../utils'; -import { SET_SETUP_COMPLETE } from '../../SetupView/SetupViewConstants'; - -export default function setSetupComplete(actionData) { - return function(dispatch) { - return sendMessageInPromise(SET_SETUP_COMPLETE, actionData).then((data) => { - dispatch({ - type: SET_SETUP_COMPLETE, - data, - }); - }).catch((err) => { - log('setupDone Action setSetupComplete Error', err); - }); - }; -} diff --git a/app/shared-hub/actions/SetupLifecycleActions.js b/app/shared-hub/actions/SetupLifecycleActions.js new file mode 100644 index 000000000..175bd55e1 --- /dev/null +++ b/app/shared-hub/actions/SetupLifecycleActions.js @@ -0,0 +1,40 @@ +/** + * Setup LIfecycle Actions for the Hubs + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { makeDeferredDispatcher } from '../utils'; +import { + INIT_SETUP_PROPS, + SET_SETUP_STEP, + SET_SETUP_NAVIGATION +} from '../constants/SetupLifecycleConstants'; +import { SET_SETUP_COMPLETE } from '../../hub/Views/SetupView/SetupViewConstants'; + +export function initOnboardingProps(data) { + return { + type: INIT_SETUP_PROPS, + data, + }; +} + +export const setSetupStep = + actionData => makeDeferredDispatcher(SET_SETUP_STEP, actionData); + +export function setSetupNavigation(data) { + return { + type: SET_SETUP_NAVIGATION, + data, + }; +} + +export const setSetupComplete = + actionData => makeDeferredDispatcher(SET_SETUP_COMPLETE, actionData); diff --git a/app/shared-hub/constants/SetupConstants.js b/app/shared-hub/constants/SetupConstants.js deleted file mode 100644 index a5f8e078a..000000000 --- a/app/shared-hub/constants/SetupConstants.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Setup Constants shared by the standard and Ghostery Browser Hubs - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -// Setup View in Hub / Onboarding View in Ghostery Browser Hub -export const INIT_SETUP_PROPS = 'INIT_SETUP_PROPS'; -export const SET_SETUP_STEP = 'SET_SETUP_STEP'; - -// Setup Blocking in Hub / Block Settings in Ghostery Browser Hub -export const SET_BLOCKING_POLICY = 'SET_BLOCKING_POLICY'; -export const BLOCKING_POLICY_RECOMMENDED = 'BLOCKING_POLICY_RECOMMENDED'; -export const BLOCKING_POLICY_NOTHING = 'BLOCKING_POLICY_NOTHING'; -export const BLOCKING_POLICY_EVERYTHING = 'BLOCKING_POLICY_EVERYTHING'; -export const BLOCKING_POLICY_CUSTOM = 'BLOCKING_POLICY_CUSTOM'; - -// Setup Human Web in Hub -export const SET_HUMAN_WEB = 'SET_HUMAN_WEB'; - -// Setup Done -export const SET_SETUP_COMPLETE = 'SET_SETUP_COMPLETE'; diff --git a/app/shared-hub/constants/SetupLifecycleConstants.js b/app/shared-hub/constants/SetupLifecycleConstants.js new file mode 100644 index 000000000..4182e27b4 --- /dev/null +++ b/app/shared-hub/constants/SetupLifecycleConstants.js @@ -0,0 +1,17 @@ +/** + * Setup Lifecycle Constants for the Hubs + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +export const INIT_SETUP_PROPS = 'INIT_SETUP_PROPS'; +export const SET_SETUP_STEP = 'SET_SETUP_STEP'; +export const SET_SETUP_NAVIGATION = 'SET_SETUP_NAVIGATION'; +export const SET_SETUP_COMPLETE = 'SET_SETUP_COMPLETE'; diff --git a/app/shared-hub/reducers/AntiSuiteReducer.js b/app/shared-hub/reducers/AntiSuiteReducer.js index 6e4de92a2..0cdf8bdbd 100644 --- a/app/shared-hub/reducers/AntiSuiteReducer.js +++ b/app/shared-hub/reducers/AntiSuiteReducer.js @@ -14,7 +14,7 @@ import { SET_AD_BLOCK, SET_ANTI_TRACKING, - SET_SMART_BLOCK, + SET_SMART_BLOCK } from '../constants/AntiSuiteConstants'; const initialState = {}; diff --git a/app/shared-hub/reducers/SetupReducer.js b/app/shared-hub/reducers/SetupReducer.js deleted file mode 100644 index de88bf26a..000000000 --- a/app/shared-hub/reducers/SetupReducer.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Reducer used throughout the Setup/Onboarding flow in the regular and Ghostery Browser Hubs - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import { - INIT_SETUP_PROPS, - SET_BLOCKING_POLICY, - SET_ANTI_TRACKING, - SET_AD_BLOCK, - SET_SMART_BLOCK, -} from '../constants/SetupConstants'; - -const initialState = {}; - -function SetupReducer(state = initialState, action) { - switch (action.type) { - case INIT_SHARED_SETUP_PROPS: { - const { - blockingPolicy, - enable_anti_tracking, - enable_ad_block, - enable_smart_block, - } = action.data; - return { - ...state, - setup: { - setup_show_warning_override, - blockingPolicy, - enable_anti_tracking, - enable_ad_block, - enable_smart_block, - enable_human_web, - } - }; - } - case SET_SETUP_NAVIGATION: { - const { - activeIndex, - hrefPrev, - hrefNext, - hrefDone, - textPrev, - textNext, - textDone, - } = action.data; - return { - ...state, - setup: { - ...state.setup, - navigation: { - activeIndex, - hrefPrev, - hrefNext, - hrefDone, - textPrev, - textNext, - textDone, - } - } - }; - } - - // Setup Blocking View - case SET_BLOCKING_POLICY: { - const { blockingPolicy } = action.data; - return { ...state, setup: { ...state.setup, blockingPolicy } }; - } - - // Setup Anti-Suite View - case SET_ANTI_TRACKING: { - const { enable_anti_tracking } = action.data; - return { ...state, setup: { ...state.setup, enable_anti_tracking } }; - } - case SET_AD_BLOCK: { - const { enable_ad_block } = action.data; - return { ...state, setup: { ...state.setup, enable_ad_block } }; - } - case SET_SMART_BLOCK: { - const { enable_smart_block } = action.data; - return { ...state, setup: { ...state.setup, enable_smart_block } }; - } - - // Setup Human Web View - case SET_HUMAN_WEB: { - const { enable_human_web } = action.data; - return { ...state, setup: { ...state.setup, enable_human_web } }; - } - - default: return state; - } -} - -export default SetupReducer; diff --git a/app/shared-hub/utils/index.js b/app/shared-hub/utils/index.js index 4b703e590..693a05465 100644 --- a/app/shared-hub/utils/index.js +++ b/app/shared-hub/utils/index.js @@ -36,17 +36,17 @@ const sendMessage = function(name, message) { /** * @since 8.5.5 * - * Build store using provided reducers and middlewares. + * Create a function that uses redux's createStore to make a store with the supplied reducers and middlewares. * * @param {Object} reducers The reducers to combine. * @param {Array} [middlewares] (Optional) The middlewares to apply, in the order they should be applied. - * @return {Object} The store created by calling redux's createStore with the provided reducers and middlewares. - * @memberof Utils + * + * @return {Function} The function that will create the store when called. */ -const createStoreFactory = function(reducers, middlewares) { +const makeStoreCreator = function(reducers, middlewares) { const reducer = combineReducers(reducers); - return createStore( + return () => createStore( reducer, compose( applyMiddleware(...middlewares), @@ -55,6 +55,33 @@ const createStoreFactory = function(reducers, middlewares) { ); }; +/** + * @since 8.5.5 + * + * Creates and returns a function that sends a Promise-wrapped message to the background, + * then dispatches the supplied action with the resolved data. + * + * @param {String} action The action to dispatch when the Promise resolves. + * @param {*} actionData The data to send to the background. + * + * @returns {Function} Calling the returned function sends a message with the supplied + * action and actionData to the background in a Promise, and dispatches + * the action with the returned data if the Promise resolves. It prints + * an error if the Promise rejects. + */ +function makeDeferredDispatcher(action, actionData) { + return function(dispatch) { + return sendMessageInPromise(action, actionData).then((data) => { + dispatch({ + type: action, + data, + }); + }).catch((err) => { + log(`${action} action creator error`, err); + }); + }; +} + /** * @since 8.5.5 * @@ -84,7 +111,8 @@ function buildReduxHOC(stateKeys, actionCreators, baseComponent) { export { buildReduxHOC, - createStoreFactory, + makeStoreCreator, + makeDeferredDispatcher, log, sendMessage, sendMessageInPromise From 382b81eadd30fc4ab1d6667d7bbe1d3c1f494c26 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 2 Dec 2020 23:57:37 -0500 Subject: [PATCH 009/113] Move blocking policy reducer to shared-hub --- .../Views/AppView/index.js | 2 +- .../Views/OnboardingView/index.js | 11 +++-- app/ghostery-browser-hub/createStore.js | 2 + .../actions/BlockingPolicyActions.js | 20 +++++++++ .../actions/SetupLifecycleActions.js | 8 ++-- .../constants/BlockingPolicyConstants.js | 16 ++++++++ app/shared-hub/reducers/AntiSuiteReducer.js | 8 ++-- .../reducers/BlockingPolicyReducer.js | 41 +++++++++++++++++++ app/shared-hub/reducers/ToastReducer.js | 2 +- 9 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 app/shared-hub/actions/BlockingPolicyActions.js create mode 100644 app/shared-hub/constants/BlockingPolicyConstants.js create mode 100644 app/shared-hub/reducers/BlockingPolicyReducer.js diff --git a/app/ghostery-browser-hub/Views/AppView/index.js b/app/ghostery-browser-hub/Views/AppView/index.js index b43f3675f..fc8848870 100644 --- a/app/ghostery-browser-hub/Views/AppView/index.js +++ b/app/ghostery-browser-hub/Views/AppView/index.js @@ -16,4 +16,4 @@ import { buildReduxHOC } from '../../../shared-hub/utils'; import AppView from './AppView'; import setToast from '../../../shared-hub/actions/ToastActions'; -export default buildReduxHOC(['toast'], { setToast }, AppView); +export default buildReduxHOC(['app'], { setToast }, AppView); diff --git a/app/ghostery-browser-hub/Views/OnboardingView/index.js b/app/ghostery-browser-hub/Views/OnboardingView/index.js index fba49112f..26fdd9dd1 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingView/index.js @@ -16,21 +16,18 @@ import { withRouter } from 'react-router-dom'; import { buildReduxHOC } from '../../../shared-hub/utils'; import OnboardingViewContainer from './OnboardingViewContainer'; -import OnboardingViewReducer from './OnboardingViewReducer'; import { initSetupProps, setSetupStep, setSetupNavigation, + setSetupComplete } from '../../../shared-hub/actions/SetupLifecycleActions'; import { setAdBlock, setAntiTracking, - setSmartBlocking, + setSmartBlocking } from '../../../shared-hub/actions/AntiSuiteActions'; import setBlockingPolicy from '../../../shared-hub/actions/BlockingPolicyActions'; -import setSetupComplete from '../OnboardingViews/SetupDoneView/SetupDoneViewActions'; - -export const reducer = OnboardingViewReducer; export default withRouter(buildReduxHOC( ['setup', 'account'], @@ -38,11 +35,13 @@ export default withRouter(buildReduxHOC( initSetupProps, setSetupStep, setSetupNavigation, + setSetupComplete, + setBlockingPolicy, + setAntiTracking, setAdBlock, setSmartBlocking, - setSetupComplete }, OnboardingViewContainer )); diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index 8b8913d6d..0cca5f1a8 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -19,6 +19,7 @@ import { makeStoreCreator } from '../shared-hub/utils/index'; import toast from '../shared-hub/reducers/ToastReducer'; import antiSuite from '../shared-hub/reducers/AntiSuiteReducer'; +import blockingPolicy from '../shared-hub/reducers/BlockingPolicyReducer'; import { reducer as onboarding } from './Views/OnboardingView'; import account from '../Account/AccountReducer'; import settings from '../panel/reducers/settings'; @@ -26,6 +27,7 @@ import settings from '../panel/reducers/settings'; const reducers = { toast, antiSuite, + blockingPolicy, onboarding, account, settings diff --git a/app/shared-hub/actions/BlockingPolicyActions.js b/app/shared-hub/actions/BlockingPolicyActions.js new file mode 100644 index 000000000..ee96e9888 --- /dev/null +++ b/app/shared-hub/actions/BlockingPolicyActions.js @@ -0,0 +1,20 @@ +/** + * Blocking Policy Action Creators for the Hubs to use + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { makeDeferredDispatcher } from '../utils'; +import SET_BLOCKING_POLICY from '../constants/BlockingPolicyConstants'; + +const setBlockingPolicy = + actionData => makeDeferredDispatcher(SET_BLOCKING_POLICY, actionData); + +export default setBlockingPolicy; diff --git a/app/shared-hub/actions/SetupLifecycleActions.js b/app/shared-hub/actions/SetupLifecycleActions.js index 175bd55e1..1dae62bcc 100644 --- a/app/shared-hub/actions/SetupLifecycleActions.js +++ b/app/shared-hub/actions/SetupLifecycleActions.js @@ -1,5 +1,5 @@ /** - * Setup LIfecycle Actions for the Hubs + * Setup Lifecycle Actions for the Hubs * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -15,11 +15,11 @@ import { makeDeferredDispatcher } from '../utils'; import { INIT_SETUP_PROPS, SET_SETUP_STEP, - SET_SETUP_NAVIGATION + SET_SETUP_NAVIGATION, + SET_SETUP_COMPLETE } from '../constants/SetupLifecycleConstants'; -import { SET_SETUP_COMPLETE } from '../../hub/Views/SetupView/SetupViewConstants'; -export function initOnboardingProps(data) { +export function initSetupProps(data) { return { type: INIT_SETUP_PROPS, data, diff --git a/app/shared-hub/constants/BlockingPolicyConstants.js b/app/shared-hub/constants/BlockingPolicyConstants.js new file mode 100644 index 000000000..5f55b30eb --- /dev/null +++ b/app/shared-hub/constants/BlockingPolicyConstants.js @@ -0,0 +1,16 @@ +/** + * Blocking Policy Constants Used By The Hubs + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +const SET_BLOCKING_POLICY = 'SET_BLOCKING_POLICY'; + +export default SET_BLOCKING_POLICY; diff --git a/app/shared-hub/reducers/AntiSuiteReducer.js b/app/shared-hub/reducers/AntiSuiteReducer.js index 0cdf8bdbd..458ce5558 100644 --- a/app/shared-hub/reducers/AntiSuiteReducer.js +++ b/app/shared-hub/reducers/AntiSuiteReducer.js @@ -1,5 +1,5 @@ /** - * Reducer used throughout the Onboarding View's flow + * Anti Suite reducer for the Hubs * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -23,15 +23,15 @@ function AntiSuiteReducer(state = initialState, action) { switch (action.type) { case SET_AD_BLOCK: { const { enable_ad_block } = action.data; - return { ...state, anti_suite: { ...state.anti_suite, enable_ad_block } }; + return { ...state, setup: { ...state.setup, enable_ad_block } }; } case SET_ANTI_TRACKING: { const { enable_anti_tracking } = action.data; - return { ...state, anti_suite: { ...state.anti_suite, enable_anti_tracking } }; + return { ...state, setup: { ...state.setup, enable_anti_tracking } }; } case SET_SMART_BLOCK: { const { enable_smart_block } = action.data; - return { ...state, anti_suite: { ...state.anti_suite, enable_smart_block } }; + return { ...state, setup: { ...state.setup, enable_smart_block } }; } default: return state; diff --git a/app/shared-hub/reducers/BlockingPolicyReducer.js b/app/shared-hub/reducers/BlockingPolicyReducer.js new file mode 100644 index 000000000..eff0b7f90 --- /dev/null +++ b/app/shared-hub/reducers/BlockingPolicyReducer.js @@ -0,0 +1,41 @@ +/** + * Reducer used throughout the Onboarding View's flow + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { + SET_AD_BLOCK, + SET_ANTI_TRACKING, + SET_SMART_BLOCK +} from '../constants/AntiSuiteConstants'; + +const initialState = {}; + +function AntiSuiteReducer(state = initialState, action) { + switch (action.type) { + case SET_AD_BLOCK: { + const { enable_ad_block } = action.data; + return { ...state, setup: { ...state.setup, enable_ad_block } }; + } + case SET_ANTI_TRACKING: { + const { enable_anti_tracking } = action.data; + return { ...state, setup: { ...state.setup, enable_anti_tracking } }; + } + case SET_SMART_BLOCK: { + const { enable_smart_block } = action.data; + return { ...state, setup: { ...state.setup, enable_smart_block } }; + } + + default: return state; + } +} + +export default AntiSuiteReducer; diff --git a/app/shared-hub/reducers/ToastReducer.js b/app/shared-hub/reducers/ToastReducer.js index b73ab656f..6d7166560 100644 --- a/app/shared-hub/reducers/ToastReducer.js +++ b/app/shared-hub/reducers/ToastReducer.js @@ -21,7 +21,7 @@ function ToastReducer(state = initialState, action) { const { toastMessage, toastClass } = action.data; return { ...state, - toast: { + app: { toastMessage, toastClass } From a51e0ae2423704a5b4ac93a5466e08e0f8582d9c Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Thu, 3 Dec 2020 14:11:24 -0500 Subject: [PATCH 010/113] GH-2205: Onboarding - Welcome & Get Started (#637) * Make browser welcome view * Add cta button * Change CTA button to a button * Add comment * Remove redux connection --- _locales/en/messages.json | 9 +++ .../Step0_WelcomeView/WelcomeView.jsx | 14 ++++- .../Step0_WelcomeView/WelcomeView.scss | 57 ++++++++++++++++++ .../Step0_WelcomeView/index.js | 2 +- app/images/hub/welcome/rocketShip.png | Bin 0 -> 74216 bytes app/scss/hub.scss | 1 + app/scss/hub_ghostery_browser.scss | 1 + 7 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss create mode 100644 app/images/hub/welcome/rocketShip.png diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 77ceba8f9..11e484c8d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1749,6 +1749,15 @@ "ghostery_browser_hub_onboarding_header_title_plan_choices": { "message": "Ghostery Browser Hub - Plan Choices" }, + "ghostery_browser_hub_onboarding_welcome": { + "message": "Welcome to Ghostery Browser" + }, + "ghostery_browser_hub_onboarding_lets_begin": { + "message": "Let's begin by choosing your privacy settings. This will be quick, we promise!" + }, + "ghostery_browser_hub_onboarding_lets_do_this": { + "message": "Let's do this" + }, "enable_when_paused": { "message": "To use this function, Resume Ghostery." }, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx index eb3b3766a..d0038f7ec 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx @@ -13,6 +13,18 @@ import React from 'react'; -const WelcomeView = () =>

Step 5: Welcome View

; +/** + * A Functional React component for rendering the Browser Welcome View + * @return {JSX} JSX for rendering the Browser Welcome View of the Hub app + * @memberof HubComponents + */ +const WelcomeView = () => ( +
+
{t('ghostery_browser_hub_onboarding_welcome')}
+
{t('ghostery_browser_hub_onboarding_lets_begin')}
+ + +
+); export default WelcomeView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss new file mode 100644 index 000000000..b6d7c6998 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss @@ -0,0 +1,57 @@ +.WelcomeView__container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.WelcomeView__title { + margin-top: 125px; + font-size: 24px; + font-weight: 600; + line-height: 2.33; +} + +.WelcomeView__subtitle { + margin-bottom: 20px; + width: 392px; + font-size: 18px; + line-height: 2.33; + text-align: center; +} + +.WelcomeView__rocketShip { + margin-bottom: 84px; + height: 330px; + width: 545px; +} + +.WelcomeView__ctaButton { + display: flex; + justify-content: center; + margin: 48px auto 0 auto; + height: 44px; + width: 162px; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + #00aef0 + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + color: #FFF; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; +} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js index 1ae8bd4ca..d5f22aa58 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js @@ -13,4 +13,4 @@ import WelcomeView from './WelcomeView'; -export default WelcomesView; +export default WelcomeView; diff --git a/app/images/hub/welcome/rocketShip.png b/app/images/hub/welcome/rocketShip.png new file mode 100644 index 0000000000000000000000000000000000000000..1982e4fb69be2999eefaee4bc5f375478e8b2604 GIT binary patch literal 74216 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 Date: Thu, 3 Dec 2020 16:05:52 -0500 Subject: [PATCH 011/113] GH-2207: Onboarding - Success Screen (#638) * Make skeleton for success view * Copy jsx over from WelcomeView * Make success screen, need to add image * Add image and adjust spacing * Re-add default view for original hub --- _locales/en/messages.json | 9 +++ .../Step5_SuccessView/SuccessView.jsx | 14 ++++- .../Step5_SuccessView/SuccessView.scss | 57 ++++++++++++++++++ app/images/hub/success/ghostery-suite.png | Bin 0 -> 47315 bytes app/scss/hub.scss | 3 +- 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss create mode 100644 app/images/hub/success/ghostery-suite.png diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 11e484c8d..42c00d1fb 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1758,6 +1758,15 @@ "ghostery_browser_hub_onboarding_lets_do_this": { "message": "Let's do this" }, + "ghostery_browser_hub_onboarding_yay_youre_all_set": { + "message": "Yay! You're all set." + }, + "ghostery_browser_hub_onboarding_start_browsing_the_web_with": { + "message": "Start browsing the web with" + }, + "ghostery_browser_hub_onboarding_lets_search": { + "message": "Let's search" + }, "enable_when_paused": { "message": "To use this function, Resume Ghostery." }, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx index 2ac283a17..815e60463 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx @@ -13,6 +13,18 @@ import React from 'react'; -const SuccessView = () =>

Step 5: Success View

; +/** + * A Functional React component for rendering the Browser Success View + * @return {JSX} JSX for rendering the Browser Success View of the Hub app + * @memberof HubComponents + */ +const SuccessView = () => ( +
+
{t('ghostery_browser_hub_onboarding_yay_youre_all_set')}
+
{`${t('ghostery_browser_hub_onboarding_start_browsing_the_web_with')} Ghostery`}
+ + +
+); export default SuccessView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss new file mode 100644 index 000000000..9ef034fb7 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss @@ -0,0 +1,57 @@ +.SuccessView__container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.SuccessView__title { + margin-top: 125px; + font-size: 24px; + font-weight: 600; + line-height: 2.33; +} + +.SuccessView__subtitle { + margin-bottom: 47px; + width: 392px; + font-size: 18px; + line-height: 2.33; + text-align: center; +} + +.SuccessView__ghosterySuite { + margin-bottom: 77px; + height: 298px; + width: 545px; +} + +.SuccessView__ctaButton { + display: flex; + justify-content: center; + margin: auto; + height: 44px; + width: 162px; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + #00aef0 + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + color: #FFF; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; +} diff --git a/app/images/hub/success/ghostery-suite.png b/app/images/hub/success/ghostery-suite.png new file mode 100644 index 0000000000000000000000000000000000000000..ef30ba38783b891b4661db5227e0e8603bcbbf00 GIT binary patch literal 47315 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

G4sx7*v0}!t)q_oQho~OB?57XuiF2Jq{Lf_79FQ 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 Rx);DlQC3Z+TFN~1{{T_lvQ+>8 literal 0 HcmV?d00001 diff --git a/app/scss/hub.scss b/app/scss/hub.scss index 9593db249..b5e5780fb 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -86,7 +86,8 @@ html, body, #root { @import '../hub/Views/LogInView/LogInView.scss'; @import '../hub/Views/CreateAccountView/CreateAccountView.scss'; @import '../hub/Views/UpgradePlanView/UpgradePlanView.scss'; -@import '../hub/Views/BrowserWelcomeView/BrowserWelcomeView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory @import '../shared-components/ExitButton/ExitButton.scss'; From 97993894a19b5154a18f707826cc786403dfdc91 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Mon, 7 Dec 2020 17:26:40 -0500 Subject: [PATCH 012/113] GH-2206: Onboarding - Step 1 - Create Account (#639) * Make form * Add skip and learn more links * Set arrow up/down depending on expanded state * Make FAQ * Make list responsive * Adjust padding of FAQContainer to scroll onto nice spot * Make sure promoString checkbox text does not wrap * Add privacy policy link * Make privacy policy link more responsive * Make BrowserLogInForm and style responsively * Refactor Create Account form into a component * Create BrowserCreateAccountFormContainer and add handlers * Add error handling to legal consent label * Remove privacy policy link if learn more is not expanded * Add handlers for log in view and replace strings * Add handlers for forgot password and throttle it * Lower throttle time in case user enters a wrong email * Make updates checkbox work * Add signed in view * Change spacing of title and subtitle for small screens * Update create account design for small screens * Center skip link on small screens * Decrease spacing for small sreens on already have account/create account link * Decrease width of faqDescription items and privacy policy link * Open privacy policy link in new tab * Add confirm password functionality * Update passwords not match string * Add global email preferences to user object on account creation success * Hold input fields in place when error messages appear * Rename components * Add propTypes * Update comments * Remove newline * Update confirm password string * Add comments and minor refactors --- _locales/en/messages.json | 66 +++++ app/Account/AccountActions.js | 10 +- app/Account/AccountConstants.js | 3 + app/Account/AccountReducer.js | 12 +- .../Step1_CreateAccountForm.jsx | 256 +++++++++++++++++ .../Step1_CreateAccountForm.scss | 107 +++++++ .../Step1_CreateAccountFormContainer.jsx | 240 ++++++++++++++++ .../Step1_CreateAccountForm/index.js | 44 +++ .../Step1_CreateAccountView.jsx | 175 ++++++++++++ .../Step1_CreateAccountView.scss | 269 ++++++++++++++++++ .../Step1_CreateAccountView/index.js | 35 +++ .../Step1_LogInForm/Step1_LogInForm.jsx | 114 ++++++++ .../Step1_LogInForm/Step1_LogInForm.scss | 109 +++++++ .../Step1_LogInFormContainer.jsx | 187 ++++++++++++ .../OnboardingViews/Step1_LogInForm/index.js | 52 ++++ .../ghosty-box.svg | 1 + .../ghosty-letter.svg | 1 + .../ghosty-lock.svg | 1 + .../ghosty-shield-letter.svg | 1 + .../ghosty-shield.svg | 1 + app/panel/utils/utils.js | 14 + app/scss/hub.scss | 3 + 22 files changed, 1699 insertions(+), 2 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js create mode 100644 app/images/hub/browser-create-account-view/ghosty-box.svg create mode 100644 app/images/hub/browser-create-account-view/ghosty-letter.svg create mode 100644 app/images/hub/browser-create-account-view/ghosty-lock.svg create mode 100644 app/images/hub/browser-create-account-view/ghosty-shield-letter.svg create mode 100644 app/images/hub/browser-create-account-view/ghosty-shield.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 42c00d1fb..b1f614eb9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1722,6 +1722,9 @@ "hub_create_account_label_password_invalid_length": { "message": "Use between 8 and 50 characters." }, + "hub_create_account_confirm_password_do_not_match": { + "message": "Your passwords do not match." + }, "hub_create_account_already_have_account": { "message": "Already have a Ghostery Account?" }, @@ -1758,6 +1761,66 @@ "ghostery_browser_hub_onboarding_lets_do_this": { "message": "Let's do this" }, + "ghostery_browser_hub_onboarding_create_a_ghostery_account": { + "message": "Create a Ghostery Account" + }, + "ghostery_browser_hub_onboarding_sync_settings": { + "message": "Allows you to sync settings across browsers and devices" + }, + "ghostery_browser_hub_onboarding_already_have_account": { + "message": "I already have an account." + }, + "ghostery_browser_hub_onboarding_send_me": { + "message": "Send me" + }, + "ghostery_browser_hub_onboarding_updates_and_promotions": { + "message": "updates & promotions." + }, + "ghostery_browser_hub_onboarding_skip": { + "message": "Skip" + }, + "ghostery_browser_hub_onboarding_we_take_your_privacy_very_seriously": { + "message": "We take your privacy very seriously. Learn more" + }, + "ghostery_browser_hub_onboarding_private_by_design": { + "message": "Private by design" + }, + "ghostery_browser_hub_onboarding_private_by_design_description": { + "message": "Privacy is incredibly important to us at Ghostery and we've taken extra care to design our products that limits the amount of personal information collected." + }, + "ghostery_browser_hub_onboarding_why_do_you_need_my_email": { + "message": "Why do you need my email?" + }, + "ghostery_browser_hub_onboarding_why_do_you_need_my_email_description": { + "message": "We've found email is the simplest way to create and secure a unique account without sharing your information. It helps sync your custom settings across your devices." + }, + "ghostery_browser_hub_onboarding_what_do_you_use_my_email_for": { + "message": "What do you use my email for?" + }, + "ghostery_browser_hub_onboarding_what_do_you_use_my_email_for_description": { + "message": "We never share or sell your personal information. Your email address is used for account management, technical support, product-related communications, and some opt-in features." + }, + "ghostery_browser_hub_onboarding_how_secure_is_": { + "message": "How secure is" + }, + "ghostery_browser_hub_onboarding_how_secure_is_ghostery_description": { + "message": "We never share or sell your personal information. Your email address is used for account management, technical support, product-related communications, and some opt-in features." + }, + "ghostery_browser_hub_onboarding_can_i_remove_my_account": { + "message": "Can I remove my account?" + }, + "ghostery_browser_hub_onboarding_can_i_remove_my_account_description": { + "message": "We hope you enjoy using Ghostery, but if you do decide to leave we will remove all of your personal information within 90 days from the date of request." + }, + "ghostery_browser_hub_onboarding_visit_our_privacy_policy": { + "message": "Visit our Privacy Policy for more information" + }, + "ghostery_browser_hub_onboarding_create_an_account": { + "message": "Create an account." + }, + "ghostery_browser_hub_onboarding_you_are_signed_in_as": { + "message": "You are signed in as" + }, "ghostery_browser_hub_onboarding_yay_youre_all_set": { "message": "Yay! You're all set." }, @@ -2087,6 +2150,9 @@ "password_colon": { "message": "Password:" }, + "confirm_password_colon": { + "message": "Confirm Password:" + }, "please_enter_a_valid_email": { "message": "Please enter a valid email." }, diff --git a/app/Account/AccountActions.js b/app/Account/AccountActions.js index 4114a025b..c51317dec 100644 --- a/app/Account/AccountActions.js +++ b/app/Account/AccountActions.js @@ -27,7 +27,8 @@ import { GET_USER_SETTINGS_SUCCESS, GET_USER_SETTINGS_FAIL, GET_USER_SUBSCRIPTION_DATA_FAIL, - GET_USER_SUBSCRIPTION_DATA_SUCCESS + GET_USER_SUBSCRIPTION_DATA_SUCCESS, + ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE } from './AccountConstants'; import { SET_TOAST } from '../hub/Views/AppView/AppViewConstants'; import { CLEAR_THEME } from '../panel/constants/constants'; @@ -201,3 +202,10 @@ export const resetPassword = email => dispatch => ( }); }) ); + +export const handleEmailPreferencesCheckboxChange = (name, checked) => dispatch => ( + dispatch({ + type: ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE, + payload: { name, checked }, + }) +); diff --git a/app/Account/AccountConstants.js b/app/Account/AccountConstants.js index 64eb24a63..0f95a09c4 100644 --- a/app/Account/AccountConstants.js +++ b/app/Account/AccountConstants.js @@ -36,3 +36,6 @@ export const GET_USER_SETTINGS_FAIL = 'GET_USER_SETTINGS_FAIL'; // Update Subscription Data export const GET_USER_SUBSCRIPTION_DATA_FAIL = 'GET_USER_SUBSCRIPTION_DATA_FAIL'; export const GET_USER_SUBSCRIPTION_DATA_SUCCESS = 'GET_USER_SUBSCRIPTION_DATA_SUCCESS'; + +// Opt into/out of updates and promotions sendgrid emails +export const ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE = 'ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE'; diff --git a/app/Account/AccountReducer.js b/app/Account/AccountReducer.js index ebc31da25..0a91127a1 100644 --- a/app/Account/AccountReducer.js +++ b/app/Account/AccountReducer.js @@ -20,7 +20,8 @@ import { GET_USER_SUBSCRIPTION_DATA_FAIL, GET_USER_SUBSCRIPTION_DATA_SUCCESS, RESET_PASSWORD_SUCCESS, - RESET_PASSWORD_FAIL + RESET_PASSWORD_FAIL, + ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE } from './AccountConstants'; import { UPDATE_PANEL_DATA } from '../panel/constants/constants'; @@ -118,6 +119,15 @@ export default (state = initialState, action) => { resetPasswordError: true }; } + case ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE: { + const { name, checked } = action.payload; + let emailPreferences; + if (name === 'global') { + emailPreferences = { ...state.user.emailPreferences, ...{ global: checked } }; + } + const user = { ...state.user, ...{ emailPreferences } }; + return { ...state, ...{ user } }; + } default: return state; } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx new file mode 100644 index 000000000..2b2782fd4 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx @@ -0,0 +1,256 @@ +/** + * Browser Create Account Form + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React, { useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import ClassNames from 'classnames'; +import { NavLink } from 'react-router-dom'; +import { ToggleCheckbox } from '../../../shared-components'; + +const promoString = `${t('ghostery_browser_hub_onboarding_send_me')} Ghostery ${t('ghostery_browser_hub_onboarding_updates_and_promotions')}`; + +/** + * A Functional React component for rendering the Browser Create Account View + * @return {JSX} JSX for rendering the Browser Create Account View of the Hub app + * @memberof HubComponents + */ +const Step1_CreateAccountForm = (props) => { + const { + email, + emailError, + confirmEmail, + confirmEmailError, + firstName, + lastName, + password, + passwordInvalidError, + passwordLengthError, + confirmPassword, + confirmPasswordError, + legalConsentChecked, + legalConsentNotCheckedError, + handleLegalConsentCheckboxChange, + isUpdatesChecked, + handleInputChange, + handleUpdatesCheckboxChange, + handleSubmit, + } = props; + + const emailInputClassNames = ClassNames('Step1_CreateAccountForm__inputBox', { + error: emailError, + }); + const confirmInputClassNames = ClassNames('Step1_CreateAccountForm__inputBox', { + error: confirmEmailError, + }); + const passwordInputClassNames = ClassNames('Step1_CreateAccountForm__inputBox', { + error: passwordInvalidError || passwordLengthError, + }); + const legalConsentClassNames = ClassNames('Step1_CreateAccountForm__legalConsentCheckedLabel', { + error: legalConsentNotCheckedError + }); + + return ( +

+
+
+ + + {emailError && ( +
+
+ {t('please_enter_a_valid_email')} +
+
+ )} +
+
+ + + {confirmEmailError && ( +
+
+ {t('your_email_do_not_match')} +
+
+ )} +
+
+
+
+ + +
+
+ + +
+
+
+
+ + + {passwordInvalidError && ( +
+
+ {t('hub_create_account_label_password_invalid')} +
+
+ )} + {passwordLengthError && ( +
+
+ {t('hub_create_account_label_password_invalid_length')} +
+
+ )} +
+
+ + + {confirmPasswordError && ( +
+
+ {t('hub_create_account_confirm_password_do_not_match')} +
+
+ )} +
+
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ +
+ + ); +}; + +// PropTypes ensure we pass required props of the correct type +Step1_CreateAccountForm.propTypes = { + email: PropTypes.string.isRequired, + emailError: PropTypes.bool.isRequired, + confirmEmail: PropTypes.string.isRequired, + confirmEmailError: PropTypes.bool.isRequired, + firstName: PropTypes.string.isRequired, + lastName: PropTypes.string.isRequired, + isUpdatesChecked: PropTypes.bool.isRequired, + password: PropTypes.string.isRequired, + confirmPassword: PropTypes.string.isRequired, + passwordInvalidError: PropTypes.bool.isRequired, + passwordLengthError: PropTypes.bool.isRequired, + handleInputChange: PropTypes.func.isRequired, + handleUpdatesCheckboxChange: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, +}; + +export default Step1_CreateAccountForm; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss new file mode 100644 index 000000000..964008ede --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss @@ -0,0 +1,107 @@ +/** + * Step1_CreateAccountForm Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +.Step1_CreateAccountForm--addPaddingTop { + padding-top: 20px; +} +.Step1_CreateAccountForm__inputLabel { + font-size: 14px; + font-weight: 500; + line-height: 2.86; + text-transform: uppercase; + color: #4a4a4a; +} +.Step1_CreateAccountForm__inputBox { + margin-bottom: 20px; + font-size: 14; + line-height: 24px; + color: #4a4a4a; + + // Foundation Overrides + border-radius: 6px; + box-shadow: none; + border: 1px solid #9b9b9b; + + &::placeholder { + color: #c4ced1; + } +} +.Step1_CreateAccountForm__inputBox.error { + border-color: $color-create-account-form-error-red; +} +.Step1_CreateAccountForm__inputErrorContainer { + position: relative; + + > .Step1_CreateAccountForm__inputError { + position: absolute; + margin-top: -15px; + color: red; + } +} +.Step1_CreateAccountForm__promoString { + font-size: 14px; + margin-top: 15px; + @include breakpoint(small down) { + margin-top: 0; + } +} +.Step1_CreateAccountForm__legalConsentCheckedLabel { + font-size: 14px; + @include breakpoint(small down) { + width: 258px; + } + + &.error { + color: red; + } +} +.Step1_CreateAccountForm__ctaButtonContainer { + display: flex; + justify-content: center; + + .Step1_CreateAccountForm__ctaButton { + margin: 48px auto 0 auto; + height: 44px; + width: 162px; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + #00aef0 + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + color: #FFF; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; + } +} +@media only screen and (max-width: 740px) { + .Step1_CreateAccountForm__checkboxContainer { + display: flex; + align-items: center; + padding-right: 12px; + justify-content: center; + } +} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx new file mode 100644 index 000000000..8d043ea9a --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx @@ -0,0 +1,240 @@ +/** + * Create Account Form Container + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { + validateEmail, + validatePassword, + validateEmailsMatch, + validateConfirmEmail, + validatePasswordsMatch +} from '../../../../panel/utils/utils'; +import BrowserCreateAccountForm from './Step1_CreateAccountForm'; + +/** + * @class Implement the Create Account View for the Ghostery Hub + * @extends Component + * @memberof HubContainers + */ +class CreateAccountFormContainer extends Component { + constructor(props) { + super(props); + this.state = { + email: '', + emailError: false, + confirmEmail: '', + confirmEmailError: false, + firstName: '', + lastName: '', + legalConsentChecked: false, + legalConsentNotCheckedError: false, + isUpdatesChecked: false, + password: '', + passwordInvalidError: false, + passwordLengthError: false, + confirmPassword: '', + confirmPasswordError: '', + validateInput: false, + }; + + const { actions } = this.props; + actions.setToast({ + toastMessage: '', + toastClass: '', + }); + } + + /** + * Update input values by updating state. + * @param {Object} event the 'change' event + */ + _handleInputChange = (event) => { + const { name, value } = event.target; + this.setState({ [name]: value }); + + const { validateInput } = this.state; + if (!validateInput) { + return; + } + + switch (name) { + case 'email': { + const emailIsValid = value && validateEmail(value); + this.setState({ + emailError: !emailIsValid, + }); + break; + } + case 'confirmEmail': { + const { email } = this.state; + const confirmIsValid = value && validateEmailsMatch(email, value); + this.setState({ + confirmEmailError: !confirmIsValid, + }); + break; + } + case 'password': { + const passwordIsValid = value && validatePassword(value); + const invalidChars = !passwordIsValid && value.length >= 8 && value.length <= 50; + const invalidLength = !passwordIsValid && !invalidChars; + this.setState({ + passwordInvalidError: invalidChars, + passwordLengthError: invalidLength, + }); + break; + } + case 'confirmPassword': { + const { password } = this.state; + const confirmPasswordIsValid = value && validatePasswordsMatch(password, value); + this.setState({ + confirmPasswordError: !confirmPasswordIsValid, + }); + break; + } + default: break; + } + } + + /** + * Update legal consent checkbox value by updating state + */ + _handleLegalConsentCheckboxChange = () => { + this.setState(prevState => ({ legalConsentChecked: !prevState.legalConsentChecked })); + } + + /** + * Update updates checkbox value by updating state + */ + _handleUpdatesCheckboxChange = () => { + this.setState(prevState => ({ isUpdatesChecked: !prevState.isUpdatesChecked })); + } + + /** + * Handle creating an account, but validate the data first. + * @param {Object} event the 'submit' event + */ + _handleCreateAccountAttempt = (event) => { + event.preventDefault(); + const { + email, + confirmEmail, + firstName, + lastName, + legalConsentChecked, + password, + confirmPassword, + isUpdatesChecked, + } = this.state; + const emailIsValid = email && validateEmail(email); + const confirmIsValid = confirmEmail && validateConfirmEmail(email, confirmEmail); + const passwordIsValid = password && validatePassword(password); + const invalidChars = !passwordIsValid && password.length >= 8 && password.length <= 50; + const invalidLength = !passwordIsValid && !invalidChars; + const confirmPasswordError = password !== confirmPassword; + + this.setState({ + emailError: !emailIsValid, + confirmEmailError: !confirmIsValid, + legalConsentNotCheckedError: !legalConsentChecked, + passwordInvalidError: invalidChars, + passwordLengthError: invalidLength, + validateInput: true, + }); + + if (!emailIsValid || !confirmIsValid || !legalConsentChecked || !passwordIsValid || confirmPasswordError) { + return; + } + const { actions } = this.props; + actions.setToast({ + toastMessage: '', + toastClass: '' + }); + actions.register(email, confirmEmail, firstName, lastName, password).then((success) => { + if (success) { + actions.getUser().then(() => { + if (isUpdatesChecked) actions.handleEmailPreferencesCheckboxChange('global', isUpdatesChecked); + }); + // Toggle legal consent checked here + actions.setToast({ + toastMessage: t('hub_create_account_toast_success'), + toastClass: 'success' + }); + // Route to next screen + } else { + actions.setToast({ + toastMessage: t('hub_create_account_toast_error'), + toastClass: 'alert' + }); + } + }); + } + + /** + * React's required render function. Returns JSX + * @return {JSX} JSX for rendering the Create Account View of the Hub app + */ + render() { + // const { user } = this.props; + const { + email, + emailError, + confirmEmail, + confirmEmailError, + firstName, + lastName, + legalConsentChecked, + legalConsentNotCheckedError, + isUpdatesChecked, + password, + passwordInvalidError, + passwordLengthError, + confirmPassword, + confirmPasswordError + } = this.state; + + return ( + + ); + } +} + +// PropTypes ensure we pass required props of the correct type +CreateAccountFormContainer.propTypes = { + actions: PropTypes.shape({ + setToast: PropTypes.func.isRequired, + register: PropTypes.func.isRequired, + getUser: PropTypes.func.isRequired, + }).isRequired, +}; + +export default CreateAccountFormContainer; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js new file mode 100644 index 000000000..30c614789 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js @@ -0,0 +1,44 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Create Account Form + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import Step1_CreateAccountFormContainer from './Step1_CreateAccountFormContainer'; +import { register, getUser, handleEmailPreferencesCheckboxChange } from '../../../../Account/AccountActions'; +import { setToast } from '../../../../hub/Views/AppView/AppViewActions'; + +/** + * Map redux store state properties to the component's own properties. + * @param {Object} state entire Redux store's state + * @return {function} this function returns a plain object, which will be merged into the component's props + * @memberof HubContainers + */ +const mapStateToProps = state => ({ ...state.account }); + +/** + * Bind the component's action creators using Redux's bindActionCreators. + * @param {function} dispatch redux store method which dispatches actions + * @return {function} to be used as an argument in redux connect call + * @memberof SetupContainers + */ +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ + setToast, + register, + getUser, + handleEmailPreferencesCheckboxChange + }, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Step1_CreateAccountFormContainer); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx new file mode 100644 index 000000000..d97910485 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx @@ -0,0 +1,175 @@ +/** + * Browser Create Account View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React, { Fragment, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import ClassNames from 'classnames'; +import { NavLink } from 'react-router-dom'; +import Step1_LogInForm from '../Step1_LogInForm'; +import Step1_CreateAccountForm from '../Step1_CreateAccountForm'; +import globals from '../../../../../src/classes/Globals'; + +const SIGN_IN = 'SIGN_IN'; +const CREATE_ACCOUNT = 'CREATE_ACCOUNT'; + +const faqList = [ + { + icon: 'ghosty-shield.svg', + label: t('ghostery_browser_hub_onboarding_private_by_design'), + description: t('ghostery_browser_hub_onboarding_private_by_design_description'), + }, + { + icon: 'ghosty-letter.svg', + label: t('ghostery_browser_hub_onboarding_why_do_you_need_my_email'), + description: t('ghostery_browser_hub_onboarding_why_do_you_need_my_email_description'), + }, + { + icon: 'ghosty-shield-letter.svg', + label: t('ghostery_browser_hub_onboarding_what_do_you_use_my_email_for'), + description: t('ghostery_browser_hub_onboarding_what_do_you_use_my_email_for_description'), + }, + { + icon: 'ghosty-lock.svg', + label: `${t('ghostery_browser_hub_onboarding_how_secure_is_')} Ghostery?`, + description: t('ghostery_browser_hub_onboarding_how_secure_is_ghostery_description'), + }, + { + icon: 'ghosty-box.svg', + label: t('ghostery_browser_hub_onboarding_can_i_remove_my_account'), + description: t('ghostery_browser_hub_onboarding_can_i_remove_my_account_description'), + } +]; + +const renderFAQListItem = (icon, label, description) => ( +
+
+ +
+
+
{label}
+
{description}
+
+
+); + +const renderSkipLink = () => ( +
+
+
+
{t('ghostery_browser_hub_onboarding_skip')}
+
+
+); + +/** + * A Functional React component for rendering the Browser Create Account View + * @return {JSX} JSX for rendering the Browser Create Account View of the Hub app + * @memberof HubComponents + */ +const Step1_CreateAccountView = (props) => { + const { user } = props; + const email = user && user.email; + + const [expanded, setExpanded] = useState(false); + const [view, setView] = useState(CREATE_ACCOUNT); + + const arrowClassNames = ClassNames('Step1_CreateAccountView__arrow', { + up: expanded, + down: !expanded, + }); + + const faqRef = useRef(null); + const scrollToFAQ = () => { + faqRef.current.scrollIntoView({ behavior: 'smooth' }); + }; + + const handleFAQLearnMoreClick = () => { + setTimeout(scrollToFAQ, 1); + setExpanded(!expanded); + }; + + return (user ? ( +
+
{t('ghostery_browser_hub_onboarding_you_are_signed_in_as')}
+
{email}
+
+ {/* Link to next page */} + +
+
+ ) : ( +
+ {view === CREATE_ACCOUNT && ( +
{t('ghostery_browser_hub_onboarding_create_a_ghostery_account')}
+ )} + {view === SIGN_IN && ( +
{t('sign_in')}
+ )} +
{ t('ghostery_browser_hub_onboarding_sync_settings') }
+
+ {view === CREATE_ACCOUNT && ( +
setView(SIGN_IN)}>{t('ghostery_browser_hub_onboarding_already_have_account')}
+ )} + {view === SIGN_IN && ( +
setView(CREATE_ACCOUNT)}>{t('ghostery_browser_hub_onboarding_create_an_account')}
+ )} +
+
+ {view === CREATE_ACCOUNT ? ( + + {/* eslint-disable-next-line react/jsx-pascal-case */} + + {renderSkipLink()} +
+
{t('ghostery_browser_hub_onboarding_we_take_your_privacy_very_seriously')}
+
+
+
+ {expanded && + faqList.map(item => renderFAQListItem(item.icon, item.label, item.description)) + } +
+ {expanded && ( + + )} + + ) : ( + + {/* eslint-disable-next-line react/jsx-pascal-case */} + + {renderSkipLink()} + + )} +
+ )); +}; + +// PropTypes ensure we pass required props of the correct type +Step1_CreateAccountView.propTypes = { + user: PropTypes.shape({ + email: PropTypes.string, + }), +}; + +// Default props used in the Plus View +Step1_CreateAccountView.defaultProps = { + user: { + email: '' + }, +}; + +export default Step1_CreateAccountView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss new file mode 100644 index 000000000..d79bf60a2 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss @@ -0,0 +1,269 @@ +/** + * Step1_CreateAccountView Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +$color-create-account-form-error-red: #e74055; + +// Browser Create Account View +.Step1_CreateAccountView { + padding-top: 80px; + padding-bottom: 40px; +} +.Step1_CreateAccountView__alreadySignedIn { + margin-top: 245px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} +.Step1_CreateAccountView__email { + font-size: 24px; + color: $ghosty-blue; +} +.Step1_CreateAccountView__ctaButtonContainer { + display: flex; + justify-content: center; + + .Step1_CreateAccountView__ctaButton { + margin: 48px auto 0 auto; + height: 44px; + width: 162px; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + #00aef0 + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + color: #FFF; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; + } +} +.Step1_CreateAccountView__title { + display: flex; + justify-content: center; + font-size: 24px; + font-weight: 600; + @include breakpoint(small down) { + margin: auto; + max-width: 237px; + text-align: center; + } +} +.Step1_CreateAccountView__subtitle { + margin-top: 10px; + display: flex; + justify-content: center; + font-size: 18px; + text-align: center; + @include breakpoint(small down) { + margin: auto; + max-width: 237px; + text-align: center; + line-height: 2.33; + } +} +.Step1_CreateAccountView__alreadyHaveAccount { + margin-top: 38px; + font-size: 16px; + color: $ghosty-blue; + text-decoration: underline; + cursor: pointer; + @include breakpoint(small down) { + margin: 38px auto 0 auto; + text-align: center; + + &:nth-child(2) { + margin: 0; + } + } +} +.Step1_CreateAccountView--addMarginSide { + margin-left: 10px; + margin-right: 10px; +} +.Step1_CreateAccountView--addMarginBottom { + margin-bottom: 20px; +} +.Step1_CreateAccountView__headerImage { + max-width: 180px; +} +.Step1_CreateAccountView__headerTitle h3 { + font-size: 24px; + line-height: 1.75; + color: #4a4a4a; + margin: 0 25px; +} +.Step1_CreateAccountView__inputBox.error { + // Foundation Overrides + box-shadow: none; + border-color: 2px solid $color-create-account-form-error-red; +} +.Step1_CreateAccountView__inputBox:focus { + // Foundation Overrides + box-shadow: none; + border-color: $color-create-account-form-error-red; +} +.Step1_CreateAccountView__inputError { + font-size: 12; + line-height: 14px; + color: $color-create-account-form-error-red; + margin-bottom: 13px; +} +.Step1_CreateAccountView__link { + font-size: 14px; + line-height: 30px; +} +.Step1_CreateAccountView__termsStatement { + font-size: 12px; +} +.Step1_CreateAccountView__button { + min-width: 180px; +} +.Step1_CreateAccountView__inputLabel { + flex-grow: 1; // ensure left-justify alignment in single column layout + padding-top: 10px; // align with checkbox top +} +.Step1_CreateAccountView__inputLabel.error{ + color: $color-create-account-form-error-red; +} +.Step1_CreateAccountView__skip { + margin-top: -37px; + float: right; + font-size: 16px; + color: $ghosty-blue; + text-decoration: underline; + cursor: pointer; + @include breakpoint(small down) { + margin: 39px auto 0 auto; + text-align: center; + float: none; + } +} +.Step1_CreateAccountView__learnMoreContainer { + margin: 25px auto 0 auto; + width: 251px; + text-align: center; +} +.Step1_CreateAccountView__learnMore { + font-size: 16px; + color: $ghosty-blue; + text-decoration: underline; + cursor: pointer; +} +.Step1_CreateAccountView__arrow { + margin: 15px auto 0 auto; + height: 12px; + width: 12px; + border-left: 2px solid #00aef0; + border-top: 2px solid #00aef0; + cursor: pointer; + + &.up { + transform: rotate(45deg); + } + &.down { + transform: rotate(225deg); + } +} +.Step1_CreateAccountView__FAQContainer { + margin-top: 10px; + padding-top: 20px; + @include breakpoint(small down) { + text-align: center; + } +} +.Step1_CreateAccountView__faqItemContainer { + margin-bottom: 30px; +} +.Step1_CreateAccountView__faqIcon { + height: 71px; + width: 71px; + min-height: 71px; + min-width: 71px; + margin-bottom: 10px; +} +.Step1_CreateAccountView__faqItemTextContainer { + display: flex; + flex-direction: column; +} +.Step1_CreateAccountView__faqItemLabel { + font-size: 18px; + font-weight: 600; + color: #4a4a4a; +} +.Step1_CreateAccountView__faqItemDescription { + margin-top: 10px; + max-width: 607px; + font-size: 16px; + color: #4a4a4a; +} +.Step1_CreateAccountView__faqItemDescription { + margin-top: 10px; + max-width: 607px; + font-size: 16px; + color: #4a4a4a; + @include breakpoint(small down) { + margin: 16px auto 0 auto; + width: 245px; + } +} +.Step1_CreateAccountView__privacyPolicyLink { + font-size: 16px; + color: $ghosty-blue; + text-decoration: underline; + cursor: pointer; + @include breakpoint(small down) { + margin: auto; + max-width: 271px; + text-align: center; + } +} +@media only screen and (max-width: 740px) { + .Step1_CreateAccountView { + margin-bottom: 40px; + padding-top: 20px; + } + .Step1_CreateAccountView__header { + flex-direction: column; + } + .Step1_CreateAccountView__headerTitle { + padding-top: 30px; + text-align: center; + } + .Step1_CreateAccountView__submit { + display: flex; + justify-content: center; + } + .Step1_CreateAccountView__linkContainer { + text-align: center; + } +} +@media only screen and (max-width: 500px) { + .Step1_CreateAccountView__headerTitle h3 { + font-size: 20px; + margin: 0 10px; + } +} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js new file mode 100644 index 000000000..d8bc43c75 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js @@ -0,0 +1,35 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Create Account View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import Step1_CreateAccountView from './Step1_CreateAccountView'; + +/** + * Map redux store state properties to the component's own properties. + * @param {Object} state entire Redux store's state + * @return {function} this function returns a plain object, which will be merged into the component's props + * @memberof HubContainers + */ +const mapStateToProps = state => ({ ...state.account }); + +/** + * Bind the component's action creators using Redux's bindActionCreators. + * @param {function} dispatch redux store method which dispatches actions + * @return {function} to be used as an argument in redux connect call + * @memberof SetupContainers + */ +const mapDispatchToProps = dispatch => ({}); + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Step1_CreateAccountView)); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx new file mode 100644 index 000000000..bc712d4cc --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx @@ -0,0 +1,114 @@ +/** + * Browser Log In Form + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import ClassNames from 'classnames'; +import { NavLink } from 'react-router-dom'; + +/** + * A Functional React component for rendering the Log In Form + * @return {JSX} JSX for rendering the Log In Form of the Browser Hub app + * @memberof HubComponents + */ +const Step1_LogInForm = (props) => { + const { + email, + password, + emailError, + passwordError, + handleSubmit, + handleInputChange, + handleForgotPassword, + } = props; + + const emailInputClassNames = ClassNames('Step1_LogInForm__inputBox', { + error: emailError, + }); + const passwordInputClassNames = ClassNames('Step1_LogInForm__inputBox', { + error: passwordError, + }); + + return ( +
+
+
+ + + {emailError && ( +
+ {t('please_enter_a_valid_email')} +
+ )} +
+
+
+
+ + + {passwordError && ( +
+ {t('hub_login_label_password_invalid')} +
+ )} +
+
+
+
+ +
+ {t('forgot_password')} +
+
+
+
+
+ +
+
+ ); +}; + +// PropTypes ensure we pass required props of the correct type +Step1_LogInForm.propTypes = { + email: PropTypes.string.isRequired, + password: PropTypes.string.isRequired, + emailError: PropTypes.bool.isRequired, + passwordError: PropTypes.bool.isRequired, + handleSubmit: PropTypes.func.isRequired, + handleInputChange: PropTypes.func.isRequired, +}; + +export default Step1_LogInForm; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss new file mode 100644 index 000000000..9121a1d53 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss @@ -0,0 +1,109 @@ +/** + * Step1_LogInForm Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +.Step1_LogInForm { + margin-top: 40px; +} +.Step1_LogInForm__headerImage { + line-height: 1.5; + color: #4a4a4a; + margin: 20px 25px 0; +} +.Step1_LogInForm__inputContainer { + margin: auto; +} +.Step1_LogInForm__inputLabel { + text-align: left; + font-size: 14px; + font-weight: 500; + line-height: 2.86; + text-transform: uppercase; + color: #4a4a4a; +} +.Step1_LogInForm__inputBox { + line-height: 24px; + color: #4a4a4a; + + // Foundation Overrides + border-radius: 6px; + box-shadow: none; + border: 1px solid #9b9b9b; + + &::placeholder { + color: #c4ced1; + } +} +.Step1_LogInForm__inputBox.error { + margin-bottom: 8px; + border: 2px solid #e74055; +} +.Step1_LogInForm__inputBox:focus { + // Foundation Overrides + box-shadow: none; + border-color: #4a4a4a; +} +.Step1_LogInForm__inputError { + font-size: 12; + line-height: 14px; + color: #e74055; + margin-bottom: 13px; +} +.Step1_LogInForm__link { + font-size: 14px; + line-height: 30px; +} +.Step1_LogInForm__forgotPassword { + color: $ghosty-blue; + text-decoration: underline; + cursor: pointer; +} +.Step1_LogInForm__button { + min-width: 180px; +} +.Step1_LogInForm__ctaButtonContainer { + display: flex; + justify-content: center; + + .Step1_LogInForm__ctaButton { + margin: 48px auto 0 auto; + height: 44px; + width: 162px; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + #00aef0 + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + color: #FFF; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; + } +} +@media only screen and (max-width: 740px) { + .Step1_LogInForm__header { + flex-direction: column; + } +} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx new file mode 100644 index 000000000..e957e3dfa --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx @@ -0,0 +1,187 @@ +/** + * Step1_LogInFormContainer + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { throttle } from 'underscore'; +import { validateEmail } from '../../../../panel/utils/utils'; +import Step1_LogInForm from './Step1_LogInForm'; + +/** + * @class Implement the Browser Log In Form for the Ghostery Hub + * @extends Component + * @memberof HubContainers + */ +class Step1_LogInFormContainer extends Component { + constructor(props) { + super(props); + this.state = { + email: '', + password: '', + emailError: false, + passwordError: false, + validateInput: false, + }; + + const { actions } = this.props; + actions.setToast({ + toastMessage: '', + toastClass: '', + }); + } + + /** + * Update input values by updating state. + * @param {Object} event the 'change' event + */ + _handleInputChange = (event) => { + const { name, value } = event.target; + this.setState({ [name]: value }); + + const { validateInput } = this.state; + if (!validateInput) { + return; + } + + switch (name) { + case 'email': { + const emailIsValid = value && validateEmail(value); + this.setState({ + emailError: !emailIsValid, + }); + break; + } + case 'password': { + this.setState({ + passwordError: !value, + }); + break; + } + default: break; + } + } + + /** + * Handle logging in, but validate the data first. + * @param {Object} event the 'submit' event + */ + _handleLoginAttempt = (event) => { + event.preventDefault(); + const { email, password } = this.state; + const emailIsValid = email && validateEmail(email); + + this.setState({ + emailError: !emailIsValid, + passwordError: !password, + validateInput: true, + }); + + if (!emailIsValid || !password) { + return; + } + + const { actions } = this.props; + actions.setToast({ + toastMessage: '', + toastClass: '' + }); + actions.login(email, password).then((success) => { + if (success) { + const { origin, pathname, hash } = window.location; + window.history.pushState({}, '', `${origin}${pathname}${hash}`); + + actions.getUser(); + actions.getUserSettings() + .then((settings) => { + const { current_theme } = settings; + return actions.getTheme(current_theme); + }); + actions.setToast({ + toastMessage: t('hub_login_toast_success'), + toastClass: 'success' + }); + // Move to second screen + } else { + actions.setToast({ + toastMessage: t('no_such_email_password_combo'), + toastClass: 'alert' + }); + } + }); + } + + _unthrottledHandleForgotPassword = (e) => { + e.preventDefault(); + const { email } = this.state; + + // validate the email and password + if (!validateEmail(email)) { + this.setState({ + emailError: true, + }); + return; + } + + const { actions } = this.props; + actions.resetPassword(email) + .then((success) => { + if (success) { + actions.setToast({ + toastMessage: t('banner_check_your_email_title'), + toastClass: 'success', + }); + } + }); + } + + // eslint-disable-next-line react/sort-comp + _handleForgotPassword = throttle(this._unthrottledHandleForgotPassword, 3000) + + /** + * React's required render function. Returns JSX + * @return {JSX} JSX for rendering the Log In View of the Hub app + */ + render() { + const { + email, + password, + emailError, + passwordError, + } = this.state; + + return ( + // eslint-disable-next-line react/jsx-pascal-case + + ); + } +} + +// PropTypes ensure we pass required props of the correct type +Step1_LogInFormContainer.propTypes = { + actions: PropTypes.shape({ + setToast: PropTypes.func.isRequired, + login: PropTypes.func.isRequired, + getUser: PropTypes.func.isRequired, + getUserSettings: PropTypes.func.isRequired, + }).isRequired, +}; + +export default Step1_LogInFormContainer; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js new file mode 100644 index 000000000..17b0ba241 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js @@ -0,0 +1,52 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Onboarding Login View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import Step1_LogInFormContainer from './Step1_LogInFormContainer'; +import { + login, + getUser, + getUserSettings, + resetPassword +} from '../../../Account/AccountActions'; +import { getTheme } from '../../../panel/actions/PanelActions'; +import { setToast } from '../AppView/AppViewActions'; + +/** + * Map redux store state properties to the component's own properties. + * @param {Object} state entire Redux store's state + * @return {function} this function returns a plain object, which will be merged into the component's props + * @memberof HubContainers + */ +const mapStateToProps = state => ({ ...state.account }); + +/** + * Bind the component's action creators using Redux's bindActionCreators. + * @param {function} dispatch redux store method which dispatches actions + * @return {function} to be used as an argument in redux connect call + * @memberof SetupContainers + */ +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ + setToast, + login, + getUser, + getUserSettings, + getTheme, + resetPassword + }, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Step1_LogInFormContainer); diff --git a/app/images/hub/browser-create-account-view/ghosty-box.svg b/app/images/hub/browser-create-account-view/ghosty-box.svg new file mode 100644 index 000000000..c2fe79ee6 --- /dev/null +++ b/app/images/hub/browser-create-account-view/ghosty-box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/hub/browser-create-account-view/ghosty-letter.svg b/app/images/hub/browser-create-account-view/ghosty-letter.svg new file mode 100644 index 000000000..2921eee09 --- /dev/null +++ b/app/images/hub/browser-create-account-view/ghosty-letter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/hub/browser-create-account-view/ghosty-lock.svg b/app/images/hub/browser-create-account-view/ghosty-lock.svg new file mode 100644 index 000000000..03f9465db --- /dev/null +++ b/app/images/hub/browser-create-account-view/ghosty-lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/hub/browser-create-account-view/ghosty-shield-letter.svg b/app/images/hub/browser-create-account-view/ghosty-shield-letter.svg new file mode 100644 index 000000000..2fb520cf9 --- /dev/null +++ b/app/images/hub/browser-create-account-view/ghosty-shield-letter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/hub/browser-create-account-view/ghosty-shield.svg b/app/images/hub/browser-create-account-view/ghosty-shield.svg new file mode 100644 index 000000000..6c7d7cc2d --- /dev/null +++ b/app/images/hub/browser-create-account-view/ghosty-shield.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/panel/utils/utils.js b/app/panel/utils/utils.js index ce438df33..632d2e1ac 100644 --- a/app/panel/utils/utils.js +++ b/app/panel/utils/utils.js @@ -165,6 +165,20 @@ export function validatePassword(pwd) { return pwd !== '' && pwdRegex.test(pwd); } +/** + * Check for confirm password equality to password + * @memberOf PanelUtils + * @param {string} password password + * @param {string} confirmPassword confirm password to validate + * @return {boolean} true if equal, false otherwise + */ +export function validatePasswordsMatch(password, confirmPassword) { + if (!password || !confirmPassword) { + return false; + } + return password === confirmPassword; +} + /** * Helper method for making XHR requests * @memberOf PanelUtils diff --git a/app/scss/hub.scss b/app/scss/hub.scss index b5e5780fb..07e777997 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -87,6 +87,9 @@ html, body, #root { @import '../hub/Views/CreateAccountView/CreateAccountView.scss'; @import '../hub/Views/UpgradePlanView/UpgradePlanView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory From edc434e3ac8bbfa17733eb80b7cb4c580fd61a82 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Mon, 7 Dec 2020 17:27:36 -0500 Subject: [PATCH 013/113] GH-2211: Onboarding - Status Bar (#641) * Make progress bar skeleton * Add ghosty images and sass to display them * Add labels * Refactor labels * Add dotted border after incomplete steps except last * Make labels and ghosties navLinks * Reset default view, done with testing * Update propTypes and add comments * Change back to default view --- _locales/en/messages.json | 9 ++ .../StepProgressBar/StepProgressBar.jsx | 115 ++++++++++++++++++ .../StepProgressBar/StepProgressBar.scss | 99 +++++++++++++++ .../OnboardingViews/StepProgressBar/index.js | 16 +++ .../hub/step-progress-bar/step-1-current.svg | 1 + .../hub/step-progress-bar/step-2-current.svg | 1 + .../step-progress-bar/step-2-incomplete.svg | 1 + .../hub/step-progress-bar/step-3-current.svg | 1 + .../step-progress-bar/step-3-incomplete.svg | 12 ++ .../hub/step-progress-bar/step-4-current.svg | 1 + .../step-progress-bar/step-4-incomplete.svg | 1 + .../hub/step-progress-bar/step-completed.svg | 1 + app/scss/hub.scss | 1 + 13 files changed, 259 insertions(+) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js create mode 100644 app/images/hub/step-progress-bar/step-1-current.svg create mode 100644 app/images/hub/step-progress-bar/step-2-current.svg create mode 100644 app/images/hub/step-progress-bar/step-2-incomplete.svg create mode 100644 app/images/hub/step-progress-bar/step-3-current.svg create mode 100644 app/images/hub/step-progress-bar/step-3-incomplete.svg create mode 100644 app/images/hub/step-progress-bar/step-4-current.svg create mode 100644 app/images/hub/step-progress-bar/step-4-incomplete.svg create mode 100644 app/images/hub/step-progress-bar/step-completed.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b1f614eb9..fea5b9082 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1752,6 +1752,15 @@ "ghostery_browser_hub_onboarding_header_title_plan_choices": { "message": "Ghostery Browser Hub - Plan Choices" }, + "ghostery_browser_hub_onboarding_privacy": { + "message": "Privacy" + }, + "ghostery_browser_hub_onboarding_search": { + "message": "Search" + }, + "ghostery_browser_hub_onboarding_plan": { + "message": "Plan" + }, "ghostery_browser_hub_onboarding_welcome": { "message": "Welcome to Ghostery Browser" }, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx new file mode 100644 index 000000000..bff59d29d --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx @@ -0,0 +1,115 @@ +/** + * Step Progress Bar Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { NavLink } from 'react-router-dom'; + +// TODO: Change routes +const steps = [ + { + label: t('sign_in'), + route: 'LINK_TO_STEP_1' + }, + { + label: t('ghostery_browser_hub_onboarding_privacy'), + route: 'LINK_TO_STEP_2' + }, + { + label: t('ghostery_browser_hub_onboarding_search'), + route: 'LINK_TO_STEP_3' + }, + { + label: t('ghostery_browser_hub_onboarding_plan'), + route: 'LINK_TO_STEP_4' + } +]; + +/** + * A React function component for rendering the Step Progress bar + * @return {JSX} JSX for rendering the Step Progress bar of the ghostery-browser-intro-hub app + * @memberof HubComponents + */ +const StepProgressBar = (props) => { + const { currentStep } = props; + const totalSteps = steps.length; + + const renderCompletedStep = step => ( +
+ +
{step.label}
+
+ +
+ ); + + const renderCurrentStep = (step, value) => ( +
+ +
{step.label}
+
+ +
+ ); + + const renderIncompleteStep = (step, value) => ( +
+ +
{step.label}
+
+ +
+ ); + + const renderProgressBar = () => ( + steps.map((value, index) => ( + + {(index + 1 < currentStep) && ( + renderCompletedStep(steps[index]) + )} + {(index + 1 === currentStep) && ( + + {renderCurrentStep(steps[index], index + 1)} + + )} + {(index + 1 > currentStep) && ( + + {renderIncompleteStep(steps[index], index + 1)} + + )} + {(index + 1 !== totalSteps) && ( + + {(index + 1 < currentStep) && ( +
+ )} + {(index + 1 >= currentStep) && ( +
+ )} + + )} + + )) + ); + + return ( +
+ {renderProgressBar()} +
+ ); +}; +// PropTypes ensure we pass required props of the correct type +StepProgressBar.propTypes = { + currentStep: PropTypes.number.isRequired, +}; + +export default StepProgressBar; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss new file mode 100644 index 000000000..d5908215e --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss @@ -0,0 +1,99 @@ +/** + * Step Progress Bar Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +.StepProgressBarContainer { + display: flex; + justify-content: space-around; + align-items: center; +} +.StepProgressBar__Step { + height: 34px; + width: 32px; + margin-left: 3px; + + &.step-1 { + &.current { + background: url('/app/images/hub/step-progress-bar/step-1-current.svg'); + background-repeat: no-repeat; + } + } + &.step-2 { + &.current { + background: url('/app/images/hub/step-progress-bar/step-2-current.svg'); + background-repeat: no-repeat; + } + &.incomplete { + background: url('/app/images/hub/step-progress-bar/step-2-incomplete.svg'); + background-repeat: no-repeat; + } + } + &.step-3 { + &.current { + background: url('/app/images/hub/step-progress-bar/step-3-current.svg'); + background-repeat: no-repeat; + } + &.incomplete { + background: url('/app/images/hub/step-progress-bar/step-3-incomplete.svg'); + background-repeat: no-repeat; + } + } + &.step-4 { + &.current { + background: url('/app/images/hub/step-progress-bar/step-4-current.svg'); + background-repeat: no-repeat; + } + &.incomplete { + background: url('/app/images/hub/step-progress-bar/step-4-incomplete.svg'); + background-repeat: no-repeat; + } + } + &.step-completed { + background: url('/app/images/hub/step-progress-bar/step-completed.svg'); + background-repeat: no-repeat; + } +} +.StepProgressBar__column { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 45px; + text-align: center; +} +.StepProgressBar__label { + margin-bottom: 14px; + width: 38px; + font-size: 12px; + color: $tundora; + &.currentStep { + font-weight: 700; + } +} +.StepProgressBar__line { + margin-top: 27px; + width: 100%; + &.completed { + border: solid 2px $ghosty-blue; + } + &.incompleted { + padding: 1em; + background-image: + radial-gradient(circle at 2.5px, $ghosty-blue 1.25px, rgba(255,255,255,0) 2.5px), + radial-gradient(circle, $ghosty-blue 1.25px, rgba(255,255,255,0) 2.5px), + radial-gradient(circle at 2.5px, $ghosty-blue 1.25px, rgba(255,255,255,0) 2.5px), + radial-gradient(circle, $ghosty-blue 1.25px, rgba(255,255,255,0) 2.5px); + background-position: top, right, bottom, left; + background-size: 12px 25px, 0px 0px; + background-repeat: repeat-x, repeat-y; + } +} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js new file mode 100644 index 000000000..21d943533 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Step Progress Bar + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import StepProgressBar from './StepProgressBar'; + +export default StepProgressBar; diff --git a/app/images/hub/step-progress-bar/step-1-current.svg b/app/images/hub/step-progress-bar/step-1-current.svg new file mode 100644 index 000000000..9fb8c8dad --- /dev/null +++ b/app/images/hub/step-progress-bar/step-1-current.svg @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/app/images/hub/step-progress-bar/step-2-current.svg b/app/images/hub/step-progress-bar/step-2-current.svg new file mode 100644 index 000000000..7d8819a59 --- /dev/null +++ b/app/images/hub/step-progress-bar/step-2-current.svg @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/app/images/hub/step-progress-bar/step-2-incomplete.svg b/app/images/hub/step-progress-bar/step-2-incomplete.svg new file mode 100644 index 000000000..1dc3fef5f --- /dev/null +++ b/app/images/hub/step-progress-bar/step-2-incomplete.svg @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/app/images/hub/step-progress-bar/step-3-current.svg b/app/images/hub/step-progress-bar/step-3-current.svg new file mode 100644 index 000000000..060d61018 --- /dev/null +++ b/app/images/hub/step-progress-bar/step-3-current.svg @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/app/images/hub/step-progress-bar/step-3-incomplete.svg b/app/images/hub/step-progress-bar/step-3-incomplete.svg new file mode 100644 index 000000000..de1957611 --- /dev/null +++ b/app/images/hub/step-progress-bar/step-3-incomplete.svg @@ -0,0 +1,12 @@ + + + + + + + 3 + + + + + diff --git a/app/images/hub/step-progress-bar/step-4-current.svg b/app/images/hub/step-progress-bar/step-4-current.svg new file mode 100644 index 000000000..019e8c5a5 --- /dev/null +++ b/app/images/hub/step-progress-bar/step-4-current.svg @@ -0,0 +1 @@ +4 \ No newline at end of file diff --git a/app/images/hub/step-progress-bar/step-4-incomplete.svg b/app/images/hub/step-progress-bar/step-4-incomplete.svg new file mode 100644 index 000000000..73db72f1b --- /dev/null +++ b/app/images/hub/step-progress-bar/step-4-incomplete.svg @@ -0,0 +1 @@ +4 \ No newline at end of file diff --git a/app/images/hub/step-progress-bar/step-completed.svg b/app/images/hub/step-progress-bar/step-completed.svg new file mode 100644 index 000000000..f4320607c --- /dev/null +++ b/app/images/hub/step-progress-bar/step-completed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/scss/hub.scss b/app/scss/hub.scss index 07e777997..a469bdb0a 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -86,6 +86,7 @@ html, body, #root { @import '../hub/Views/LogInView/LogInView.scss'; @import '../hub/Views/CreateAccountView/CreateAccountView.scss'; @import '../hub/Views/UpgradePlanView/UpgradePlanView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/StepProgressbar/StepProgressbar.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; From fe5ef4a8dbd745ab78014ae53e2305b74ba7668d Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Tue, 8 Dec 2020 18:41:29 -0500 Subject: [PATCH 014/113] Work towards connecting and reconciling new intro hub views --- .../OnboardingView/OnboardingViewConstants.js | 30 ------------------- .../OnboardingViewContainer.jsx | 2 +- app/ghostery-browser-hub/createStore.js | 4 +-- .../actions/BlockingPolicyActions.js | 2 +- .../constants/BlockingPolicyConstants.js | 8 +++-- .../reducers/BlockingPolicyReducer.js | 24 ++++----------- .../reducers/SetupLifecycleReducer.js} | 23 +++++--------- 7 files changed, 23 insertions(+), 70 deletions(-) delete mode 100644 app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js rename app/{ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js => shared-hub/reducers/SetupLifecycleReducer.js} (72%) diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js deleted file mode 100644 index 67e6937b1..000000000 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewConstants.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Onboarding Flow Constants - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -export const INIT_ONBOARDING_PROPS = 'INIT_ONBOARDING_PROPS'; -export const SET_ONBOARDING_STEP = 'SET_ONBOARDING_STEP'; -export const SET_ONBOARDING_NAVIGATION = 'SET_ONBOARDING_NAVIGATION'; - -export const SET_TRACKER_BLOCKING_POLICY = 'SET_TRACKER_BLOCKING_POLICY'; -export const BLOCKING_POLICY_RECOMMENDED = 'BLOCKING_POLICY_RECOMMENDED'; -export const BLOCKING_POLICY_NOTHING = 'BLOCKING_POLICY_NOTHING'; -export const BLOCKING_POLICY_EVERYTHING = 'BLOCKING_POLICY_EVERYTHING'; -export const BLOCKING_POLICY_CUSTOM = 'BLOCKING_POLICY_CUSTOM'; - -export const SET_ANTI_TRACKING = 'SET_ANTI_TRACKING'; -export const SET_AD_BLOCK = 'SET_AD_BLOCK'; -export const SET_SMART_BLOCK = 'SET_SMART_BLOCK'; - -export const SET_HUMAN_WEB = 'SET_HUMAN_WEB'; - -export const SET_ONBOARDING_COMPLETE = 'SET_ONBOARDING_COMPLETE'; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index 838f495a0..4df973784 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -14,7 +14,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import OnboardingView from './OnboardingView'; -import { BLOCKING_POLICY_RECOMMENDED } from './OnboardingViewConstants'; +import { BLOCKING_POLICY_RECOMMENDED } from '../../../shared-hub/constants/BlockingPolicyConstants'; // Component Views import WelcomeView from '../OnboardingViews/Step0_WelcomeView'; diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index 0cca5f1a8..d94cf838d 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -20,7 +20,7 @@ import { makeStoreCreator } from '../shared-hub/utils/index'; import toast from '../shared-hub/reducers/ToastReducer'; import antiSuite from '../shared-hub/reducers/AntiSuiteReducer'; import blockingPolicy from '../shared-hub/reducers/BlockingPolicyReducer'; -import { reducer as onboarding } from './Views/OnboardingView'; +import setupLifecycle from '../shared-hub/reducers/SetupLifecycleReducer'; import account from '../Account/AccountReducer'; import settings from '../panel/reducers/settings'; @@ -28,7 +28,7 @@ const reducers = { toast, antiSuite, blockingPolicy, - onboarding, + setupLifecycle, account, settings }; diff --git a/app/shared-hub/actions/BlockingPolicyActions.js b/app/shared-hub/actions/BlockingPolicyActions.js index ee96e9888..15f19c257 100644 --- a/app/shared-hub/actions/BlockingPolicyActions.js +++ b/app/shared-hub/actions/BlockingPolicyActions.js @@ -12,7 +12,7 @@ */ import { makeDeferredDispatcher } from '../utils'; -import SET_BLOCKING_POLICY from '../constants/BlockingPolicyConstants'; +import { SET_BLOCKING_POLICY } from '../constants/BlockingPolicyConstants'; const setBlockingPolicy = actionData => makeDeferredDispatcher(SET_BLOCKING_POLICY, actionData); diff --git a/app/shared-hub/constants/BlockingPolicyConstants.js b/app/shared-hub/constants/BlockingPolicyConstants.js index 5f55b30eb..5f51aae43 100644 --- a/app/shared-hub/constants/BlockingPolicyConstants.js +++ b/app/shared-hub/constants/BlockingPolicyConstants.js @@ -11,6 +11,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -const SET_BLOCKING_POLICY = 'SET_BLOCKING_POLICY'; - -export default SET_BLOCKING_POLICY; +export const SET_BLOCKING_POLICY = 'SET_BLOCKING_POLICY'; +export const BLOCKING_POLICY_RECOMMENDED = 'BLOCKING_POLICY_RECOMMENDED'; +export const BLOCKING_POLICY_NOTHING = 'BLOCKING_POLICY_NOTHING'; +export const BLOCKING_POLICY_EVERYTHING = 'BLOCKING_POLICY_EVERYTHING'; +export const BLOCKING_POLICY_CUSTOM = 'BLOCKING_POLICY_CUSTOM'; diff --git a/app/shared-hub/reducers/BlockingPolicyReducer.js b/app/shared-hub/reducers/BlockingPolicyReducer.js index eff0b7f90..81e677584 100644 --- a/app/shared-hub/reducers/BlockingPolicyReducer.js +++ b/app/shared-hub/reducers/BlockingPolicyReducer.js @@ -11,31 +11,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { - SET_AD_BLOCK, - SET_ANTI_TRACKING, - SET_SMART_BLOCK -} from '../constants/AntiSuiteConstants'; +import { SET_BLOCKING_POLICY } from '../constants/BlockingPolicyConstants'; const initialState = {}; -function AntiSuiteReducer(state = initialState, action) { +function BlockingPolicyReducer(state = initialState, action) { switch (action.type) { - case SET_AD_BLOCK: { - const { enable_ad_block } = action.data; - return { ...state, setup: { ...state.setup, enable_ad_block } }; - } - case SET_ANTI_TRACKING: { - const { enable_anti_tracking } = action.data; - return { ...state, setup: { ...state.setup, enable_anti_tracking } }; - } - case SET_SMART_BLOCK: { - const { enable_smart_block } = action.data; - return { ...state, setup: { ...state.setup, enable_smart_block } }; + case SET_BLOCKING_POLICY: { + const { blockingPolicy } = action.data; + return { ...state, setup: { ...state.setup, blockingPolicy } }; } default: return state; } } -export default AntiSuiteReducer; +export default BlockingPolicyReducer; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js b/app/shared-hub/reducers/SetupLifecycleReducer.js similarity index 72% rename from app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js rename to app/shared-hub/reducers/SetupLifecycleReducer.js index 50f64e0cc..b79966076 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewReducer.js +++ b/app/shared-hub/reducers/SetupLifecycleReducer.js @@ -1,5 +1,5 @@ /** - * Reducer used throughout the Onboarding View's flow + * Reducer for the setup flow's lifecycle events, for use by the Hubs * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -12,16 +12,15 @@ */ import { - SET_TRACKER_BLOCKING_POLICY, - INIT_ONBOARDING_PROPS, - SET_ONBOARDING_NAVIGATION, -} from './OnboardingViewConstants'; + INIT_SETUP_PROPS, + SET_SETUP_NAVIGATION, +} from '../constants/SetupLifecycleConstants'; const initialState = {}; -function OnboardingViewReducer(state = initialState, action) { +function SetupLifecycleReducer(state = initialState, action) { switch (action.type) { - case INIT_ONBOARDING_PROPS: { + case INIT_SETUP_PROPS: { const { navigation, setup_show_warning_override, @@ -29,7 +28,6 @@ function OnboardingViewReducer(state = initialState, action) { enable_anti_tracking, enable_ad_block, enable_smart_block, - enable_ghostery_rewards, enable_human_web, } = action.data; const { @@ -58,12 +56,11 @@ function OnboardingViewReducer(state = initialState, action) { enable_anti_tracking, enable_ad_block, enable_smart_block, - enable_ghostery_rewards, enable_human_web, } }; } - case SET_ONBOARDING_NAVIGATION: { + case SET_SETUP_NAVIGATION: { const { activeIndex, hrefPrev, @@ -89,13 +86,9 @@ function OnboardingViewReducer(state = initialState, action) { } }; } - case SET_TRACKER_BLOCKING_POLICY: { - const { blockingPolicy } = action.data; - return { ...state, setup: { ...state.setup, blockingPolicy } }; - } default: return state; } } -export default OnboardingViewReducer; +export default SetupLifecycleReducer; From 80f0fcf39bd165342c33669b3d58e61223311e6f Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 9 Dec 2020 11:56:12 -0500 Subject: [PATCH 015/113] Connecting intro hub pieces --- .../Views/OnboardingView/OnboardingViewContainer.jsx | 9 +++++++-- app/scss/hub_ghostery_browser.scss | 6 ++++++ app/scss/panel.scss | 2 +- webpack.config.js | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index 4df973784..883c06fd0 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -38,7 +38,7 @@ class OnboardingViewContainer extends Component { const { history } = this.props; if (!props.preventRedirect) { - history.push('/onboarding/1'); + history.push('/onboarding/0'); } // TODO verify what document title we should use @@ -81,7 +81,7 @@ class OnboardingViewContainer extends Component { index: 0, path: '/onboarding/0', bodyComponent: WelcomeView, - } + }, { index: 1, path: '/onboarding/1', @@ -102,6 +102,11 @@ class OnboardingViewContainer extends Component { path: '/onboarding/4', bodyComponent: ChoosePlanView, }, + { + index: 5, + path: '/onboarding/5', + bodyComponent: SuccessView, + } ]; return ( diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss index 29c1449df..c132286af 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/hub_ghostery_browser.scss @@ -88,6 +88,12 @@ html, body, #root { @import '../hub/Views/CreateAccountView/CreateAccountView.scss'; @import '../hub/Views/UpgradePlanView/UpgradePlanView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/StepProgressbar/StepProgressbar.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory @import '../shared-components/ExitButton/ExitButton.scss'; diff --git a/app/scss/panel.scss b/app/scss/panel.scss index a073a33dc..3e6585680 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -82,4 +82,4 @@ html body { @import '../shared-components/ModalContent/PremiumPromoModalContent/PremiumPromoModalContent.scss'; @import '../shared-components/ModalContent/PlusPromoModalContent/PlusPromoModalContent.scss'; @import '../shared-components/ModalContent/InsightsPromoModalContent/InsightsPromoModalContent.scss'; -@import '../shared-components/ForgotPassword/ForgotPassword.scss' +@import '../shared-components/ForgotPassword/ForgotPassword.scss'; diff --git a/webpack.config.js b/webpack.config.js index b356a99b9..357da260e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -65,7 +65,7 @@ module.exports = { foundation_hub: [`${SASS_DIR}/vendor/foundation_hub.scss`], ghostery_dot_com_css: [`${SASS_DIR}/ghostery_dot_com.scss`], hub: [`${SASS_DIR}/hub.scss`], - hub_ghostery_browser: [`${HUB_GHOSTERY_BROWSER_DIR}/hub_ghostery_browser.scss`], + hub_ghostery_browser: [`${SASS_DIR}/hub_ghostery_browser.scss`], licenses: [`${SASS_DIR}/licenses.scss`], panel: [`${SASS_DIR}/panel.scss`], panel_android: [`${SASS_DIR}/panel_android.scss`], From 5ebb12c48b79122a1f8cbb0d41ec91ed63dc45c8 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 9 Dec 2020 13:15:51 -0500 Subject: [PATCH 016/113] Cleaning up new intro hub SCSS --- app/scss/hub.scss | 6 ------ app/scss/hub_ghostery_browser.scss | 26 +------------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/app/scss/hub.scss b/app/scss/hub.scss index a469bdb0a..759692615 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -86,12 +86,6 @@ html, body, #root { @import '../hub/Views/LogInView/LogInView.scss'; @import '../hub/Views/CreateAccountView/CreateAccountView.scss'; @import '../hub/Views/UpgradePlanView/UpgradePlanView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/StepProgressbar/StepProgressbar.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory @import '../shared-components/ExitButton/ExitButton.scss'; diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss index c132286af..6b44586a6 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/hub_ghostery_browser.scss @@ -69,38 +69,14 @@ html, body, #root { @import './partials/_fonts'; @import './partials/_loader'; -// Imports from ../hub directory -@import '../hub/Views/SideNavigationView/SideNavigationView.scss'; -@import '../hub/Views/HomeView/HomeView.scss'; -@import '../hub/Views/SetupView/SetupView.scss'; -@import '../hub/Views/SetupViews/SetupHeader/SetupHeader.scss'; -@import '../hub/Views/SetupViews/SetupBlockingView/SetupBlockingView.scss'; -@import '../hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdown.scss'; -@import '../hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteView.scss'; -@import '../hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebView.scss'; -@import '../hub/Views/SetupViews/SetupDoneView/SetupDoneView.scss'; -@import '../hub/Views/TutorialView/TutorialView.scss'; -@import '../hub/Views/PlusView/PlusView.scss'; -@import '../hub/Views/RewardsView/RewardsView.scss'; -@import '../hub/Views/ProductsView/ProductsView.scss'; -@import '../hub/Views/SignedInView/SignedInView.scss'; -@import '../hub/Views/LogInView/LogInView.scss'; -@import '../hub/Views/CreateAccountView/CreateAccountView.scss'; -@import '../hub/Views/UpgradePlanView/UpgradePlanView.scss'; +// Imports from ../ghostery-browser-hub directory @import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/StepProgressbar/StepProgressbar.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory -@import '../shared-components/ExitButton/ExitButton.scss'; -@import '../shared-components/Modal/Modal.scss'; -@import '../shared-components/ModalContent/PremiumPromoModalContent/PremiumPromoModalContent.scss'; -@import '../shared-components/SteppedNavigation/SteppedNavigation.scss'; @import '../shared-components/ToastMessage/ToastMessage.scss'; @import '../shared-components/ToggleCheckbox/ToggleCheckbox.scss'; -@import '../shared-components/ToggleSwitch/ToggleSwitch.scss'; -@import '../shared-components/ForgotPassword/ForgotPassword.scss'; From 50bd0c69643e9f923371358e3bbf51f9d3f13698 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 9 Dec 2020 16:36:11 -0500 Subject: [PATCH 017/113] Get inline script that branches to the correct hub to run --- app/templates/hub.html | 7 +++---- manifest.json | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/templates/hub.html b/app/templates/hub.html index 7f10239b2..490f38974 100644 --- a/app/templates/hub.html +++ b/app/templates/hub.html @@ -26,10 +26,9 @@ const js = document.createElement("script"); js.type = "text/javascript"; - // if (true) js.src = '../../dist/hub_react.js'; - // else js.src = '../../dist/hub_ghostery_browser_react.js'; // TODO: update to point to hub - - js.src = '../../dist/hub_ghostery_browser_react.js'; + // TODO branch on actual user agent value + if (false) js.src = '../../dist/hub_react.js'; + else js.src = '../../dist/hub_ghostery_browser_react.js'; body.appendChild(js); diff --git a/manifest.json b/manifest.json index f1b8a7cb8..f575f26a7 100644 --- a/manifest.json +++ b/manifest.json @@ -80,7 +80,7 @@ "run_at": "document_start" } ], - "content_security_policy": "script-src 'self'; object-src 'self'", + "content_security_policy": "script-src 'self' 'sha256-9fjlqggImjwAZN8KPwHSmZQlWo4CgkMFJR3cp7VC0jk='; object-src 'self'", "permissions": [ "webNavigation", "webRequest", @@ -111,4 +111,4 @@ "web_accessible_resources": [ "app/images/*" ] -} \ No newline at end of file +} From cf1ceaaa0a1c4e1acbbea3ef55679061824f5364 Mon Sep 17 00:00:00 2001 From: Leury Rodriguez Date: Thu, 10 Dec 2020 11:09:23 -0500 Subject: [PATCH 018/113] Update BlockSettingsView and localization messages (#643) --- _locales/en/messages.json | 42 ++++ .../BlockSettingsView.jsx | 188 +++++++++++++++++- .../BlockSettingsView.scss | 95 +++++++++ .../Step2_BlockSettingsView/index.js | 23 ++- app/images/hub/setup/info.svg | 18 ++ app/scss/hub_ghostery_browser.scss | 1 + 6 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss create mode 100644 app/images/hub/setup/info.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index fea5b9082..07ba8bba7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1839,6 +1839,48 @@ "ghostery_browser_hub_onboarding_lets_search": { "message": "Let's search" }, + "ghostery_browser_hub_onboarding_which_privacy_plan": { + "message": "Which privacy plan is right for you?" + }, + "ghostery_browser_hub_onboarding_tell_us_your_preferences": { + "message": "Tell us your preferences, we'll offer you our recommendation." + }, + "ghostery_browser_hub_onboarding_recommended_choices": { + "message": "Recommended choices" + }, + "ghostery_browser_hub_onboarding_question_block_ads": { + "message": "Do you want to block ads?" + }, + "ghostery_browser_hub_onboarding_question_kinds_of_trackers": { + "message": "What kinds of trackers do you want to block?" + }, + "ghostery_browser_hub_onboarding_kinds_of_trackers_all": { + "message": "All trackers" + }, + "ghostery_browser_hub_onboarding_kinds_of_trackers_none": { + "message": "No trackers" + }, + "ghostery_browser_hub_onboarding_kinds_of_trackers_ad_and_analytics": { + "message": "All ad and analytics trackers" + }, + "ghostery_browser_hub_onboarding_question_anti_tracking": { + "message": "Do you want to turn on anti-tracking?" + }, + "ghostery_browser_hub_onboarding_question_smart_browsing": { + "message": "Do you want to turn on smart-browsing?" + }, + "ghostery_browser_hub_info_blocking_all": { + "message": "Blocking \"all trackers\" may cause some websites to break." + }, + "ghostery_browser_hub_info_anti_tracking": { + "message": "Anti-tracking anonymizes uniquely identifiable data that trackers try to collect" + }, + "ghostery_browser_hub_info_smart_browsing": { + "message": "Smart-browsing adjusts your blocking settings to decrease page breakage and accelerate page loads." + }, + "ghostery_browser_hub_toast_error": { + "message": "Please answer all questions" + }, "enable_when_paused": { "message": "To use this function, Resume Ghostery." }, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index e3e30de9f..13cc9fe6d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -11,8 +11,192 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React from 'react'; +import React, { Component } from 'react'; -const BlockSettingsView = () =>

Step 2: Block Settings View

; +// import Tooltip from '../../../../panel/components/Tooltip'; +import RadioButton from '../../../../shared-components/RadioButton/RadioButton'; +import ToggleCheckbox from '../../../../shared-components/ToggleCheckbox/ToggleCheckbox'; + +/** + * @class Implement the Block Settings View for the Ghostery Browser Hub + * @extends Component + * @memberof HubComponents + */ +class BlockSettingsView extends Component { + constructor(props) { + super(props); + this.state = { + recommendedChoices: false, + blockAds: null, + kindsOfTrackers: null, + antiTracking: null, + smartBrowsing: null + }; + } + + toggleRecommendedChoices = (value) => { + if (value === true) { + this.setState({ + recommendedChoices: true, + blockAds: true, + kindsOfTrackers: 1, + antiTracking: true, + smartBrowsing: true + }); + } else { + this.setState({ + recommendedChoices: false, + blockAds: null, + kindsOfTrackers: null, + antiTracking: null, + smartBrowsing: null + }); + } + } + + handleAnswerChange = (category, answer) => { + this.setState({ [category]: answer }); + } + + handleSubmit = () => { + const { + blockAds, kindsOfTrackers, antiTracking, smartBrowsing + } = this.state; + + // Will only change user settings if all questions are answered + if (blockAds !== null && kindsOfTrackers !== null && antiTracking !== null && smartBrowsing !== null) { + const { + setAdBlock, setAntiTracking, setSmartBlocking, setBlockingPolicy + } = this.props; + + setAdBlock(blockAds); + setAntiTracking(antiTracking); + setSmartBlocking(smartBrowsing); + + let blockingPolicy; + switch (kindsOfTrackers) { + case 0: + blockingPolicy = 'BLOCKING_POLICY_EVERYTHING'; + break; + case 1: + blockingPolicy = 'BLOCKING_POLICY_RECOMMENDED'; + break; + case 2: + blockingPolicy = 'BLOCKING_POLICY_NOTHING'; + break; + default: + break; + } + setBlockingPolicy({ blockingPolicy }); + } else { + const { setToast } = this.props; + + setToast({ + toastMessage: t('ghostery_browser_hub_toast_error'), + toastClass: 'error' + }); + } + } + + render() { + const { + recommendedChoices, blockAds, kindsOfTrackers, antiTracking, smartBrowsing + } = this.state; + return ( +
+
{t('ghostery_browser_hub_onboarding_which_privacy_plan')}
+
{t('ghostery_browser_hub_onboarding_tell_us_your_preferences')}
+
+
+ this.toggleRecommendedChoices(!recommendedChoices)} + /> +
{t('ghostery_browser_hub_onboarding_recommended_choices')}
+
+
    +
  1. {t('ghostery_browser_hub_onboarding_question_block_ads')}
  2. +
    +
    + this.handleAnswerChange('blockAds', true)} altDesign /> +
    +
    {t('hub_setup_modal_button_yes')}
    +
    +
    +
    + this.handleAnswerChange('blockAds', false)} altDesign /> +
    +
    {t('hub_setup_modal_button_no')}
    +
    +
  3. +
    + {t('ghostery_browser_hub_onboarding_question_kinds_of_trackers')} +
    +
    +
  4. +
    +
    + this.handleAnswerChange('kindsOfTrackers', 0)} altDesign /> +
    +
    {t('ghostery_browser_hub_onboarding_kinds_of_trackers_all')}
    +
    +
    +
    + this.handleAnswerChange('kindsOfTrackers', 1)} altDesign /> +
    +
    {t('ghostery_browser_hub_onboarding_kinds_of_trackers_ad_and_analytics')}
    +
    +
    +
    + this.handleAnswerChange('kindsOfTrackers', 2)} altDesign /> +
    +
    {t('ghostery_browser_hub_onboarding_kinds_of_trackers_none')}
    +
    +
  5. + {t('ghostery_browser_hub_onboarding_question_anti_tracking')} +
    +
  6. +
    +
    + this.handleAnswerChange('antiTracking', true)} altDesign /> +
    +
    {t('hub_setup_modal_button_yes')}
    +
    +
    +
    + this.handleAnswerChange('antiTracking', false)} altDesign /> +
    +
    {t('hub_setup_modal_button_no')}
    +
    +
  7. + {t('ghostery_browser_hub_onboarding_question_smart_browsing')} +
    +
  8. +
    +
    + this.handleAnswerChange('smartBrowsing', true)} altDesign /> +
    +
    {t('hub_setup_modal_button_yes')}
    +
    +
    +
    + this.handleAnswerChange('smartBrowsing', false)} altDesign /> +
    +
    {t('hub_setup_modal_button_no')}
    +
    +
+
+ +
+ ); + } +} export default BlockSettingsView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss new file mode 100644 index 000000000..fb5731d04 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss @@ -0,0 +1,95 @@ +.BlockSettingsView__container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + font-size: 18px; + line-height: 2.33; + color: $tundora; + margin: 45px auto 83px auto; +} + +.BlockSettingsView__title { + font-size: 24px; + font-weight: 500; + line-height: 2.33; + text-align: center; +} + +.BlockSettingsView__subtitle { + margin-bottom: 47px; + text-align: center; +} + +.BlockSettingsView_formBlock { + align-self: stretch; + text-align: left; +} + +.BlockSettingsView__ctaButton { + display: flex; + justify-content: center; + margin: auto; + height: 44px; + width: 162px; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + #00aef0 + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + color: #FFF; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; +} + +.BlockSettingsView_question { + font-weight: 500; +} + +.BlockSettingsView__radioButtonContainer { + padding: 11px; + display: flex; + justify-content: center; + cursor: pointer; +} + +.BlockSettingsView_answerBlock { + display: flex; + align-items: center; +} + +.BlockSettingsView_checkboxBlock { + display: flex; + align-items: center; + font-size: 14px; +} + +.BlockSettingsView_checkbox { + width: 18px; + height: 18px; +} + +.BlockSettingsView__infoIcon { + width: 18px; + background-repeat: no-repeat; + background-image: url('/app/images/hub/setup/info.svg'); + margin-left: 8px; +} + +.BlockSettingsView_questionBlock { + display: flex; +} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js index ec251f216..93095cc5a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js @@ -11,6 +11,27 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + import BlockSettingsView from './BlockSettingsView'; +import { setAntiTracking, setAdBlock, setSmartBlocking } from '../../../../shared-hub/actions/AntiSuiteActions'; +import { setBlockingPolicy } from '../../../../shared-hub/actions/BlockingPolicyActions'; +import { setToast } from '../../../../shared-hub/actions/ToastActions'; + +/** + * Bind the component's action creators using Redux's bindActionCreators. + * @param {function} dispatch redux store method which dispatches actions + * @return {function} to be used as an argument in redux connect call + */ +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ + setAntiTracking, + setAdBlock, + setSmartBlocking, + setBlockingPolicy, + setToast, + }, dispatch), +}); -export default BlockSettingsView; +export default connect(null, mapDispatchToProps)(BlockSettingsView); diff --git a/app/images/hub/setup/info.svg b/app/images/hub/setup/info.svg new file mode 100644 index 000000000..7bcbcb9ef --- /dev/null +++ b/app/images/hub/setup/info.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss index 6b44586a6..3c6f67af5 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/hub_ghostery_browser.scss @@ -75,6 +75,7 @@ html, body, #root { @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory From d87f5c293f6438b8be34c5c3a52639e7d613fa7a Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Thu, 10 Dec 2020 11:16:21 -0500 Subject: [PATCH 019/113] GH-2210: Onboarding - Step 4 - Choose Plan (#640) * Create ghostery-search promo in plan view * Add scroll arrow and cards * Add value prop list to cards * Add radio button and hover effects to cards * Add click handler to full card, not only radio button * Select default plan depending on current plan * Add logic to display title and subtitle texts * Style radio button alt design * Touch up radio button alt design * Make responsive * Change CTA button to a button * Add back backgrounds on small screens and add checkmarks for free card * Add working links * Refactor basic/plus/premium states * Make cards responsive with OR * Add padding and change CTA button strings * Add hover effects to search and premium CTA buttons * Conditionally show cards based on users subscriptions and refactor arrow to show/hide plan cards instead of scrolling * Fix logic for showing caret * Fix and refactor cta button links * Fix color of premium CTA button when displaying to a basic user * Refactor and add comments * Remove unused css * Update plus and premium checkout links to specify english * Move RadioButton and RadioButtonGroup to shared components * Rename Step4_ChoosePlanView to ChoosePlanView * Use BASIC, PLUS, and PREMIUM constants from UpgradePlanViewConstants and fix search.svg path * Update copywrite year * Replace hex color codes with variables * Add border to unchecked radio button Co-authored-by: Ilya Zarembsky --- _locales/en/messages.json | 66 ++++ .../Step4_ChoosePlanView/ChoosePlanView.jsx | 360 +++++++++++++++++- .../Step4_ChoosePlanView/ChoosePlanView.scss | 318 ++++++++++++++++ .../Step4_ChoosePlanView/index.jsx | 35 ++ app/images/hub/ChoosePlanView/search.svg | 1 + app/panel/components/BuildingBlocks/index.js | 4 - app/panel/components/Settings/AdBlocker.jsx | 2 +- .../Subscription/SubscriptionThemes.jsx | 2 +- app/scss/hub.scss | 4 + app/scss/partials/_colors.scss | 4 +- app/scss/partials/_radio_button.scss | 22 +- .../RadioButton}/RadioButton.jsx | 4 +- .../RadioButton}/index.js | 6 +- .../RadioButtonGroup}/RadioButtonGroup.jsx | 2 +- .../RadioButtonGroup/index.js | 16 + 15 files changed, 830 insertions(+), 16 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx create mode 100644 app/images/hub/ChoosePlanView/search.svg rename app/{panel/components/BuildingBlocks => shared-components/RadioButton}/RadioButton.jsx (93%) rename app/{ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView => shared-components/RadioButton}/index.js (68%) rename app/{panel/components/BuildingBlocks => shared-components/RadioButtonGroup}/RadioButtonGroup.jsx (97%) create mode 100644 app/shared-components/RadioButtonGroup/index.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 07ba8bba7..ab6c6cdc5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1366,6 +1366,9 @@ "hub_upgrade_additional_protection": { "message": "Additional Protection" }, + "hub_upgrade_maximum_protection": { + "message": "Maximum Protection" + }, "hub_upgrade_browser_tracker_blocking": { "message": "Browser Tracker Blocking" }, @@ -1770,6 +1773,69 @@ "ghostery_browser_hub_onboarding_lets_do_this": { "message": "Let's do this" }, + "ghostery_browser_hub_onboarding_your_privacy_plan": { + "message": "Your Privacy Plan" + }, + "ghostery_browser_hub_onboarding_based_on_your_privacy_preferences": { + "message": "Based on your privacy preferences" + }, + "ghostery_browser_hub_onboarding_ad_free_with_ghostery_plus_subscription": { + "message": "Ad-free with Ghostery Plus subscription" + }, + "ghostery_browser_hub_onboarding_ad_free_promo": { + "message": "40% off for the first 12 months" + }, + "ghostery_browser_hub_onboarding_ad_free_promo_description": { + "message": "Get to what you want faster. A plus subscription gives Ghostery the support we need to provide YOU with an ad-free experience." + }, + "ghostery_browser_hub_onboarding_or": { + "message": "OR" + }, + "ghostery_browser_hub_onboarding_keep": { + "message": "Keep" + }, + "ghostery_browser_hub_onboarding_upgrade": { + "message": "Upgrade" + }, + "ghostery_browser_hub_onboarding_start_trial": { + "message": "Start Trial" + }, + "ghostery_browser_hub_onboarding_see_all_plans": { + "message": "See all plans" + }, + "ghostery_browser_hub_onboarding_private_search": { + "message": "Private search" + }, + "ghostery_browser_hub_onboarding_tracker_protection": { + "message": "Tracker protection" + }, + "ghostery_browser_hub_onboarding_speedy_page_loads": { + "message": "Speedy page loads" + }, + "ghostery_browser_hub_onboarding_intelligence_technology": { + "message": "Intelligence technology" + }, + "ghostery_browser_hub_onboarding_ad_free": { + "message": "Ad free" + }, + "ghostery_browser_hub_onboarding_supports_ghosterys_mission": { + "message": "Supports Ghostery's Mission" + }, + "ghostery_browser_hub_onboarding_unlimited_bandwidth": { + "message": "Unlimited Bandwidth" + }, + "ghostery_browser_hub_onboarding_already_premium_subscriber": { + "message": "You are already a premium subscriber" + }, + "ghostery_browser_hub_onboarding_already_plus_subscriber": { + "message": "You are already a plus subscriber" + }, + "ghostery_browser_hub_onboarding_keep_your_current_plan_or_upgrade": { + "message": "Keep your current plan or upgrade" + }, + "ghostery_browser_hub_onboarding_choose_an_option": { + "message": "Choose an option" + }, "ghostery_browser_hub_onboarding_create_a_ghostery_account": { "message": "Create a Ghostery Account" }, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index abaddba4d..4ce39dc8a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -11,8 +11,364 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React from 'react'; +import React, { Fragment } from 'react'; +import ClassNames from 'classnames'; +import PropTypes from 'prop-types'; +import RadioButton from '../../../../shared-components/RadioButton'; +import globals from '../../../../../src/classes/Globals'; +import { BASIC, PLUS, PREMIUM } from '../../../../hub/Views/UpgradePlanView/UpgradePlanViewConstants'; -const ChoosePlanView = () =>

Step 4: Choose Plan View

; +const plusCheckoutLink = `${globals.CHECKOUT_BASE_URL}/en/plus`; +const premiumCheckoutLink = `${globals.CHECKOUT_BASE_URL}/en/premium`; + +const searchPromo = () => ( +
+
+
{ t('ghostery_browser_hub_onboarding_ad_free_with_ghostery_plus_subscription') }
+
{ t('ghostery_browser_hub_onboarding_ad_free_promo') }
+
{ t('ghostery_browser_hub_onboarding_ad_free_promo_description') }
+
+); + +const basicCard = (checked, handleClick) => { + const cardClassNames = ClassNames('ChoosePlanView__card basic', { + checked + }); + return ( +
+
+
+ +
+
+
+
+
+

Ghostery

+
+

{t('hub_upgrade_plan_free')}

+
+

{t('hub_upgrade_basic_protection')}

+
+
+ + {t('ghostery_browser_hub_onboarding_private_search')} +
+
+ + {t('ghostery_browser_hub_onboarding_tracker_protection')} +
+
+ + {t('ghostery_browser_hub_onboarding_speedy_page_loads')} +
+
+ + {t('ghostery_browser_hub_onboarding_intelligence_technology')} +
+
+
+
+ ); +}; + +const plusCard = (checked, handleClick, showCTAButton = false) => { + const cardClassNames = ClassNames('ChoosePlanView__card plus', { + checked + }); + return ( + +
+
+
+ +
+
+
+
+

Ghostery Plus

+
+ +

$4.99

+

{t('per_month')}

+
+
+

{t('hub_upgrade_additional_protection')}

+
+
+ + {t('ghostery_browser_hub_onboarding_private_search')} +
+
+ + {t('ghostery_browser_hub_onboarding_tracker_protection')} +
+
+ + {t('ghostery_browser_hub_onboarding_speedy_page_loads')} +
+
+ + {t('ghostery_browser_hub_onboarding_intelligence_technology')} +
+
+ + {t('ghostery_browser_hub_onboarding_ad_free')} +
+
+ + {t('ghostery_browser_hub_onboarding_supports_ghosterys_mission')} +
+
+
+
+ {showCTAButton && ( + // Route to next screen + + )} + + ); +}; + +const premiumCard = (checked, handleClick, showCTAButton = false) => { + const cardClassNames = ClassNames('ChoosePlanView__card premium', { + checked + }); + return ( + +
+
+
+ +
+
+
+
+
+

Ghostery Premium

+
+ +

$11.99

+

{t('per_month')}

+
+
+

{t('hub_upgrade_maximum_protection')}

+
+
+ + {t('ghostery_browser_hub_onboarding_private_search')} +
+
+ + {t('ghostery_browser_hub_onboarding_tracker_protection')} +
+
+ + {t('ghostery_browser_hub_onboarding_speedy_page_loads')} +
+
+ + {t('ghostery_browser_hub_onboarding_intelligence_technology')} +
+
+ + {t('ghostery_browser_hub_onboarding_ad_free')} +
+
+ + {t('ghostery_browser_hub_onboarding_supports_ghosterys_mission')} +
+
+ + VPN +
+
+ + {t('ghostery_browser_hub_onboarding_unlimited_bandwidth')} +
+
+
+
+ {showCTAButton && ( + {t('ghostery_browser_hub_onboarding_upgrade')} + )} + + ); +}; + +class ChoosePlanView extends React.Component { + constructor(props) { + super(props); + this.state = { + selectedPlan: '', + expanded: false + }; + // User object doesn't get populated immediately, let's delay the first render + setTimeout(this.setDefaultPlan, 200); + } + + setDefaultPlan = () => { + const { user } = this.props; + const isPlus = (user && user.plusAccess) || false; + const isPremium = (user && user.premiumAccess) || false; + + if (isPremium) { + this.selectPremiumPlan(); + return; + } + if (isPlus) { + this.selectPlusPlan(); + return; + } + this.selectBasicPlan(); + } + + isBasicPlanChecked = () => { + const { selectedPlan } = this.state; + return (selectedPlan === BASIC); + }; + + isPlusPlanChecked = () => { + const { selectedPlan } = this.state; + return (selectedPlan === PLUS); + }; + + isPremiumPlanChecked = () => { + const { selectedPlan } = this.state; + return (selectedPlan === PREMIUM); + }; + + selectBasicPlan = () => this.setState({ selectedPlan: BASIC }); + + selectPlusPlan = () => this.setState({ selectedPlan: PLUS }); + + selectPremiumPlan = () => this.setState({ selectedPlan: PREMIUM }); + + toggleSection = () => { + const { expanded } = this.state; + if (expanded) { + this.setState({ expanded: !expanded }); + } else { + this.setState({ expanded: !expanded }); + } + }; + + renderTitleText = () => { + const { user } = this.props; + const isPlus = (user && user.plusAccess) || false; + const isPremium = (user && user.premiumAccess) || false; + + if (isPremium) return t('ghostery_browser_hub_onboarding_already_premium_subscriber'); + if (isPlus) return t('ghostery_browser_hub_onboarding_already_plus_subscriber'); + return t('ghostery_browser_hub_onboarding_your_privacy_plan'); + }; + + renderSubtitleText = (fromSearchSelectionScreen) => { + const { user } = this.props; + const isPlus = (user && user.plusAccess) || false; + const isPremium = (user && user.premiumAccess) || false; + + if (fromSearchSelectionScreen) return t('ghostery_browser_hub_onboarding_based_on_your_privacy_preferences'); + if (isPremium) return ''; + if (isPlus) return t('ghostery_browser_hub_onboarding_keep_your_current_plan_or_upgrade'); + return t('ghostery_browser_hub_onboarding_choose_an_option'); + }; + + render() { + const { user, didNotSelectGhosterySearch } = this.props; + const { expanded, selectedPlan } = this.state; + + const isBasic = !user; + const isPlus = (user && user.plusAccess && !user.premiumAccess) || false; + const isPremium = (user && user.premiumAccess) || false; + + const arrowClassNames = ClassNames('ChoosePlanView__arrow', { + up: !expanded, + down: expanded + }); + + return ( +
+
{this.renderTitleText()}
+
{this.renderSubtitleText(didNotSelectGhosterySearch)}
+ {didNotSelectGhosterySearch && isBasic && ( + + {searchPromo()} + {/* TODO: For the CTA button below, + 1. If user is signed in, activate the user’s 7-day free trial for the Ghostery Search Plus plan + and move them to Step 5 if signed in + 2. If user is signed out, clicking this should take them to Step 4b (linked) + */} +
{t('ghostery_browser_hub_onboarding_start_trial')}
+
{t('ghostery_browser_hub_onboarding_see_all_plans')}
+
+ + )} + {((isBasic && !didNotSelectGhosterySearch) || expanded || isPlus || isPremium) && ( +
+ {(isPlus) ? ( +
+
+ {plusCard(this.isPlusPlanChecked(), this.selectPlusPlan, isPlus)} +
+
{t('ghostery_browser_hub_onboarding_or')}
+
+ {premiumCard(this.isPremiumPlanChecked(), this.selectPremiumPlan, isPlus)} +
+
+ ) : ( +
+ {isBasic && ( + basicCard(this.isBasicPlanChecked(), this.selectBasicPlan) + )} + {!isPremium && ( + + {plusCard(this.isPlusPlanChecked(), this.selectPlusPlan)} + + )} + {premiumCard(this.isPremiumPlanChecked(), this.selectPremiumPlan)} +
+ )} + {(isBasic && ( +
+ {(selectedPlan === BASIC) && ( + // Change to route to next page + + )} + {selectedPlan === PLUS && ( + {t('next')} + )} + {selectedPlan === PREMIUM && ( + {t('next')} + )} +
+ ))} + {isPremium && ( + // Change to route to next page + + )} +
+ )} +
+ ); + } +} + +// PropTypes ensure we pass required props of the correct type +ChoosePlanView.propTypes = { + user: PropTypes.shape({ + plusAccess: PropTypes.bool, + premiumAccess: PropTypes.bool, + }), + didNotSelectGhosterySearch: PropTypes.bool.isRequired, +}; + +// Default props used in the Plus View +ChoosePlanView.defaultProps = { + user: { + plusAccess: false, + premiumAccess: false, + }, +}; export default ChoosePlanView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss new file mode 100644 index 000000000..90d077b78 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss @@ -0,0 +1,318 @@ +.ChoosePlanView { + padding-bottom: 20px; +} +.ChoosePlanView__yourPrivacyPlan { + margin: auto; + font-size: 24px; + display: flex; + justify-content: center; +} +.ChoosePlanView__subtitle { + margin-top: 12px; + display: flex; + justify-content: center; + font-size: 18px; +} +.ChoosePlanView__searchPromoContainer { + width: 354px; + height: 381px; + margin: 48px auto 0 auto; + border-radius: 4px; + border: solid 4px $ghosty-blue; +} +.ChoosePlanView__searchLogo { + height: 37px; + width: 193px; + margin: 65px auto 0 auto; + background-image: url('/app/images/hub/ChoosePlanView/search.svg'); +} +.ChoosePlanView__adFree { + margin: 16px auto 0 auto; + color: #b8860b; + font-size: 18px; + line-height: 32px; + font-weight: 600; + width: 220px; + text-align: center; +} +.ChoosePlanView__adFreePromo { + margin: auto; + font-size: 18px; + line-height: 32px; + text-align: center; +} +.ChoosePlanView__adFreePromoDescription { + margin: 16px auto 0 auto; + height: 88px; + width: 250px; + font-size: 16px; + line-height: 1.38; + text-align: center; + color: $tundora; +} +.ChoosePlanView__searchCTAButton { + display: flex; + justify-content: center; + margin: 48px auto 0 auto; + height: 44px; + width: 162px; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + $ghosty-blue + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + color: $white; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; + + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + &:focus { + color: $white; + } +} +.ChoosePlanView__premiumCTAButton { + display: flex; + justify-content: center; + margin: 48px auto 0 auto; + height: 44px; + width: 162px; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #6c097a 50%, + #03a9ec + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + color: $white; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; + + &:hover { + color: $white; + background-position: 0% 50%; + transition: 0.25s all; + } +} +.ChoosePlanView__seeAllPlans { + margin-top: 48px; + display: flex; + justify-content: center; + color: $ghosty-blue; + font-size: 16px; + text-decoration: underline; + cursor: pointer; +} +.ChoosePlanView__plansContainer { + margin-top: 20px; + padding-top: 20px; + + .ChoosePlanView__cardOuter { + display: flex; + flex-direction: column; + justify-content: center; + margin: 0 15px; + } +} +.ChoosePlanView__orCardContainer { + display: flex; +} +.ChoosePlanView__or { + display: flex; + align-items: center; + justify-content: center; + padding: 0 66px; + height: 180px; + font-size: 24px; + color: $tundora; +} +.ChoosePlanView__ctaButtonContainer { + display: flex; + justify-content: center; +} +.ChoosePlanView__keepOrUpgradeContainer { + margin: 40px auto; +} +.ChoosePlanView__arrow { + margin: 15px auto 0 auto; + height: 12px; + width: 12px; + border-left: 2px solid $ghosty-blue; + border-top: 2px solid $ghosty-blue; + cursor: pointer; + + &.down { + transform: rotate(45deg); + } + &.up { + transform: rotate(225deg); + } +} +.ChoosePlanView__card { + height: 670px; + box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.5); + margin-bottom: rem-calc(40); + width: 250px; + margin-left: auto; + margin-right: auto; + text-align: center; + padding-bottom: rem-calc(40); + border: 4px solid white; + + &:hover, &.checked { + border: 4px solid $ghosty-blue; + .ghostery-free-image-container, + .ghostery-plus-image-container, + .ghostery-premium-image-background { + width: 244px; + margin-left: -1px; + } + } + h2 { + color: $tundora; + font-family: "Roboto Condensed"; + font-size: 18px; + font-weight: 400; + margin-top: 22px; + margin-bottom: 6px; + } + &-image-top { + margin-top: -76px; + width: 100%; + } + &-sub-header { + margin-bottom: rem-calc(15); + } + .ghostery-free-image-container { + margin: -3px 0 0 -4px; + width: 250px; + height: 85px; + background-color: rgba(0, 174, 240, .25); + display: flex; + justify-content: center; + align-items: center; + + .ghostery-free-image { + width: 55px; + height: 65px; + background-image: url('/app/images/hub/upgrade/ghostery-basic-card.svg'); + background-repeat: no-repeat; + background-position: center; + } + } + .ghostery-plus-image-container { + margin: -3px 0 0 -4px; + width: 250px; + height: 85px; + background-image: linear-gradient(to right, rgba(240,174,133,.25), rgba(241,216,158,.25)); + .ghostery-plus-image { + margin: auto; + width: 225px; + height: 87px; + background-image: url('/app/images/hub/upgrade/ghostery-plus-card.svg') + } + } + .ghostery-premium-image-container { + margin-left: -4px; + position: relative; + } + .ghostery-premium-image { + position: absolute; + margin-top: -79px; + margin-left: 9px; + width: 233px; + height: 161px; + background-image: url('/app/images/hub/upgrade/ghostery-premium-card.svg'); + } + .ghostery-premium-image-background { + margin: -3px 0 0 -4px; + width: 250px; + height: 85px; + background-image: linear-gradient(to right, rgba(113,1,116,.25), rgba(90,37,142,.25) 18%, rgba(42,110,194,.25) 57%, rgba(12,156,227,.25) 86%, rgba(0,174,240,.25)); + } + .card-sub-header { + font-size: 14px; + } +} +.ChoosePlanView__price { + min-height: 85px; + margin-bottom: rem-calc(20); + &-blue { + color: $price-blue; + } + &-blue-alt { + color: $blue-alt; + } + &-gold { + color: $price-gold; + } + &-purple { + color: $price-purple; + } + p { + margin-bottom: 0; + @include breakpoint($medium-large-breakpoint down) { + &.sub-text:first-child { + margin-top: 4px; + } + } + &.sub-text { + font-weight: 500; + } + } +} +.ChoosePlanView__valuePropList { + margin: auto; + width: 174px; + + &.basic { + width: 145px; + } +} +.ChoosePlanView__cardSubCopy { + display: flex; + justify-content: flex-start; + margin-bottom: 5px; +} +.check { + display: inline-block; + width: 20px; + height: 20px; + background-repeat: no-repeat; + margin-top: 3px; + &.blue { + background-image: buildCheckIcon($price-blue); + } + &.yellow { + background-image: buildCheckIcon($price-gold); + } + &.purple { + background-image: buildCheckIcon($price-purple); + } +} +.ChoosePlanView__radioButtonContainer { + padding: 11px; + margin-bottom: 84px; + display: flex; + justify-content: center; +} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx new file mode 100644 index 000000000..f21eb0832 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx @@ -0,0 +1,35 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Choose Plan View + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import ChoosePlanView from './ChoosePlanView'; + +/** + * Map redux store state properties to the component's own properties. + * @param {Object} state entire Redux store's state + * @return {function} this function returns a plain object, which will be merged into the component's props + * @memberof HubContainers + */ +const mapStateToProps = state => ({ ...state.account }); + +/** + * Bind the component's action creators using Redux's bindActionCreators. + * @param {function} dispatch redux store method which dispatches actions + * @return {function} to be used as an argument in redux connect call + * @memberof SetupContainers + */ +const mapDispatchToProps = dispatch => ({}); + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChoosePlanView)); diff --git a/app/images/hub/ChoosePlanView/search.svg b/app/images/hub/ChoosePlanView/search.svg new file mode 100644 index 000000000..1c52a8174 --- /dev/null +++ b/app/images/hub/ChoosePlanView/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/panel/components/BuildingBlocks/index.js b/app/panel/components/BuildingBlocks/index.js index 8f73d6052..edc925cbc 100644 --- a/app/panel/components/BuildingBlocks/index.js +++ b/app/panel/components/BuildingBlocks/index.js @@ -21,8 +21,6 @@ import GhosteryFeature from './GhosteryFeature'; import NotScanned from './NotScanned'; import PauseButton from './PauseButton'; import ToggleSlider from './ToggleSlider'; -import RadioButtonGroup from './RadioButtonGroup'; -import RadioButton from './RadioButton'; import ModalExitButton from './ModalExitButton'; export { @@ -33,7 +31,5 @@ export { NotScanned, PauseButton, ToggleSlider, - RadioButtonGroup, - RadioButton, ModalExitButton }; diff --git a/app/panel/components/Settings/AdBlocker.jsx b/app/panel/components/Settings/AdBlocker.jsx index 2a5466602..dff505050 100644 --- a/app/panel/components/Settings/AdBlocker.jsx +++ b/app/panel/components/Settings/AdBlocker.jsx @@ -13,7 +13,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { RadioButtonGroup } from '../BuildingBlocks'; +import RadioButtonGroup from '../../../shared-components/RadioButtonGroup/RadioButtonGroup'; /** * @class Implement Ad Blocker Settings subview as a React component. diff --git a/app/panel/components/Subscription/SubscriptionThemes.jsx b/app/panel/components/Subscription/SubscriptionThemes.jsx index f486277d6..72087f438 100644 --- a/app/panel/components/Subscription/SubscriptionThemes.jsx +++ b/app/panel/components/Subscription/SubscriptionThemes.jsx @@ -13,7 +13,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { RadioButtonGroup } from '../BuildingBlocks'; +import RadioButtonGroup from '../../../shared-components/RadioButtonGroup/RadioButtonGroup'; /** * @class Implement Themes subview as a React component. diff --git a/app/scss/hub.scss b/app/scss/hub.scss index 759692615..d259e061c 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -86,6 +86,9 @@ html, body, #root { @import '../hub/Views/LogInView/LogInView.scss'; @import '../hub/Views/CreateAccountView/CreateAccountView.scss'; @import '../hub/Views/UpgradePlanView/UpgradePlanView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/ChoosePlanView/ChoosePlanView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory @import '../shared-components/ExitButton/ExitButton.scss'; @@ -96,3 +99,4 @@ html, body, #root { @import '../shared-components/ToggleCheckbox/ToggleCheckbox.scss'; @import '../shared-components/ToggleSwitch/ToggleSwitch.scss'; @import '../shared-components/ForgotPassword/ForgotPassword.scss'; +@import './partials/radio_button'; diff --git a/app/scss/partials/_colors.scss b/app/scss/partials/_colors.scss index cd15e484b..3b99e216c 100644 --- a/app/scss/partials/_colors.scss +++ b/app/scss/partials/_colors.scss @@ -49,8 +49,8 @@ $spring-green: #6aa103; /* MARKETING COLORS */ $red: #E74055; $purple: #720174; -$dark-purple: #5B005C; -$dark-magenta: #920094; +$dark-purple: #5B005C; +$dark-magenta: #920094; $sinopia: #D3451E; $apple: #67A73A; //button_special $caper: #CEECAF; //success-color diff --git a/app/scss/partials/_radio_button.scss b/app/scss/partials/_radio_button.scss index 44a33a3fd..7675d62e6 100644 --- a/app/scss/partials/_radio_button.scss +++ b/app/scss/partials/_radio_button.scss @@ -40,7 +40,17 @@ align-items: center; justify-content: center; box-sizing: border-box; - &.checked { + + &.altDesign { + height: 24px; + width: 24px; + border: 1px solid black; + &.checked { + background-color: #00aef0; + border: none; + } + } + &.checked:not(.altDesign) { border: 2px solid #2092bf; } } @@ -51,4 +61,14 @@ background-color: #2092bf; border-radius: 50%; } + + &.altDesign { + height: 14px; + width: 14px; + background-image: buildCheckIcon(#FFF); + border: none; + &.checked { + background-color: transparent; + } + } } diff --git a/app/panel/components/BuildingBlocks/RadioButton.jsx b/app/shared-components/RadioButton/RadioButton.jsx similarity index 93% rename from app/panel/components/BuildingBlocks/RadioButton.jsx rename to app/shared-components/RadioButton/RadioButton.jsx index 12924cfb3..63cfba0fe 100644 --- a/app/panel/components/BuildingBlocks/RadioButton.jsx +++ b/app/shared-components/RadioButton/RadioButton.jsx @@ -21,12 +21,14 @@ import ClassNames from 'classnames'; */ const RadioButton = (props) => { - const { checked, handleClick } = props; + const { checked, handleClick, altDesign } = props; const OuterCircleClassNames = ClassNames('RadioButton__outerCircle', { checked, + altDesign }); const InnerCircleClassNames = ClassNames('RadioButton__innerCircle', { checked, + altDesign }); return ( diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.js b/app/shared-components/RadioButton/index.js similarity index 68% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.js rename to app/shared-components/RadioButton/index.js index 72ed11332..a4160127a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.js +++ b/app/shared-components/RadioButton/index.js @@ -1,5 +1,5 @@ /** - * Point of entry index.js file for Ghostery Browser Hub Choose Plan View + * Point of entry index.js file for Radio Button * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -11,6 +11,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import ChoosePlanView from './ChoosePlanView'; +import RadioButton from './RadioButton'; -export default ChoosePlanView; +export default RadioButton; diff --git a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx b/app/shared-components/RadioButtonGroup/RadioButtonGroup.jsx similarity index 97% rename from app/panel/components/BuildingBlocks/RadioButtonGroup.jsx rename to app/shared-components/RadioButtonGroup/RadioButtonGroup.jsx index 2722ceddc..a91d4b8f4 100644 --- a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx +++ b/app/shared-components/RadioButtonGroup/RadioButtonGroup.jsx @@ -13,7 +13,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import RadioButton from './RadioButton'; +import RadioButton from '../RadioButton'; /** * @class Implements a radio button group diff --git a/app/shared-components/RadioButtonGroup/index.js b/app/shared-components/RadioButtonGroup/index.js new file mode 100644 index 000000000..a7d9aa0d8 --- /dev/null +++ b/app/shared-components/RadioButtonGroup/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Radio Button Group + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import RadioButtonGroup from './RadioButtonGroup'; + +export default RadioButtonGroup; From e901117b3dc2a0553c7d2593d4e339d79b071dce Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Thu, 10 Dec 2020 11:31:04 -0500 Subject: [PATCH 020/113] Post-PR-merge SCSS imports cleanup --- app/scss/hub.scss | 3 --- app/scss/hub_ghostery_browser.scss | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/scss/hub.scss b/app/scss/hub.scss index d259e061c..a085ae814 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -86,9 +86,6 @@ html, body, #root { @import '../hub/Views/LogInView/LogInView.scss'; @import '../hub/Views/CreateAccountView/CreateAccountView.scss'; @import '../hub/Views/UpgradePlanView/UpgradePlanView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/ChoosePlanView/ChoosePlanView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory @import '../shared-components/ExitButton/ExitButton.scss'; diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss index 3c6f67af5..ec7a0ebb2 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/hub_ghostery_browser.scss @@ -68,6 +68,7 @@ html, body, #root { @import './partials/_shared_components_svgs'; @import './partials/_fonts'; @import './partials/_loader'; +@import './partials/radio_button'; // Imports from ../ghostery-browser-hub directory @import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; @@ -76,8 +77,11 @@ html, body, #root { @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory @import '../shared-components/ToastMessage/ToastMessage.scss'; @import '../shared-components/ToggleCheckbox/ToggleCheckbox.scss'; + + From 3e92e7d22f60fc374d6d816be38d460666cbea8f Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Fri, 11 Dec 2020 13:22:45 -0500 Subject: [PATCH 021/113] Cleanup of index files and debugging in new intro hub --- .../Views/AppView/AppView.jsx | 2 +- .../Step0_WelcomeView/WelcomeView.jsx | 2 +- .../Step1_CreateAccountForm.jsx | 9 +++-- .../Step1_CreateAccountFormContainer.jsx | 4 +-- .../Step1_CreateAccountForm/index.js | 35 +++++-------------- .../Step2_BlockSettingsView/index.js | 31 ++++++---------- 6 files changed, 28 insertions(+), 55 deletions(-) diff --git a/app/ghostery-browser-hub/Views/AppView/AppView.jsx b/app/ghostery-browser-hub/Views/AppView/AppView.jsx index e0c65d84e..1ec0923da 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppView.jsx +++ b/app/ghostery-browser-hub/Views/AppView/AppView.jsx @@ -18,7 +18,7 @@ import { ToastMessage } from '../../../shared-components'; /** * A functional React component that implements the App View for the Ghostery Browser Hub * @extends Component - * @memberof GhosteryHubViews + * @memberof GhosteryBrowserHubViews */ const AppView = (props) => { const { app, children } = props; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx index d0038f7ec..ea37fb8bb 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx @@ -16,7 +16,7 @@ import React from 'react'; /** * A Functional React component for rendering the Browser Welcome View * @return {JSX} JSX for rendering the Browser Welcome View of the Hub app - * @memberof HubComponents + * @memberof GhosteryBrowserHubViews */ const WelcomeView = () => (
diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx index 2b2782fd4..73a6a8026 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx @@ -4,25 +4,24 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React, { useRef, useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; -import { NavLink } from 'react-router-dom'; -import { ToggleCheckbox } from '../../../shared-components'; +import { ToggleCheckbox } from '../../../../shared-components/ToggleCheckbox'; const promoString = `${t('ghostery_browser_hub_onboarding_send_me')} Ghostery ${t('ghostery_browser_hub_onboarding_updates_and_promotions')}`; /** * A Functional React component for rendering the Browser Create Account View * @return {JSX} JSX for rendering the Browser Create Account View of the Hub app - * @memberof HubComponents + * @memberof GhosteryBrowserHubViews */ const Step1_CreateAccountForm = (props) => { const { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx index 8d043ea9a..8c0180897 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -25,7 +25,7 @@ import BrowserCreateAccountForm from './Step1_CreateAccountForm'; /** * @class Implement the Create Account View for the Ghostery Hub * @extends Component - * @memberof HubContainers + * @memberof GhosteryBrowserHubContainers */ class CreateAccountFormContainer extends Component { constructor(props) { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js index 30c614789..4dcac5d05 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js @@ -11,34 +11,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; - +import { buildReduxHOC } from '../../../../shared-hub/utils'; import Step1_CreateAccountFormContainer from './Step1_CreateAccountFormContainer'; import { register, getUser, handleEmailPreferencesCheckboxChange } from '../../../../Account/AccountActions'; import { setToast } from '../../../../hub/Views/AppView/AppViewActions'; -/** - * Map redux store state properties to the component's own properties. - * @param {Object} state entire Redux store's state - * @return {function} this function returns a plain object, which will be merged into the component's props - * @memberof HubContainers - */ -const mapStateToProps = state => ({ ...state.account }); - -/** - * Bind the component's action creators using Redux's bindActionCreators. - * @param {function} dispatch redux store method which dispatches actions - * @return {function} to be used as an argument in redux connect call - * @memberof SetupContainers - */ -const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators({ - setToast, - register, - getUser, - handleEmailPreferencesCheckboxChange - }, dispatch), -}); +const stateSlices = ['account']; +const actionCreators = { + setToast, + register, + getUser, + handleEmailPreferencesCheckboxChange +}; -export default connect(mapStateToProps, mapDispatchToProps)(Step1_CreateAccountFormContainer); +export default buildReduxHOC(stateSlices, actionCreators, Step1_CreateAccountFormContainer); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js index 93095cc5a..93e4cd51f 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js @@ -11,27 +11,18 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; - import BlockSettingsView from './BlockSettingsView'; +import { buildReduxHOC } from '../../../../shared-hub/utils'; import { setAntiTracking, setAdBlock, setSmartBlocking } from '../../../../shared-hub/actions/AntiSuiteActions'; -import { setBlockingPolicy } from '../../../../shared-hub/actions/BlockingPolicyActions'; -import { setToast } from '../../../../shared-hub/actions/ToastActions'; +import setBlockingPolicy from '../../../../shared-hub/actions/BlockingPolicyActions'; +import setToast from '../../../../shared-hub/actions/ToastActions'; -/** - * Bind the component's action creators using Redux's bindActionCreators. - * @param {function} dispatch redux store method which dispatches actions - * @return {function} to be used as an argument in redux connect call - */ -const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators({ - setAntiTracking, - setAdBlock, - setSmartBlocking, - setBlockingPolicy, - setToast, - }, dispatch), -}); +const actionCreators = { + setAntiTracking, + setAdBlock, + setSmartBlocking, + setBlockingPolicy, + setToast, +}; -export default connect(null, mapDispatchToProps)(BlockSettingsView); +export default buildReduxHOC(null, actionCreators, BlockSettingsView); From bfaf1f33a806b3fb4d387f78efae79061a440e46 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Fri, 11 Dec 2020 13:39:41 -0500 Subject: [PATCH 022/113] Continue cleanup of Step1 JSX --- .../Step1_CreateAccountView.jsx | 5 +-- .../Step1_CreateAccountView.scss | 2 +- .../Step1_CreateAccountView/index.js | 21 +-------- .../Step1_LogInFormContainer.jsx | 1 + .../OnboardingViews/Step1_LogInForm/index.js | 44 ++++++------------- 5 files changed, 20 insertions(+), 53 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx index d97910485..4b284e200 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -14,7 +14,6 @@ import React, { Fragment, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; -import { NavLink } from 'react-router-dom'; import Step1_LogInForm from '../Step1_LogInForm'; import Step1_CreateAccountForm from '../Step1_CreateAccountForm'; import globals from '../../../../../src/classes/Globals'; @@ -74,7 +73,7 @@ const renderSkipLink = () => ( /** * A Functional React component for rendering the Browser Create Account View * @return {JSX} JSX for rendering the Browser Create Account View of the Hub app - * @memberof HubComponents + * @memberof GhosteryBrowserHubViews */ const Step1_CreateAccountView = (props) => { const { user } = props; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss index d79bf60a2..b61778e86 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js index d8bc43c75..ebf16f74d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js @@ -11,25 +11,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; - +import { buildReduxHOC } from '../../../../shared-hub/utils'; import Step1_CreateAccountView from './Step1_CreateAccountView'; -/** - * Map redux store state properties to the component's own properties. - * @param {Object} state entire Redux store's state - * @return {function} this function returns a plain object, which will be merged into the component's props - * @memberof HubContainers - */ -const mapStateToProps = state => ({ ...state.account }); - -/** - * Bind the component's action creators using Redux's bindActionCreators. - * @param {function} dispatch redux store method which dispatches actions - * @return {function} to be used as an argument in redux connect call - * @memberof SetupContainers - */ -const mapDispatchToProps = dispatch => ({}); - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Step1_CreateAccountView)); +export default withRouter(buildReduxHOC(['account'], null, Step1_CreateAccountView)); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx index e957e3dfa..67d15b3ae 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx @@ -101,6 +101,7 @@ class Step1_LogInFormContainer extends Component { window.history.pushState({}, '', `${origin}${pathname}${hash}`); actions.getUser(); + // TODO investigate whether this is needed here actions.getUserSettings() .then((settings) => { const { current_theme } = settings; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js index 17b0ba241..0cecb8185 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js @@ -11,8 +11,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; +import { buildReduxHOC } from '../../../../shared-hub/utils'; import Step1_LogInFormContainer from './Step1_LogInFormContainer'; import { @@ -20,33 +19,18 @@ import { getUser, getUserSettings, resetPassword -} from '../../../Account/AccountActions'; -import { getTheme } from '../../../panel/actions/PanelActions'; -import { setToast } from '../AppView/AppViewActions'; +} from '../../../../Account/AccountActions'; +import { getTheme } from '../../../../panel/actions/PanelActions'; +import setToast from '../../../../shared-hub/actions/ToastActions'; -/** - * Map redux store state properties to the component's own properties. - * @param {Object} state entire Redux store's state - * @return {function} this function returns a plain object, which will be merged into the component's props - * @memberof HubContainers - */ -const mapStateToProps = state => ({ ...state.account }); - -/** - * Bind the component's action creators using Redux's bindActionCreators. - * @param {function} dispatch redux store method which dispatches actions - * @return {function} to be used as an argument in redux connect call - * @memberof SetupContainers - */ -const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators({ - setToast, - login, - getUser, - getUserSettings, - getTheme, - resetPassword - }, dispatch), -}); +const stateSlices = ['account']; +const actionCreators = { + setToast, + login, + getUser, + getUserSettings, + getTheme, + resetPassword +}; -export default connect(mapStateToProps, mapDispatchToProps)(Step1_LogInFormContainer); +export default buildReduxHOC(stateSlices, actionCreators, Step1_LogInFormContainer); From 11f3a7e2950e08618413c1f693ad6ceabae31045 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Fri, 11 Dec 2020 14:16:21 -0500 Subject: [PATCH 023/113] Render hub step progress bar across all views --- .../Views/OnboardingView/OnboardingView.jsx | 3 + .../StepProgressBar/StepProgressBar.jsx | 58 ++++++++++--------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx index 4bad14783..fb6dd8d3b 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx @@ -15,6 +15,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Route } from 'react-router-dom'; +import StepProgressBar from '../OnboardingViews/StepProgressBar' + /** * A Functional React component for rendering the Onboarding View * @return {JSX} JSX for rendering the Onboarding View of the Ghostery Browser Hub app @@ -32,6 +34,7 @@ const OnboardingView = (props) => { path={step.path} render={() => (
+
)} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx index bff59d29d..877a70d5c 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx @@ -72,33 +72,37 @@ const StepProgressBar = (props) => { ); const renderProgressBar = () => ( - steps.map((value, index) => ( - - {(index + 1 < currentStep) && ( - renderCompletedStep(steps[index]) - )} - {(index + 1 === currentStep) && ( - - {renderCurrentStep(steps[index], index + 1)} - - )} - {(index + 1 > currentStep) && ( - - {renderIncompleteStep(steps[index], index + 1)} - - )} - {(index + 1 !== totalSteps) && ( - - {(index + 1 < currentStep) && ( -
- )} - {(index + 1 >= currentStep) && ( -
- )} - - )} - - )) + steps.map((value, index) => { + const step = index + 1; + + return ( + + {(step < currentStep) && ( + renderCompletedStep(steps[index]) + )} + {(step === currentStep) && ( + + {renderCurrentStep(steps[index], index + 1)} + + )} + {(step > currentStep) && ( + + {renderIncompleteStep(steps[index], index + 1)} + + )} + {(step !== totalSteps) && ( + + {(index + 1 < currentStep) && ( +
+ )} + {(index + 1 >= currentStep) && ( +
+ )} + + )} + + ); + }) ); return ( From 077f680d32712fa959429f2e21f51fe2a52ae87b Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Fri, 11 Dec 2020 16:51:06 -0500 Subject: [PATCH 024/113] Add StepNavigator component to manage navigation within a step --- .../Views/OnboardingView/OnboardingView.jsx | 7 ++- .../OnboardingViewContainer.jsx | 1 + .../StepNavigator/StepNavigator.jsx | 58 +++++++++++++++++++ .../OnboardingViews/StepNavigator/index.js | 16 +++++ 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/index.js diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx index fb6dd8d3b..22e7bc75d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx @@ -15,7 +15,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Route } from 'react-router-dom'; -import StepProgressBar from '../OnboardingViews/StepProgressBar' +import StepProgressBar from '../OnboardingViews/StepProgressBar'; +import StepNavigator from '../OnboardingViews/StepNavigator'; /** * A Functional React component for rendering the Onboarding View @@ -35,7 +36,7 @@ const OnboardingView = (props) => { render={() => (
- +
)} /> @@ -50,7 +51,7 @@ OnboardingView.propTypes = { steps: PropTypes.arrayOf(PropTypes.shape({ index: PropTypes.number.isRequired, path: PropTypes.string.isRequired, - bodyComponent: PropTypes.shape.isRequired, + bodyComponents: PropTypes.arrayOf(PropTypes.element.isRequired).isRequired, })).isRequired, sendMountActions: PropTypes.bool.isRequired, }; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index 883c06fd0..db5fb4a24 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -101,6 +101,7 @@ class OnboardingViewContainer extends Component { index: 4, path: '/onboarding/4', bodyComponent: ChoosePlanView, + // bodyComponents: [ChoosePlanView, LoginView] }, { index: 5, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx new file mode 100644 index 000000000..f59e08779 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx @@ -0,0 +1,58 @@ +/** + * Step Navigator Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +/** + * A React class component for rendering the StepNavigator, a container for all the views in a setup step + * @return {JSX} JSX for rendering the Step Progress bar of the ghostery-browser-intro-hub app + * @memberof GhosteryBrowserHubContainers + */ +class StepNavigator extends Component { + constructor(props) { + super(props); + this.state = { + screen: 0, + }; + } + + next() { + const { components } = this.props; + this.setState(state => ({ screen: (state.screen + 1) % components.length })); + } + + prev() { + const { components } = this.props; + this.setState({ screen: ((this.state.screen - 1) + components.length) % components.length }); + } + + render() { + const { components, step } = this.props; + const { screen } = this.state; + + const Screen = components[screen]; + + return ( + + ); + } +} + +// PropTypes ensure we pass required props of the correct type +StepNavigator.propTypes = { + step: PropTypes.number.isRequired, + components: PropTypes.arrayOf(PropTypes.element.isRequired).isRequired, +}; + +export default StepNavigator; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/index.js new file mode 100644 index 000000000..9324423ed --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Ghostery Browser Hub Step Navigator + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import StepNavigator from './StepNavigator'; + +export default StepNavigator; From 4b55efd246be8d610f3e24174153470179742eb4 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Fri, 11 Dec 2020 17:10:12 -0500 Subject: [PATCH 025/113] Update data structure passed to OnboardingView by the container to match the requirements of the new StepNavigator component --- .../OnboardingView/OnboardingViewContainer.jsx | 13 ++++++------- .../OnboardingViews/StepNavigator/StepNavigator.jsx | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index db5fb4a24..0cfee5734 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -80,33 +80,32 @@ class OnboardingViewContainer extends Component { { index: 0, path: '/onboarding/0', - bodyComponent: WelcomeView, + bodyComponents: [WelcomeView], }, { index: 1, path: '/onboarding/1', - bodyComponent: LoginView, + bodyComponents: [LoginView], }, { index: 2, path: '/onboarding/2', - bodyComponent: BlockSettingsView, + bodyComponents: [BlockSettingsView], }, { index: 3, path: '/onboarding/3', - bodyComponent: ChooseDefaultSearchView, + bodyComponents: [ChooseDefaultSearchView], }, { index: 4, path: '/onboarding/4', - bodyComponent: ChoosePlanView, - // bodyComponents: [ChoosePlanView, LoginView] + bodyComponents: [ChoosePlanView, LoginView], }, { index: 5, path: '/onboarding/5', - bodyComponent: SuccessView, + bodyComponents: [SuccessView], } ]; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx index f59e08779..4659b6bba 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx @@ -34,7 +34,7 @@ class StepNavigator extends Component { prev() { const { components } = this.props; - this.setState({ screen: ((this.state.screen - 1) + components.length) % components.length }); + this.setState(state => ({ screen: ((state.screen - 1) + components.length) % components.length })); } render() { From 89edc23e8cf10072e82f85247c18ac5328a31daf Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Fri, 11 Dec 2020 17:27:46 -0500 Subject: [PATCH 026/113] Finish converting index files to use buildReduxHOC --- .../Step1_CreateAccountForm.jsx | 2 +- .../Step1_LogInForm/Step1_LogInForm.jsx | 1 - .../Step4_ChoosePlanView/index.jsx | 21 ++----------------- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx index 73a6a8026..5cfdfdc89 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx @@ -14,7 +14,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; -import { ToggleCheckbox } from '../../../../shared-components/ToggleCheckbox'; +import ToggleCheckbox from '../../../../shared-components/ToggleCheckbox'; const promoString = `${t('ghostery_browser_hub_onboarding_send_me')} Ghostery ${t('ghostery_browser_hub_onboarding_updates_and_promotions')}`; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx index bc712d4cc..0f70b5e26 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx @@ -14,7 +14,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; -import { NavLink } from 'react-router-dom'; /** * A Functional React component for rendering the Log In Form diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx index f21eb0832..402d19108 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx @@ -11,25 +11,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; +import { buildReduxHOC } from '../../../../shared-hub/utils'; import ChoosePlanView from './ChoosePlanView'; -/** - * Map redux store state properties to the component's own properties. - * @param {Object} state entire Redux store's state - * @return {function} this function returns a plain object, which will be merged into the component's props - * @memberof HubContainers - */ -const mapStateToProps = state => ({ ...state.account }); - -/** - * Bind the component's action creators using Redux's bindActionCreators. - * @param {function} dispatch redux store method which dispatches actions - * @return {function} to be used as an argument in redux connect call - * @memberof SetupContainers - */ -const mapDispatchToProps = dispatch => ({}); - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChoosePlanView)); +export default buildReduxHOC(['account'], null, ChoosePlanView); From 7ad29161bceac90da3e42298f2899908abe0c024 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 16 Dec 2020 14:36:05 -0500 Subject: [PATCH 027/113] Assorted intro hub fixes --- .../Views/AppView/AppView.jsx | 5 +-- .../Views/OnboardingView/OnboardingView.jsx | 4 ++- .../OnboardingViewContainer.jsx | 34 +++++++------------ .../Step4_ChoosePlanView/ChoosePlanView.scss | 2 ++ .../StepNavigator/StepNavigator.jsx | 2 +- app/ghostery-browser-hub/index.jsx | 19 ++++++++--- .../UpgradePlanView/UpgradePlanView.scss | 8 ----- app/scss/partials/_colors.scss | 11 ++++++ 8 files changed, 47 insertions(+), 38 deletions(-) diff --git a/app/ghostery-browser-hub/Views/AppView/AppView.jsx b/app/ghostery-browser-hub/Views/AppView/AppView.jsx index 1ec0923da..21a7559a4 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppView.jsx +++ b/app/ghostery-browser-hub/Views/AppView/AppView.jsx @@ -22,6 +22,7 @@ import { ToastMessage } from '../../../shared-components'; */ const AppView = (props) => { const { app, children } = props; + const { toastMessage, toastClass } = app; /** * Handle clicking to exit the Toast Message. @@ -35,9 +36,9 @@ const AppView = (props) => { }; return ( -
+
- + {children}
diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx index 22e7bc75d..bb224f6fe 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx @@ -26,6 +26,8 @@ import StepNavigator from '../OnboardingViews/StepNavigator'; const OnboardingView = (props) => { const { sendMountActions, steps } = props; + console.log('in OnboardingView'); + return (
@@ -51,7 +53,7 @@ OnboardingView.propTypes = { steps: PropTypes.arrayOf(PropTypes.shape({ index: PropTypes.number.isRequired, path: PropTypes.string.isRequired, - bodyComponents: PropTypes.arrayOf(PropTypes.element.isRequired).isRequired, + bodyComponents: PropTypes.arrayOf(PropTypes.elementType.isRequired).isRequired, })).isRequired, sendMountActions: PropTypes.bool.isRequired, }; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index 0cfee5734..5026a24e9 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -53,21 +53,18 @@ class OnboardingViewContainer extends Component { // TODO modify this as needed const { origin, pathname, hash } = window.location; window.history.pushState({}, '', `${origin}${pathname}${hash}`); - this._setDefaultSettings(); - } - /** - * Function to persist the default settings to background - */ - // TODO modify this as needed - _setDefaultSettings() { - this.setState({ sendMountActions: true }); - const { actions } = this.props; - actions.setSetupStep({ setup_step: 8 }); - actions.setBlockingPolicy({ blockingPolicy: BLOCKING_POLICY_RECOMMENDED }); - actions.setAntiTracking({ enable_anti_tracking: true }); // covered - actions.setAdBlock({ enable_ad_block: true }); // covered - actions.setSmartBlocking({ enable_smart_block: true }); // covered + // TODO only invoke if there are no existing settings + if (true) { + this.state = { + sendMountActions: true + }; + actions.setSetupStep({ setup_step: 8 }); + actions.setBlockingPolicy({ blockingPolicy: BLOCKING_POLICY_RECOMMENDED }); + actions.setAntiTracking({ enable_anti_tracking: true }); // covered + actions.setAdBlock({ enable_ad_block: true }); // covered + actions.setSmartBlocking({ enable_smart_block: true }); // covered + } } /** @@ -149,25 +146,20 @@ OnboardingViewContainer.propTypes = { PropTypes.string, ]), }), - setup_show_warning_override: PropTypes.bool, blockingPolicy: PropTypes.string, enable_anti_tracking: PropTypes.bool, enable_ad_block: PropTypes.bool, enable_smart_block: PropTypes.bool, - enable_human_web: PropTypes.bool, }), actions: PropTypes.shape({ - getSetupShowWarningOverride: PropTypes.func.isRequired, - setSetupShowWarningOverride: PropTypes.func.isRequired, initSetupProps: PropTypes.func.isRequired, setSetupStep: PropTypes.func.isRequired, setSetupNavigation: PropTypes.func.isRequired, + setSetupComplete: PropTypes.func.isRequired, setBlockingPolicy: PropTypes.func.isRequired, setAntiTracking: PropTypes.func.isRequired, setAdBlock: PropTypes.func.isRequired, setSmartBlocking: PropTypes.func.isRequired, - setHumanWeb: PropTypes.func.isRequired, - setSetupComplete: PropTypes.func.isRequired, }).isRequired, }; @@ -184,12 +176,10 @@ OnboardingViewContainer.defaultProps = { textNext: false, textDone: false, }, - setup_show_warning_override: true, blockingPolicy: BLOCKING_POLICY_RECOMMENDED, enable_anti_tracking: true, enable_ad_block: true, enable_smart_block: true, - enable_human_web: true, }, }; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss index 90d077b78..034fa539f 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss @@ -1,3 +1,5 @@ +$medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow to next line + .ChoosePlanView { padding-bottom: 20px; } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx index 4659b6bba..86ed9c650 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx @@ -52,7 +52,7 @@ class StepNavigator extends Component { // PropTypes ensure we pass required props of the correct type StepNavigator.propTypes = { step: PropTypes.number.isRequired, - components: PropTypes.arrayOf(PropTypes.element.isRequired).isRequired, + components: PropTypes.arrayOf(PropTypes.elementType.isRequired).isRequired, }; export default StepNavigator; diff --git a/app/ghostery-browser-hub/index.jsx b/app/ghostery-browser-hub/index.jsx index d9c998c9e..67d2e3d2b 100644 --- a/app/ghostery-browser-hub/index.jsx +++ b/app/ghostery-browser-hub/index.jsx @@ -12,10 +12,11 @@ * * @namespace GhosteryBrowserHubComponents */ - import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; +import { HashRouter as Router, Route } from 'react-router-dom'; + import createStore from './createStore'; // Views @@ -24,12 +25,22 @@ import OnboardingView from './Views/OnboardingView'; const store = createStore(); +/** + * Top-Level Component for the Ghostery Browser Hub + * @memberof GhosteryBrowserHubComponents + */ +const Hub = () => ( + + + +); + ReactDOM.render( ( - - - + + + ), document.getElementById('root'), ); diff --git a/app/hub/Views/UpgradePlanView/UpgradePlanView.scss b/app/hub/Views/UpgradePlanView/UpgradePlanView.scss index 0d75ecd9e..ec4390f48 100644 --- a/app/hub/Views/UpgradePlanView/UpgradePlanView.scss +++ b/app/hub/Views/UpgradePlanView/UpgradePlanView.scss @@ -1,11 +1,3 @@ -$price-blue: #00AEF0; -$price-blue-hover: #0078CA; -$blue-alt: #4caeea; -$price-purple: #611b87; -$price-purple-alt: #720174; -$price-purple-hover: #710d72; -$price-gold: #b8860b; -$price-gold-hover: #906908; $medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow to next line .font-size-36 { diff --git a/app/scss/partials/_colors.scss b/app/scss/partials/_colors.scss index 3b99e216c..d9d5cbde2 100644 --- a/app/scss/partials/_colors.scss +++ b/app/scss/partials/_colors.scss @@ -67,3 +67,14 @@ $transparent-red: rgba($red, 0.08); /* ADD PROJECT COLORS HERE */ $gold: #F8E71C; $bright-gold: #FF9D00; + + +/* UPGRADE PLAN VIEW COLORS */ +$price-blue: #00AEF0; +$price-blue-hover: #0078CA; +$blue-alt: #4CAEEA; +$price-purple: #611B87; +$price-purple-alt: #720174; +$price-purple-hover: #710d72; +$price-gold: #B8860B; +$price-gold-hover: #906908; From 47172693e029f8b9cf73aad588fa9ad4d47447c5 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 21 Dec 2020 22:24:25 -0500 Subject: [PATCH 028/113] Use constants for paths --- .../OnboardingView/OnboardingConstants.js | 12 ++++++++++ .../OnboardingViewContainer.jsx | 22 ++++++++++++++----- .../StepProgressBar/StepProgressBar.jsx | 2 +- 3 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingView/OnboardingConstants.js diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingConstants.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingConstants.js new file mode 100644 index 000000000..fc325d8fb --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingConstants.js @@ -0,0 +1,12 @@ +/** +Constants for Ghostery Browser Intro Hub Onboarding +These constants map the onboarding step numbers to more memorable names + */ + +export const ONBOARDING = 'onboarding'; +export const WELCOME = '0'; +export const LOGIN = '1'; +export const BLOCK_SETTINGS = '2'; +export const CHOOSE_DEFAULT_SEARCH = '3'; +export const CHOOSE_PLAN = '4'; +export const SUCCESS = '5'; diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index 5026a24e9..46d510ab2 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -23,6 +23,16 @@ import BlockSettingsView from '../OnboardingViews/Step2_BlockSettingsView'; import ChooseDefaultSearchView from '../OnboardingViews/Step3_ChooseDefaultSearchView'; import ChoosePlanView from '../OnboardingViews/Step4_ChoosePlanView'; import SuccessView from '../OnboardingViews/Step5_SuccessView'; +import { + ONBOARDING, + WELCOME, + LOGIN, + BLOCK_SETTINGS, + CHOOSE_DEFAULT_SEARCH, + CHOOSE_PLAN, + SUCCESS + +} from './OnboardingConstants'; /** * @class Implement the Onboarding View for the Ghostery Browser Hub @@ -76,32 +86,32 @@ class OnboardingViewContainer extends Component { const steps = [ { index: 0, - path: '/onboarding/0', + path: `${ONBOARDING}/${WELCOME}`, bodyComponents: [WelcomeView], }, { index: 1, - path: '/onboarding/1', + path: `${ONBOARDING}/${LOGIN}`, bodyComponents: [LoginView], }, { index: 2, - path: '/onboarding/2', + path: `${ONBOARDING}/${BLOCK_SETTINGS}`, bodyComponents: [BlockSettingsView], }, { index: 3, - path: '/onboarding/3', + path: `${ONBOARDING}/${CHOOSE_DEFAULT_SEARCH}`, bodyComponents: [ChooseDefaultSearchView], }, { index: 4, - path: '/onboarding/4', + path: `${ONBOARDING}/${CHOOSE_PLAN}`, bodyComponents: [ChoosePlanView, LoginView], }, { index: 5, - path: '/onboarding/5', + path: `${ONBOARDING}/${SUCCESS}`, bodyComponents: [SuccessView], } ]; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx index 877a70d5c..3e9de9c17 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx @@ -19,7 +19,7 @@ import { NavLink } from 'react-router-dom'; const steps = [ { label: t('sign_in'), - route: 'LINK_TO_STEP_1' + route: '/onboarding/' }, { label: t('ghostery_browser_hub_onboarding_privacy'), From 9552fc4c822d732d96736ba965bdc861e984e508 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 21 Dec 2020 23:52:07 -0500 Subject: [PATCH 029/113] Cleanup StepProgressBar --- .../OnboardingViewContainer.jsx | 13 ++- .../StepProgressBar/StepProgressBar.jsx | 86 +++++++++---------- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index 46d510ab2..de05d183e 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -31,7 +31,6 @@ import { CHOOSE_DEFAULT_SEARCH, CHOOSE_PLAN, SUCCESS - } from './OnboardingConstants'; /** @@ -86,32 +85,32 @@ class OnboardingViewContainer extends Component { const steps = [ { index: 0, - path: `${ONBOARDING}/${WELCOME}`, + path: `/${ONBOARDING}/${WELCOME}`, bodyComponents: [WelcomeView], }, { index: 1, - path: `${ONBOARDING}/${LOGIN}`, + path: `/${ONBOARDING}/${LOGIN}`, bodyComponents: [LoginView], }, { index: 2, - path: `${ONBOARDING}/${BLOCK_SETTINGS}`, + path: `/${ONBOARDING}/${BLOCK_SETTINGS}`, bodyComponents: [BlockSettingsView], }, { index: 3, - path: `${ONBOARDING}/${CHOOSE_DEFAULT_SEARCH}`, + path: `/${ONBOARDING}/${CHOOSE_DEFAULT_SEARCH}`, bodyComponents: [ChooseDefaultSearchView], }, { index: 4, - path: `${ONBOARDING}/${CHOOSE_PLAN}`, + path: `/${ONBOARDING}/${CHOOSE_PLAN}`, bodyComponents: [ChoosePlanView, LoginView], }, { index: 5, - path: `${ONBOARDING}/${SUCCESS}`, + path: `/${ONBOARDING}/${SUCCESS}`, bodyComponents: [SuccessView], } ]; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx index 3e9de9c17..98ed54698 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx @@ -12,26 +12,37 @@ */ import React, { Fragment } from 'react'; +import ClassNames from 'classnames'; import PropTypes from 'prop-types'; import { NavLink } from 'react-router-dom'; +import { + ONBOARDING, + LOGIN, + BLOCK_SETTINGS, + CHOOSE_DEFAULT_SEARCH, + CHOOSE_PLAN +} from '../../OnboardingView/OnboardingConstants'; -// TODO: Change routes const steps = [ { label: t('sign_in'), - route: '/onboarding/' + route: `/${ONBOARDING}/${LOGIN}`, + id: LOGIN }, { label: t('ghostery_browser_hub_onboarding_privacy'), - route: 'LINK_TO_STEP_2' + route: `/${ONBOARDING}/${BLOCK_SETTINGS}`, + id: BLOCK_SETTINGS }, { label: t('ghostery_browser_hub_onboarding_search'), - route: 'LINK_TO_STEP_3' + route: `/${ONBOARDING}/${CHOOSE_DEFAULT_SEARCH}`, + id: CHOOSE_DEFAULT_SEARCH }, { label: t('ghostery_browser_hub_onboarding_plan'), - route: 'LINK_TO_STEP_4' + route: `/${ONBOARDING}/${CHOOSE_PLAN}`, + id: CHOOSE_PLAN } ]; @@ -44,32 +55,29 @@ const StepProgressBar = (props) => { const { currentStep } = props; const totalSteps = steps.length; - const renderCompletedStep = step => ( -
- -
{step.label}
-
- -
- ); + const renderStep = (step, isCurrent, stepClass) => { + const labelClasses = ClassNames('StepProgressBar__label', { + current: isCurrent, + }); + const stepClasses = ClassNames('StepProgressBar__Step', stepClass, { + [`step-${step.id}`]: stepClass !== 'step-completed', + }); - const renderCurrentStep = (step, value) => ( -
- -
{step.label}
-
- -
- ); + return ( +
+ +
{step.label}
+
+ +
+ ); + }; - const renderIncompleteStep = (step, value) => ( -
- -
{step.label}
-
- -
- ); + const renderCompletedStep = step => renderStep(step, false, 'step-completed'); + + const renderCurrentStep = step => renderStep(step, true, 'current'); + + const renderIncompleteStep = step => renderStep(step, false, 'incomplete'); const renderProgressBar = () => ( steps.map((value, index) => { @@ -77,25 +85,15 @@ const StepProgressBar = (props) => { return ( - {(step < currentStep) && ( - renderCompletedStep(steps[index]) - )} - {(step === currentStep) && ( - - {renderCurrentStep(steps[index], index + 1)} - - )} - {(step > currentStep) && ( - - {renderIncompleteStep(steps[index], index + 1)} - - )} + {(step < currentStep) && renderCompletedStep(steps[index])} + {(step === currentStep) && renderCurrentStep(steps[index])} + {(step > currentStep) && renderIncompleteStep(steps[index])} {(step !== totalSteps) && ( - {(index + 1 < currentStep) && ( + {(step < currentStep) && (
)} - {(index + 1 >= currentStep) && ( + {(step >= currentStep) && (
)} From d2374482d7aab231323af79ecb5b91322c991573 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Tue, 22 Dec 2020 00:59:07 -0500 Subject: [PATCH 030/113] Add new intro hub css to template --- app/templates/hub.html | 1 + 1 file changed, 1 insertion(+) diff --git a/app/templates/hub.html b/app/templates/hub.html index 490f38974..d41cb3a7c 100644 --- a/app/templates/hub.html +++ b/app/templates/hub.html @@ -18,6 +18,7 @@ +
From 4d7a47ebc5688a493adf4ec91bdedee6ac279793 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Tue, 22 Dec 2020 01:27:47 -0500 Subject: [PATCH 031/113] Remove unused navigation mechanism --- .../OnboardingViewContainer.jsx | 43 +------------------ .../Views/OnboardingView/index.js | 2 - 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index de05d183e..8295db886 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -46,9 +46,7 @@ class OnboardingViewContainer extends Component { }; const { history } = this.props; - if (!props.preventRedirect) { - history.push('/onboarding/0'); - } + history.push(`/${ONBOARDING}/${WELCOME}`); // TODO verify what document title we should use const title = t('ghostery_browser_hub_onboarding_page_title'); @@ -126,35 +124,7 @@ class OnboardingViewContainer extends Component { // PropTypes ensure we pass required props of the correct type // Note: isRequired is not needed when a prop has a default value OnboardingViewContainer.propTypes = { - preventRedirect: PropTypes.bool, setup: PropTypes.shape({ - navigation: PropTypes.shape({ - activeIndex: PropTypes.number, - hrefPrev: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - ]), - hrefNext: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - ]), - hrefDone: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - ]), - textPrev: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - ]), - textNext: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - ]), - textDone: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - ]), - }), blockingPolicy: PropTypes.string, enable_anti_tracking: PropTypes.bool, enable_ad_block: PropTypes.bool, @@ -163,7 +133,6 @@ OnboardingViewContainer.propTypes = { actions: PropTypes.shape({ initSetupProps: PropTypes.func.isRequired, setSetupStep: PropTypes.func.isRequired, - setSetupNavigation: PropTypes.func.isRequired, setSetupComplete: PropTypes.func.isRequired, setBlockingPolicy: PropTypes.func.isRequired, setAntiTracking: PropTypes.func.isRequired, @@ -174,17 +143,7 @@ OnboardingViewContainer.propTypes = { // Default props used throughout the Onboarding flow OnboardingViewContainer.defaultProps = { - preventRedirect: false, setup: { - navigation: { - activeIndex: 0, - hrefPrev: false, - hrefNext: false, - hrefDone: false, - textPrev: false, - textNext: false, - textDone: false, - }, blockingPolicy: BLOCKING_POLICY_RECOMMENDED, enable_anti_tracking: true, enable_ad_block: true, diff --git a/app/ghostery-browser-hub/Views/OnboardingView/index.js b/app/ghostery-browser-hub/Views/OnboardingView/index.js index 26fdd9dd1..a332709b4 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingView/index.js @@ -19,7 +19,6 @@ import OnboardingViewContainer from './OnboardingViewContainer'; import { initSetupProps, setSetupStep, - setSetupNavigation, setSetupComplete } from '../../../shared-hub/actions/SetupLifecycleActions'; import { @@ -34,7 +33,6 @@ export default withRouter(buildReduxHOC( { initSetupProps, setSetupStep, - setSetupNavigation, setSetupComplete, setBlockingPolicy, From e0e806c504b46352ff054ae83bc7eb331b9b8f9d Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 23 Dec 2020 12:56:42 -0500 Subject: [PATCH 032/113] Removed unused setup lifecycle actions --- .../actions/SetupLifecycleActions.js | 8 --- .../constants/SetupLifecycleConstants.js | 1 - .../reducers/SetupLifecycleReducer.js | 53 +------------------ app/templates/hub.html | 13 ++++- manifest.json | 2 +- 5 files changed, 14 insertions(+), 63 deletions(-) diff --git a/app/shared-hub/actions/SetupLifecycleActions.js b/app/shared-hub/actions/SetupLifecycleActions.js index 1dae62bcc..3f40698e9 100644 --- a/app/shared-hub/actions/SetupLifecycleActions.js +++ b/app/shared-hub/actions/SetupLifecycleActions.js @@ -15,7 +15,6 @@ import { makeDeferredDispatcher } from '../utils'; import { INIT_SETUP_PROPS, SET_SETUP_STEP, - SET_SETUP_NAVIGATION, SET_SETUP_COMPLETE } from '../constants/SetupLifecycleConstants'; @@ -29,12 +28,5 @@ export function initSetupProps(data) { export const setSetupStep = actionData => makeDeferredDispatcher(SET_SETUP_STEP, actionData); -export function setSetupNavigation(data) { - return { - type: SET_SETUP_NAVIGATION, - data, - }; -} - export const setSetupComplete = actionData => makeDeferredDispatcher(SET_SETUP_COMPLETE, actionData); diff --git a/app/shared-hub/constants/SetupLifecycleConstants.js b/app/shared-hub/constants/SetupLifecycleConstants.js index 4182e27b4..97c968d3e 100644 --- a/app/shared-hub/constants/SetupLifecycleConstants.js +++ b/app/shared-hub/constants/SetupLifecycleConstants.js @@ -13,5 +13,4 @@ export const INIT_SETUP_PROPS = 'INIT_SETUP_PROPS'; export const SET_SETUP_STEP = 'SET_SETUP_STEP'; -export const SET_SETUP_NAVIGATION = 'SET_SETUP_NAVIGATION'; export const SET_SETUP_COMPLETE = 'SET_SETUP_COMPLETE'; diff --git a/app/shared-hub/reducers/SetupLifecycleReducer.js b/app/shared-hub/reducers/SetupLifecycleReducer.js index b79966076..3d30ac6c8 100644 --- a/app/shared-hub/reducers/SetupLifecycleReducer.js +++ b/app/shared-hub/reducers/SetupLifecycleReducer.js @@ -12,77 +12,28 @@ */ import { - INIT_SETUP_PROPS, - SET_SETUP_NAVIGATION, + INIT_SETUP_PROPS } from '../constants/SetupLifecycleConstants'; const initialState = {}; function SetupLifecycleReducer(state = initialState, action) { switch (action.type) { + // TODO add navigation, override warning, and human web props init as separate case to share this with original hub case INIT_SETUP_PROPS: { const { - navigation, - setup_show_warning_override, blockingPolicy, enable_anti_tracking, enable_ad_block, enable_smart_block, - enable_human_web, } = action.data; - const { - activeIndex, - hrefPrev, - hrefNext, - hrefDone, - textPrev, - textNext, - textDone, - } = navigation; return { ...state, setup: { - navigation: { - activeIndex, - hrefPrev, - hrefNext, - hrefDone, - textPrev, - textNext, - textDone, - }, - setup_show_warning_override, blockingPolicy, enable_anti_tracking, enable_ad_block, enable_smart_block, - enable_human_web, - } - }; - } - case SET_SETUP_NAVIGATION: { - const { - activeIndex, - hrefPrev, - hrefNext, - hrefDone, - textPrev, - textNext, - textDone, - } = action.data; - return { - ...state, - setup: { - ...state.setup, - navigation: { - activeIndex, - hrefPrev, - hrefNext, - hrefDone, - textPrev, - textNext, - textDone, - } } }; } diff --git a/app/templates/hub.html b/app/templates/hub.html index d41cb3a7c..92ad97ea9 100644 --- a/app/templates/hub.html +++ b/app/templates/hub.html @@ -17,8 +17,6 @@ - -
@@ -32,6 +30,17 @@ else js.src = '../../dist/hub_ghostery_browser_react.js'; body.appendChild(js); + + const head = document.getElementsByTagName('head')[0]; + const hubCSS = document.createElement("link"); + + hubCSS.type = "text/css"; + hubCSS.rel = "stylesheet"; + hubCSS.media = "screen"; + // TODO branch on actual user agent value + hubCSS.href="../../dist/css/hub_ghosterY_browser.css"; + + head.appendChild(hubCSS); diff --git a/manifest.json b/manifest.json index f575f26a7..4b7746e1a 100644 --- a/manifest.json +++ b/manifest.json @@ -80,7 +80,7 @@ "run_at": "document_start" } ], - "content_security_policy": "script-src 'self' 'sha256-9fjlqggImjwAZN8KPwHSmZQlWo4CgkMFJR3cp7VC0jk='; object-src 'self'", + "content_security_policy": "script-src 'self' 'sha256-601d136ee1c4f4ca17e5bc9cf55e4f126999dd986f1491ef23899721222b7d57'; object-src 'self'", "permissions": [ "webNavigation", "webRequest", From 3fc6876dcb5ce73d35d0302f1bb9c95e28128ef9 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 23 Dec 2020 13:04:29 -0500 Subject: [PATCH 033/113] Conditionally include the appropriate hub css --- app/scss/hub_ghostery_browser.scss | 1 - manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss index ec7a0ebb2..0bdeefacf 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/hub_ghostery_browser.scss @@ -25,7 +25,6 @@ html, body, #root { z-index: 0; } .App__mainContent { - margin-left: 230px; overflow-y: auto; // Very small windows/phone screens in Firefox slightly overflows on the x-axis overflow-x: hidden; diff --git a/manifest.json b/manifest.json index 4b7746e1a..3bda86366 100644 --- a/manifest.json +++ b/manifest.json @@ -80,7 +80,7 @@ "run_at": "document_start" } ], - "content_security_policy": "script-src 'self' 'sha256-601d136ee1c4f4ca17e5bc9cf55e4f126999dd986f1491ef23899721222b7d57'; object-src 'self'", + "content_security_policy": "script-src 'self' 'sha256-Acnr7AdCM0m5xUApOa3qWqxq0HwdT0Zy66d9fNAiaz0='; object-src 'self'", "permissions": [ "webNavigation", "webRequest", From c444764017887bb436d2477df00842119564398e Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 23 Dec 2020 13:29:14 -0500 Subject: [PATCH 034/113] Clean up welcome view css, fix up button link --- _locales/en/messages.json | 6 +++--- .../Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx | 5 ++++- .../OnboardingViews/Step0_WelcomeView/WelcomeView.scss | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2009bfa8d..1846609a0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1765,13 +1765,13 @@ "message": "Plan" }, "ghostery_browser_hub_onboarding_welcome": { - "message": "Welcome to Ghostery Browser" + "message": "Welcome to Ghostery Browser!" }, "ghostery_browser_hub_onboarding_lets_begin": { - "message": "Let's begin by choosing your privacy settings. This will be quick, we promise!" + "message": "We've centralized online privacy by integrating our signature as well as novel technologies." }, "ghostery_browser_hub_onboarding_lets_do_this": { - "message": "Let's do this" + "message": "Set up My Browser" }, "ghostery_browser_hub_onboarding_your_privacy_plan": { "message": "Your Privacy Plan" diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx index ea37fb8bb..6b7fab917 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx @@ -12,6 +12,7 @@ */ import React from 'react'; +import { NavLink } from 'react-router-dom'; /** * A Functional React component for rendering the Browser Welcome View @@ -23,7 +24,9 @@ const WelcomeView = () => (
{t('ghostery_browser_hub_onboarding_welcome')}
{t('ghostery_browser_hub_onboarding_lets_begin')}
- + + { t('ghostery_browser_hub_onboarding_lets_do_this') } +
); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss index b6d7c6998..cbcf6e03c 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss @@ -46,6 +46,7 @@ &:hover { background-position: 0% 50%; transition: 0.25s all; + color: #FFF; } color: #FFF; font-size: 14.1px; From a24d53c51a80dccf87f607f63f0e89701500d355 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 23 Dec 2020 13:56:22 -0500 Subject: [PATCH 035/113] More welcome view cleanup. Make sure progress bar does not show on Welcome screen --- .../OnboardingViews/Step0_WelcomeView/WelcomeView.scss | 8 ++++---- .../OnboardingViews/StepProgressBar/StepProgressBar.jsx | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss index cbcf6e03c..21530b88e 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss @@ -29,7 +29,7 @@ .WelcomeView__ctaButton { display: flex; justify-content: center; - margin: 48px auto 0 auto; + margin: 0 auto; height: 44px; width: 162px; padding: 7.7px 14px; @@ -37,7 +37,7 @@ background: linear-gradient( 45deg, #ff7e74 50%, - #00aef0 + $ghosty_blue ); background-size: 200% 100%; background-position: 100% 50%; @@ -46,9 +46,9 @@ &:hover { background-position: 0% 50%; transition: 0.25s all; - color: #FFF; + color: $white; } - color: #FFF; + color: $white; font-size: 14.1px; font-weight: 700; border-radius: 3.5px; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx index 98ed54698..93029a213 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx @@ -17,6 +17,7 @@ import PropTypes from 'prop-types'; import { NavLink } from 'react-router-dom'; import { ONBOARDING, + WELCOME, LOGIN, BLOCK_SETTINGS, CHOOSE_DEFAULT_SEARCH, @@ -105,7 +106,7 @@ const StepProgressBar = (props) => { return (
- {renderProgressBar()} + {(currentStep !== parseInt(WELCOME, 10)) && renderProgressBar()}
); }; From 38601b9726d817b381162960bccfd111f54ac8f7 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Sun, 3 Jan 2021 23:30:05 -0500 Subject: [PATCH 036/113] GH-2220: Onboarding - Telemetry (#650) * Set the setup_step ConfData property for Welcome and Login/Create Account screens * History not working * Add setup step pings to Step4_ChoosePlanView * Send gb_onboarding and gb_onboarding_success pings. Refactor setSetupStep to use OnboardingConstants. Add react-router to onboarding hub * Update gb_onboarding setup_step to index 0-4 instead of 1-5 * Remove LoginView since it is replaced with Step1_CreateAccountView * Add TODO --- .../OnboardingViewContainer.jsx | 10 +- .../Step0_WelcomeView/WelcomeView.jsx | 25 ++-- .../Step0_WelcomeView/index.js | 8 +- .../Step1_CreateAccountFormContainer.jsx | 2 +- .../Step1_CreateAccountView.jsx | 30 ++-- .../Step1_CreateAccountView/index.js | 7 +- .../OnboardingViews/Step1_LogInForm/index.js | 4 +- .../Step1_LoginView/LoginView.jsx | 18 --- .../OnboardingViews/Step1_LoginView/index.js | 16 -- .../BlockSettingsView.jsx | 9 +- .../Step2_BlockSettingsView/index.js | 2 + .../ChooseDefaultSearchView.jsx | 1 + .../Step4_ChoosePlanView/ChoosePlanView.jsx | 138 +++++++++--------- .../Step4_ChoosePlanView/index.jsx | 8 +- .../Step5_SuccessView/SuccessView.jsx | 20 ++- .../Step5_SuccessView/index.js | 8 +- app/shared-hub/utils/index.js | 3 +- src/background.js | 5 + src/classes/Metrics.js | 24 ++- 19 files changed, 193 insertions(+), 145 deletions(-) delete mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx delete mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/index.js diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index 8295db886..a09ada285 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -18,7 +18,7 @@ import { BLOCKING_POLICY_RECOMMENDED } from '../../../shared-hub/constants/Block // Component Views import WelcomeView from '../OnboardingViews/Step0_WelcomeView'; -import LoginView from '../OnboardingViews/Step1_LoginView'; +import Step1_CreateAccountView from '../OnboardingViews/Step1_CreateAccountView'; import BlockSettingsView from '../OnboardingViews/Step2_BlockSettingsView'; import ChooseDefaultSearchView from '../OnboardingViews/Step3_ChooseDefaultSearchView'; import ChoosePlanView from '../OnboardingViews/Step4_ChoosePlanView'; @@ -54,7 +54,7 @@ class OnboardingViewContainer extends Component { // TODO modify this as needed const { actions, setup } = this.props; - actions.setSetupStep({ setup_step: 7 }); + actions.setSetupStep({ setup_step: 7, origin: ONBOARDING }); actions.initSetupProps(setup); // TODO modify this as needed @@ -66,7 +66,7 @@ class OnboardingViewContainer extends Component { this.state = { sendMountActions: true }; - actions.setSetupStep({ setup_step: 8 }); + actions.setSetupStep({ setup_step: 8, origin: ONBOARDING }); actions.setBlockingPolicy({ blockingPolicy: BLOCKING_POLICY_RECOMMENDED }); actions.setAntiTracking({ enable_anti_tracking: true }); // covered actions.setAdBlock({ enable_ad_block: true }); // covered @@ -89,7 +89,7 @@ class OnboardingViewContainer extends Component { { index: 1, path: `/${ONBOARDING}/${LOGIN}`, - bodyComponents: [LoginView], + bodyComponents: [Step1_CreateAccountView], }, { index: 2, @@ -104,7 +104,7 @@ class OnboardingViewContainer extends Component { { index: 4, path: `/${ONBOARDING}/${CHOOSE_PLAN}`, - bodyComponents: [ChoosePlanView, LoginView], + bodyComponents: [ChoosePlanView], }, { index: 5, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx index 6b7fab917..d11dbfe1a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx @@ -13,21 +13,26 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; +import { LOGIN, WELCOME } from '../../OnboardingView/OnboardingConstants'; /** * A Functional React component for rendering the Browser Welcome View * @return {JSX} JSX for rendering the Browser Welcome View of the Hub app * @memberof GhosteryBrowserHubViews */ -const WelcomeView = () => ( -
-
{t('ghostery_browser_hub_onboarding_welcome')}
-
{t('ghostery_browser_hub_onboarding_lets_begin')}
- - - { t('ghostery_browser_hub_onboarding_lets_do_this') } - -
-); +const WelcomeView = (props) => { + const { actions } = props; + const { setSetupStep } = actions; + return ( +
+
{t('ghostery_browser_hub_onboarding_welcome')}
+
{t('ghostery_browser_hub_onboarding_lets_begin')}
+ + setSetupStep({ setup_step: LOGIN, origin: WELCOME })}> + {t('ghostery_browser_hub_onboarding_lets_do_this')} + +
+ ); +}; export default WelcomeView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js index d5f22aa58..c649d5a74 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/index.js @@ -11,6 +11,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ +import { buildReduxHOC } from '../../../../shared-hub/utils'; import WelcomeView from './WelcomeView'; +import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; -export default WelcomeView; +const actionCreators = { + setSetupStep, +}; + +export default buildReduxHOC([], actionCreators, WelcomeView); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx index 8c0180897..31f9ffc76 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx @@ -162,6 +162,7 @@ class CreateAccountFormContainer extends Component { }); actions.register(email, confirmEmail, firstName, lastName, password).then((success) => { if (success) { + // User is automatically logged in, and redirected to the logged in view of BrowserCreateAccountForm actions.getUser().then(() => { if (isUpdatesChecked) actions.handleEmailPreferencesCheckboxChange('global', isUpdatesChecked); }); @@ -170,7 +171,6 @@ class CreateAccountFormContainer extends Component { toastMessage: t('hub_create_account_toast_success'), toastClass: 'success' }); - // Route to next screen } else { actions.setToast({ toastMessage: t('hub_create_account_toast_error'), diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx index 4b284e200..0451de371 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx @@ -12,11 +12,13 @@ */ import React, { Fragment, useRef, useState } from 'react'; +import { NavLink } from 'react-router-dom'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; import Step1_LogInForm from '../Step1_LogInForm'; import Step1_CreateAccountForm from '../Step1_CreateAccountForm'; import globals from '../../../../../src/classes/Globals'; +import { LOGIN, ONBOARDING } from '../../OnboardingView/OnboardingConstants'; const SIGN_IN = 'SIGN_IN'; const CREATE_ACCOUNT = 'CREATE_ACCOUNT'; @@ -61,22 +63,14 @@ const renderFAQListItem = (icon, label, description) => (
); -const renderSkipLink = () => ( -
-
-
-
{t('ghostery_browser_hub_onboarding_skip')}
-
-
-); - /** * A Functional React component for rendering the Browser Create Account View * @return {JSX} JSX for rendering the Browser Create Account View of the Hub app * @memberof GhosteryBrowserHubViews */ const Step1_CreateAccountView = (props) => { - const { user } = props; + const { user, actions } = props; + const { setSetupStep } = actions; const email = user && user.email; const [expanded, setExpanded] = useState(false); @@ -97,13 +91,25 @@ const Step1_CreateAccountView = (props) => { setExpanded(!expanded); }; + const renderSkipLink = () => ( +
+
+
+ setSetupStep({ setup_step: LOGIN, origin: ONBOARDING })}> + {t('ghostery_browser_hub_onboarding_skip')} + +
+
+ ); + return (user ? (
{t('ghostery_browser_hub_onboarding_you_are_signed_in_as')}
{email}
- {/* Link to next page */} - + setSetupStep({ setup_step: LOGIN, origin: ONBOARDING })}> + {t('next')} +
) : ( diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js index ebf16f74d..3015cae6f 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js @@ -14,5 +14,10 @@ import { withRouter } from 'react-router-dom'; import { buildReduxHOC } from '../../../../shared-hub/utils'; import Step1_CreateAccountView from './Step1_CreateAccountView'; +import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; -export default withRouter(buildReduxHOC(['account'], null, Step1_CreateAccountView)); +const actionCreators = { + setSetupStep, +}; + +export default withRouter(buildReduxHOC(['account'], actionCreators, Step1_CreateAccountView)); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js index 0cecb8185..21a7378e6 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js @@ -22,6 +22,7 @@ import { } from '../../../../Account/AccountActions'; import { getTheme } from '../../../../panel/actions/PanelActions'; import setToast from '../../../../shared-hub/actions/ToastActions'; +import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; const stateSlices = ['account']; const actionCreators = { @@ -30,7 +31,8 @@ const actionCreators = { getUser, getUserSettings, getTheme, - resetPassword + resetPassword, + setSetupStep, }; export default buildReduxHOC(stateSlices, actionCreators, Step1_LogInFormContainer); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx deleted file mode 100644 index 29806db95..000000000 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/LoginView.jsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Ghostery Browser Hub Login View Component - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import React from 'react'; - -const LoginView = () =>

Step 1: Login View

; - -export default LoginView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/index.js deleted file mode 100644 index d059cb767..000000000 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LoginView/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Point of entry index.js file for Ghostery Browser Hub Onboarding Login View - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import LoginView from './LoginView'; - -export default LoginView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index 13cc9fe6d..b281816db 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -16,6 +16,7 @@ import React, { Component } from 'react'; // import Tooltip from '../../../../panel/components/Tooltip'; import RadioButton from '../../../../shared-components/RadioButton/RadioButton'; import ToggleCheckbox from '../../../../shared-components/ToggleCheckbox/ToggleCheckbox'; +import { CHOOSE_DEFAULT_SEARCH, ONBOARDING } from '../../OnboardingView/OnboardingConstants'; /** * @class Implement the Block Settings View for the Ghostery Browser Hub @@ -65,9 +66,11 @@ class BlockSettingsView extends Component { // Will only change user settings if all questions are answered if (blockAds !== null && kindsOfTrackers !== null && antiTracking !== null && smartBrowsing !== null) { + const { actions } = this.props; const { - setAdBlock, setAntiTracking, setSmartBlocking, setBlockingPolicy - } = this.props; + setAdBlock, setAntiTracking, setSmartBlocking, setBlockingPolicy, setSetupStep + } = actions; + const { history } = this.props; setAdBlock(blockAds); setAntiTracking(antiTracking); @@ -88,6 +91,8 @@ class BlockSettingsView extends Component { break; } setBlockingPolicy({ blockingPolicy }); + setSetupStep({ setup_step: CHOOSE_DEFAULT_SEARCH, origin: ONBOARDING }); + history.push('/onboarding/3'); } else { const { setToast } = this.props; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js index 93e4cd51f..12c2c9950 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js @@ -16,6 +16,7 @@ import { buildReduxHOC } from '../../../../shared-hub/utils'; import { setAntiTracking, setAdBlock, setSmartBlocking } from '../../../../shared-hub/actions/AntiSuiteActions'; import setBlockingPolicy from '../../../../shared-hub/actions/BlockingPolicyActions'; import setToast from '../../../../shared-hub/actions/ToastActions'; +import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; const actionCreators = { setAntiTracking, @@ -23,6 +24,7 @@ const actionCreators = { setSmartBlocking, setBlockingPolicy, setToast, + setSetupStep, }; export default buildReduxHOC(null, actionCreators, BlockSettingsView); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index dd0f23058..5c8ad8e93 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -13,6 +13,7 @@ import React from 'react'; +// TODO: Add setSetupStep({ setup_step: CHOOSE_DEFAULT_SEARCH, origin: WELCOME }) to next button const ChooseDefaultSearchView = () =>

Step 3: Choose Default Search View

; export default ChooseDefaultSearchView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 4ce39dc8a..93960f467 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -12,11 +12,13 @@ */ import React, { Fragment } from 'react'; +import { NavLink } from 'react-router-dom'; import ClassNames from 'classnames'; import PropTypes from 'prop-types'; import RadioButton from '../../../../shared-components/RadioButton'; import globals from '../../../../../src/classes/Globals'; import { BASIC, PLUS, PREMIUM } from '../../../../hub/Views/UpgradePlanView/UpgradePlanViewConstants'; +import { CHOOSE_PLAN, ONBOARDING } from '../../OnboardingView/OnboardingConstants'; const plusCheckoutLink = `${globals.CHECKOUT_BASE_URL}/en/plus`; const premiumCheckoutLink = `${globals.CHECKOUT_BASE_URL}/en/premium`; @@ -72,64 +74,6 @@ const basicCard = (checked, handleClick) => { ); }; -const plusCard = (checked, handleClick, showCTAButton = false) => { - const cardClassNames = ClassNames('ChoosePlanView__card plus', { - checked - }); - return ( - -
-
-
- -
-
-
-
-

Ghostery Plus

-
- -

$4.99

-

{t('per_month')}

-
-
-

{t('hub_upgrade_additional_protection')}

-
-
- - {t('ghostery_browser_hub_onboarding_private_search')} -
-
- - {t('ghostery_browser_hub_onboarding_tracker_protection')} -
-
- - {t('ghostery_browser_hub_onboarding_speedy_page_loads')} -
-
- - {t('ghostery_browser_hub_onboarding_intelligence_technology')} -
-
- - {t('ghostery_browser_hub_onboarding_ad_free')} -
-
- - {t('ghostery_browser_hub_onboarding_supports_ghosterys_mission')} -
-
-
-
- {showCTAButton && ( - // Route to next screen - - )} - - ); -}; - const premiumCard = (checked, handleClick, showCTAButton = false) => { const cardClassNames = ClassNames('ChoosePlanView__card premium', { checked @@ -274,8 +218,70 @@ class ChoosePlanView extends React.Component { return t('ghostery_browser_hub_onboarding_choose_an_option'); }; + plusCard = (checked, handleClick, showCTAButton = false) => { + const { actions } = this.props; + const { setSetupStep } = actions; + const cardClassNames = ClassNames('ChoosePlanView__card plus', { + checked + }); + return ( + +
+
+
+ +
+
+
+
+

Ghostery Plus

+
+ +

$4.99

+

{t('per_month')}

+
+
+

{t('hub_upgrade_additional_protection')}

+
+
+ + {t('ghostery_browser_hub_onboarding_private_search')} +
+
+ + {t('ghostery_browser_hub_onboarding_tracker_protection')} +
+
+ + {t('ghostery_browser_hub_onboarding_speedy_page_loads')} +
+
+ + {t('ghostery_browser_hub_onboarding_intelligence_technology')} +
+
+ + {t('ghostery_browser_hub_onboarding_ad_free')} +
+
+ + {t('ghostery_browser_hub_onboarding_supports_ghosterys_mission')} +
+
+
+
+ {showCTAButton && ( + setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING })}> + {t('ghostery_browser_hub_onboarding_keep')} + + )} + + ); + }; + render() { - const { user, didNotSelectGhosterySearch } = this.props; + const { user, didNotSelectGhosterySearch, actions } = this.props; + const { setSetupStep } = actions; const { expanded, selectedPlan } = this.state; const isBasic = !user; @@ -309,7 +315,7 @@ class ChoosePlanView extends React.Component { {(isPlus) ? (
- {plusCard(this.isPlusPlanChecked(), this.selectPlusPlan, isPlus)} + {this.plusCard(this.isPlusPlanChecked(), this.selectPlusPlan, isPlus)}
{t('ghostery_browser_hub_onboarding_or')}
@@ -323,7 +329,7 @@ class ChoosePlanView extends React.Component { )} {!isPremium && ( - {plusCard(this.isPlusPlanChecked(), this.selectPlusPlan)} + {this.plusCard(this.isPlusPlanChecked(), this.selectPlusPlan)} )} {premiumCard(this.isPremiumPlanChecked(), this.selectPremiumPlan)} @@ -332,8 +338,9 @@ class ChoosePlanView extends React.Component { {(isBasic && (
{(selectedPlan === BASIC) && ( - // Change to route to next page - + setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING })}> + {t('next')} + )} {selectedPlan === PLUS && ( {t('next')} @@ -344,8 +351,9 @@ class ChoosePlanView extends React.Component {
))} {isPremium && ( - // Change to route to next page - + setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING })}> + {t('next')} + )}
)} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx index 402d19108..397a0ab66 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx @@ -12,7 +12,11 @@ */ import { buildReduxHOC } from '../../../../shared-hub/utils'; - import ChoosePlanView from './ChoosePlanView'; +import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; + +const actionCreators = { + setSetupStep, +}; -export default buildReduxHOC(['account'], null, ChoosePlanView); +export default buildReduxHOC(['account'], actionCreators, ChoosePlanView); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx index 815e60463..c1a87cc37 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx @@ -18,13 +18,17 @@ import React from 'react'; * @return {JSX} JSX for rendering the Browser Success View of the Hub app * @memberof HubComponents */ -const SuccessView = () => ( -
-
{t('ghostery_browser_hub_onboarding_yay_youre_all_set')}
-
{`${t('ghostery_browser_hub_onboarding_start_browsing_the_web_with')} Ghostery`}
- - -
-); +const SuccessView = (props) => { + const { actions } = props; + const { sendPing } = actions; + return ( +
+
{t('ghostery_browser_hub_onboarding_yay_youre_all_set')}
+
{`${t('ghostery_browser_hub_onboarding_start_browsing_the_web_with')} Ghostery`}
+ + +
+ ); +}; export default SuccessView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js index cdcea8832..9b5cf3814 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js @@ -12,5 +12,11 @@ */ import SuccessView from './SuccessView'; +import { buildReduxHOC } from '../../../../shared-hub/utils'; +import sendPing from '../../../../shared-hub/actions/MetricsActions'; -export default SuccessView; +const actionCreators = { + sendPing +}; + +export default buildReduxHOC(null, actionCreators, SuccessView); diff --git a/app/shared-hub/utils/index.js b/app/shared-hub/utils/index.js index 693a05465..55f478196 100644 --- a/app/shared-hub/utils/index.js +++ b/app/shared-hub/utils/index.js @@ -20,6 +20,7 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; // Imports utilities from elsewhere in the codebase to reduce duplicate code import { log } from '../../../src/utils/common'; @@ -106,7 +107,7 @@ function buildReduxHOC(stateKeys, actionCreators, baseComponent) { actions: bindActionCreators(actionCreators, dispatch) }); - return connect(mapStateToProps, mapDispatchToProps)(baseComponent); + return withRouter(connect(mapStateToProps, mapDispatchToProps)(baseComponent)); } export { diff --git a/src/background.js b/src/background.js index 64c6fb206..89e7f08c4 100644 --- a/src/background.js +++ b/src/background.js @@ -557,6 +557,11 @@ function handleGhosteryHub(name, message, callback) { } else { ({ setup_step } = setup_step); } + const origin = message.origin || ''; + if (origin === 'onboarding') { + conf.setup_step = message.setup_step; + metrics.ping('gb_onboarding'); + } callback({ setup_step }); break; } diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 0a4d7c16d..3f7280635 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -184,6 +184,12 @@ class Metrics { this._sendReq(type, ['all']); break; + // Ghostery Browser Hub - Ghostery 8.5.5+ + case 'gb_onboarding': + case 'gb_onboarding_success': + this._sendReq(type, ['all']); + break; + // Uncaught Pings default: log(`metrics ping() error: ping name ${type} not found`); @@ -285,7 +291,7 @@ class Metrics { // Antitracking state this._buildQueryPair('at', conf.enable_anti_tracking ? '1' : '0') + // The deepest setup page reached by user during setup - this._buildQueryPair('ss', (conf.metrics.install_complete_all || type === 'install_complete') ? conf.setup_step.toString() : '-1') + + this._buildQueryPair('ss', Metrics._getSetupStep(type).toString()) + // The number of times the user has gone through setup this._buildQueryPair('sl', conf.setup_number.toString()) + // Type of blocking selected during setup @@ -393,6 +399,22 @@ class Metrics { return 'gbe'; } + /** + * Get the Setup step + * + * @private + * + * @return {number} The deepest setup page reached by user during setup + */ + static _getSetupStep(type) { + console.log('conf.setup_step', conf.setup_step); + if (conf.metrics.install_complete_all + || type === 'install_complete' + || type === 'gb_onboarding' + || type === 'gb_onboarding_success') return conf.setup_step; + return -1; + } + /** * Calculate days since the last daily active ping. * From 6dd38b68024c050384ab97fdc20969c009966ebf Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 4 Jan 2021 09:06:58 -0500 Subject: [PATCH 037/113] Replace search view element --- _locales/en/messages.json | 6 ++ .../ChooseDefaultSearchView.jsx | 38 ++++++++++++- .../ChooseDefaultSearchView.scss | 57 +++++++++++++++++++ .../Step3_ChooseDefaultSearchView/index.js | 8 ++- 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1846609a0..aecc3fb24 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2393,5 +2393,11 @@ }, "too_many_failed_logins_text": { "message": "Too many failed logins. Try again in one hour." + }, + "choose_your_default_search": { + "message": "Choose your default search" + }, + "pick_a_default_search_enginge": { + "message": "Pick a default engine for all your searches" } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 5c8ad8e93..6a9800c97 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -11,9 +11,41 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React from 'react'; +import React, { Component } from 'react'; +import { ONBOARDING, CHOOSE_PLAN, CHOOSE_DEFAULT_SEARCH } from '../../OnboardingView/OnboardingConstants'; -// TODO: Add setSetupStep({ setup_step: CHOOSE_DEFAULT_SEARCH, origin: WELCOME }) to next button -const ChooseDefaultSearchView = () =>

Step 3: Choose Default Search View

; +class ChooseDefaultSearchView extends Component { + handleSubmit = () => { + const { actions, history } = this.props; + const { setSetupStep } = actions; + + setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING }); + + history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); + } + + render() { + return ( +
+
{t('choose_your_default_search')}
+
{t('pick_a_default_search_engine')}
+
+
Ghostery Search
+
StartPage
+
Bing
+
Choose Your Own
+
Yahoo
+
+ +
+ ); + } +} export default ChooseDefaultSearchView; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss new file mode 100644 index 000000000..ffea2db59 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -0,0 +1,57 @@ +.ChooseSearchView__container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + font-size: 18px; + line-height: 2.33; + color: $tundora; + margin: 45px auto 83px auto; +} + +.ChooseSearchView__title { + font-size: 24px; + font-weight: 500; + line-height: 2.33; + text-align: center; +} + +.ChooseSearchView__subtitle { + margin-bottom: 47px; + text-align: center; +} + +.ChooseSearchView__optionContainer { + width: 45%; + border: 2px black; +} + +.ChooseSearchView__nextButton { + display: flex; + justify-content: center; + margin: auto; + height: 44px; + width: 162px; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + #00aef0 + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + color: #FFF; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; +} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js index 1b5f2141f..a3a19d61d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js @@ -12,5 +12,11 @@ */ import ChooseDefaultSearchView from './ChooseDefaultSearchView'; +import { buildReduxHOC } from '../../../../shared-hub/utils'; +import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; -export default ChooseDefaultSearchView; +const actionCreators = { + setSetupStep, +}; + +export default buildReduxHOC(null, actionCreators, ChooseDefaultSearchView); From 66e7b5c563221a0494bdfa98983e4d91618f22a2 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 4 Jan 2021 09:40:30 -0500 Subject: [PATCH 038/113] Iterate on search view layout --- _locales/en/messages.json | 2 +- .../ChooseDefaultSearchView.jsx | 10 ++++++++++ .../ChooseDefaultSearchView.scss | 10 ++++++++-- app/scss/hub_ghostery_browser.scss | 1 + 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index aecc3fb24..208af51b1 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2397,7 +2397,7 @@ "choose_your_default_search": { "message": "Choose your default search" }, - "pick_a_default_search_enginge": { + "pick_a_default_search_engine": { "message": "Pick a default engine for all your searches" } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 6a9800c97..6cc8e643f 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -15,6 +15,16 @@ import React, { Component } from 'react'; import { ONBOARDING, CHOOSE_PLAN, CHOOSE_DEFAULT_SEARCH } from '../../OnboardingView/OnboardingConstants'; class ChooseDefaultSearchView extends Component { + // TODO update state on user choice and send choice on submit + // constructor(props) { + // super(props); + // + // + // this.state = { + // chosenSearch: "GhosterySearch", + // }; + // } + handleSubmit = () => { const { actions, history } = this.props; const { setSetupStep } = actions; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index ffea2db59..0f0a46dd8 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -21,9 +21,15 @@ text-align: center; } +.ChooseSearchView__optionsContainer { + display: grid; + grid-template-columns: 1fr 1fr; +} + .ChooseSearchView__optionContainer { - width: 45%; - border: 2px black; + padding: 20px; + margin: 20px; + border: 2px solid black; } .ChooseSearchView__nextButton { diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss index 0bdeefacf..78abaa56e 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/hub_ghostery_browser.scss @@ -76,6 +76,7 @@ html, body, #root { @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss'; +@import '../ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; From b35a01c5336b14796e3a52f2ef1b54938bd485c1 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 4 Jan 2021 23:09:06 -0500 Subject: [PATCH 039/113] Option container design on default search select view --- .../ChooseDefaultSearchView.jsx | 48 +++++++++++++------ .../ChooseDefaultSearchView.scss | 16 ++++++- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 6cc8e643f..c16351340 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -11,19 +11,26 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; +import RadioButton from '../../../../shared-components/RadioButton'; import { ONBOARDING, CHOOSE_PLAN, CHOOSE_DEFAULT_SEARCH } from '../../OnboardingView/OnboardingConstants'; +const SEARCH_GHOSTERY = 'Ghostery'; +const SEARCH_BING = 'Bing'; +const SEARCH_YAHOO = 'Yahoo'; +const SEARCH_STARTPAGE = 'StartPage'; +const SEARCH_CUSTOM = 'Custom'; + class ChooseDefaultSearchView extends Component { - // TODO update state on user choice and send choice on submit - // constructor(props) { - // super(props); - // - // - // this.state = { - // chosenSearch: "GhosterySearch", - // }; - // } + constructor(props) { + super(props); + + this.state = { + chosenSearch: SEARCH_GHOSTERY, + }; + } + + updateSelection = newSelection => this.setState({ chosenSearch: newSelection }); handleSubmit = () => { const { actions, history } = this.props; @@ -34,17 +41,30 @@ class ChooseDefaultSearchView extends Component { history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); } + renderOptionContainer = (chosenSearch, optionName, optionDesc) => ( +
+
+ this.updateSelection(optionName)} altDesign /> +
+
+ {optionDesc} +
+
+ ); + render() { + const { chosenSearch } = this.state; + return (
{t('choose_your_default_search')}
{t('pick_a_default_search_engine')}
-
Ghostery Search
-
StartPage
-
Bing
+ {this.renderOptionContainer(chosenSearch, SEARCH_GHOSTERY, 'Ghostery Search')} + {this.renderOptionContainer(chosenSearch, SEARCH_STARTPAGE, 'StartPage')} + {this.renderOptionContainer(chosenSearch, SEARCH_BING, 'Bing')}
Choose Your Own
-
Yahoo
+ {this.renderOptionContainer(chosenSearch, SEARCH_YAHOO, 'Yahoo')}
+
+ ); + } + + renderSearchOptions = () => { const { chosenSearch } = this.state; return ( @@ -86,6 +111,14 @@ class ChooseDefaultSearchView extends Component {
); } + + render() { + const { modalActive } = this.state; + + if (modalActive) return (this.renderConfirmationModal()); + + return (this.renderSearchOptions()); + } } export default ChooseDefaultSearchView; From 13b9fbe0b5dc06aa4622eaf5e9e8259fe05fac7d Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Tue, 5 Jan 2021 08:17:51 -0500 Subject: [PATCH 042/113] Message search extension with user choice --- .../ChooseDefaultSearchView.jsx | 20 +++++++++++++++---- src/classes/Metrics.js | 3 +-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 46a32aa8f..11dc4e765 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -15,6 +15,7 @@ import React, { Component, Fragment } from 'react'; import ClassNames from 'classnames'; import RadioButton from '../../../../shared-components/RadioButton'; import { ONBOARDING, CHOOSE_PLAN, CHOOSE_DEFAULT_SEARCH } from '../../OnboardingView/OnboardingConstants'; +import { Modal } from '../../../../shared-components'; const SEARCH_GHOSTERY = 'Ghostery'; const SEARCH_BING = 'Bing'; @@ -28,6 +29,7 @@ class ChooseDefaultSearchView extends Component { this.state = { chosenSearch: SEARCH_GHOSTERY, + customSearchURL: null, modal: null, modalActive: false, }; @@ -40,12 +42,22 @@ class ChooseDefaultSearchView extends Component { triggerConfirmationModal = selection => this.setState({ modalActive: true, modal: selection }); handleSubmit = () => { + const { chosenSearch, customSearchURL } = this.state; const { actions, history } = this.props; const { setSetupStep } = actions; - setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING }); + const payload = { + type: 'setDefaultSearch', + search: chosenSearch, + customSearchURL, + }; - history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); + chrome.runtime.sendMessage('search@ghostery.com', payload, () => { + // TODO handle errors if needed + // TODO save user's search setting to redux / background if needed + setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING }); + history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); + }); } renderOptionContainer = (chosenSearch, optionName, optionDesc) => { @@ -72,7 +84,7 @@ class ChooseDefaultSearchView extends Component { const { modal } = this.state; return ( -
+
Modal of type {modal} @@ -83,7 +95,7 @@ class ChooseDefaultSearchView extends Component { > Cancel -
+
); } diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 3f7280635..3f5112d61 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -257,8 +257,7 @@ class Metrics { this._buildQueryPair('id', conf.install_date) + // Showing campaign messages (former show_cmp) this._buildQueryPair('sc', conf.show_cmp ? '1' : '0') + - // Subscription Type - this._buildQueryPair('st', Metrics._getSubscriptionType().toString()) + + // Subscription Typerics._getSubscriptionType().toString()) + // New parameters for Ghostery 8.5.2 // Subscription Interval From 0781671ccb8b31c5f2ca404b425e5b0e0ab368f7 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Tue, 5 Jan 2021 10:18:26 -0500 Subject: [PATCH 043/113] Fix typo in template. Tidy up status bar --- .../Views/OnboardingViews/StepProgressBar/StepProgressBar.scss | 3 +++ app/templates/hub.html | 2 +- manifest.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss index d5908215e..b6fcdb196 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss @@ -15,6 +15,9 @@ display: flex; justify-content: space-around; align-items: center; + max-width: 800px; + margin: auto; + padding-top: 10px; } .StepProgressBar__Step { height: 34px; diff --git a/app/templates/hub.html b/app/templates/hub.html index 92ad97ea9..04b069514 100644 --- a/app/templates/hub.html +++ b/app/templates/hub.html @@ -38,7 +38,7 @@ hubCSS.rel = "stylesheet"; hubCSS.media = "screen"; // TODO branch on actual user agent value - hubCSS.href="../../dist/css/hub_ghosterY_browser.css"; + hubCSS.href="../../dist/css/hub_ghostery_browser.css"; head.appendChild(hubCSS); diff --git a/manifest.json b/manifest.json index 3bda86366..5acc4cd8e 100644 --- a/manifest.json +++ b/manifest.json @@ -80,7 +80,7 @@ "run_at": "document_start" } ], - "content_security_policy": "script-src 'self' 'sha256-Acnr7AdCM0m5xUApOa3qWqxq0HwdT0Zy66d9fNAiaz0='; object-src 'self'", + "content_security_policy": "script-src 'self' 'sha256-iu+nxHOBLSMqKFuIk9bZIUxScKTqR9rnGxVunOSBJ5k='; object-src 'self'", "permissions": [ "webNavigation", "webRequest", From fa3b348c3cb39af169cffed825fc6b2138f389b0 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Tue, 5 Jan 2021 11:41:44 -0500 Subject: [PATCH 044/113] Implement updating search preference --- .../BlockSettingsView.scss | 2 +- .../ChooseDefaultSearchView.jsx | 24 ++++++++++++++----- .../ChooseDefaultSearchView.scss | 1 - 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss index fb5731d04..5f07941d6 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss @@ -22,7 +22,7 @@ } .BlockSettingsView_formBlock { - align-self: stretch; + width: 800px; text-align: left; } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 11dc4e765..bd85d26b7 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -30,16 +30,22 @@ class ChooseDefaultSearchView extends Component { this.state = { chosenSearch: SEARCH_GHOSTERY, customSearchURL: null, - modal: null, + searchBeingConsidered: null, modalActive: false, }; } - updateSelection = newSelection => this.setState({ chosenSearch: newSelection }); + updateSelection = () => this.setState(prevState => ( + { + chosenSearch: prevState.searchBeingConsidered, + searchBeingConsidered: null, + modalActive: false + } + )); - cancelSelection = () => this.setState({ modalActive: false, modal: null }); + cancelSelection = () => this.setState({ modalActive: false, searchBeingConsidered: null }); - triggerConfirmationModal = selection => this.setState({ modalActive: true, modal: selection }); + triggerConfirmationModal = selection => this.setState({ modalActive: true, searchBeingConsidered: selection }); handleSubmit = () => { const { chosenSearch, customSearchURL } = this.state; @@ -81,13 +87,13 @@ class ChooseDefaultSearchView extends Component { } renderConfirmationModal = () => { - const { modal } = this.state; + const { searchBeingConsidered } = this.state; return (
Modal of type - {modal} + {searchBeingConsidered}
+
); } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index 6111b480b..8a21bb3c6 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -54,7 +54,6 @@ .ChooseSearchView__nextButton { display: flex; justify-content: center; - margin: auto; height: 44px; width: 162px; padding: 7.7px 14px; From a9e9cf21852f7277fd3643847d5de22e5838cafe Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Tue, 5 Jan 2021 11:47:49 -0500 Subject: [PATCH 045/113] GH-2218: Browser Onboarding Copy Edits & Tooltips (#652) * Add tooltips for BlockSettingsView and move to shared-hub. Remove some unused translation messages * Remove unused svgs * Remove unused scss * Add messages for step 3 choose your search * Add back message --- _locales/en/messages.json | 73 +++++++++++-------- .../Step1_CreateAccountView.jsx | 15 ---- .../Step1_CreateAccountView.scss | 3 +- .../BlockSettingsView.jsx | 22 ++++-- .../Step5_SuccessView/SuccessView.jsx | 4 +- .../ghosty-letter.svg | 1 - .../ghosty-lock.svg | 1 - .../ghosty-shield-letter.svg | 1 - .../content/__tests__/OverviewTab.jsx | 2 +- .../BuildingBlocks/CliqzFeature.jsx | 2 +- .../components/BuildingBlocks/DonutGraph.jsx | 2 +- .../BuildingBlocks/GhosteryFeature.jsx | 2 +- .../components/BuildingBlocks/PauseButton.jsx | 2 +- .../BuildingBlocks/__tests__/CliqzFeature.jsx | 2 +- .../BuildingBlocks/__tests__/DonutGraph.jsx | 2 +- .../__tests__/GhosteryFeature.jsx | 2 +- app/panel/components/Header.jsx | 2 +- app/panel/components/Summary.jsx | 2 +- app/scss/hub_ghostery_browser.scss | 3 +- app/scss/panel.scss | 2 +- .../Tooltip}/Tooltip.jsx | 3 +- .../Tooltip/Tooltip.scss} | 13 ++++ app/shared-components/Tooltip/index.js | 16 ++++ src/classes/Metrics.js | 1 - 24 files changed, 107 insertions(+), 71 deletions(-) delete mode 100644 app/images/hub/browser-create-account-view/ghosty-letter.svg delete mode 100644 app/images/hub/browser-create-account-view/ghosty-lock.svg delete mode 100644 app/images/hub/browser-create-account-view/ghosty-shield-letter.svg rename app/{panel/components => shared-components/Tooltip}/Tooltip.jsx (97%) rename app/{scss/partials/_tooltip.scss => shared-components/Tooltip/Tooltip.scss} (94%) create mode 100644 app/shared-components/Tooltip/index.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e7a7cb3c8..535d66e6c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1819,7 +1819,7 @@ "message": "Ad free" }, "ghostery_browser_hub_onboarding_supports_ghosterys_mission": { - "message": "Supports Ghostery's Mission" + "message": "Supports Ghostery's mission" }, "ghostery_browser_hub_onboarding_unlimited_bandwidth": { "message": "Unlimited Bandwidth" @@ -1854,6 +1854,9 @@ "ghostery_browser_hub_onboarding_skip": { "message": "Skip" }, + "ghostery_browser_hub_onboarding_back": { + "message": "Back" + }, "ghostery_browser_hub_onboarding_we_take_your_privacy_very_seriously": { "message": "We take your privacy very seriously. Learn more" }, @@ -1861,25 +1864,7 @@ "message": "Private by design" }, "ghostery_browser_hub_onboarding_private_by_design_description": { - "message": "Privacy is incredibly important to us at Ghostery and we've taken extra care to design our products that limits the amount of personal information collected." - }, - "ghostery_browser_hub_onboarding_why_do_you_need_my_email": { - "message": "Why do you need my email?" - }, - "ghostery_browser_hub_onboarding_why_do_you_need_my_email_description": { - "message": "We've found email is the simplest way to create and secure a unique account without sharing your information. It helps sync your custom settings across your devices." - }, - "ghostery_browser_hub_onboarding_what_do_you_use_my_email_for": { - "message": "What do you use my email for?" - }, - "ghostery_browser_hub_onboarding_what_do_you_use_my_email_for_description": { - "message": "We never share or sell your personal information. Your email address is used for account management, technical support, product-related communications, and some opt-in features." - }, - "ghostery_browser_hub_onboarding_how_secure_is_": { - "message": "How secure is" - }, - "ghostery_browser_hub_onboarding_how_secure_is_ghostery_description": { - "message": "We never share or sell your personal information. Your email address is used for account management, technical support, product-related communications, and some opt-in features." + "message": "Privacy is incredibly important to us at Ghostery. That’s why we enforce a strict policy when it comes to your data. When you create an account at Ghostery, the only data we collect is your name and email. This information allows us to offer you a user experience wherein you can sync your settings across devices and hear from us about important updates. Under no circumstances do we share or sell this data with any kind of third party." }, "ghostery_browser_hub_onboarding_can_i_remove_my_account": { "message": "Can I remove my account?" @@ -1896,11 +1881,11 @@ "ghostery_browser_hub_onboarding_you_are_signed_in_as": { "message": "You are signed in as" }, - "ghostery_browser_hub_onboarding_yay_youre_all_set": { - "message": "Yay! You're all set." + "ghostery_browser_hub_onboarding_youve_successfully_set_up_your_browser": { + "message": "You've successfully set up your browser!" }, - "ghostery_browser_hub_onboarding_start_browsing_the_web_with": { - "message": "Start browsing the web with" + "ghostery_browser_hub_onboarding_surf_with_ease": { + "message": "Now you can surf with the greatest of ease." }, "ghostery_browser_hub_onboarding_lets_search": { "message": "Let's search" @@ -1935,16 +1920,46 @@ "ghostery_browser_hub_onboarding_question_smart_browsing": { "message": "Do you want to turn on smart-browsing?" }, - "ghostery_browser_hub_info_blocking_all": { + "ghostery_browser_hub_onboarding_info_blocking_all": { "message": "Blocking \"all trackers\" may cause some websites to break." }, - "ghostery_browser_hub_info_anti_tracking": { - "message": "Anti-tracking anonymizes uniquely identifiable data that trackers try to collect" + "ghostery_browser_hub_onboarding_info_anti_tracking": { + "message": "Anti-tracking anonymizes uniquely identifiable data that trackers try to collect." }, - "ghostery_browser_hub_info_smart_browsing": { + "ghostery_browser_hub_onboarding_info_smart_browsing": { "message": "Smart-browsing adjusts your blocking settings to decrease page breakage and accelerate page loads." }, - "ghostery_browser_hub_toast_error": { + "ghostery_browser_hub__onboarding_choose_your_default_search": { + "message": "Choose your default search" + }, + "ghostery_browser_hub_onboarding_pick_a_default_search_engine": { + "message": "Pick a default search engine for all your searches." + }, + "ghostery_browser_hub_onboarding_ad_free_private_search": { + "message": "Ad-free private search" + }, + "ghostery_browser_hub_onboarding_ad_supported_private_search": { + "message": "Ad-supported private search" + }, + "ghostery_browser_hub_onboarding_type_in_search_domain": { + "message": "Type in search domain" + }, + "ghostery_browser_hub_onboarding_just_so_you_know": { + "message": "Just so you know:" + }, + "ghostery_browser_hub_onboarding_search_engine_will_log_your_data": { + "message": "search engine will log your data and use it to serve you targeted ads." + }, + "ghostery_browser_hub_onboarding_go_back": { + "message": "Go Back" + }, + "ghostery_browser_hub_onboarding_confirm": { + "message": "Confirm" + }, + "ghostery_browser_hub_onboarding_other": { + "message": "Other" + }, + "ghostery_browser_hub_onboarding_toast_error": { "message": "Please answer all questions" }, "enable_when_paused": { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx index 0451de371..13400c4e8 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx @@ -29,21 +29,6 @@ const faqList = [ label: t('ghostery_browser_hub_onboarding_private_by_design'), description: t('ghostery_browser_hub_onboarding_private_by_design_description'), }, - { - icon: 'ghosty-letter.svg', - label: t('ghostery_browser_hub_onboarding_why_do_you_need_my_email'), - description: t('ghostery_browser_hub_onboarding_why_do_you_need_my_email_description'), - }, - { - icon: 'ghosty-shield-letter.svg', - label: t('ghostery_browser_hub_onboarding_what_do_you_use_my_email_for'), - description: t('ghostery_browser_hub_onboarding_what_do_you_use_my_email_for_description'), - }, - { - icon: 'ghosty-lock.svg', - label: `${t('ghostery_browser_hub_onboarding_how_secure_is_')} Ghostery?`, - description: t('ghostery_browser_hub_onboarding_how_secure_is_ghostery_description'), - }, { icon: 'ghosty-box.svg', label: t('ghostery_browser_hub_onboarding_can_i_remove_my_account'), diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss index b61778e86..93fabacfd 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss @@ -158,7 +158,8 @@ $color-create-account-form-error-red: #e74055; cursor: pointer; @include breakpoint(small down) { margin: 39px auto 0 auto; - text-align: center; + display: flex; + justify-content: center; float: none; } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index b281816db..51bed3a5a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -13,7 +13,7 @@ import React, { Component } from 'react'; -// import Tooltip from '../../../../panel/components/Tooltip'; +import Tooltip from '../../../../shared-components/Tooltip'; import RadioButton from '../../../../shared-components/RadioButton/RadioButton'; import ToggleCheckbox from '../../../../shared-components/ToggleCheckbox/ToggleCheckbox'; import { CHOOSE_DEFAULT_SEARCH, ONBOARDING } from '../../OnboardingView/OnboardingConstants'; @@ -137,7 +137,9 @@ class BlockSettingsView extends Component {
  • {t('ghostery_browser_hub_onboarding_question_kinds_of_trackers')} -
    +
    + +
  • @@ -159,8 +161,12 @@ class BlockSettingsView extends Component {
    {t('ghostery_browser_hub_onboarding_kinds_of_trackers_none')}
  • - {t('ghostery_browser_hub_onboarding_question_anti_tracking')} -
    +
    + {t('ghostery_browser_hub_onboarding_question_anti_tracking')} +
    + +
    +
  • @@ -175,8 +181,12 @@ class BlockSettingsView extends Component {
    {t('hub_setup_modal_button_no')}
  • - {t('ghostery_browser_hub_onboarding_question_smart_browsing')} -
    +
    + {t('ghostery_browser_hub_onboarding_question_smart_browsing')} +
    + +
    +
  • diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx index c1a87cc37..aaedb4b67 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx @@ -23,8 +23,8 @@ const SuccessView = (props) => { const { sendPing } = actions; return (
    -
    {t('ghostery_browser_hub_onboarding_yay_youre_all_set')}
    -
    {`${t('ghostery_browser_hub_onboarding_start_browsing_the_web_with')} Ghostery`}
    +
    {t('ghostery_browser_hub_onboarding_youve_successfully_set_up_your_browser')}
    +
    {`${t('ghostery_browser_hub_onboarding_surf_with_ease')}`}
    diff --git a/app/images/hub/browser-create-account-view/ghosty-letter.svg b/app/images/hub/browser-create-account-view/ghosty-letter.svg deleted file mode 100644 index 2921eee09..000000000 --- a/app/images/hub/browser-create-account-view/ghosty-letter.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/images/hub/browser-create-account-view/ghosty-lock.svg b/app/images/hub/browser-create-account-view/ghosty-lock.svg deleted file mode 100644 index 03f9465db..000000000 --- a/app/images/hub/browser-create-account-view/ghosty-lock.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/images/hub/browser-create-account-view/ghosty-shield-letter.svg b/app/images/hub/browser-create-account-view/ghosty-shield-letter.svg deleted file mode 100644 index 2fb520cf9..000000000 --- a/app/images/hub/browser-create-account-view/ghosty-shield-letter.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/panel-android/components/content/__tests__/OverviewTab.jsx b/app/panel-android/components/content/__tests__/OverviewTab.jsx index 4fec3f938..411a9d52a 100644 --- a/app/panel-android/components/content/__tests__/OverviewTab.jsx +++ b/app/panel-android/components/content/__tests__/OverviewTab.jsx @@ -16,7 +16,7 @@ import renderer from 'react-test-renderer'; import { mount } from 'enzyme'; import OverviewTab from '../OverviewTab'; -jest.mock('../../../../panel/components/Tooltip'); +jest.mock('../../../../shared-components/Tooltip'); describe('app/panel-android/components/content/OverviewTab.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { diff --git a/app/panel/components/BuildingBlocks/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/CliqzFeature.jsx index 3abc03dc9..ca3053c88 100644 --- a/app/panel/components/BuildingBlocks/CliqzFeature.jsx +++ b/app/panel/components/BuildingBlocks/CliqzFeature.jsx @@ -14,7 +14,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; -import Tooltip from '../Tooltip'; +import Tooltip from '../../../shared-components/Tooltip'; /** * @class Implements rendering and interaction for Cliqz feature icon toggles diff --git a/app/panel/components/BuildingBlocks/DonutGraph.jsx b/app/panel/components/BuildingBlocks/DonutGraph.jsx index defd5607f..0bc49cfb4 100644 --- a/app/panel/components/BuildingBlocks/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/DonutGraph.jsx @@ -23,7 +23,7 @@ import { scaleLinear, select } from 'd3'; -import Tooltip from '../Tooltip'; +import Tooltip from '../../../shared-components/Tooltip'; /** * @class Generate donut graph. Used to display tracker data in the Summary View. diff --git a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx index d66e943bd..679f5bae3 100644 --- a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx +++ b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx @@ -14,7 +14,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; -import Tooltip from '../Tooltip'; +import Tooltip from '../../../shared-components/Tooltip'; import globals from '../../../../src/classes/Globals'; const { BLACKLISTED, WHITELISTED } = globals; diff --git a/app/panel/components/BuildingBlocks/PauseButton.jsx b/app/panel/components/BuildingBlocks/PauseButton.jsx index d7ab0e287..f02f8bdf2 100644 --- a/app/panel/components/BuildingBlocks/PauseButton.jsx +++ b/app/panel/components/BuildingBlocks/PauseButton.jsx @@ -14,7 +14,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; -import Tooltip from '../Tooltip'; +import Tooltip from '../../../shared-components/Tooltip'; /** * @class Implements the Pause button on the Summary view. diff --git a/app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx index cba12eac1..e2503a56e 100644 --- a/app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx @@ -22,7 +22,7 @@ global.t = function(str) { }; // Fake the Tooltip implementation -jest.mock('../../Tooltip'); +jest.mock('../../../../shared-components/Tooltip'); describe('app/panel/components/CliqzFeature.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { diff --git a/app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx b/app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx index 0968d0058..80891ef8e 100644 --- a/app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx @@ -22,7 +22,7 @@ global.t = function(str) { }; // Fake the Tooltip implementation -jest.mock('../../Tooltip'); +jest.mock('../../../../shared-components/Tooltip'); describe('app/panel/components/DonutGraph.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { diff --git a/app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx b/app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx index e8516f147..30b96e109 100644 --- a/app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx @@ -22,7 +22,7 @@ global.t = function(str) { }; // Fake the Tooltip implementation -jest.mock('../../Tooltip'); +jest.mock('../../../../shared-components/Tooltip'); describe('app/panel/components/GhosteryFeature.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { diff --git a/app/panel/components/Header.jsx b/app/panel/components/Header.jsx index f20bb7ac5..81bc35bf3 100644 --- a/app/panel/components/Header.jsx +++ b/app/panel/components/Header.jsx @@ -15,7 +15,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { ReactSVG } from 'react-svg'; import ClassNames from 'classnames'; -import Tooltip from './Tooltip'; +import Tooltip from '../../shared-components/Tooltip'; import HeaderMenu from './HeaderMenu'; import { sendMessage, sendMessageInPromise } from '../utils/msg'; import { log } from '../../../src/utils/common'; diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index f89f64d0e..bd56b9018 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -14,7 +14,7 @@ import React from 'react'; import { ReactSVG } from 'react-svg'; import ClassNames from 'classnames'; -import Tooltip from './Tooltip'; +import Tooltip from '../../shared-components/Tooltip'; import DynamicUIPortContext from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import globals from '../../../src/classes/Globals'; diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss index 78abaa56e..6d641e247 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/hub_ghostery_browser.scss @@ -83,5 +83,4 @@ html, body, #root { // Imports from ../shared-components directory @import '../shared-components/ToastMessage/ToastMessage.scss'; @import '../shared-components/ToggleCheckbox/ToggleCheckbox.scss'; - - +@import '../shared-components/Tooltip/Tooltip.scss'; diff --git a/app/scss/panel.scss b/app/scss/panel.scss index 3e6585680..839e8ff3e 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -69,7 +69,6 @@ html body { @import './partials/_donut_graph'; @import './partials/_ghostery_feature'; @import './partials/_cliqz_feature'; -@import './partials/_tooltip'; @import './partials/_not_scanned'; @import './partials/_subscribe'; @import './partials/_stats'; @@ -83,3 +82,4 @@ html body { @import '../shared-components/ModalContent/PlusPromoModalContent/PlusPromoModalContent.scss'; @import '../shared-components/ModalContent/InsightsPromoModalContent/InsightsPromoModalContent.scss'; @import '../shared-components/ForgotPassword/ForgotPassword.scss'; +@import '../shared-components/Tooltip/Tooltip.scss'; diff --git a/app/panel/components/Tooltip.jsx b/app/shared-components/Tooltip/Tooltip.jsx similarity index 97% rename from app/panel/components/Tooltip.jsx rename to app/shared-components/Tooltip/Tooltip.jsx index 8e7ac4255..028edf59b 100644 --- a/app/panel/components/Tooltip.jsx +++ b/app/shared-components/Tooltip/Tooltip.jsx @@ -87,11 +87,12 @@ class Tooltip extends React.Component { */ render() { const { - theme, position, header, body + theme, position, header, body, isOnboardingHub } = this.props; const { show } = this.state; const compClassNames = ClassNames({ 'dark-theme': theme === 'dark', + onboarding: isOnboardingHub }); return ( diff --git a/app/scss/partials/_tooltip.scss b/app/shared-components/Tooltip/Tooltip.scss similarity index 94% rename from app/scss/partials/_tooltip.scss rename to app/shared-components/Tooltip/Tooltip.scss index 9ae6d4711..13a8d1f64 100644 --- a/app/scss/partials/_tooltip.scss +++ b/app/shared-components/Tooltip/Tooltip.scss @@ -108,6 +108,19 @@ line-height: 1.5; } } + .onboarding .tooltip-content { + color: $white; + background-color: $black; + font-size: 14px; + opacity: .96; + border-radius: 4px; + &:after { + border-color: transparent transparent $black $black; + } + .tooltip-header { + font-weight: 500; + } + } .dark-theme .tooltip-content { background-color: #333333; color: #ffffff; diff --git a/app/shared-components/Tooltip/index.js b/app/shared-components/Tooltip/index.js new file mode 100644 index 000000000..0a5a9a3ae --- /dev/null +++ b/app/shared-components/Tooltip/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Tooltip + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import Tooltip from './Tooltip'; + +export default Tooltip; diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 3f5112d61..69cf8ab9d 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -406,7 +406,6 @@ class Metrics { * @return {number} The deepest setup page reached by user during setup */ static _getSetupStep(type) { - console.log('conf.setup_step', conf.setup_step); if (conf.metrics.install_complete_all || type === 'install_complete' || type === 'gb_onboarding' From bcf1a34527bac1c8bbb23497fd5609f092049c65 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Thu, 7 Jan 2021 10:36:04 -0500 Subject: [PATCH 046/113] There was a coup attempt at The Capitol yesterday --- .../ChooseDefaultSearchView.jsx | 41 +++++++++++-------- .../ChooseDefaultSearchView.scss | 11 +++++ app/scss/hub_ghostery_browser.scss | 1 + 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index bd85d26b7..3ac9e8a58 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -91,22 +91,24 @@ class ChooseDefaultSearchView extends Component { return ( -
    - Modal of type - {searchBeingConsidered} +
    +
    + Modal of type + {searchBeingConsidered} +
    + +
    - - ); } @@ -139,9 +141,12 @@ class ChooseDefaultSearchView extends Component { render() { const { modalActive } = this.state; - if (modalActive) return (this.renderConfirmationModal()); - - return (this.renderSearchOptions()); + return ( +
    + {modalActive && this.renderConfirmationModal()} + {!modalActive && this.renderSearchOptions()} +
    + ); } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index 8a21bb3c6..7ec3e9c7b 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -79,3 +79,14 @@ line-height: 2.05; cursor: pointer; } + +.ChooseSearchViewModal__content { + position: relative; + width: 901px; + min-height: 632px; + padding-top: 45px; + color: #4a4a4a; + background-color: #f7f7f7; + border: 1px solid $tundora; + z-index: 10; +} diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss index 6d641e247..19e17a59d 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/hub_ghostery_browser.scss @@ -82,5 +82,6 @@ html, body, #root { // Imports from ../shared-components directory @import '../shared-components/ToastMessage/ToastMessage.scss'; +@import '../shared-components/Modal/Modal.scss'; @import '../shared-components/ToggleCheckbox/ToggleCheckbox.scss'; @import '../shared-components/Tooltip/Tooltip.scss'; From c05c7ccfe7c7dfdff5432a82e6b819d47bb24cc4 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Thu, 7 Jan 2021 10:38:31 -0500 Subject: [PATCH 047/113] GH-2218: 8.5.5 Translations (#654) * Add tooltips for BlockSettingsView and move to shared-hub. Remove some unused translation messages * Remove unused svgs * Remove unused scss * Add messages for step 3 choose your search * Add back message * Change CTA button string --- _locales/en/messages.json | 4 ++-- .../Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 535d66e6c..1a34d8c4e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1887,8 +1887,8 @@ "ghostery_browser_hub_onboarding_surf_with_ease": { "message": "Now you can surf with the greatest of ease." }, - "ghostery_browser_hub_onboarding_lets_search": { - "message": "Let's search" + "ghostery_browser_hub_onboarding_start_browsing": { + "message": "Start Browsing" }, "ghostery_browser_hub_onboarding_which_privacy_plan": { "message": "Which privacy plan is right for you?" diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx index aaedb4b67..addf3bea4 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx @@ -26,7 +26,7 @@ const SuccessView = (props) => {
    {t('ghostery_browser_hub_onboarding_youve_successfully_set_up_your_browser')}
    {`${t('ghostery_browser_hub_onboarding_surf_with_ease')}`}
    - +
    ); }; From 9100e95f22d0dc565ec2648d8db8b1dd97311c71 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Thu, 7 Jan 2021 10:41:21 -0500 Subject: [PATCH 048/113] Ghostery Dawn Intro Hub Miscellaneous Fixes (#653) * Fix the StepProgressBar width for each screen, step4 needs to be a little bigger * Add back button to all pages * Add margin top to Step Progress Bar * Add back button for success view * Refactor back button for all pages * Log the user out if they are returning to step 1 * Add logout behavior to navbar * Add highestSetupStepReached() method to next buttons * Trying to add setup_step to SetupLifeCycleReducer * Revert "Trying to add setup_step to SetupLifeCycleReducer" This reverts commit fd2fb31b591ceca1e727d391170cf1fd2bc1baf4. * Revert "Add highestSetupStepReached() method to next buttons" This reverts commit b7cdd1e975ad127ec1886ecef39dba269c525392. * Prevent user from moving forwards in the StepProgress bar, only back * Add back console log * Remove test class * Fix tooltips * Get rid of margin-left on all screens. Fix success view strings. Center CTA button on BlockSettings * Fix back button on small screen sizes for SuccessView * Bold current step label * Fix ChoosePlanView price font sizes Co-authored-by: Ilya Zarembsky --- .../Views/OnboardingView/OnboardingView.jsx | 3 +- .../Views/OnboardingView/OnboardingView.scss | 9 + .../Step1_CreateAccountForm.jsx | 12 +- .../Step1_CreateAccountView.jsx | 10 +- .../Step1_LogInForm/Step1_LogInForm.jsx | 6 +- .../BlockSettingsView.jsx | 203 ++++++++++-------- .../BlockSettingsView.scss | 32 ++- .../Step2_BlockSettingsView/index.js | 2 + .../ChooseDefaultSearchView.jsx | 45 ++-- .../ChooseDefaultSearchView.scss | 33 +++ .../Step4_ChoosePlanView/ChoosePlanView.jsx | 129 ++++++----- .../Step4_ChoosePlanView/ChoosePlanView.scss | 30 +++ .../Step5_SuccessView/SuccessView.jsx | 26 ++- .../Step5_SuccessView/SuccessView.scss | 30 +++ .../StepProgressBar/StepProgressBar.jsx | 17 +- .../StepProgressBar/StepProgressBar.scss | 4 +- .../OnboardingViews/StepProgressBar/index.js | 8 +- app/scss/hub_ghostery_browser.scss | 4 +- 18 files changed, 401 insertions(+), 202 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.scss diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx index bb224f6fe..2a3ee0c9d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx @@ -25,7 +25,6 @@ import StepNavigator from '../OnboardingViews/StepNavigator'; */ const OnboardingView = (props) => { const { sendMountActions, steps } = props; - console.log('in OnboardingView'); return ( @@ -36,7 +35,7 @@ const OnboardingView = (props) => { key={`route-${step.index}`} path={step.path} render={() => ( -
    +
    diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.scss b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.scss new file mode 100644 index 000000000..e05ccd34f --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.scss @@ -0,0 +1,9 @@ +.OnboardingView__screenContainer { + max-width: 724px; + margin: 0 auto; + padding: 0 20px; + + &.step4 { + max-width: 910px; + } +} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx index 5cfdfdc89..33c96999c 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx @@ -61,7 +61,7 @@ const Step1_CreateAccountForm = (props) => { return (
    -
    +
    @@ -84,7 +84,7 @@ const Step1_CreateAccountForm = (props) => {
    )}
    -
    +
    @@ -108,7 +108,7 @@ const Step1_CreateAccountForm = (props) => {
    -
    +
    @@ -123,7 +123,7 @@ const Step1_CreateAccountForm = (props) => { autoComplete="off" />
    -
    +
    @@ -140,7 +140,7 @@ const Step1_CreateAccountForm = (props) => {
    -
    +
    @@ -169,7 +169,7 @@ const Step1_CreateAccountForm = (props) => {
    )}
    -
    +
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx index 13400c4e8..1f8044c43 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx @@ -78,8 +78,8 @@ const Step1_CreateAccountView = (props) => { const renderSkipLink = () => (
    -
    -
    +
    +
    setSetupStep({ setup_step: LOGIN, origin: ONBOARDING })}> {t('ghostery_browser_hub_onboarding_skip')} @@ -108,12 +108,12 @@ const Step1_CreateAccountView = (props) => {
    { t('ghostery_browser_hub_onboarding_sync_settings') }
    {view === CREATE_ACCOUNT && ( -
    setView(SIGN_IN)}>{t('ghostery_browser_hub_onboarding_already_have_account')}
    +
    setView(SIGN_IN)}>{t('ghostery_browser_hub_onboarding_already_have_account')}
    )} {view === SIGN_IN && ( -
    setView(CREATE_ACCOUNT)}>{t('ghostery_browser_hub_onboarding_create_an_account')}
    +
    setView(CREATE_ACCOUNT)}>{t('ghostery_browser_hub_onboarding_create_an_account')}
    )} -
    +
    {view === CREATE_ACCOUNT ? ( diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx index 0f70b5e26..26c86f4f2 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx @@ -41,7 +41,7 @@ const Step1_LogInForm = (props) => { return (
    -
    +
    @@ -64,7 +64,7 @@ const Step1_LogInForm = (props) => {
    -
    +
    @@ -85,7 +85,7 @@ const Step1_LogInForm = (props) => {
    -
    +
    {t('forgot_password')} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index 51bed3a5a..6e74690cb 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -11,7 +11,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React, { Component } from 'react'; +import React, { Fragment, Component } from 'react'; +import { NavLink } from 'react-router-dom'; import Tooltip from '../../../../shared-components/Tooltip'; import RadioButton from '../../../../shared-components/RadioButton/RadioButton'; @@ -107,109 +108,121 @@ class BlockSettingsView extends Component { const { recommendedChoices, blockAds, kindsOfTrackers, antiTracking, smartBrowsing } = this.state; + const { actions } = this.props; + const { logout } = actions; return ( -
    -
    {t('ghostery_browser_hub_onboarding_which_privacy_plan')}
    -
    {t('ghostery_browser_hub_onboarding_tell_us_your_preferences')}
    -
    -
    - this.toggleRecommendedChoices(!recommendedChoices)} - /> -
    {t('ghostery_browser_hub_onboarding_recommended_choices')}
    + +
    +
    + + logout()}> + {t('ghostery_browser_hub_onboarding_back')} +
    -
      -
    1. {t('ghostery_browser_hub_onboarding_question_block_ads')}
    2. -
      -
      - this.handleAnswerChange('blockAds', true)} altDesign /> -
      -
      {t('hub_setup_modal_button_yes')}
      -
      -
      -
      - this.handleAnswerChange('blockAds', false)} altDesign /> -
      -
      {t('hub_setup_modal_button_no')}
      +
      +
      +
      {t('ghostery_browser_hub_onboarding_which_privacy_plan')}
      +
      {t('ghostery_browser_hub_onboarding_tell_us_your_preferences')}
      +
      +
      + this.toggleRecommendedChoices(!recommendedChoices)} + /> +
      {t('ghostery_browser_hub_onboarding_recommended_choices')}
      -
    3. -
      - {t('ghostery_browser_hub_onboarding_question_kinds_of_trackers')} -
      - +
        +
      1. {t('ghostery_browser_hub_onboarding_question_block_ads')}
      2. +
        +
        + this.handleAnswerChange('blockAds', true)} altDesign />
        +
        {t('hub_setup_modal_button_yes')}
        - -
        -
        - this.handleAnswerChange('kindsOfTrackers', 0)} altDesign /> -
        -
        {t('ghostery_browser_hub_onboarding_kinds_of_trackers_all')}
        -
        -
        -
        - this.handleAnswerChange('kindsOfTrackers', 1)} altDesign /> -
        -
        {t('ghostery_browser_hub_onboarding_kinds_of_trackers_ad_and_analytics')}
        -
        -
        -
        - this.handleAnswerChange('kindsOfTrackers', 2)} altDesign /> -
        -
        {t('ghostery_browser_hub_onboarding_kinds_of_trackers_none')}
        -
        -
      3. -
        - {t('ghostery_browser_hub_onboarding_question_anti_tracking')} -
        - +
        +
        + this.handleAnswerChange('blockAds', false)} altDesign />
        +
        {t('hub_setup_modal_button_no')}
        -
      4. -
        -
        - this.handleAnswerChange('antiTracking', true)} altDesign /> -
        -
        {t('hub_setup_modal_button_yes')}
        -
        -
        -
        - this.handleAnswerChange('antiTracking', false)} altDesign /> -
        -
        {t('hub_setup_modal_button_no')}
        -
        -
      5. -
        - {t('ghostery_browser_hub_onboarding_question_smart_browsing')} -
        - +
      6. +
        + {t('ghostery_browser_hub_onboarding_question_kinds_of_trackers')} +
        + +
        -
      -
    4. -
      -
      - this.handleAnswerChange('smartBrowsing', true)} altDesign /> -
      -
      {t('hub_setup_modal_button_yes')}
      -
      -
      -
      - this.handleAnswerChange('smartBrowsing', false)} altDesign /> -
      -
      {t('hub_setup_modal_button_no')}
      -
      -
    +
    +
    + this.handleAnswerChange('kindsOfTrackers', 0)} altDesign /> +
    +
    {t('ghostery_browser_hub_onboarding_kinds_of_trackers_all')}
    +
    +
    +
    + this.handleAnswerChange('kindsOfTrackers', 1)} altDesign /> +
    +
    {t('ghostery_browser_hub_onboarding_kinds_of_trackers_ad_and_analytics')}
    +
    +
    +
    + this.handleAnswerChange('kindsOfTrackers', 2)} altDesign /> +
    +
    {t('ghostery_browser_hub_onboarding_kinds_of_trackers_none')}
    +
    +
  • +
    + {t('ghostery_browser_hub_onboarding_question_anti_tracking')} +
    + +
    +
    +
  • +
    +
    + this.handleAnswerChange('antiTracking', true)} altDesign /> +
    +
    {t('hub_setup_modal_button_yes')}
    +
    +
    +
    + this.handleAnswerChange('antiTracking', false)} altDesign /> +
    +
    {t('hub_setup_modal_button_no')}
    +
    +
  • +
    + {t('ghostery_browser_hub_onboarding_question_smart_browsing')} +
    + +
    +
    +
  • +
    +
    + this.handleAnswerChange('smartBrowsing', true)} altDesign /> +
    +
    {t('hub_setup_modal_button_yes')}
    +
    +
    +
    + this.handleAnswerChange('smartBrowsing', false)} altDesign /> +
    +
    {t('hub_setup_modal_button_no')}
    +
    + + +
    +
    - -
    + ); } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss index 5f07941d6..85b35e75c 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss @@ -1,7 +1,5 @@ .BlockSettingsView__container { display: flex; - justify-content: center; - align-items: center; flex-direction: column; font-size: 18px; line-height: 2.33; @@ -9,6 +7,36 @@ margin: 45px auto 83px auto; } +.BlockSettingsView__relativeContainer { + position: relative; +} + +.BlockSettingsView__caret.left { + margin: 7px auto 0 auto; + height: 10px; + width: 10px; + border-left: 2px solid $tundora; + border-top: 2px solid $tundora; + cursor: pointer; + transform: rotate(-45deg); +} + +.BlockSettingsView__backContainer { + position: absolute; + display: flex; + width: 68px; + margin-top: 60px; + @include breakpoint(small down) { + margin-top: 22px; + } + .BlockSettingsView__back { + margin-top: 8px; + font-size: 16px; + color: $tundora; + text-decoration: underline; + } +} + .BlockSettingsView__title { font-size: 24px; font-weight: 500; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js index 12c2c9950..9394bfdc0 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js @@ -13,12 +13,14 @@ import BlockSettingsView from './BlockSettingsView'; import { buildReduxHOC } from '../../../../shared-hub/utils'; +import { logout } from '../../../../Account/AccountActions'; import { setAntiTracking, setAdBlock, setSmartBlocking } from '../../../../shared-hub/actions/AntiSuiteActions'; import setBlockingPolicy from '../../../../shared-hub/actions/BlockingPolicyActions'; import setToast from '../../../../shared-hub/actions/ToastActions'; import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; const actionCreators = { + logout, setAntiTracking, setAdBlock, setSmartBlocking, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 3ac9e8a58..bbda9943a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -12,6 +12,7 @@ */ import React, { Component, Fragment } from 'react'; +import { NavLink } from 'react-router-dom'; import ClassNames from 'classnames'; import RadioButton from '../../../../shared-components/RadioButton'; import { ONBOARDING, CHOOSE_PLAN, CHOOSE_DEFAULT_SEARCH } from '../../OnboardingView/OnboardingConstants'; @@ -117,24 +118,34 @@ class ChooseDefaultSearchView extends Component { const { chosenSearch } = this.state; return ( -
    -
    {t('choose_your_default_search')}
    -
    {t('pick_a_default_search_engine')}
    -
    - {this.renderOptionContainer(chosenSearch, SEARCH_GHOSTERY, 'Ghostery Search')} - {this.renderOptionContainer(chosenSearch, SEARCH_STARTPAGE, 'StartPage')} - {this.renderOptionContainer(chosenSearch, SEARCH_BING, 'Bing')} -
    Choose Your Own
    - {this.renderOptionContainer(chosenSearch, SEARCH_YAHOO, 'Yahoo')} + +
    +
    + + + {t('ghostery_browser_hub_onboarding_back')} + +
    - -
    +
    +
    {t('choose_your_default_search')}
    +
    {t('pick_a_default_search_engine')}
    +
    + {this.renderOptionContainer(chosenSearch, SEARCH_GHOSTERY, 'Ghostery Search')} + {this.renderOptionContainer(chosenSearch, SEARCH_STARTPAGE, 'StartPage')} + {this.renderOptionContainer(chosenSearch, SEARCH_BING, 'Bing')} +
    Choose Your Own
    + {this.renderOptionContainer(chosenSearch, SEARCH_YAHOO, 'Yahoo')} +
    + +
    + ); } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index 7ec3e9c7b..28f207bfa 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -9,6 +9,39 @@ margin: 45px auto 83px auto; } +.ChooseSearchView__relativeContainer { + position: relative; + @include breakpoint(small down) { + margin-left: 0; + } +} +.ChooseSearchView__caret.left { + margin: 7px auto 0 auto; + height: 10px; + width: 10px; + border-left: 2px solid $tundora; + border-top: 2px solid $tundora; + cursor: pointer; + transform: rotate(-45deg); +} + +.ChooseSearchView__backContainer { + position: absolute; + display: flex; + width: 68px; + margin-top: 61px; + @include breakpoint(small down) { + margin-top: 20px; + } + .ChooseSearchView__back { + margin-top: 8px; + font-size: 16px; + color: $tundora; + text-decoration: underline; + } +} + + .ChooseSearchView__title { font-size: 24px; font-weight: 500; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 93960f467..71ac9b443 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -92,7 +92,7 @@ const premiumCard = (checked, handleClick, showCTAButton = false) => {

    Ghostery Premium

    -

    $11.99

    +

    $11.99

    {t('per_month')}

    @@ -294,70 +294,81 @@ class ChoosePlanView extends React.Component { }); return ( -
    -
    {this.renderTitleText()}
    -
    {this.renderSubtitleText(didNotSelectGhosterySearch)}
    - {didNotSelectGhosterySearch && isBasic && ( - - {searchPromo()} - {/* TODO: For the CTA button below, + +
    +
    + + + {t('ghostery_browser_hub_onboarding_back')} + +
    +
    +
    + +
    {this.renderTitleText()}
    +
    {this.renderSubtitleText(didNotSelectGhosterySearch)}
    + {didNotSelectGhosterySearch && isBasic && ( + + {searchPromo()} + {/* TODO: For the CTA button below, 1. If user is signed in, activate the user’s 7-day free trial for the Ghostery Search Plus plan and move them to Step 5 if signed in 2. If user is signed out, clicking this should take them to Step 4b (linked) */} -
    {t('ghostery_browser_hub_onboarding_start_trial')}
    -
    {t('ghostery_browser_hub_onboarding_see_all_plans')}
    -
    - - )} - {((isBasic && !didNotSelectGhosterySearch) || expanded || isPlus || isPremium) && ( -
    - {(isPlus) ? ( -
    -
    - {this.plusCard(this.isPlusPlanChecked(), this.selectPlusPlan, isPlus)} +
    {t('ghostery_browser_hub_onboarding_start_trial')}
    +
    {t('ghostery_browser_hub_onboarding_see_all_plans')}
    +
    + + )} + {((isBasic && !didNotSelectGhosterySearch) || expanded || isPlus || isPremium) && ( +
    + {(isPlus) ? ( +
    +
    + {this.plusCard(this.isPlusPlanChecked(), this.selectPlusPlan, isPlus)} +
    +
    {t('ghostery_browser_hub_onboarding_or')}
    +
    + {premiumCard(this.isPremiumPlanChecked(), this.selectPremiumPlan, isPlus)} +
    -
    {t('ghostery_browser_hub_onboarding_or')}
    -
    - {premiumCard(this.isPremiumPlanChecked(), this.selectPremiumPlan, isPlus)} + ) : ( +
    + {isBasic && ( + basicCard(this.isBasicPlanChecked(), this.selectBasicPlan) + )} + {!isPremium && ( + + {this.plusCard(this.isPlusPlanChecked(), this.selectPlusPlan)} + + )} + {premiumCard(this.isPremiumPlanChecked(), this.selectPremiumPlan)}
    -
    - ) : ( -
    - {isBasic && ( - basicCard(this.isBasicPlanChecked(), this.selectBasicPlan) - )} - {!isPremium && ( - - {this.plusCard(this.isPlusPlanChecked(), this.selectPlusPlan)} - - )} - {premiumCard(this.isPremiumPlanChecked(), this.selectPremiumPlan)} -
    - )} - {(isBasic && ( -
    - {(selectedPlan === BASIC) && ( - setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING })}> - {t('next')} - - )} - {selectedPlan === PLUS && ( - {t('next')} - )} - {selectedPlan === PREMIUM && ( - {t('next')} - )} -
    - ))} - {isPremium && ( - setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING })}> - {t('next')} - - )} -
    - )} -
    + )} + {(isBasic && ( +
    + {(selectedPlan === BASIC) && ( + setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING })}> + {t('next')} + + )} + {selectedPlan === PLUS && ( + {t('next')} + )} + {selectedPlan === PREMIUM && ( + {t('next')} + )} +
    + ))} + {isPremium && ( + setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING })}> + {t('next')} + + )} +
    + )} +
    + ); } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss index 034fa539f..31312470f 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss @@ -1,8 +1,36 @@ $medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow to next line .ChoosePlanView { + margin: 45px auto 0 auto; padding-bottom: 20px; } +.ChoosePlanView__relativeContainer { + position: relative; +} +.ChoosePlanView__caret.left { + margin: 7px auto 0 auto; + height: 10px; + width: 10px; + border-left: 2px solid $tundora; + border-top: 2px solid $tundora; + cursor: pointer; + transform: rotate(-45deg); +} +.ChoosePlanView__backContainer { + position: absolute; + display: flex; + width: 68px; + margin-top: 52px; + @include breakpoint(small down) { + position: relative; + } + .ChoosePlanView__back { + margin-top: 8px; + font-size: 16px; + color: $tundora; + text-decoration: underline; + } +} .ChoosePlanView__yourPrivacyPlan { margin: auto; font-size: 24px; @@ -259,6 +287,7 @@ $medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow t .ChoosePlanView__price { min-height: 85px; margin-bottom: rem-calc(20); + font-size: 36px; &-blue { color: $price-blue; } @@ -279,6 +308,7 @@ $medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow t } } &.sub-text { + font-size: 12px; font-weight: 500; } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx index addf3bea4..e4ae3351e 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx @@ -11,7 +11,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React from 'react'; +import React, { Fragment } from 'react'; +import { NavLink } from 'react-router-dom'; /** * A Functional React component for rendering the Browser Success View @@ -22,12 +23,23 @@ const SuccessView = (props) => { const { actions } = props; const { sendPing } = actions; return ( -
    -
    {t('ghostery_browser_hub_onboarding_youve_successfully_set_up_your_browser')}
    -
    {`${t('ghostery_browser_hub_onboarding_surf_with_ease')}`}
    - - -
    + +
    +
    + + + {t('ghostery_browser_hub_onboarding_back')} + +
    +
    +
    +
    {t('ghostery_browser_hub_onboarding_youve_successfully_set_up_your_browser')}
    + +
    {`${t('ghostery_browser_hub_onboarding_surf_with_ease')} Ghostery`}
    + + +
    +
    ); }; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss index 9ef034fb7..074ce5302 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss @@ -5,6 +5,36 @@ flex-direction: column; } +.SuccessView__relativeContainer { + position: relative; +} + +.SuccessView__caret.left { + margin: 7px auto 0 auto; + height: 10px; + width: 10px; + border-left: 2px solid $tundora; + border-top: 2px solid $tundora; + cursor: pointer; + transform: rotate(-45deg); +} + +.SuccessView__backContainer { + position: absolute; + display: flex; + width: 68px; + margin-top: 142px; + @include breakpoint(small down) { + margin-top: 90px; + } + .SuccessView__back { + margin-top: 8px; + font-size: 16px; + color: $tundora; + text-decoration: underline; + } +} + .SuccessView__title { margin-top: 125px; font-size: 24px; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx index 93029a213..d4be7e485 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx @@ -56,6 +56,12 @@ const StepProgressBar = (props) => { const { currentStep } = props; const totalSteps = steps.length; + const logoutIfStepOne = (stepId) => { + const { actions } = props; + const { logout } = actions; + if (stepId === LOGIN) logout(); + }; + const renderStep = (step, isCurrent, stepClass) => { const labelClasses = ClassNames('StepProgressBar__label', { current: isCurrent, @@ -64,9 +70,18 @@ const StepProgressBar = (props) => { [`step-${step.id}`]: stepClass !== 'step-completed', }); + if (stepClass === 'incomplete') { + return ( +
    +
    {step.label}
    +
    +
    + ); + } + return (
    - + logoutIfStepOne(step.id)}>
    {step.label}
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss index b6fcdb196..7fbbb2a02 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss @@ -12,6 +12,7 @@ */ .StepProgressBarContainer { + margin-top: 40px; display: flex; justify-content: space-around; align-items: center; @@ -19,6 +20,7 @@ margin: auto; padding-top: 10px; } + .StepProgressBar__Step { height: 34px; width: 32px; @@ -78,7 +80,7 @@ width: 38px; font-size: 12px; color: $tundora; - &.currentStep { + &.current { font-weight: 700; } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js index 21d943533..127b75783 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js @@ -12,5 +12,11 @@ */ import StepProgressBar from './StepProgressBar'; +import { buildReduxHOC } from '../../../../shared-hub/utils'; +import { logout } from '../../../../Account/AccountActions'; -export default StepProgressBar; +const actionCreators = { + logout +}; + +export default buildReduxHOC(null, actionCreators, StepProgressBar); diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/hub_ghostery_browser.scss index 19e17a59d..ef1d248c8 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/hub_ghostery_browser.scss @@ -30,9 +30,6 @@ html, body, #root { overflow-x: hidden; } @media only screen and (max-width: 740px) { - .App__mainContent { - margin-left: 54px; - } .android-relative {position: relative;} } @@ -70,6 +67,7 @@ html, body, #root { @import './partials/radio_button'; // Imports from ../ghostery-browser-hub directory +@import '../ghostery-browser-hub/Views/OnboardingView/OnboardingView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/StepProgressbar/StepProgressbar.scss'; @import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss'; From 2c72617161504fe8b94255bf70470856ae6e056e Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Sun, 10 Jan 2021 22:42:27 -0500 Subject: [PATCH 049/113] Reduce tooltip delay in Block Settings view --- .../Step2_BlockSettingsView/BlockSettingsView.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index 6e74690cb..6984d00ec 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -150,7 +150,7 @@ class BlockSettingsView extends Component {
    {t('ghostery_browser_hub_onboarding_question_kinds_of_trackers')}
    - +
    @@ -175,7 +175,7 @@ class BlockSettingsView extends Component {
    {t('ghostery_browser_hub_onboarding_question_anti_tracking')}
    - +
    @@ -195,7 +195,7 @@ class BlockSettingsView extends Component {
    {t('ghostery_browser_hub_onboarding_question_smart_browsing')}
    - +
    From 0bf679b72f4eb56bc9aa5b88dc8a35da879427ea Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Sun, 10 Jan 2021 22:53:23 -0500 Subject: [PATCH 050/113] Make entire option box clickable on default search view --- .../Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx | 4 ++-- .../ChooseDefaultSearchView.scss | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index bbda9943a..c166a561b 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -72,11 +72,11 @@ class ChooseDefaultSearchView extends Component { const containerClasses = ClassNames('ChooseSearchView__optionContainer', { selected }); return ( -
    +
    this.triggerConfirmationModal(optionName)} className={containerClasses}>
    this.triggerConfirmationModal(optionName)} + handleClick={() => {}} altDesign />
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index 28f207bfa..b3adbebda 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -89,6 +89,7 @@ justify-content: center; height: 44px; width: 162px; + cursor: pointer; padding: 7.7px 14px; line-height: 22px; background: linear-gradient( From bf548b98a43ee6b22bd7894b84033323fd9ee397 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 11 Jan 2021 09:15:27 -0500 Subject: [PATCH 051/113] Rough out custom URL selector --- .../ChooseDefaultSearchView.jsx | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index c166a561b..1d8f6dd2b 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -46,6 +46,8 @@ class ChooseDefaultSearchView extends Component { cancelSelection = () => this.setState({ modalActive: false, searchBeingConsidered: null }); + handleInputChange = event => this.setState({ customSearchURL: event.target.value }); + triggerConfirmationModal = selection => this.setState({ modalActive: true, searchBeingConsidered: selection }); handleSubmit = () => { @@ -59,6 +61,8 @@ class ChooseDefaultSearchView extends Component { customSearchURL, }; + console.log('Cross-extension payload: ', payload); + chrome.runtime.sendMessage('search@ghostery.com', payload, () => { // TODO handle errors if needed // TODO save user's search setting to redux / background if needed @@ -87,6 +91,25 @@ class ChooseDefaultSearchView extends Component { ); } + renderCustomURLContainer = () => { + const { chosenSearch, customSearchURL } = this.state; + + const selected = (chosenSearch === SEARCH_CUSTOM); + const containerClasses = ClassNames('ChooseSearchView__optionContainer', { selected }); + + return ( +
    this.setState({ chosenSearch: SEARCH_CUSTOM })} className={containerClasses}> +

    Choose Your Own

    + +
    + + ); + } + renderConfirmationModal = () => { const { searchBeingConsidered } = this.state; @@ -134,7 +157,7 @@ class ChooseDefaultSearchView extends Component { {this.renderOptionContainer(chosenSearch, SEARCH_GHOSTERY, 'Ghostery Search')} {this.renderOptionContainer(chosenSearch, SEARCH_STARTPAGE, 'StartPage')} {this.renderOptionContainer(chosenSearch, SEARCH_BING, 'Bing')} -
    Choose Your Own
    + {this.renderCustomURLContainer()} {this.renderOptionContainer(chosenSearch, SEARCH_YAHOO, 'Yahoo')}
    ); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index b3adbebda..bf2fa4941 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -59,18 +59,11 @@ grid-template-columns: 1fr 1fr; } -.ChooseSearchView__radioButtonContainer { - padding: 11px; - display: flex; - justify-content: center; - cursor: pointer; -} .ChooseSearchView__optionContainer { display: flex; width: 354px; height: 125px; - padding: 20px; margin: 20px; border: solid 1px $tundora; border-radius: 4px; @@ -84,6 +77,22 @@ } } +.ChooseSearchView__radioButtonContainer { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + width: 25%; +} + +.ChooseSearchView__optionContainerDescription { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 75%; +} + .ChooseSearchView__nextButton { display: flex; justify-content: center; diff --git a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-bing.svg b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-bing.svg new file mode 100644 index 000000000..7d08cd756 --- /dev/null +++ b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-bing.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-ghostery.svg b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-ghostery.svg new file mode 100644 index 000000000..7ded65717 --- /dev/null +++ b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-ghostery.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + GLOW + + + + + + + + + + + + + + + + diff --git a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-startpage.svg b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-startpage.svg new file mode 100644 index 000000000..7ed830f93 --- /dev/null +++ b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-startpage.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-yahoo.svg b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-yahoo.svg new file mode 100644 index 000000000..9bc76d1c9 --- /dev/null +++ b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-yahoo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + From 7c15c2f256bf829b773928c4c0e37c7b4b66881a Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 11 Jan 2021 14:31:36 -0500 Subject: [PATCH 053/113] Make toasts work --- _locales/en/messages.json | 3 +++ .../Views/AppView/AppView.jsx | 12 ++++++------ app/ghostery-browser-hub/Views/AppView/index.js | 2 +- .../Step2_BlockSettingsView/BlockSettingsView.jsx | 5 +++-- .../ChooseDefaultSearchView.jsx | 15 +++++++++------ app/shared-hub/actions/ToastActions.js | 9 +++------ app/shared-hub/reducers/ToastReducer.js | 6 +++--- 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1a34d8c4e..5e8824783 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2414,5 +2414,8 @@ }, "pick_a_default_search_engine": { "message": "Pick a default engine for all your searches." + }, + "ghostery_browser_hub_blocking_settings_view_toast_error_message": { + "message": "Error: Please answer all questions" } } diff --git a/app/ghostery-browser-hub/Views/AppView/AppView.jsx b/app/ghostery-browser-hub/Views/AppView/AppView.jsx index 21a7559a4..6f42793e4 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppView.jsx +++ b/app/ghostery-browser-hub/Views/AppView/AppView.jsx @@ -21,8 +21,8 @@ import { ToastMessage } from '../../../shared-components'; * @memberof GhosteryBrowserHubViews */ const AppView = (props) => { - const { app, children } = props; - const { toastMessage, toastClass } = app; + const { toast, children } = props; + const { toastMessage, toastClass } = toast; /** * Handle clicking to exit the Toast Message. @@ -36,9 +36,9 @@ const AppView = (props) => { }; return ( -
    +
    - + {} {children}
    @@ -49,7 +49,7 @@ AppView.propTypes = { actions: PropTypes.shape({ setToast: PropTypes.func.isRequired, }).isRequired, - app: PropTypes.shape({ + toast: PropTypes.shape({ toastMessage: PropTypes.string, toastClass: PropTypes.string, }), @@ -57,7 +57,7 @@ AppView.propTypes = { // Default props used in the App AppView.defaultProps = { - app: { + toast: { toastMessage: '', toastClass: '', }, diff --git a/app/ghostery-browser-hub/Views/AppView/index.js b/app/ghostery-browser-hub/Views/AppView/index.js index fc8848870..b43f3675f 100644 --- a/app/ghostery-browser-hub/Views/AppView/index.js +++ b/app/ghostery-browser-hub/Views/AppView/index.js @@ -16,4 +16,4 @@ import { buildReduxHOC } from '../../../shared-hub/utils'; import AppView from './AppView'; import setToast from '../../../shared-hub/actions/ToastActions'; -export default buildReduxHOC(['app'], { setToast }, AppView); +export default buildReduxHOC(['toast'], { setToast }, AppView); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index 6984d00ec..8ac15511a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -95,10 +95,11 @@ class BlockSettingsView extends Component { setSetupStep({ setup_step: CHOOSE_DEFAULT_SEARCH, origin: ONBOARDING }); history.push('/onboarding/3'); } else { - const { setToast } = this.props; + const { actions } = this.props; + const { setToast } = actions; setToast({ - toastMessage: t('ghostery_browser_hub_toast_error'), + toastMessage: t('ghostery_browser_hub_blocking_settings_view_toast_error_message'), toastClass: 'error' }); } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 6c7c0b91b..5f12ee637 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -63,12 +63,15 @@ class ChooseDefaultSearchView extends Component { console.log('Cross-extension payload: ', payload); - chrome.runtime.sendMessage('search@ghostery.com', payload, () => { - // TODO handle errors if needed - // TODO save user's search setting to redux / background if needed - setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING }); - history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); - }); + // chrome.runtime.sendMessage('search@ghostery.com', payload, () => { + // // TODO handle errors if needed + // // TODO save user's search setting to redux / background if needed + // setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING }); + // history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); + // }); + + setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING }); + history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); } renderOptionContainer = (chosenSearch, optionName, optionDesc) => { diff --git a/app/shared-hub/actions/ToastActions.js b/app/shared-hub/actions/ToastActions.js index ed38ccb93..7fa065c3c 100644 --- a/app/shared-hub/actions/ToastActions.js +++ b/app/shared-hub/actions/ToastActions.js @@ -13,9 +13,6 @@ import SET_TOAST from '../constants/ToastConstants'; -export default function setToast(data) { - return { - type: SET_TOAST, - data, - }; -} +const setToast = data => ({ type: SET_TOAST, data }); + +export default setToast; diff --git a/app/shared-hub/reducers/ToastReducer.js b/app/shared-hub/reducers/ToastReducer.js index 6d7166560..d602416a8 100644 --- a/app/shared-hub/reducers/ToastReducer.js +++ b/app/shared-hub/reducers/ToastReducer.js @@ -21,10 +21,10 @@ function ToastReducer(state = initialState, action) { const { toastMessage, toastClass } = action.data; return { ...state, - app: { + toast: { toastMessage, - toastClass - } + toastClass, + }, }; } default: return state; From 9472e038324a20ea5275c97bd2035d916c0461cc Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 11 Jan 2021 16:36:59 -0500 Subject: [PATCH 054/113] Improvements to UX of custom URL input in choose default search view --- .../ChooseDefaultSearchView.jsx | 45 ++++++++++++++----- .../ChooseDefaultSearchView.scss | 25 +++++++++++ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 5f12ee637..2a2f15946 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -28,6 +28,8 @@ class ChooseDefaultSearchView extends Component { constructor(props) { super(props); + this.customURLInputRef = React.createRef(); + this.state = { chosenSearch: SEARCH_GHOSTERY, customSearchURL: null, @@ -39,6 +41,7 @@ class ChooseDefaultSearchView extends Component { updateSelection = () => this.setState(prevState => ( { chosenSearch: prevState.searchBeingConsidered, + customSearchURL: null, searchBeingConsidered: null, modalActive: false } @@ -46,6 +49,14 @@ class ChooseDefaultSearchView extends Component { cancelSelection = () => this.setState({ modalActive: false, searchBeingConsidered: null }); + selectCustom = () => { + this.customURLInputRef.current.focus(); + + this.setState({ + chosenSearch: SEARCH_CUSTOM, + }); + } + handleInputChange = event => this.setState({ customSearchURL: event.target.value }); triggerConfirmationModal = selection => this.setState({ modalActive: true, searchBeingConsidered: selection }); @@ -74,7 +85,7 @@ class ChooseDefaultSearchView extends Component { history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); } - renderOptionContainer = (chosenSearch, optionName, optionDesc) => { + renderOptionContainer = (chosenSearch, optionName) => { const selected = (chosenSearch === optionName); const containerClasses = ClassNames('ChooseSearchView__optionContainer', { selected }); const logoFilename = `/app/images/hub/ChooseDefaultSearchView/search-engine-logo-${optionName.toLocaleLowerCase()}.svg`; @@ -102,13 +113,25 @@ class ChooseDefaultSearchView extends Component { const containerClasses = ClassNames('ChooseSearchView__optionContainer', { selected }); return ( -
    this.setState({ chosenSearch: SEARCH_CUSTOM })} className={containerClasses}> -

    Choose Your Own

    - +
    +
    + {}} + altDesign + /> +
    +
    +

    Other

    +

    Type in search domain

    + +
    ); @@ -158,9 +181,9 @@ class ChooseDefaultSearchView extends Component {
    {t('choose_your_default_search')}
    {t('pick_a_default_search_engine')}
    - {this.renderOptionContainer(chosenSearch, SEARCH_GHOSTERY, 'Ghostery Search')} - {this.renderOptionContainer(chosenSearch, SEARCH_STARTPAGE, 'StartPage')} - {this.renderOptionContainer(chosenSearch, SEARCH_BING, 'Bing')} + {this.renderOptionContainer(chosenSearch, SEARCH_GHOSTERY)} + {this.renderOptionContainer(chosenSearch, SEARCH_STARTPAGE)} + {this.renderOptionContainer(chosenSearch, SEARCH_BING)} {this.renderCustomURLContainer()} {this.renderOptionContainer(chosenSearch, SEARCH_YAHOO, 'Yahoo')}
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index bf2fa4941..25d7dcdce 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -93,6 +93,31 @@ width: 75%; } +.ChooseSearchView__customURLTitle { + margin-bottom: 0; + font-size: 18px; + font-weight: 500; +} + +.ChooseSearchView__customURLSubtitle { + margin-bottom: 0; + font-size: 14px; +} + +.ChooseSearchView__customURLInput { + border: none; + box-shadow: none; + border-radius: 0px; + border-bottom: 1px solid $dark-silver; + + &:focus { + border: none; + box-shadow: none; + border-radius: 0px; + border-bottom: 2px solid $tundora; + } +} + .ChooseSearchView__nextButton { display: flex; justify-content: center; From c2a7204a599eeb059682322f2d35fa18a071f803 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 11 Jan 2021 16:42:11 -0500 Subject: [PATCH 055/113] Tidy up custom URL input a little more --- .../Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index 25d7dcdce..51708fa4d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -109,6 +109,8 @@ box-shadow: none; border-radius: 0px; border-bottom: 1px solid $dark-silver; + width: 70%; + margin-bottom: 0; &:focus { border: none; From 0db8f7c728b547aca3ae3d67cc311b0d35647ea6 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Tue, 12 Jan 2021 08:41:30 -0500 Subject: [PATCH 056/113] Remove console statement --- .../Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 2a2f15946..4d95539fb 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -72,8 +72,6 @@ class ChooseDefaultSearchView extends Component { customSearchURL, }; - console.log('Cross-extension payload: ', payload); - // chrome.runtime.sendMessage('search@ghostery.com', payload, () => { // // TODO handle errors if needed // // TODO save user's search setting to redux / background if needed From e6e4392f26a607fb4aa5513c4c4235bc3b14f751 Mon Sep 17 00:00:00 2001 From: Leury Rodriguez Date: Tue, 12 Jan 2021 16:01:38 -0500 Subject: [PATCH 057/113] GH-2236: Update onboarding branding (#658) * Update Dawn copy, add new logos, minor CSS change * Add message for install page copy change --- _locales/en/messages.json | 15 ++++++---- .../ChooseDefaultSearchView.jsx | 1 + .../ChooseDefaultSearchView.scss | 5 ++-- .../Step4_ChoosePlanView/ChoosePlanView.scss | 2 +- .../ghostery-browser-logo.svg | 30 +++++++++++++++++++ .../search-engine-logo-ghostery.svg | 14 ++++----- app/images/hub/ChoosePlanView/search.svg | 1 - 7 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 app/images/hub/ChooseDefaultSearchView/ghostery-browser-logo.svg delete mode 100644 app/images/hub/ChoosePlanView/search.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5e8824783..d77171d7e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1740,20 +1740,23 @@ "hub_create_account_toast_error": { "message": "That email address is already in use. Please choose another." }, + "ghostery_browser_hub_onboarding_install_heading": { + "message": "Ghostery Dawn now installing..." + }, "ghostery_browser_hub_onboarding_page_title": { - "message": "Ghostery Browser Hub - Welcome" + "message": "Ghostery Dawn Hub - Welcome" }, "ghostery_browser_hub_onboarding_header_title_login": { - "message": "Ghostery Browser Hub - Login" + "message": "Ghostery Dawn Hub - Login" }, "ghostery_browser_hub_onboarding_header_title_block_settings": { - "message": "Ghostery Browser Hub - Block Settings" + "message": "Ghostery Dawn Hub - Block Settings" }, "ghostery_browser_hub_onboarding_header_title_search_choice": { - "message": "Ghostery Browser Hub - Search Choice" + "message": "Ghostery Dawn Hub - Search Choice" }, "ghostery_browser_hub_onboarding_header_title_plan_choices": { - "message": "Ghostery Browser Hub - Plan Choices" + "message": "Ghostery Dawn Hub - Plan Choices" }, "ghostery_browser_hub_onboarding_privacy": { "message": "Privacy" @@ -1765,7 +1768,7 @@ "message": "Plan" }, "ghostery_browser_hub_onboarding_welcome": { - "message": "Welcome to Ghostery Browser!" + "message": "Welcome to Ghostery Dawn!" }, "ghostery_browser_hub_onboarding_lets_begin": { "message": "We've centralized online privacy by integrating our signature as well as novel technologies." diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 4d95539fb..c4e729ed3 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -141,6 +141,7 @@ class ChooseDefaultSearchView extends Component { return (
    +
    Modal of type {searchBeingConsidered} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index 51708fa4d..1a49c8d4c 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -90,7 +90,8 @@ flex-direction: column; align-items: center; justify-content: center; - width: 75%; + width: 75%; + padding-right: 15%; } .ChooseSearchView__customURLTitle { @@ -154,7 +155,7 @@ position: relative; width: 901px; min-height: 632px; - padding-top: 45px; + padding: 20px; color: #4a4a4a; background-color: #f7f7f7; border: 1px solid $tundora; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss index 31312470f..3ba414181 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss @@ -54,7 +54,7 @@ $medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow t height: 37px; width: 193px; margin: 65px auto 0 auto; - background-image: url('/app/images/hub/ChoosePlanView/search.svg'); + background-image: url('/app/images/hub/ChooseDefaultSearchView/search-engine-logo-ghostery.svg'); } .ChoosePlanView__adFree { margin: 16px auto 0 auto; diff --git a/app/images/hub/ChooseDefaultSearchView/ghostery-browser-logo.svg b/app/images/hub/ChooseDefaultSearchView/ghostery-browser-logo.svg new file mode 100644 index 000000000..40de7e6ea --- /dev/null +++ b/app/images/hub/ChooseDefaultSearchView/ghostery-browser-logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-ghostery.svg b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-ghostery.svg index 7ded65717..7d682ffa3 100644 --- a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-ghostery.svg +++ b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-ghostery.svg @@ -1,6 +1,6 @@ - + @@ -9,20 +9,18 @@ - + - - GLOW - + - + - + - + diff --git a/app/images/hub/ChoosePlanView/search.svg b/app/images/hub/ChoosePlanView/search.svg deleted file mode 100644 index 1c52a8174..000000000 --- a/app/images/hub/ChoosePlanView/search.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 8addd798b1eee885f6929e647ffdc6b9f402989a Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 13 Jan 2021 16:04:34 -0500 Subject: [PATCH 058/113] Remove custom URL input from choose default search view. Style and copy updates --- _locales/en/messages.json | 2 +- .../ChooseDefaultSearchView.jsx | 66 +++++-------------- .../ChooseDefaultSearchView.scss | 32 +++------ 3 files changed, 26 insertions(+), 74 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d77171d7e..17ebeae87 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1843,7 +1843,7 @@ "message": "Create a Ghostery Account" }, "ghostery_browser_hub_onboarding_sync_settings": { - "message": "Allows you to sync settings across browsers and devices" + "message": "Allows you to sync settings across browsers and devices." }, "ghostery_browser_hub_onboarding_already_have_account": { "message": "I already have an account." diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index c4e729ed3..f23932855 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -32,7 +32,6 @@ class ChooseDefaultSearchView extends Component { this.state = { chosenSearch: SEARCH_GHOSTERY, - customSearchURL: null, searchBeingConsidered: null, modalActive: false, }; @@ -41,7 +40,6 @@ class ChooseDefaultSearchView extends Component { updateSelection = () => this.setState(prevState => ( { chosenSearch: prevState.searchBeingConsidered, - customSearchURL: null, searchBeingConsidered: null, modalActive: false } @@ -49,27 +47,16 @@ class ChooseDefaultSearchView extends Component { cancelSelection = () => this.setState({ modalActive: false, searchBeingConsidered: null }); - selectCustom = () => { - this.customURLInputRef.current.focus(); - - this.setState({ - chosenSearch: SEARCH_CUSTOM, - }); - } - - handleInputChange = event => this.setState({ customSearchURL: event.target.value }); - triggerConfirmationModal = selection => this.setState({ modalActive: true, searchBeingConsidered: selection }); handleSubmit = () => { - const { chosenSearch, customSearchURL } = this.state; + const { chosenSearch } = this.state; const { actions, history } = this.props; const { setSetupStep } = actions; const payload = { type: 'setDefaultSearch', search: chosenSearch, - customSearchURL, }; // chrome.runtime.sendMessage('search@ghostery.com', payload, () => { @@ -83,6 +70,17 @@ class ChooseDefaultSearchView extends Component { history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); } + renderGhosteryOptionDescription = () => ( + +
    Ad-free private search
    +
    (Recommended)
    + + ); + + renderStartpageOptionDescription = () => ( +
    Ad-supported private search
    + ) + renderOptionContainer = (chosenSearch, optionName) => { const selected = (chosenSearch === optionName); const containerClasses = ClassNames('ChooseSearchView__optionContainer', { selected }); @@ -90,51 +88,22 @@ class ChooseDefaultSearchView extends Component { return (
    this.triggerConfirmationModal(optionName)} className={containerClasses}> -
    +
    {}} altDesign />
    -
    +
    + {(optionName === SEARCH_GHOSTERY) && this.renderGhosteryOptionDescription()} + {(optionName === SEARCH_STARTPAGE) && this.renderStartpageOptionDescription()}
    ); } - renderCustomURLContainer = () => { - const { chosenSearch, customSearchURL } = this.state; - - const selected = (chosenSearch === SEARCH_CUSTOM); - const containerClasses = ClassNames('ChooseSearchView__optionContainer', { selected }); - - return ( -
    -
    - {}} - altDesign - /> -
    -
    -

    Other

    -

    Type in search domain

    - -
    -
    - - ); - } - renderConfirmationModal = () => { const { searchBeingConsidered } = this.state; @@ -183,8 +152,7 @@ class ChooseDefaultSearchView extends Component { {this.renderOptionContainer(chosenSearch, SEARCH_GHOSTERY)} {this.renderOptionContainer(chosenSearch, SEARCH_STARTPAGE)} {this.renderOptionContainer(chosenSearch, SEARCH_BING)} - {this.renderCustomURLContainer()} - {this.renderOptionContainer(chosenSearch, SEARCH_YAHOO, 'Yahoo')} + {this.renderOptionContainer(chosenSearch, SEARCH_YAHOO)}
    + +
    - - +
    ); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index 9a19c8db2..918b78458 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -135,7 +135,72 @@ cursor: pointer; } -.ChooseSearchViewModal__content { +.ChooseSearchView__modalCancelButton { + display: flex; + justify-content: center; + height: 44px; + width: 162px; + cursor: pointer; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + #00aef0 + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + color: #FFF; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; +} + +.ChooseSearchView__modalConfirmButton { + display: flex; + justify-content: center; + height: 44px; + width: 162px; + cursor: pointer; + padding: 7.7px 14px; + line-height: 22px; + background: linear-gradient( + 45deg, + #ff7e74 50%, + #00aef0 + ); + background-size: 200% 100%; + background-position: 100% 50%; + transition: 0.25s all; + border: none; + &:hover { + background-position: 0% 50%; + transition: 0.25s all; + } + color: #FFF; + font-size: 14.1px; + font-weight: 700; + border-radius: 3.5px; + text-align: center; + line-height: 2.05; + cursor: pointer; +} + +.ChooseSearchView__modalButtonsContainer { + display: flex; + flex-direction: row; +} + +.ChooseSearchView__modalContent { position: relative; width: 901px; min-height: 632px; @@ -145,3 +210,9 @@ border: 1px solid $tundora; z-index: 10; } + +.ChooseSearchView__modalMain { + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-bing.svg b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-bing.svg index 7d08cd756..9e3a092b1 100644 --- a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-bing.svg +++ b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-bing.svg @@ -1,12 +1,15 @@ - + - - - - - + + + + + + + + From 153077fbbf7075b6a62c92111e5cb3e775faea4a Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Thu, 14 Jan 2021 09:03:20 -0500 Subject: [PATCH 060/113] More choose default search view modal styling --- .../ChooseDefaultSearchView.jsx | 6 +-- .../ChooseDefaultSearchView.scss | 39 ++++++++++++------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index d1f5ab3d8..8ea228222 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -113,10 +113,9 @@ class ChooseDefaultSearchView extends Component {
    - +
    {`Just so you know: ${searchBeingConsidered}'s search engine will log your data and use it to serve you targeted ads.`} - {searchBeingConsidered}
    +
    +
    + +`; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js index 4dcac5d05..b6d7f7592 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx index 1f8044c43..d664ad1ea 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss index 93fabacfd..f2fa8acb5 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/Step1_CreateAccountView.test.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/Step1_CreateAccountView.test.jsx new file mode 100644 index 000000000..7ab8e56a5 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/Step1_CreateAccountView.test.jsx @@ -0,0 +1,61 @@ +/** + * Create Account View Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import { MemoryRouter } from 'react-router'; +import Step1_CreateAccountView from '../Step1_CreateAccountView'; + +jest.mock('../../Step1_CreateAccountForm', () => { + const CreateAccountForm = () =>
    ; + return CreateAccountForm; +}); + +jest.mock('../../Step1_LogInForm', () => { + const LogInForm = () =>
    ; + return LogInForm; +}); + +const noop = () => {}; +describe('app/hub/Views/Step1_CreateAccountView component', () => { + const initialState = { + user: null, + actions: { + setSetupStep: noop + } + }; + describe('Snapshot tests with react-test-renderer', () => { + test('Create Account Form view is rendered correctly', () => { + const component = renderer.create( + + + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('Create Account Form view is rendered correctly when user is logged in', () => { + const plusUserState = { + ...initialState, + user: { + plusAccess: true + } + }; + + const component = shallow(); + }); + }); +}); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/__snapshots__/Step1_CreateAccountView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/__snapshots__/Step1_CreateAccountView.test.jsx.snap new file mode 100644 index 000000000..ef2454236 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/__snapshots__/Step1_CreateAccountView.test.jsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/hub/Views/Step1_CreateAccountView component Snapshot tests with react-test-renderer Create Account Form view is rendered correctly 1`] = ` +
    +
    + ghostery_browser_hub_onboarding_create_a_ghostery_account +
    +
    + ghostery_browser_hub_onboarding_sync_settings +
    +
    +
    + ghostery_browser_hub_onboarding_already_have_account +
    +
    +
    +
    +
    + +
    +
    + ghostery_browser_hub_onboarding_we_take_your_privacy_very_seriously +
    +
    +
    +
    +
    +`; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js index 3015cae6f..09b839fa8 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js @@ -4,14 +4,13 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { withRouter } from 'react-router-dom'; import { buildReduxHOC } from '../../../../shared-hub/utils'; import Step1_CreateAccountView from './Step1_CreateAccountView'; import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; @@ -20,4 +19,4 @@ const actionCreators = { setSetupStep, }; -export default withRouter(buildReduxHOC(['account'], actionCreators, Step1_CreateAccountView)); +export default buildReduxHOC(['account'], actionCreators, Step1_CreateAccountView); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx index 26c86f4f2..70972a1f7 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss index 9121a1d53..bb8bfdf30 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx index 67d15b3ae..bcb6b3ff0 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInFormContainer.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/__tests__/Step1_LoginForm.test.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/__tests__/Step1_LoginForm.test.jsx new file mode 100644 index 000000000..c64fb0a34 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/__tests__/Step1_LoginForm.test.jsx @@ -0,0 +1,75 @@ +/** + * Create Account View Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import { MemoryRouter } from 'react-router'; +import Step1_LoginForm from '../Step1_LoginForm'; + +const noop = () => {}; +describe('app/hub/Views/Step1_LoginForm component', () => { + const initialState = { + email: '', + password: '', + emailError: false, + passwordError: false, + handleSubmit: noop, + handleInputChange: noop, + handleForgotPassword: noop + }; + describe('Snapshot tests with react-test-renderer', () => { + test('Login Form view is rendered correctly', () => { + + const component = renderer.create( + + + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('the happy path of the component', () => { + const happyState = { + ...initialState, + email: 'test@example.com', + password: 'examplePassword', + handleSubmit: jest.fn(), + } + + const component = shallow(); + expect(happyState.handleSubmit.mock.calls.length).toBe(0); + component.find('form').simulate('submit'); + expect(happyState.handleSubmit.mock.calls.length).toBe(1); + }); + + test('the sad path of the component with errors', () => { + const sadState = { + ...initialState, + email: 'test@example.com', + password: 'examplePassword', + emailError: true, + passwordError: true, + handleSubmit: jest.fn(), + }; + + const component = shallow(); + + expect(sadState.handleSubmit.mock.calls.length).toBe(0); + component.find('form').simulate('submit'); + expect(sadState.handleSubmit.mock.calls.length).toBe(1); + }) + }); +}); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/__tests__/__snapshots__/Step1_LoginForm.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/__tests__/__snapshots__/Step1_LoginForm.test.jsx.snap new file mode 100644 index 000000000..b747c6459 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/__tests__/__snapshots__/Step1_LoginForm.test.jsx.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/hub/Views/Step1_LoginForm component Snapshot tests with react-test-renderer Login Form view is rendered correctly 1`] = ` +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + +
    + forgot_password +
    +
    +
    +
    +
    + +
    +
    +`; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js index 21a7378e6..f6b4ecd25 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/index.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index 8ac15511a..1f2d3074c 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,6 +12,7 @@ */ import React, { Fragment, Component } from 'react'; +import PropTypes from 'prop-types'; import { NavLink } from 'react-router-dom'; import Tooltip from '../../../../shared-components/Tooltip'; @@ -229,3 +230,16 @@ class BlockSettingsView extends Component { } export default BlockSettingsView; + +// PropTypes ensure we pass required props of the correct type +BlockSettingsView.propTypes = { + actions: PropTypes.shape({ + logout: PropTypes.func.isRequired, + setAntiTracking: PropTypes.func.isRequired, + setAdBlock: PropTypes.func.isRequired, + setSmartBlocking: PropTypes.func.isRequired, + setBlockingPolicy: PropTypes.func.isRequired, + setToast: PropTypes.func.isRequired, + setSetupStep: PropTypes.func.isRequired, + }).isRequired, +}; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss index 85b35e75c..8482b7f7a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss @@ -1,3 +1,17 @@ +/** + * BlockSettingsView Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + + .BlockSettingsView__container { display: flex; flex-direction: column; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/BlockSettingsView.test.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/BlockSettingsView.test.jsx new file mode 100644 index 000000000..5b9d5229c --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/BlockSettingsView.test.jsx @@ -0,0 +1,86 @@ +/** + * BlockSettings View Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import { MemoryRouter } from 'react-router'; +import BlockSettingsView from '../BlockSettingsView'; + +const noop = () => {}; +jest.mock('../../../../../shared-components/Tooltip'); + +describe('app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.test.jsx', () => { + const initialState = { + actions: { + logout: noop, + setAntiTracking: noop, + setAdBlock: noop, + setSmartBlocking: noop, + setBlockingPolicy: noop, + setToast: noop, + setSetupStep: noop, + } + }; + describe('Snapshot tests with react-test-renderer', () => { + test('BlockSettings View is rendered correctly', () => { + const component = renderer.create( + + + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('BlockSettings View happy path', () => { + const happyState = { + ...initialState, + actions: { + logout: noop, + setAntiTracking: jest.fn(), + setAdBlock: jest.fn(), + setSmartBlocking: jest.fn(), + setBlockingPolicy: jest.fn(), + setToast: noop, + setSetupStep: jest.fn(), + }, + history: { + push: noop + } + }; + const component = shallow(); + + const instance = component.instance(); + + instance.toggleRecommendedChoices(true); + expect(component.state('blockAds')).toBe(true); + expect(component.state('kindsOfTrackers')).toBe(1); + expect(component.state('antiTracking')).toBe(true); + expect(component.state('smartBrowsing')).toBe(true); + + instance.handleAnswerChange('blockAds', false); + expect(component.state('blockAds')).toBe(false); + + instance.handleSubmit(); + expect(happyState.actions.setAntiTracking.mock.calls.length).toBe(1); + expect(happyState.actions.setAdBlock.mock.calls.length).toBe(1); + expect(happyState.actions.setSmartBlocking.mock.calls.length).toBe(1); + expect(happyState.actions.setBlockingPolicy.mock.calls.length).toBe(1); + expect(happyState.actions.setSetupStep.mock.calls.length).toBe(1); + + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap new file mode 100644 index 000000000..e4753d50d --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap @@ -0,0 +1,324 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.test.jsx Shallow snapshot tests rendered with Enzyme BlockSettings View happy path 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.test.jsx Snapshot tests with react-test-renderer BlockSettings View is rendered correctly 1`] = ` +Array [ + , +
    +
    + ghostery_browser_hub_onboarding_which_privacy_plan +
    +
    + ghostery_browser_hub_onboarding_tell_us_your_preferences +
    +
    +
    +
    + + + +
    +
    + ghostery_browser_hub_onboarding_recommended_choices +
    +
    +
      +
    1. + ghostery_browser_hub_onboarding_question_block_ads +
    2. +
      +
      + + + + + +
      +
      + hub_setup_modal_button_yes +
      +
      +
      +
      + + + + + +
      +
      + hub_setup_modal_button_no +
      +
      +
    3. +
      + ghostery_browser_hub_onboarding_question_kinds_of_trackers +
      +
      +
      +
      + + + + + +
      +
      + ghostery_browser_hub_onboarding_kinds_of_trackers_all +
      +
      +
      +
      + + + + + +
      +
      + ghostery_browser_hub_onboarding_kinds_of_trackers_ad_and_analytics +
      +
      +
      +
      + + + + + +
      +
      + ghostery_browser_hub_onboarding_kinds_of_trackers_none +
      +
      +
    4. +
      + ghostery_browser_hub_onboarding_question_anti_tracking +
      +
      +
    5. +
      +
      + + + + + +
      +
      + hub_setup_modal_button_yes +
      +
      +
      +
      + + + + + +
      +
      + hub_setup_modal_button_no +
      +
      +
    6. +
      + ghostery_browser_hub_onboarding_question_smart_browsing +
      +
      +
    7. +
      +
      + + + + + +
      +
      + hub_setup_modal_button_yes +
      +
      +
      +
      + + + + + +
      +
      + hub_setup_modal_button_no +
      +
      + +
    +
    + +
    , +] +`; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js index 9394bfdc0..d3bfb3ae0 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 71ac9b443..6ca34da40 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -284,7 +284,7 @@ class ChoosePlanView extends React.Component { const { setSetupStep } = actions; const { expanded, selectedPlan } = this.state; - const isBasic = !user; + const isBasic = !user || (user && !user.plusAccess && !user.premiumAccess); const isPlus = (user && user.plusAccess && !user.premiumAccess) || false; const isPremium = (user && user.premiumAccess) || false; @@ -294,7 +294,7 @@ class ChoosePlanView extends React.Component { }); return ( - +
    @@ -304,17 +304,16 @@ class ChoosePlanView extends React.Component {
    -
    {this.renderTitleText()}
    {this.renderSubtitleText(didNotSelectGhosterySearch)}
    {didNotSelectGhosterySearch && isBasic && ( {searchPromo()} {/* TODO: For the CTA button below, - 1. If user is signed in, activate the user’s 7-day free trial for the Ghostery Search Plus plan - and move them to Step 5 if signed in - 2. If user is signed out, clicking this should take them to Step 4b (linked) - */} + 1. If user is signed in, activate the user’s 7-day free trial for the Ghostery Search Plus plan + and move them to Step 5 if signed in + 2. If user is signed out, clicking this should take them to Step 4b (linked) + */}
    {t('ghostery_browser_hub_onboarding_start_trial')}
    {t('ghostery_browser_hub_onboarding_see_all_plans')}
    @@ -368,7 +367,7 @@ class ChoosePlanView extends React.Component {
    )}
    - +
    ); } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss index 3ba414181..02a71cd6b 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss @@ -1,3 +1,16 @@ +/** + * WelcomeView Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + $medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow to next line .ChoosePlanView { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx new file mode 100644 index 000000000..9457ee6e8 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx @@ -0,0 +1,101 @@ +/** + * ChoosePlanView Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import { MemoryRouter } from 'react-router'; +import ChoosePlanView from '../ChoosePlanView'; + +const noop = () => {}; + +describe('app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx', () => { + const initialState = { + user: null, + didNotSelectGhosterySearch: false, + actions: { + setSetupStep: noop + } + }; + describe('Snapshot tests with react-test-renderer', () => { + test('ChoosePlanView is rendered correctly', () => { + const component = renderer.create( + + + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('ChoosePlanView View with user not logged in', () => { + const component = shallow(); + + const instance = component.instance(); + + instance.selectBasicPlan(); + expect(component.state('selectedPlan')).toBe('BASIC'); + + instance.selectPlusPlan(); + expect(component.state('selectedPlan')).toBe('PLUS'); + + instance.selectPremiumPlan(); + expect(component.state('selectedPlan')).toBe('PREMIUM'); + + expect(component).toMatchSnapshot(); + }); + + test('ChoosePlanView View with basic user logged in', () => { + const basicUserState = { + ...initialState, + user: { + plusAccess: false, + premiumAccess: false + } + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('ChoosePlanView View with plus user logged in', () => { + const plusUserState = { + ...initialState, + user: { + plusAccess: true, + premiumAccess: false + }, + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('ChoosePlanView View with premium user logged in', () => { + const premiumUserState = { + ...initialState, + user: { + plusAccess: true, + premiumAccess: true + } + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap new file mode 100644 index 000000000..28e39c2ea --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap @@ -0,0 +1,387 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with basic user logged in 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with plus user logged in 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with premium user logged in 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with user not logged in 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Snapshot tests with react-test-renderer ChoosePlanView is rendered correctly 1`] = ` +
    + +
    +
    + ghostery_browser_hub_onboarding_your_privacy_plan +
    +
    + ghostery_browser_hub_onboarding_choose_an_option +
    +
    +
    +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +

    + Ghostery +

    +
    +

    + hub_upgrade_plan_free +

    +
    +

    + + hub_upgrade_basic_protection + +

    +
    +
    + + ghostery_browser_hub_onboarding_private_search +
    +
    + + ghostery_browser_hub_onboarding_tracker_protection +
    +
    + + ghostery_browser_hub_onboarding_speedy_page_loads +
    +
    + + ghostery_browser_hub_onboarding_intelligence_technology +
    +
    +
    +
    +
    +
    +
    + + + + + +
    +
    +
    +
    +

    + Ghostery Plus +

    +
    +

    + $4.99 +

    +

    + per_month +

    +
    +

    + + hub_upgrade_additional_protection + +

    +
    +
    + + ghostery_browser_hub_onboarding_private_search +
    +
    + + ghostery_browser_hub_onboarding_tracker_protection +
    +
    + + ghostery_browser_hub_onboarding_speedy_page_loads +
    +
    + + ghostery_browser_hub_onboarding_intelligence_technology +
    +
    + + ghostery_browser_hub_onboarding_ad_free +
    +
    + + ghostery_browser_hub_onboarding_supports_ghosterys_mission +
    +
    +
    +
    +
    +
    +
    + + + + + +
    +
    +
    +
    +
    +

    + Ghostery Premium +

    +
    +

    + $11.99 +

    +

    + per_month +

    +
    +

    + + hub_upgrade_maximum_protection + +

    +
    +
    + + ghostery_browser_hub_onboarding_private_search +
    +
    + + ghostery_browser_hub_onboarding_tracker_protection +
    +
    + + ghostery_browser_hub_onboarding_speedy_page_loads +
    +
    + + ghostery_browser_hub_onboarding_intelligence_technology +
    +
    + + ghostery_browser_hub_onboarding_ad_free +
    +
    + + ghostery_browser_hub_onboarding_supports_ghosterys_mission +
    +
    + + VPN +
    +
    + + ghostery_browser_hub_onboarding_unlimited_bandwidth +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx index e4ae3351e..94ca0a611 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -37,7 +37,7 @@ const SuccessView = (props) => {
    {`${t('ghostery_browser_hub_onboarding_surf_with_ease')} Ghostery`}
    - +
    ); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss index 074ce5302..9ac706a72 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss @@ -1,3 +1,16 @@ +/** + * SuccessView Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + .SuccessView__container { display: flex; justify-content: center; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/SuccessView.test.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/SuccessView.test.jsx new file mode 100644 index 000000000..7136d4ad6 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/SuccessView.test.jsx @@ -0,0 +1,37 @@ +/** + * SuccessView View Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { MemoryRouter } from 'react-router'; +import SuccessView from '../SuccessView'; + +const noop = () => {}; + +describe('app/ghostery-browser-hub/Views/OnboardingViews/Step0_SuccessView/SuccessView.test.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('Success View is rendered correctly', () => { + const initialState = { + actions: { + sendPing: noop + } + }; + const component = renderer.create( + + + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap new file mode 100644 index 000000000..6508140e8 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step0_SuccessView/SuccessView.test.jsx Snapshot tests with react-test-renderer Success View is rendered correctly 1`] = ` +Array [ + , +
    +
    + ghostery_browser_hub_onboarding_youve_successfully_set_up_your_browser +
    +
    + ghostery_browser_hub_onboarding_surf_with_ease Ghostery +
    + + +
    , +] +`; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js index 9b5cf3814..ae5abc940 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/index.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx index 86ed9c650..8d6af03e0 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/index.js index 9324423ed..4228a3345 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/index.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx index d4be7e485..7d52f15c1 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -16,8 +16,8 @@ import ClassNames from 'classnames'; import PropTypes from 'prop-types'; import { NavLink } from 'react-router-dom'; import { - ONBOARDING, WELCOME, + ONBOARDING, LOGIN, BLOCK_SETTINGS, CHOOSE_DEFAULT_SEARCH, @@ -54,6 +54,7 @@ const steps = [ */ const StepProgressBar = (props) => { const { currentStep } = props; + const currentStepNumber = parseInt(currentStep, 10); const totalSteps = steps.length; const logoutIfStepOne = (stepId) => { @@ -100,16 +101,16 @@ const StepProgressBar = (props) => { const step = index + 1; return ( - - {(step < currentStep) && renderCompletedStep(steps[index])} - {(step === currentStep) && renderCurrentStep(steps[index])} - {(step > currentStep) && renderIncompleteStep(steps[index])} + + {(step < currentStepNumber) && renderCompletedStep(steps[index])} + {(step === currentStepNumber) && renderCurrentStep(steps[index])} + {(step > currentStepNumber) && renderIncompleteStep(steps[index])} {(step !== totalSteps) && ( - {(step < currentStep) && ( + {(step < currentStepNumber) && (
    )} - {(step >= currentStep) && ( + {(step >= currentStepNumber) && (
    )} @@ -121,13 +122,13 @@ const StepProgressBar = (props) => { return (
    - {(currentStep !== parseInt(WELCOME, 10)) && renderProgressBar()} + {(currentStep !== WELCOME) && renderProgressBar()}
    ); }; // PropTypes ensure we pass required props of the correct type StepProgressBar.propTypes = { - currentStep: PropTypes.number.isRequired, + currentStep: PropTypes.string.isRequired, }; export default StepProgressBar; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss index 7fbbb2a02..da4abb86e 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/StepProgressBar.test.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/StepProgressBar.test.jsx new file mode 100644 index 000000000..a4320310e --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/StepProgressBar.test.jsx @@ -0,0 +1,93 @@ +/** + * StepProgressBar Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import { MemoryRouter } from 'react-router'; +import StepProgressBar from '../StepProgressBar'; +import { WELCOME, LOGIN, BLOCK_SETTINGS, CHOOSE_DEFAULT_SEARCH, CHOOSE_PLAN, SUCCESS } from '../../../OnboardingView/OnboardingConstants'; + +const noop = () => {}; + +describe('app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx', () => { + const initialState = { + currentStep: LOGIN, + actions: { + logout: noop + } + }; + describe('Snapshot tests with react-test-renderer', () => { + test('StepProgressBar is rendered correctly', () => { + const component = renderer.create( + + + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('StepProgressBar View step 0', () => { + const step_0_initialState = { + ...initialState, + currentStep: WELCOME, + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + test('StepProgressBar View step 1', () => { + const step_1_initialState = { + ...initialState, + currentStep: LOGIN + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('StepProgressBar View step 2', () => { + const step_2_initialState = { + ...initialState, + currentStep: BLOCK_SETTINGS + }; + + const component = shallow(); + }); + + test('StepProgressBar View step 4', () => { + const step_4_initialState = { + currentStep: CHOOSE_PLAN + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('StepProgressBar View step 5', () => { + const step_5_initialState = { + ...initialState, + currentStep: SUCCESS + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap new file mode 100644 index 000000000..e389612c4 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 0 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 1 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 2 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 4 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 5 1`] = `ShallowWrapper {}`; + +exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Snapshot tests with react-test-renderer StepProgressBar is rendered correctly 1`] = ` +
    +
    + +
    + sign_in +
    +
    + +
    +
    +
    +
    + ghostery_browser_hub_onboarding_privacy +
    +
    +
    +
    +
    +
    + ghostery_browser_hub_onboarding_search +
    +
    +
    +
    +
    +
    + ghostery_browser_hub_onboarding_plan +
    +
    +
    +
    +`; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js index 127b75783..1f20a3fc0 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index d94cf838d..44c6d9fcc 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/index.jsx b/app/ghostery-browser-hub/index.jsx index 67d2e3d2b..9a9aa4a39 100644 --- a/app/ghostery-browser-hub/index.jsx +++ b/app/ghostery-browser-hub/index.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/__snapshots__/SetupAntiSuiteView.test.jsx.snap b/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/__snapshots__/SetupAntiSuiteView.test.jsx.snap index ae5d25fe6..38e132141 100644 --- a/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/__snapshots__/SetupAntiSuiteView.test.jsx.snap +++ b/app/hub/Views/SetupViews/SetupAntiSuiteView/__tests__/__snapshots__/SetupAntiSuiteView.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/hub/Views/SetupViews/SetupAntiSuiteView component More Snapshot tests with react-test-renderer, but for edge cases edge case where features is an empty array 1`] = ` +exports[`app/hub/Views/OnboardingViews/SetupAntiSuiteView component More Snapshot tests with react-test-renderer, but for edge cases edge case where features is an empty array 1`] = `
    @@ -10,7 +10,7 @@ exports[`app/hub/Views/SetupViews/SetupAntiSuiteView component More Snapshot tes
    `; -exports[`app/hub/Views/SetupViews/SetupAntiSuiteView component Snapshot tests with react-test-renderer setup anti-suite view is rendered correctly 1`] = ` +exports[`app/hub/Views/OnboardingViews/SetupAntiSuiteView component Snapshot tests with react-test-renderer setup anti-suite view is rendered correctly 1`] = `
    diff --git a/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/__snapshots__/SetupBlockingDropdown.test.jsx.snap b/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/__snapshots__/SetupBlockingDropdown.test.jsx.snap index 1bbb9a35c..48144a180 100644 --- a/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/__snapshots__/SetupBlockingDropdown.test.jsx.snap +++ b/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/__snapshots__/SetupBlockingDropdown.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/hub/Views/SetupViews/SetupBlockingDropdown component Snapshot tests with react-test-renderer setup blocking dropdown component is rendered correctly 1`] = ` +exports[`app/hub/Views/OnboardingViews/SetupBlockingDropdown component Snapshot tests with react-test-renderer setup blocking dropdown component is rendered correctly 1`] = `
    diff --git a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/__snapshots__/SetupBlockingView.test.jsx.snap b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/__snapshots__/SetupBlockingView.test.jsx.snap index 10bd7022a..c6059f7a7 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/__snapshots__/SetupBlockingView.test.jsx.snap +++ b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/__snapshots__/SetupBlockingView.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/hub/Views/SetupViews/SetupBlockingView component More Snapshot tests with react-test-renderer, but for edge cases edge case where choices is an empty array 1`] = ` +exports[`app/hub/Views/OnboardingViews/SetupBlockingView component More Snapshot tests with react-test-renderer, but for edge cases edge case where choices is an empty array 1`] = `
    @@ -18,7 +18,7 @@ exports[`app/hub/Views/SetupViews/SetupBlockingView component More Snapshot test
    `; -exports[`app/hub/Views/SetupViews/SetupBlockingView component Snapshot tests with react-test-renderer setup blocking view is rendered correctly 1`] = ` +exports[`app/hub/Views/OnboardingViews/SetupBlockingView component Snapshot tests with react-test-renderer setup blocking view is rendered correctly 1`] = `
    diff --git a/app/hub/Views/SetupViews/SetupDoneView/__tests__/__snapshots__/SetupDoneView.test.jsx.snap b/app/hub/Views/SetupViews/SetupDoneView/__tests__/__snapshots__/SetupDoneView.test.jsx.snap index 6509da93e..30f6e540b 100644 --- a/app/hub/Views/SetupViews/SetupDoneView/__tests__/__snapshots__/SetupDoneView.test.jsx.snap +++ b/app/hub/Views/SetupViews/SetupDoneView/__tests__/__snapshots__/SetupDoneView.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/hub/Views/SetupViews/SetupDoneView component Snapshot tests with react-test-renderer setup human web view is rendered correctly 1`] = ` +exports[`app/hub/Views/OnboardingViews/SetupDoneView component Snapshot tests with react-test-renderer setup human web view is rendered correctly 1`] = `
    diff --git a/app/hub/Views/SetupViews/SetupHeader/__tests__/__snapshots__/SetupHeader.test.jsx.snap b/app/hub/Views/SetupViews/SetupHeader/__tests__/__snapshots__/SetupHeader.test.jsx.snap index 8b72c56c1..86061f3b4 100644 --- a/app/hub/Views/SetupViews/SetupHeader/__tests__/__snapshots__/SetupHeader.test.jsx.snap +++ b/app/hub/Views/SetupViews/SetupHeader/__tests__/__snapshots__/SetupHeader.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/hub/Views/SetupViews/SetupHeader component Snapshot tests with react-test-renderer setup header is rendered correctly 1`] = ` +exports[`app/hub/Views/OnboardingViews/SetupHeader component Snapshot tests with react-test-renderer setup header is rendered correctly 1`] = `
    diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/__snapshots__/SetupHumanWebView.test.jsx.snap b/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/__snapshots__/SetupHumanWebView.test.jsx.snap index a6ad2f881..6b94ff87b 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/__snapshots__/SetupHumanWebView.test.jsx.snap +++ b/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/__snapshots__/SetupHumanWebView.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/hub/Views/SetupViews/SetupHumanWebView component Snapshot tests with react-test-renderer setup human web view is rendered correctly 1`] = ` +exports[`app/hub/Views/OnboardingViews/SetupHumanWebView component Snapshot tests with react-test-renderer setup human web view is rendered correctly 1`] = `
    diff --git a/app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx b/app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx index 37ba604ee..6855ad663 100644 --- a/app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx @@ -22,7 +22,7 @@ global.t = function(str) { }; // Fake the Tooltip implementation -jest.mock('../../Tooltip'); +jest.mock('../../../../shared-components/Tooltip'); describe('app/panel/components/BuildingBlocks/PauseButton.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { diff --git a/app/shared-hub/actions/__tests__/AntiSuiteActions.test.js b/app/shared-hub/actions/__tests__/AntiSuiteActions.test.js new file mode 100644 index 000000000..f61816760 --- /dev/null +++ b/app/shared-hub/actions/__tests__/AntiSuiteActions.test.js @@ -0,0 +1,88 @@ +/** + * Test file for Anti Suite Actions + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import * as utils from '../../utils'; +import * as AntiSuiteActions from '../AntiSuiteActions'; +import { SET_AD_BLOCK, SET_ANTI_TRACKING, SET_SMART_BLOCK } from '../../constants/AntiSuiteConstants'; + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +const testData = { test: true }; + +const mockSendMessageInPromise = jest.fn((dispatch, name, message) => new Promise((resolve, reject) => { + dispatch({ + type: name, + testData + }); + switch (name) { + case 'SET_ANTI_TRACKING': { + resolve(testData); + break; + } + case 'SET_AD_BLOCK': { + resolve(testData); + break; + } + case 'SET_SMART_BLOCK': { + resolve(testData); + break; + } + default: resolve(testData); + } +})); + +utils.sendMessageInPromise = mockSendMessageInPromise; + +utils.makeDeferredDispatcher = jest.fn((action, actionData) => dispatch => { + return mockSendMessageInPromise(dispatch, action, actionData); +}) + +describe('app/shared-hub/actions/AntiSuiteActions', () => { + test('setAdBlock action should return correctly', () => { + const initialState = {}; + const store = mockStore(initialState); + + const expectedPayload = { testData, type: SET_AD_BLOCK }; + return store.dispatch(AntiSuiteActions.setAdBlock()).then(() => { + const actions = store.getActions(); + expect(actions).toEqual([expectedPayload]); + }); + }); + + test('setAntiTracking action should return correctly', () => { + const initialState = {}; + const store = mockStore(initialState); + + const data = testData; + const expectedPayload = { testData, type: SET_ANTI_TRACKING }; + return store.dispatch(AntiSuiteActions.setAntiTracking()).then(() => { + const actions = store.getActions(); + expect(actions).toEqual([expectedPayload]); + }); + }); + + test('setSmartBlock action should return correctly', () => { + const initialState = {}; + const store = mockStore(initialState); + + const data = testData; + const expectedPayload = { testData, type: SET_SMART_BLOCK }; + return store.dispatch(AntiSuiteActions.setSmartBlocking()).then(() => { + const actions = store.getActions(); + expect(actions).toEqual([expectedPayload]); + }); + }); +}); diff --git a/app/shared-hub/actions/__tests__/BlockingPolicyActions.test.js b/app/shared-hub/actions/__tests__/BlockingPolicyActions.test.js new file mode 100644 index 000000000..a99af2443 --- /dev/null +++ b/app/shared-hub/actions/__tests__/BlockingPolicyActions.test.js @@ -0,0 +1,56 @@ +/** + * Test file for Blocking Policy Actions + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import * as utils from '../../utils'; +import setBlockingPolicy from '../BlockingPolicyActions'; +import { SET_BLOCKING_POLICY } from '../../constants/BlockingPolicyConstants' + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +const testData = { test: true }; + +const mockSendMessageInPromise = jest.fn((dispatch, name, message) => new Promise((resolve, reject) => { + dispatch({ + type: name, + testData + }); + switch (name) { + case 'SET_BLOCKING_POLICY': { + resolve(testData); + break; + } + default: resolve(testData); + } +})); + +utils.sendMessageInPromise = mockSendMessageInPromise; + +utils.makeDeferredDispatcher = jest.fn((action, actionData) => dispatch => { + return mockSendMessageInPromise(dispatch, action, actionData); +}) + +describe('app/shared-hub/actions/BlockingPolicyActions', () => { + test('setBlockingPolicy action should return correctly', () => { + const initialState = {}; + const store = mockStore(initialState); + + const expectedPayload = { testData, type: SET_BLOCKING_POLICY }; + return store.dispatch(setBlockingPolicy()).then(() => { + const actions = store.getActions(); + expect(actions).toEqual([expectedPayload]); + }); + }); +}); diff --git a/app/shared-hub/actions/__tests__/MetricsActions.test.js b/app/shared-hub/actions/__tests__/MetricsActions.test.js new file mode 100644 index 000000000..9dc24db65 --- /dev/null +++ b/app/shared-hub/actions/__tests__/MetricsActions.test.js @@ -0,0 +1,56 @@ +/** + * Test file for Metrics Actions + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import * as utils from '../../utils'; +import sendPing from '../MetricsActions'; +import SEND_PING from '../../constants/MetricsConstants' + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +const testData = { test: true }; + +const mockSendMessageInPromise = jest.fn((dispatch, name, message) => new Promise((resolve, reject) => { + dispatch({ + type: name, + testData + }); + switch (name) { + case 'SEND_PING': { + resolve(testData); + break; + } + default: resolve(testData); + } +})); + +utils.sendMessageInPromise = mockSendMessageInPromise; + +utils.makeDeferredDispatcher = jest.fn((action, actionData) => dispatch => { + return mockSendMessageInPromise(dispatch, action, actionData); +}) + +describe('app/shared-hub/actions/MetricsActions', () => { + test('sendPing action should return correctly', () => { + const initialState = {}; + const store = mockStore(initialState); + + const expectedPayload = { testData, type: SEND_PING }; + return store.dispatch(sendPing()).then(() => { + const actions = store.getActions(); + expect(actions).toEqual([expectedPayload]); + }); + }); +}); diff --git a/app/shared-hub/actions/__tests__/SetupLifecycleActions.test.js b/app/shared-hub/actions/__tests__/SetupLifecycleActions.test.js new file mode 100644 index 000000000..18a780804 --- /dev/null +++ b/app/shared-hub/actions/__tests__/SetupLifecycleActions.test.js @@ -0,0 +1,89 @@ +/** + * Test file for Setup Lifecycle Actions + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import * as utils from '../../utils'; +import * as SetupLifecycleActions from '../SetupLifecycleActions'; +import { INIT_SETUP_PROPS, SET_SETUP_STEP, SET_SETUP_COMPLETE } from '../../constants/SetupLifecycleConstants'; + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +const testData = { test: true }; + +const mockSendMessageInPromise = jest.fn((dispatch, name, message) => new Promise((resolve, reject) => { + dispatch({ + type: name, + testData + }); + switch (name) { + case 'INIT_SETUP_PROPS': { + resolve(testData); + break; + } + case 'SET_SETUP_STEP': { + resolve(testData); + break; + } + case 'SET_SETUP_COMPLETE': { + resolve(testData); + break; + } + default: resolve(testData); + } +})); + +utils.sendMessageInPromise = mockSendMessageInPromise; + +utils.makeDeferredDispatcher = jest.fn((action, actionData) => dispatch => { + return mockSendMessageInPromise(dispatch, action, actionData); +}) + + +describe('app/shared-hub/actions/AntiSuiteActions', () => { + test('initSetupProps action should return correctly', () => { + const initialState = {}; + const store = mockStore(initialState); + + const data = testData; + const expectedPayload = { data, type: INIT_SETUP_PROPS }; + store.dispatch(SetupLifecycleActions.initSetupProps(testData)); + const actions = store.getActions(); + expect(actions).toEqual([expectedPayload]); + }); + + test('setSetupStep action should return correctly', () => { + const initialState = {}; + const store = mockStore(initialState); + + const data = testData; + const expectedPayload = { testData, type: SET_SETUP_STEP }; + return store.dispatch(SetupLifecycleActions.setSetupStep()).then(() => { + const actions = store.getActions(); + expect(actions).toEqual([expectedPayload]); + }); + }); + + test('setSetupStep action should return correctly', () => { + const initialState = {}; + const store = mockStore(initialState); + + const data = testData; + const expectedPayload = { testData, type: SET_SETUP_COMPLETE }; + return store.dispatch(SetupLifecycleActions.setSetupComplete()).then(() => { + const actions = store.getActions(); + expect(actions).toEqual([expectedPayload]); + }); + }); +}); diff --git a/app/shared-hub/actions/__tests__/ToastActions.test.js b/app/shared-hub/actions/__tests__/ToastActions.test.js new file mode 100644 index 000000000..bed3b286c --- /dev/null +++ b/app/shared-hub/actions/__tests__/ToastActions.test.js @@ -0,0 +1,35 @@ +/** + * Test file for Toast Actions + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import setToast from '../ToastActions'; +import SET_TOAST from '../../constants/ToastConstants' + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +const testData = { test: true }; + +describe('app/shared-hub/actions/ToastActions', () => { + test('setToast action should return correctly', () => { + const initialState = {}; + const store = mockStore(initialState); + + const data = testData; + const expectedPayload = { data, type: SET_TOAST }; + store.dispatch(setToast(testData)); + const actions = store.getActions(); + expect(actions).toEqual([expectedPayload]); + }); +}); diff --git a/app/shared-hub/reducers/__tests__/AntiSuiteReducer.test.js b/app/shared-hub/reducers/__tests__/AntiSuiteReducer.test.js new file mode 100644 index 000000000..3bd25668f --- /dev/null +++ b/app/shared-hub/reducers/__tests__/AntiSuiteReducer.test.js @@ -0,0 +1,69 @@ +/** + * AntiSuite Test Reducer + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import Immutable from 'seamless-immutable'; +import AntiSuiteReducer from '../AntiSuiteReducer'; +import { SET_AD_BLOCK, SET_ANTI_TRACKING, SET_SMART_BLOCK } from '../../constants/AntiSuiteConstants'; + +const initialState = Immutable({ + setup: { + enable_ad_block: false, + enable_anti_tracking: false, + enable_smart_block: false + } +}); + +describe('app/shared-hub/reducers/AntiSuiteReducer', () => { + test('initial state is correct', () => { + expect(AntiSuiteReducer(undefined, {})).toEqual({}); + }); + + test('reducer correctly handles SET_AD_BLOCK', () => { + const data = { + enable_ad_block: true, + }; + const action = { data, type: SET_AD_BLOCK }; + + const updatedAntiSuiteState = Immutable.merge(initialState.setup, data); + + expect(AntiSuiteReducer(initialState, action)).toEqual({ + setup: updatedAntiSuiteState + }); + }); + + test('reducer correctly handles SET_ANTI_TRACKING', () => { + const data = { + enable_anti_tracking: true, + }; + const action = { data, type: SET_ANTI_TRACKING }; + + const updatedAntiSuiteState = Immutable.merge(initialState.setup, data); + + expect(AntiSuiteReducer(initialState, action)).toEqual({ + setup: updatedAntiSuiteState + }); + }); + + test('reducer correctly handles SET_SMART_BLOCK', () => { + const data = { + enable_smart_block: true, + }; + const action = { data, type: SET_SMART_BLOCK }; + + const updatedAntiSuiteState = Immutable.merge(initialState.setup, data); + + expect(AntiSuiteReducer(initialState, action)).toEqual({ + setup: updatedAntiSuiteState + }); + }); +}); diff --git a/app/shared-hub/reducers/__tests__/BlockingPolicyReducer.test.js b/app/shared-hub/reducers/__tests__/BlockingPolicyReducer.test.js new file mode 100644 index 000000000..b97efc8b1 --- /dev/null +++ b/app/shared-hub/reducers/__tests__/BlockingPolicyReducer.test.js @@ -0,0 +1,41 @@ +/** + * BlockingPolicy Test Reducer + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import Immutable from 'seamless-immutable'; +import BlockingPolicyReducer from '../BlockingPolicyReducer'; +import { SET_BLOCKING_POLICY } from '../../constants/BlockingPolicyConstants'; + +const initialState = Immutable({ + setup: { + blockingPolicy: true + } +}); + +describe('app/shared-hub/reducers/BlockingPolicy', () => { + test('initial state is correct', () => { + expect(BlockingPolicyReducer(undefined, {})).toEqual({}); + }); + + test('reducer correctly handles SET_BLOCKING_POLICY', () => { + const data = { + blockingPolicy: true, + }; + const action = { data, type: SET_BLOCKING_POLICY }; + + const updatedBlockingPolicyState = Immutable.merge(initialState.setup, data); + + expect(BlockingPolicyReducer(initialState, action)).toEqual({ + setup: updatedBlockingPolicyState + }); + }); +}); diff --git a/app/shared-hub/reducers/__tests__/SetupLifeCycleReducer.test.js b/app/shared-hub/reducers/__tests__/SetupLifeCycleReducer.test.js new file mode 100644 index 000000000..49659a6c1 --- /dev/null +++ b/app/shared-hub/reducers/__tests__/SetupLifeCycleReducer.test.js @@ -0,0 +1,42 @@ +/** + * SetupLifecycle Test Reducer + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import Immutable from 'seamless-immutable'; +import SetupLifecycleReducer from '../SetupLifecycleReducer'; +import { INIT_SETUP_PROPS } from '../../constants/SetupLifecycleConstants'; + +const initialState = Immutable({ + setup: {} +}); + +describe('app/shared-hub/reducers/SetupLifecycleReducer', () => { + test('initial state is correct', () => { + expect(SetupLifecycleReducer(undefined, {})).toEqual({}); + }); + + test('reducer correctly handles INIT_SETUP_PROPS', () => { + const data = { + blockingPolicy: true, + enable_anti_tracking: true, + enable_ad_block: true, + enable_smart_block: true, + }; + const action = { data, type: INIT_SETUP_PROPS }; + + const updatedSetupLifecycleState = Immutable.merge(initialState.setup, data); + + expect(SetupLifecycleReducer(initialState, action)).toEqual({ + setup: updatedSetupLifecycleState + }); + }); +}); diff --git a/app/shared-hub/reducers/__tests__/ToastReducer.test.js b/app/shared-hub/reducers/__tests__/ToastReducer.test.js new file mode 100644 index 000000000..e0808d3f1 --- /dev/null +++ b/app/shared-hub/reducers/__tests__/ToastReducer.test.js @@ -0,0 +1,39 @@ +/** + * Toast Test Reducer + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import Immutable from 'seamless-immutable'; +import ToastReducer from '../ToastReducer'; +import SET_TOAST from '../../constants/ToastConstants'; + +const initialState = Immutable({ + toast: {} +}); + +describe('app/shared-hub/reducers/ToastReducer', () => { + test('initial state is correct', () => { + expect(ToastReducer(undefined, {})).toEqual({}); + }); + + test('reducer correctly handles SET_TOAST', () => { + const data = { + toastMessage: 'Toaster', + toastClass: 'danger' + }; + const action = { data, type: SET_TOAST }; + + const updatedToastReducerState = Immutable.merge(initialState.toast, data); + expect(ToastReducer(initialState, action)).toEqual({ + toast: updatedToastReducerState + }); + }); +}); diff --git a/app/shared-hub/utils/index.js b/app/shared-hub/utils/index.js index 55f478196..601e9eb22 100644 --- a/app/shared-hub/utils/index.js +++ b/app/shared-hub/utils/index.js @@ -20,7 +20,6 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; // Imports utilities from elsewhere in the codebase to reduce duplicate code import { log } from '../../../src/utils/common'; @@ -107,14 +106,14 @@ function buildReduxHOC(stateKeys, actionCreators, baseComponent) { actions: bindActionCreators(actionCreators, dispatch) }); - return withRouter(connect(mapStateToProps, mapDispatchToProps)(baseComponent)); + return connect(mapStateToProps, mapDispatchToProps)(baseComponent); } export { buildReduxHOC, makeStoreCreator, - makeDeferredDispatcher, log, sendMessage, - sendMessageInPromise + sendMessageInPromise, + makeDeferredDispatcher }; From 3630e939afc16698daab4863a7d32480fb03bab1 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Tue, 19 Jan 2021 09:27:49 -0500 Subject: [PATCH 062/113] Remove updates and promotions checkbox and dropdown (#659) * Remove learn more dropdown and update design * Remove updates and promotions checkbox and automatically subscribe the user to emails upon registering in the hub instead * Remove , --- _locales/en/messages.json | 6 --- app/Account/AccountActions.js | 8 ++-- app/Account/AccountConstants.js | 2 +- app/Account/AccountReducer.js | 8 ++-- .../Step1_CreateAccountForm.jsx | 23 ---------- .../Step1_CreateAccountForm.scss | 7 --- .../Step1_CreateAccountFormContainer.jsx | 11 +---- .../Step1_CreateAccountForm/index.js | 4 +- .../Step1_CreateAccountView.jsx | 43 ++++--------------- .../Step1_CreateAccountView.scss | 22 +++------- app/hub/Views/CreateAccountView/index.js | 2 +- 11 files changed, 28 insertions(+), 108 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 17ebeae87..041c350bd 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1848,12 +1848,6 @@ "ghostery_browser_hub_onboarding_already_have_account": { "message": "I already have an account." }, - "ghostery_browser_hub_onboarding_send_me": { - "message": "Send me" - }, - "ghostery_browser_hub_onboarding_updates_and_promotions": { - "message": "updates & promotions." - }, "ghostery_browser_hub_onboarding_skip": { "message": "Skip" }, diff --git a/app/Account/AccountActions.js b/app/Account/AccountActions.js index c51317dec..9211d5c8b 100644 --- a/app/Account/AccountActions.js +++ b/app/Account/AccountActions.js @@ -28,7 +28,7 @@ import { GET_USER_SETTINGS_FAIL, GET_USER_SUBSCRIPTION_DATA_FAIL, GET_USER_SUBSCRIPTION_DATA_SUCCESS, - ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE + SUBSCRIBE_TO_EMAIL_LIST } from './AccountConstants'; import { SET_TOAST } from '../hub/Views/AppView/AppViewConstants'; import { CLEAR_THEME } from '../panel/constants/constants'; @@ -203,9 +203,9 @@ export const resetPassword = email => dispatch => ( }) ); -export const handleEmailPreferencesCheckboxChange = (name, checked) => dispatch => ( +export const subscribeToEmailList = name => dispatch => ( dispatch({ - type: ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE, - payload: { name, checked }, + type: SUBSCRIBE_TO_EMAIL_LIST, + payload: { name }, }) ); diff --git a/app/Account/AccountConstants.js b/app/Account/AccountConstants.js index 0f95a09c4..e3a4f2bae 100644 --- a/app/Account/AccountConstants.js +++ b/app/Account/AccountConstants.js @@ -38,4 +38,4 @@ export const GET_USER_SUBSCRIPTION_DATA_FAIL = 'GET_USER_SUBSCRIPTION_DATA_FAIL' export const GET_USER_SUBSCRIPTION_DATA_SUCCESS = 'GET_USER_SUBSCRIPTION_DATA_SUCCESS'; // Opt into/out of updates and promotions sendgrid emails -export const ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE = 'ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE'; +export const SUBSCRIBE_TO_EMAIL_LIST = 'SUBSCRIBE_TO_EMAIL_LIST'; diff --git a/app/Account/AccountReducer.js b/app/Account/AccountReducer.js index 0a91127a1..e8b63cc0b 100644 --- a/app/Account/AccountReducer.js +++ b/app/Account/AccountReducer.js @@ -21,7 +21,7 @@ import { GET_USER_SUBSCRIPTION_DATA_SUCCESS, RESET_PASSWORD_SUCCESS, RESET_PASSWORD_FAIL, - ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE + SUBSCRIBE_TO_EMAIL_LIST } from './AccountConstants'; import { UPDATE_PANEL_DATA } from '../panel/constants/constants'; @@ -119,11 +119,11 @@ export default (state = initialState, action) => { resetPasswordError: true }; } - case ACCOUNT_DATA_EMAIL_PREFERENCES_CHECKBOX_CHANGE: { - const { name, checked } = action.payload; + case SUBSCRIBE_TO_EMAIL_LIST: { + const { name } = action.payload; let emailPreferences; if (name === 'global') { - emailPreferences = { ...state.user.emailPreferences, ...{ global: checked } }; + emailPreferences = { ...state.user.emailPreferences, ...{ global: true } }; } const user = { ...state.user, ...{ emailPreferences } }; return { ...state, ...{ user } }; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx index 5d2c74352..fb5634a9d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx @@ -16,8 +16,6 @@ import PropTypes from 'prop-types'; import ClassNames from 'classnames'; import ToggleCheckbox from '../../../../shared-components/ToggleCheckbox'; -const promoString = `${t('ghostery_browser_hub_onboarding_send_me')} Ghostery ${t('ghostery_browser_hub_onboarding_updates_and_promotions')}`; - /** * A Functional React component for rendering the Browser Create Account View * @return {JSX} JSX for rendering the Browser Create Account View of the Hub app @@ -39,9 +37,7 @@ export const Step1_CreateAccountForm = (props) => { legalConsentChecked, legalConsentNotCheckedError, handleLegalConsentCheckboxChange, - isUpdatesChecked, handleInputChange, - handleUpdatesCheckboxChange, handleSubmit, } = props; @@ -192,23 +188,6 @@ export const Step1_CreateAccountForm = (props) => { )}
    -
    -
    -
    - - -
    -
    -
    -
    @@ -242,13 +221,11 @@ Step1_CreateAccountForm.propTypes = { confirmEmailError: PropTypes.bool.isRequired, firstName: PropTypes.string.isRequired, lastName: PropTypes.string.isRequired, - isUpdatesChecked: PropTypes.bool.isRequired, password: PropTypes.string.isRequired, confirmPassword: PropTypes.string.isRequired, passwordInvalidError: PropTypes.bool.isRequired, passwordLengthError: PropTypes.bool.isRequired, handleInputChange: PropTypes.func.isRequired, - handleUpdatesCheckboxChange: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired, }; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss index 2b217c66b..c7e02dcfd 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss @@ -48,13 +48,6 @@ color: red; } } -.Step1_CreateAccountForm__promoString { - font-size: 14px; - margin-top: 15px; - @include breakpoint(small down) { - margin-top: 0; - } -} .Step1_CreateAccountForm__legalConsentCheckedLabel { font-size: 14px; @include breakpoint(small down) { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx index f8c922431..0cd3e4fac 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx @@ -113,13 +113,6 @@ class CreateAccountFormContainer extends Component { this.setState(prevState => ({ legalConsentChecked: !prevState.legalConsentChecked })); } - /** - * Update updates checkbox value by updating state - */ - _handleUpdatesCheckboxChange = () => { - this.setState(prevState => ({ isUpdatesChecked: !prevState.isUpdatesChecked })); - } - /** * Handle creating an account, but validate the data first. * @param {Object} event the 'submit' event @@ -134,7 +127,6 @@ class CreateAccountFormContainer extends Component { legalConsentChecked, password, confirmPassword, - isUpdatesChecked, } = this.state; const emailIsValid = email && validateEmail(email); const confirmIsValid = confirmEmail && validateConfirmEmail(email, confirmEmail); @@ -164,7 +156,7 @@ class CreateAccountFormContainer extends Component { if (success) { // User is automatically logged in, and redirected to the logged in view of BrowserCreateAccountForm actions.getUser().then(() => { - if (isUpdatesChecked) actions.handleEmailPreferencesCheckboxChange('global', isUpdatesChecked); + actions.subscribeToEmailList('global'); }); // Toggle legal consent checked here actions.setToast({ @@ -222,7 +214,6 @@ class CreateAccountFormContainer extends Component { handleLegalConsentCheckboxChange={this._handleLegalConsentCheckboxChange} handleSubmit={this._handleCreateAccountAttempt} isUpdatesChecked={isUpdatesChecked} - handleUpdatesCheckboxChange={this._handleUpdatesCheckboxChange} /> ); } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js index b6d7f7592..daa83996b 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/index.js @@ -13,7 +13,7 @@ import { buildReduxHOC } from '../../../../shared-hub/utils'; import Step1_CreateAccountFormContainer from './Step1_CreateAccountFormContainer'; -import { register, getUser, handleEmailPreferencesCheckboxChange } from '../../../../Account/AccountActions'; +import { register, getUser, subscribeToEmailList } from '../../../../Account/AccountActions'; import { setToast } from '../../../../hub/Views/AppView/AppViewActions'; const stateSlices = ['account']; @@ -21,7 +21,7 @@ const actionCreators = { setToast, register, getUser, - handleEmailPreferencesCheckboxChange + subscribeToEmailList }; export default buildReduxHOC(stateSlices, actionCreators, Step1_CreateAccountFormContainer); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx index d664ad1ea..ca105bfa4 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx @@ -11,10 +11,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React, { Fragment, useRef, useState } from 'react'; +import React, { Fragment, useState } from 'react'; import { NavLink } from 'react-router-dom'; import PropTypes from 'prop-types'; -import ClassNames from 'classnames'; import Step1_LogInForm from '../Step1_LogInForm'; import Step1_CreateAccountForm from '../Step1_CreateAccountForm'; import globals from '../../../../../src/classes/Globals'; @@ -38,10 +37,10 @@ const faqList = [ const renderFAQListItem = (icon, label, description) => (
    -
    +
    -
    +
    {label}
    {description}
    @@ -58,24 +57,8 @@ const Step1_CreateAccountView = (props) => { const { setSetupStep } = actions; const email = user && user.email; - const [expanded, setExpanded] = useState(false); const [view, setView] = useState(CREATE_ACCOUNT); - const arrowClassNames = ClassNames('Step1_CreateAccountView__arrow', { - up: expanded, - down: !expanded, - }); - - const faqRef = useRef(null); - const scrollToFAQ = () => { - faqRef.current.scrollIntoView({ behavior: 'smooth' }); - }; - - const handleFAQLearnMoreClick = () => { - setTimeout(scrollToFAQ, 1); - setExpanded(!expanded); - }; - const renderSkipLink = () => (
    @@ -120,22 +103,14 @@ const Step1_CreateAccountView = (props) => { {/* eslint-disable-next-line react/jsx-pascal-case */} {renderSkipLink()} -
    -
    {t('ghostery_browser_hub_onboarding_we_take_your_privacy_very_seriously')}
    +
    + {faqList.map(item => renderFAQListItem(item.icon, item.label, item.description))}
    -
    -
    - {expanded && - faqList.map(item => renderFAQListItem(item.icon, item.label, item.description)) - } + - {expanded && ( - - )} ) : ( diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss index f2fa8acb5..9239c1012 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss @@ -174,27 +174,17 @@ $color-create-account-form-error-red: #e74055; text-decoration: underline; cursor: pointer; } -.Step1_CreateAccountView__arrow { - margin: 15px auto 0 auto; - height: 12px; - width: 12px; - border-left: 2px solid #00aef0; - border-top: 2px solid #00aef0; - cursor: pointer; - - &.up { - transform: rotate(45deg); - } - &.down { - transform: rotate(225deg); - } -} .Step1_CreateAccountView__FAQContainer { - margin-top: 10px; + margin-top: 48px; padding-top: 20px; @include breakpoint(small down) { + margin-top: 42px; text-align: center; } + @include breakpoint(large up) { + width: 108%; + margin-left: -4%; + } } .Step1_CreateAccountView__faqItemContainer { margin-bottom: 30px; diff --git a/app/hub/Views/CreateAccountView/index.js b/app/hub/Views/CreateAccountView/index.js index da18f1190..e3da2f3f4 100644 --- a/app/hub/Views/CreateAccountView/index.js +++ b/app/hub/Views/CreateAccountView/index.js @@ -15,7 +15,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import CreateAccountViewContainer from './CreateAccountViewContainer'; -import { register, getUser } from '../../../Account/AccountActions'; +import { register, getUser, subscribeToEmailList } from '../../../Account/AccountActions'; import { setToast } from '../AppView/AppViewActions'; /** From 3e7e2b96ad1f3701bafd203b6a1d06750f81f676 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Wed, 20 Jan 2021 10:05:10 -0500 Subject: [PATCH 063/113] Fix routing bug --- .../Views/OnboardingViews/Step2_BlockSettingsView/index.js | 3 ++- .../OnboardingViews/Step3_ChooseDefaultSearchView/index.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js index d3bfb3ae0..1d8b1c048 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js @@ -11,6 +11,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ +import { withRouter } from 'react-router-dom'; import BlockSettingsView from './BlockSettingsView'; import { buildReduxHOC } from '../../../../shared-hub/utils'; import { logout } from '../../../../Account/AccountActions'; @@ -29,4 +30,4 @@ const actionCreators = { setSetupStep, }; -export default buildReduxHOC(null, actionCreators, BlockSettingsView); +export default withRouter(buildReduxHOC(null, actionCreators, BlockSettingsView)); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js index a3a19d61d..e7740acf9 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js @@ -11,6 +11,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ +import { withRouter } from 'react-router-dom'; import ChooseDefaultSearchView from './ChooseDefaultSearchView'; import { buildReduxHOC } from '../../../../shared-hub/utils'; import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; @@ -19,4 +20,4 @@ const actionCreators = { setSetupStep, }; -export default buildReduxHOC(null, actionCreators, ChooseDefaultSearchView); +export default withRouter(buildReduxHOC(null, actionCreators, ChooseDefaultSearchView)); From c423fd770a74361a05ce85fd0f6a7f8345fe8f05 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Wed, 20 Jan 2021 10:46:01 -0500 Subject: [PATCH 064/113] Step Progress Bar Bug (#661) * Fix cross browser progress bar lines and reduce size of progress bar on step 4 * Keep ghosty steps in place and fix the line lengths * Fix navigation --- .../Step4_ChoosePlanView/ChoosePlanView.scss | 1 + .../StepProgressBar/StepProgressBar.jsx | 6 +++++- .../StepProgressBar/StepProgressBar.scss | 16 ++++++++++++++-- .../hub/step-progress-bar/step-1-current.svg | 2 +- .../hub/step-progress-bar/step-2-current.svg | 2 +- .../hub/step-progress-bar/step-2-incomplete.svg | 2 +- .../hub/step-progress-bar/step-3-current.svg | 2 +- .../hub/step-progress-bar/step-3-incomplete.svg | 2 +- .../hub/step-progress-bar/step-4-current.svg | 2 +- .../hub/step-progress-bar/step-4-incomplete.svg | 2 +- .../hub/step-progress-bar/step-completed.svg | 2 +- 11 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss index 02a71cd6b..a82c80be1 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss @@ -121,6 +121,7 @@ $medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow t &:hover { background-position: 0% 50%; transition: 0.25s all; + color: $white; } &:focus { color: $white; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx index 7d52f15c1..48ad68076 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx @@ -57,6 +57,10 @@ const StepProgressBar = (props) => { const currentStepNumber = parseInt(currentStep, 10); const totalSteps = steps.length; + const progressBarContainerClasses = ClassNames('StepProgressBarContainer', { + step4: currentStepNumber === 4 + }); + const logoutIfStepOne = (stepId) => { const { actions } = props; const { logout } = actions; @@ -121,7 +125,7 @@ const StepProgressBar = (props) => { ); return ( -
    +
    {(currentStep !== WELCOME) && renderProgressBar()}
    ); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss index da4abb86e..a8bd86786 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss @@ -18,7 +18,12 @@ align-items: center; max-width: 800px; margin: auto; - padding-top: 10px; + padding-top: 10px; + + &.step4 { + max-width: 724px; + padding: 10px 20px 0 20px; + } } .StepProgressBar__Step { @@ -40,6 +45,8 @@ &.incomplete { background: url('/app/images/hub/step-progress-bar/step-2-incomplete.svg'); background-repeat: no-repeat; + margin: 0 2px 3px 0; + height: 32px; } } &.step-3 { @@ -50,6 +57,8 @@ &.incomplete { background: url('/app/images/hub/step-progress-bar/step-3-incomplete.svg'); background-repeat: no-repeat; + margin: 0 2px 3px 0; + height: 32px; } } &.step-4 { @@ -60,6 +69,8 @@ &.incomplete { background: url('/app/images/hub/step-progress-bar/step-4-incomplete.svg'); background-repeat: no-repeat; + margin: 0 2px 3px 0; + height: 32px; } } &.step-completed { @@ -89,9 +100,10 @@ width: 100%; &.completed { border: solid 2px $ghosty-blue; + width: 110%; } &.incompleted { - padding: 1em; + padding: 13px; background-image: radial-gradient(circle at 2.5px, $ghosty-blue 1.25px, rgba(255,255,255,0) 2.5px), radial-gradient(circle, $ghosty-blue 1.25px, rgba(255,255,255,0) 2.5px), diff --git a/app/images/hub/step-progress-bar/step-1-current.svg b/app/images/hub/step-progress-bar/step-1-current.svg index 9fb8c8dad..f909351a2 100644 --- a/app/images/hub/step-progress-bar/step-1-current.svg +++ b/app/images/hub/step-progress-bar/step-1-current.svg @@ -1 +1 @@ -1 \ No newline at end of file +1 diff --git a/app/images/hub/step-progress-bar/step-2-current.svg b/app/images/hub/step-progress-bar/step-2-current.svg index 7d8819a59..334222a0f 100644 --- a/app/images/hub/step-progress-bar/step-2-current.svg +++ b/app/images/hub/step-progress-bar/step-2-current.svg @@ -1 +1 @@ -2 \ No newline at end of file +2 diff --git a/app/images/hub/step-progress-bar/step-2-incomplete.svg b/app/images/hub/step-progress-bar/step-2-incomplete.svg index 1dc3fef5f..b85269220 100644 --- a/app/images/hub/step-progress-bar/step-2-incomplete.svg +++ b/app/images/hub/step-progress-bar/step-2-incomplete.svg @@ -1 +1 @@ -2 \ No newline at end of file +2 diff --git a/app/images/hub/step-progress-bar/step-3-current.svg b/app/images/hub/step-progress-bar/step-3-current.svg index 060d61018..6e8c4a48f 100644 --- a/app/images/hub/step-progress-bar/step-3-current.svg +++ b/app/images/hub/step-progress-bar/step-3-current.svg @@ -1 +1 @@ -3 \ No newline at end of file +3 diff --git a/app/images/hub/step-progress-bar/step-3-incomplete.svg b/app/images/hub/step-progress-bar/step-3-incomplete.svg index de1957611..3e5ac18a2 100644 --- a/app/images/hub/step-progress-bar/step-3-incomplete.svg +++ b/app/images/hub/step-progress-bar/step-3-incomplete.svg @@ -1,4 +1,4 @@ - + diff --git a/app/images/hub/step-progress-bar/step-4-current.svg b/app/images/hub/step-progress-bar/step-4-current.svg index 019e8c5a5..e558755dc 100644 --- a/app/images/hub/step-progress-bar/step-4-current.svg +++ b/app/images/hub/step-progress-bar/step-4-current.svg @@ -1 +1 @@ -4 \ No newline at end of file +4 diff --git a/app/images/hub/step-progress-bar/step-4-incomplete.svg b/app/images/hub/step-progress-bar/step-4-incomplete.svg index 73db72f1b..ee06f911b 100644 --- a/app/images/hub/step-progress-bar/step-4-incomplete.svg +++ b/app/images/hub/step-progress-bar/step-4-incomplete.svg @@ -1 +1 @@ -4 \ No newline at end of file +4 diff --git a/app/images/hub/step-progress-bar/step-completed.svg b/app/images/hub/step-progress-bar/step-completed.svg index f4320607c..0ff6754ad 100644 --- a/app/images/hub/step-progress-bar/step-completed.svg +++ b/app/images/hub/step-progress-bar/step-completed.svg @@ -1 +1 @@ - \ No newline at end of file + From f8777913691f26874b1f1388eac9e58c02c2ad48 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Wed, 20 Jan 2021 12:58:06 -0500 Subject: [PATCH 065/113] Update locale string keys to match new Dawn branding --- _locales/en/messages.json | 147 +++++++++--------- .../OnboardingViewContainer.jsx | 2 +- .../Step0_WelcomeView/WelcomeView.jsx | 6 +- .../Step1_CreateAccountView.jsx | 22 +-- .../BlockSettingsView.jsx | 28 ++-- .../ChooseDefaultSearchView.jsx | 9 +- .../Step4_ChoosePlanView/ChoosePlanView.jsx | 64 ++++---- .../Step5_SuccessView/SuccessView.jsx | 8 +- .../StepProgressBar/StepProgressBar.jsx | 6 +- 9 files changed, 146 insertions(+), 146 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 041c350bd..7c87a4a6d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1740,223 +1740,226 @@ "hub_create_account_toast_error": { "message": "That email address is already in use. Please choose another." }, - "ghostery_browser_hub_onboarding_install_heading": { + "ghostery_dawn_onboarding_install_heading": { "message": "Ghostery Dawn now installing..." }, - "ghostery_browser_hub_onboarding_page_title": { + "ghostery_dawn_onboarding_page_title": { "message": "Ghostery Dawn Hub - Welcome" }, - "ghostery_browser_hub_onboarding_header_title_login": { + "ghostery_dawn_onboarding_header_title_login": { "message": "Ghostery Dawn Hub - Login" }, - "ghostery_browser_hub_onboarding_header_title_block_settings": { + "ghostery_dawn_onboarding_header_title_block_settings": { "message": "Ghostery Dawn Hub - Block Settings" }, - "ghostery_browser_hub_onboarding_header_title_search_choice": { + "ghostery_dawn_onboarding_header_title_search_choice": { "message": "Ghostery Dawn Hub - Search Choice" }, - "ghostery_browser_hub_onboarding_header_title_plan_choices": { + "ghostery_dawn_onboarding_header_title_plan_choices": { "message": "Ghostery Dawn Hub - Plan Choices" }, - "ghostery_browser_hub_onboarding_privacy": { + "ghostery_dawn_onboarding_privacy": { "message": "Privacy" }, - "ghostery_browser_hub_onboarding_search": { + "ghostery_dawn_onboarding_search": { "message": "Search" }, - "ghostery_browser_hub_onboarding_plan": { + "ghostery_dawn_onboarding_plan": { "message": "Plan" }, - "ghostery_browser_hub_onboarding_welcome": { + "ghostery_dawn_onboarding_welcome": { "message": "Welcome to Ghostery Dawn!" }, - "ghostery_browser_hub_onboarding_lets_begin": { + "ghostery_dawn_onboarding_lets_begin": { "message": "We've centralized online privacy by integrating our signature as well as novel technologies." }, - "ghostery_browser_hub_onboarding_lets_do_this": { + "ghostery_dawn_onboarding_lets_do_this": { "message": "Set up My Browser" }, - "ghostery_browser_hub_onboarding_your_privacy_plan": { + "ghostery_dawn_onboarding_your_privacy_plan": { "message": "Your Privacy Plan" }, - "ghostery_browser_hub_onboarding_based_on_your_privacy_preferences": { + "ghostery_dawn_onboarding_based_on_your_privacy_preferences": { "message": "Based on your privacy preferences" }, - "ghostery_browser_hub_onboarding_ad_free_with_ghostery_plus_subscription": { + "ghostery_dawn_onboarding_ad_free_with_ghostery_plus_subscription": { "message": "Ad-free with Ghostery Plus subscription" }, - "ghostery_browser_hub_onboarding_ad_free_promo": { + "ghostery_dawn_onboarding_ad_free_promo": { "message": "40% off for the first 12 months" }, - "ghostery_browser_hub_onboarding_ad_free_promo_description": { + "ghostery_dawn_onboarding_ad_free_promo_description": { "message": "Get to what you want faster. A plus subscription gives Ghostery the support we need to provide YOU with an ad-free experience." }, - "ghostery_browser_hub_onboarding_or": { + "ghostery_dawn_onboarding_or": { "message": "OR" }, - "ghostery_browser_hub_onboarding_keep": { + "ghostery_dawn_onboarding_keep": { "message": "Keep" }, - "ghostery_browser_hub_onboarding_upgrade": { + "ghostery_dawn_onboarding_upgrade": { "message": "Upgrade" }, - "ghostery_browser_hub_onboarding_start_trial": { + "ghostery_dawn_onboarding_start_trial": { "message": "Start Trial" }, - "ghostery_browser_hub_onboarding_see_all_plans": { + "ghostery_dawn_onboarding_see_all_plans": { "message": "See all plans" }, - "ghostery_browser_hub_onboarding_private_search": { + "ghostery_dawn_onboarding_private_search": { "message": "Private search" }, - "ghostery_browser_hub_onboarding_tracker_protection": { + "ghostery_dawn_onboarding_tracker_protection": { "message": "Tracker protection" }, - "ghostery_browser_hub_onboarding_speedy_page_loads": { + "ghostery_dawn_onboarding_speedy_page_loads": { "message": "Speedy page loads" }, - "ghostery_browser_hub_onboarding_intelligence_technology": { + "ghostery_dawn_onboarding_intelligence_technology": { "message": "Intelligence technology" }, - "ghostery_browser_hub_onboarding_ad_free": { + "ghostery_dawn_onboarding_ad_free": { "message": "Ad free" }, - "ghostery_browser_hub_onboarding_supports_ghosterys_mission": { + "ghostery_dawn_onboarding_supports_ghosterys_mission": { "message": "Supports Ghostery's mission" }, - "ghostery_browser_hub_onboarding_unlimited_bandwidth": { + "ghostery_dawn_onboarding_unlimited_bandwidth": { "message": "Unlimited Bandwidth" }, - "ghostery_browser_hub_onboarding_already_premium_subscriber": { + "ghostery_dawn_onboarding_already_premium_subscriber": { "message": "You are already a premium subscriber" }, - "ghostery_browser_hub_onboarding_already_plus_subscriber": { + "ghostery_dawn_onboarding_already_plus_subscriber": { "message": "You are already a plus subscriber" }, - "ghostery_browser_hub_onboarding_keep_your_current_plan_or_upgrade": { + "ghostery_dawn_onboarding_keep_your_current_plan_or_upgrade": { "message": "Keep your current plan or upgrade" }, - "ghostery_browser_hub_onboarding_choose_an_option": { + "ghostery_dawn_onboarding_choose_an_option": { "message": "Choose an option" }, - "ghostery_browser_hub_onboarding_create_a_ghostery_account": { + "ghostery_dawn_onboarding_create_a_ghostery_account": { "message": "Create a Ghostery Account" }, - "ghostery_browser_hub_onboarding_sync_settings": { + "ghostery_dawn_onboarding_sync_settings": { "message": "Allows you to sync settings across browsers and devices." }, - "ghostery_browser_hub_onboarding_already_have_account": { + "ghostery_dawn_onboarding_already_have_account": { "message": "I already have an account." }, - "ghostery_browser_hub_onboarding_skip": { + "ghostery_dawn_onboarding_skip": { "message": "Skip" }, - "ghostery_browser_hub_onboarding_back": { + "ghostery_dawn_onboarding_back": { "message": "Back" }, - "ghostery_browser_hub_onboarding_we_take_your_privacy_very_seriously": { + "ghostery_dawn_onboarding_we_take_your_privacy_very_seriously": { "message": "We take your privacy very seriously. Learn more" }, - "ghostery_browser_hub_onboarding_private_by_design": { + "ghostery_dawn_onboarding_private_by_design": { "message": "Private by design" }, - "ghostery_browser_hub_onboarding_private_by_design_description": { + "ghostery_dawn_onboarding_private_by_design_description": { "message": "Privacy is incredibly important to us at Ghostery. That’s why we enforce a strict policy when it comes to your data. When you create an account at Ghostery, the only data we collect is your name and email. This information allows us to offer you a user experience wherein you can sync your settings across devices and hear from us about important updates. Under no circumstances do we share or sell this data with any kind of third party." }, - "ghostery_browser_hub_onboarding_can_i_remove_my_account": { + "ghostery_dawn_onboarding_can_i_remove_my_account": { "message": "Can I remove my account?" }, - "ghostery_browser_hub_onboarding_can_i_remove_my_account_description": { + "ghostery_dawn_onboarding_can_i_remove_my_account_description": { "message": "We hope you enjoy using Ghostery, but if you do decide to leave we will remove all of your personal information within 90 days from the date of request." }, - "ghostery_browser_hub_onboarding_visit_our_privacy_policy": { + "ghostery_dawn_onboarding_visit_our_privacy_policy": { "message": "Visit our Privacy Policy for more information" }, - "ghostery_browser_hub_onboarding_create_an_account": { + "ghostery_dawn_onboarding_create_an_account": { "message": "Create an account." }, - "ghostery_browser_hub_onboarding_you_are_signed_in_as": { + "ghostery_dawn_onboarding_you_are_signed_in_as": { "message": "You are signed in as" }, - "ghostery_browser_hub_onboarding_youve_successfully_set_up_your_browser": { + "ghostery_dawn_onboarding_youve_successfully_set_up_your_browser": { "message": "You've successfully set up your browser!" }, - "ghostery_browser_hub_onboarding_surf_with_ease": { + "ghostery_dawn_onboarding_surf_with_ease": { "message": "Now you can surf with the greatest of ease." }, - "ghostery_browser_hub_onboarding_start_browsing": { + "ghostery_dawn_onboarding_start_browsing": { "message": "Start Browsing" }, - "ghostery_browser_hub_onboarding_which_privacy_plan": { + "ghostery_dawn_onboarding_which_privacy_plan": { "message": "Which privacy plan is right for you?" }, - "ghostery_browser_hub_onboarding_tell_us_your_preferences": { + "ghostery_dawn_onboarding_tell_us_your_preferences": { "message": "Tell us your preferences, we'll offer you our recommendation." }, - "ghostery_browser_hub_onboarding_recommended_choices": { + "ghostery_dawn_onboarding_recommended_choices": { "message": "Recommended choices" }, - "ghostery_browser_hub_onboarding_question_block_ads": { + "ghostery_dawn_onboarding_question_block_ads": { "message": "Do you want to block ads?" }, - "ghostery_browser_hub_onboarding_question_kinds_of_trackers": { + "ghostery_dawn_onboarding_question_kinds_of_trackers": { "message": "What kinds of trackers do you want to block?" }, - "ghostery_browser_hub_onboarding_kinds_of_trackers_all": { + "ghostery_dawn_onboarding_kinds_of_trackers_all": { "message": "All trackers" }, - "ghostery_browser_hub_onboarding_kinds_of_trackers_none": { + "ghostery_dawn_onboarding_kinds_of_trackers_none": { "message": "No trackers" }, - "ghostery_browser_hub_onboarding_kinds_of_trackers_ad_and_analytics": { + "ghostery_dawn_onboarding_kinds_of_trackers_ad_and_analytics": { "message": "All ad and analytics trackers" }, - "ghostery_browser_hub_onboarding_question_anti_tracking": { + "ghostery_dawn_onboarding_question_anti_tracking": { "message": "Do you want to turn on anti-tracking?" }, - "ghostery_browser_hub_onboarding_question_smart_browsing": { + "ghostery_dawn_onboarding_question_smart_browsing": { "message": "Do you want to turn on smart-browsing?" }, - "ghostery_browser_hub_onboarding_info_blocking_all": { + "ghostery_dawn_onboarding_info_blocking_all": { "message": "Blocking \"all trackers\" may cause some websites to break." }, - "ghostery_browser_hub_onboarding_info_anti_tracking": { + "ghostery_dawn_onboarding_info_anti_tracking": { "message": "Anti-tracking anonymizes uniquely identifiable data that trackers try to collect." }, - "ghostery_browser_hub_onboarding_info_smart_browsing": { + "ghostery_dawn_onboarding_info_smart_browsing": { "message": "Smart-browsing adjusts your blocking settings to decrease page breakage and accelerate page loads." }, "ghostery_browser_hub__onboarding_choose_your_default_search": { "message": "Choose your default search" }, - "ghostery_browser_hub_onboarding_pick_a_default_search_engine": { + "ghostery_dawn_onboarding_pick_a_default_search_engine": { "message": "Pick a default search engine for all your searches." }, - "ghostery_browser_hub_onboarding_ad_free_private_search": { + "ghostery_dawn_onboarding_ad_free_private_search": { "message": "Ad-free private search" }, - "ghostery_browser_hub_onboarding_ad_supported_private_search": { + "ghostery_dawn_onboarding_recommended" : { + "message": "(Recommended)" + }, + "ghostery_dawn_onboarding_ad_supported_private_search": { "message": "Ad-supported private search" }, - "ghostery_browser_hub_onboarding_type_in_search_domain": { + "ghostery_dawn_onboarding_type_in_search_domain": { "message": "Type in search domain" }, - "ghostery_browser_hub_onboarding_just_so_you_know": { + "ghostery_dawn_onboarding_just_so_you_know": { "message": "Just so you know:" }, - "ghostery_browser_hub_onboarding_search_engine_will_log_your_data": { + "ghostery_dawn_onboarding_search_engine_will_log_your_data": { "message": "search engine will log your data and use it to serve you targeted ads." }, - "ghostery_browser_hub_onboarding_go_back": { + "ghostery_dawn_onboarding_go_back": { "message": "Go Back" }, - "ghostery_browser_hub_onboarding_confirm": { + "ghostery_dawn_onboarding_confirm": { "message": "Confirm" }, - "ghostery_browser_hub_onboarding_other": { + "ghostery_dawn_onboarding_other": { "message": "Other" }, - "ghostery_browser_hub_onboarding_toast_error": { + "ghostery_dawn_onboarding_toast_error": { "message": "Please answer all questions" }, "enable_when_paused": { diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index dbb9aef80..afa0afc25 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -49,7 +49,7 @@ class OnboardingViewContainer extends Component { history.push(`/${ONBOARDING}/${WELCOME}`); // TODO verify what document title we should use - const title = t('ghostery_browser_hub_onboarding_page_title'); + const title = t('ghostery_dawn_onboarding_page_title'); window.document.title = title; // TODO modify this as needed diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx index 7f53c3b5d..1b25e5772 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx @@ -26,11 +26,11 @@ const WelcomeView = (props) => { const { setSetupStep } = actions; return (
    -
    {t('ghostery_browser_hub_onboarding_welcome')}
    -
    {t('ghostery_browser_hub_onboarding_lets_begin')}
    +
    {t('ghostery_dawn_onboarding_welcome')}
    +
    {t('ghostery_dawn_onboarding_lets_begin')}
    setSetupStep({ setup_step: LOGIN, origin: WELCOME })}> - {t('ghostery_browser_hub_onboarding_lets_do_this')} + {t('ghostery_dawn_onboarding_lets_do_this')}
    ); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx index ca105bfa4..d03416f34 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx @@ -25,13 +25,13 @@ const CREATE_ACCOUNT = 'CREATE_ACCOUNT'; const faqList = [ { icon: 'ghosty-shield.svg', - label: t('ghostery_browser_hub_onboarding_private_by_design'), - description: t('ghostery_browser_hub_onboarding_private_by_design_description'), + label: t('ghostery_dawn_onboarding_private_by_design'), + description: t('ghostery_dawn_onboarding_private_by_design_description'), }, { icon: 'ghosty-box.svg', - label: t('ghostery_browser_hub_onboarding_can_i_remove_my_account'), - description: t('ghostery_browser_hub_onboarding_can_i_remove_my_account_description'), + label: t('ghostery_dawn_onboarding_can_i_remove_my_account'), + description: t('ghostery_dawn_onboarding_can_i_remove_my_account_description'), } ]; @@ -64,7 +64,7 @@ const Step1_CreateAccountView = (props) => {
    setSetupStep({ setup_step: LOGIN, origin: ONBOARDING })}> - {t('ghostery_browser_hub_onboarding_skip')} + {t('ghostery_dawn_onboarding_skip')}
    @@ -72,7 +72,7 @@ const Step1_CreateAccountView = (props) => { return (user ? (
    -
    {t('ghostery_browser_hub_onboarding_you_are_signed_in_as')}
    +
    {t('ghostery_dawn_onboarding_you_are_signed_in_as')}
    {email}
    setSetupStep({ setup_step: LOGIN, origin: ONBOARDING })}> @@ -83,18 +83,18 @@ const Step1_CreateAccountView = (props) => { ) : (
    {view === CREATE_ACCOUNT && ( -
    {t('ghostery_browser_hub_onboarding_create_a_ghostery_account')}
    +
    {t('ghostery_dawn_onboarding_create_a_ghostery_account')}
    )} {view === SIGN_IN && (
    {t('sign_in')}
    )} -
    { t('ghostery_browser_hub_onboarding_sync_settings') }
    +
    { t('ghostery_dawn_onboarding_sync_settings') }
    {view === CREATE_ACCOUNT && ( -
    setView(SIGN_IN)}>{t('ghostery_browser_hub_onboarding_already_have_account')}
    +
    setView(SIGN_IN)}>{t('ghostery_dawn_onboarding_already_have_account')}
    )} {view === SIGN_IN && ( -
    setView(CREATE_ACCOUNT)}>{t('ghostery_browser_hub_onboarding_create_an_account')}
    +
    setView(CREATE_ACCOUNT)}>{t('ghostery_dawn_onboarding_create_an_account')}
    )}
    @@ -108,7 +108,7 @@ const Step1_CreateAccountView = (props) => {
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index 1f2d3074c..1e8242294 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -118,13 +118,13 @@ class BlockSettingsView extends Component {
    logout()}> - {t('ghostery_browser_hub_onboarding_back')} + {t('ghostery_dawn_onboarding_back')}
    -
    {t('ghostery_browser_hub_onboarding_which_privacy_plan')}
    -
    {t('ghostery_browser_hub_onboarding_tell_us_your_preferences')}
    +
    {t('ghostery_dawn_onboarding_which_privacy_plan')}
    +
    {t('ghostery_dawn_onboarding_tell_us_your_preferences')}
    this.toggleRecommendedChoices(!recommendedChoices)} /> -
    {t('ghostery_browser_hub_onboarding_recommended_choices')}
    +
    {t('ghostery_dawn_onboarding_recommended_choices')}
      -
    1. {t('ghostery_browser_hub_onboarding_question_block_ads')}
    2. +
    3. {t('ghostery_dawn_onboarding_question_block_ads')}
    4. this.handleAnswerChange('blockAds', true)} altDesign /> @@ -150,34 +150,34 @@ class BlockSettingsView extends Component {
    5. - {t('ghostery_browser_hub_onboarding_question_kinds_of_trackers')} + {t('ghostery_dawn_onboarding_question_kinds_of_trackers')}
      - +
      this.handleAnswerChange('kindsOfTrackers', 0)} altDesign />
      -
      {t('ghostery_browser_hub_onboarding_kinds_of_trackers_all')}
      +
      {t('ghostery_dawn_onboarding_kinds_of_trackers_all')}
      this.handleAnswerChange('kindsOfTrackers', 1)} altDesign />
      -
      {t('ghostery_browser_hub_onboarding_kinds_of_trackers_ad_and_analytics')}
      +
      {t('ghostery_dawn_onboarding_kinds_of_trackers_ad_and_analytics')}
      this.handleAnswerChange('kindsOfTrackers', 2)} altDesign />
      -
      {t('ghostery_browser_hub_onboarding_kinds_of_trackers_none')}
      +
      {t('ghostery_dawn_onboarding_kinds_of_trackers_none')}
    6. - {t('ghostery_browser_hub_onboarding_question_anti_tracking')} + {t('ghostery_dawn_onboarding_question_anti_tracking')}
      - +
    7. @@ -195,9 +195,9 @@ class BlockSettingsView extends Component {
    8. - {t('ghostery_browser_hub_onboarding_question_smart_browsing')} + {t('ghostery_dawn_onboarding_question_smart_browsing')}
      - +
    9. diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 8ea228222..e332c456a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -22,14 +22,11 @@ const SEARCH_GHOSTERY = 'Ghostery'; const SEARCH_BING = 'Bing'; const SEARCH_YAHOO = 'Yahoo'; const SEARCH_STARTPAGE = 'StartPage'; -const SEARCH_CUSTOM = 'Custom'; class ChooseDefaultSearchView extends Component { constructor(props) { super(props); - this.customURLInputRef = React.createRef(); - this.state = { chosenSearch: SEARCH_GHOSTERY, searchBeingConsidered: null, @@ -72,8 +69,8 @@ class ChooseDefaultSearchView extends Component { renderGhosteryOptionDescription = () => ( -
      Ad-free private search
      -
      (Recommended)
      +
      {t('ghostery_dawn_onboarding_ad_free_private_search')}
      +
      {t('ghostery_dawn_onboarding_recommended')}
      ); @@ -150,7 +147,7 @@ class ChooseDefaultSearchView extends Component {
      - {t('ghostery_browser_hub_onboarding_back')} + {t('ghostery_dawn_onboarding_back')}
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 6ca34da40..2804941cf 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -26,9 +26,9 @@ const premiumCheckoutLink = `${globals.CHECKOUT_BASE_URL}/en/premium`; const searchPromo = () => (
    -
    { t('ghostery_browser_hub_onboarding_ad_free_with_ghostery_plus_subscription') }
    -
    { t('ghostery_browser_hub_onboarding_ad_free_promo') }
    -
    { t('ghostery_browser_hub_onboarding_ad_free_promo_description') }
    +
    { t('ghostery_dawn_onboarding_ad_free_with_ghostery_plus_subscription') }
    +
    { t('ghostery_dawn_onboarding_ad_free_promo') }
    +
    { t('ghostery_dawn_onboarding_ad_free_promo_description') }
    ); @@ -54,19 +54,19 @@ const basicCard = (checked, handleClick) => {
    - {t('ghostery_browser_hub_onboarding_private_search')} + {t('ghostery_dawn_onboarding_private_search')}
    - {t('ghostery_browser_hub_onboarding_tracker_protection')} + {t('ghostery_dawn_onboarding_tracker_protection')}
    - {t('ghostery_browser_hub_onboarding_speedy_page_loads')} + {t('ghostery_dawn_onboarding_speedy_page_loads')}
    - {t('ghostery_browser_hub_onboarding_intelligence_technology')} + {t('ghostery_dawn_onboarding_intelligence_technology')}
    @@ -100,27 +100,27 @@ const premiumCard = (checked, handleClick, showCTAButton = false) => {
    - {t('ghostery_browser_hub_onboarding_private_search')} + {t('ghostery_dawn_onboarding_private_search')}
    - {t('ghostery_browser_hub_onboarding_tracker_protection')} + {t('ghostery_dawn_onboarding_tracker_protection')}
    - {t('ghostery_browser_hub_onboarding_speedy_page_loads')} + {t('ghostery_dawn_onboarding_speedy_page_loads')}
    - {t('ghostery_browser_hub_onboarding_intelligence_technology')} + {t('ghostery_dawn_onboarding_intelligence_technology')}
    - {t('ghostery_browser_hub_onboarding_ad_free')} + {t('ghostery_dawn_onboarding_ad_free')}
    - {t('ghostery_browser_hub_onboarding_supports_ghosterys_mission')} + {t('ghostery_dawn_onboarding_supports_ghosterys_mission')}
    @@ -128,13 +128,13 @@ const premiumCard = (checked, handleClick, showCTAButton = false) => {
    - {t('ghostery_browser_hub_onboarding_unlimited_bandwidth')} + {t('ghostery_dawn_onboarding_unlimited_bandwidth')}
    {showCTAButton && ( - {t('ghostery_browser_hub_onboarding_upgrade')} + {t('ghostery_dawn_onboarding_upgrade')} )} ); @@ -202,9 +202,9 @@ class ChoosePlanView extends React.Component { const isPlus = (user && user.plusAccess) || false; const isPremium = (user && user.premiumAccess) || false; - if (isPremium) return t('ghostery_browser_hub_onboarding_already_premium_subscriber'); - if (isPlus) return t('ghostery_browser_hub_onboarding_already_plus_subscriber'); - return t('ghostery_browser_hub_onboarding_your_privacy_plan'); + if (isPremium) return t('ghostery_dawn_onboarding_already_premium_subscriber'); + if (isPlus) return t('ghostery_dawn_onboarding_already_plus_subscriber'); + return t('ghostery_dawn_onboarding_your_privacy_plan'); }; renderSubtitleText = (fromSearchSelectionScreen) => { @@ -212,10 +212,10 @@ class ChoosePlanView extends React.Component { const isPlus = (user && user.plusAccess) || false; const isPremium = (user && user.premiumAccess) || false; - if (fromSearchSelectionScreen) return t('ghostery_browser_hub_onboarding_based_on_your_privacy_preferences'); + if (fromSearchSelectionScreen) return t('ghostery_dawn_onboarding_based_on_your_privacy_preferences'); if (isPremium) return ''; - if (isPlus) return t('ghostery_browser_hub_onboarding_keep_your_current_plan_or_upgrade'); - return t('ghostery_browser_hub_onboarding_choose_an_option'); + if (isPlus) return t('ghostery_dawn_onboarding_keep_your_current_plan_or_upgrade'); + return t('ghostery_dawn_onboarding_choose_an_option'); }; plusCard = (checked, handleClick, showCTAButton = false) => { @@ -245,34 +245,34 @@ class ChoosePlanView extends React.Component {
    - {t('ghostery_browser_hub_onboarding_private_search')} + {t('ghostery_dawn_onboarding_private_search')}
    - {t('ghostery_browser_hub_onboarding_tracker_protection')} + {t('ghostery_dawn_onboarding_tracker_protection')}
    - {t('ghostery_browser_hub_onboarding_speedy_page_loads')} + {t('ghostery_dawn_onboarding_speedy_page_loads')}
    - {t('ghostery_browser_hub_onboarding_intelligence_technology')} + {t('ghostery_dawn_onboarding_intelligence_technology')}
    - {t('ghostery_browser_hub_onboarding_ad_free')} + {t('ghostery_dawn_onboarding_ad_free')}
    - {t('ghostery_browser_hub_onboarding_supports_ghosterys_mission')} + {t('ghostery_dawn_onboarding_supports_ghosterys_mission')}
    {showCTAButton && ( setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING })}> - {t('ghostery_browser_hub_onboarding_keep')} + {t('ghostery_dawn_onboarding_keep')} )}
    @@ -299,7 +299,7 @@ class ChoosePlanView extends React.Component {
    - {t('ghostery_browser_hub_onboarding_back')} + {t('ghostery_dawn_onboarding_back')}
    @@ -314,8 +314,8 @@ class ChoosePlanView extends React.Component { and move them to Step 5 if signed in 2. If user is signed out, clicking this should take them to Step 4b (linked) */} -
    {t('ghostery_browser_hub_onboarding_start_trial')}
    -
    {t('ghostery_browser_hub_onboarding_see_all_plans')}
    +
    {t('ghostery_dawn_onboarding_start_trial')}
    +
    {t('ghostery_dawn_onboarding_see_all_plans')}
    )} @@ -326,7 +326,7 @@ class ChoosePlanView extends React.Component {
    {this.plusCard(this.isPlusPlanChecked(), this.selectPlusPlan, isPlus)}
    -
    {t('ghostery_browser_hub_onboarding_or')}
    +
    {t('ghostery_dawn_onboarding_or')}
    {premiumCard(this.isPremiumPlanChecked(), this.selectPremiumPlan, isPlus)}
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx index 94ca0a611..db0ce4475 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx @@ -28,16 +28,16 @@ const SuccessView = (props) => {
    - {t('ghostery_browser_hub_onboarding_back')} + {t('ghostery_dawn_onboarding_back')}
    -
    {t('ghostery_browser_hub_onboarding_youve_successfully_set_up_your_browser')}
    +
    {t('ghostery_dawn_onboarding_youve_successfully_set_up_your_browser')}
    -
    {`${t('ghostery_browser_hub_onboarding_surf_with_ease')} Ghostery`}
    +
    {`${t('ghostery_dawn_onboarding_surf_with_ease')} Ghostery`}
    - +
    ); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx index 48ad68076..ce154121b 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.jsx @@ -31,17 +31,17 @@ const steps = [ id: LOGIN }, { - label: t('ghostery_browser_hub_onboarding_privacy'), + label: t('ghostery_dawn_onboarding_privacy'), route: `/${ONBOARDING}/${BLOCK_SETTINGS}`, id: BLOCK_SETTINGS }, { - label: t('ghostery_browser_hub_onboarding_search'), + label: t('ghostery_dawn_onboarding_search'), route: `/${ONBOARDING}/${CHOOSE_DEFAULT_SEARCH}`, id: CHOOSE_DEFAULT_SEARCH }, { - label: t('ghostery_browser_hub_onboarding_plan'), + label: t('ghostery_dawn_onboarding_plan'), route: `/${ONBOARDING}/${CHOOSE_PLAN}`, id: CHOOSE_PLAN } From 0a8b70bf90f8bb8230a6f95d12928d664105cd43 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Thu, 21 Jan 2021 09:37:05 -0500 Subject: [PATCH 066/113] Redirect user to new onboarding from background script instead of through template --- .../ChooseDefaultSearchView.jsx | 2 +- app/templates/dawn_onboarding.html | 46 +++++++++++++++++++ src/background.js | 15 ++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 app/templates/dawn_onboarding.html diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index e332c456a..265c0e916 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -75,7 +75,7 @@ class ChooseDefaultSearchView extends Component { ); renderStartpageOptionDescription = () => ( -
    Ad-supported private search
    +
    {t('ghostery_dawn_onboarding_ad_supported_private_search')}
    ) renderOptionContainer = (chosenSearch, optionName) => { diff --git a/app/templates/dawn_onboarding.html b/app/templates/dawn_onboarding.html new file mode 100644 index 000000000..04b069514 --- /dev/null +++ b/app/templates/dawn_onboarding.html @@ -0,0 +1,46 @@ + + + + + + + + + + +
    + + + diff --git a/src/background.js b/src/background.js index 89e7f08c4..89dfe57b3 100644 --- a/src/background.js +++ b/src/background.js @@ -1649,6 +1649,21 @@ function initializeGhosteryModules() { active: true }); } + if (globals.JUST_INSTALLED) { + if (BROWSER_INFO.name !== 'ghostery_desktop') { + const showAlternateHub = conf.hub_layout === 'alternate'; + const route = showAlternateHub ? '#home' : ''; + chrome.tabs.create({ + url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), + active: true + }); + } else { + chrome.tabs.create({ + url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html'), + active: true + }); + } + } }); }); } From 8abda5e12c3b198cfd9c22a833a7190d3632811a Mon Sep 17 00:00:00 2001 From: Leury Rodriguez Date: Thu, 21 Jan 2021 09:40:50 -0500 Subject: [PATCH 067/113] Fixes for Dawn Onboarding toast messages (#660) * Fixes for Dawn toast styling and layout * Add missing toast error message for Step 1 * Ensure toast remains above step progress bar * Comment update * Update snapshot for ToastMessage component * Fix toast not clearing after block settings submit * Move setToast code up in if block * Center tooltip info icon * Make toast message i18n-friendly --- _locales/en/messages.json | 16 +++++-- .../Views/AppView/AppView.jsx | 2 +- .../Step1_CreateAccountFormContainer.jsx | 7 ++- .../BlockSettingsView.jsx | 17 ++++--- .../BlockSettingsView.scss | 8 +++- app/images/hub/toast/toast-alert.svg | 18 +++++++ app/images/hub/toast/toast-success.svg | 18 +++++++ .../ToastMessage/ToastMessage.jsx | 48 +++++++++++++------ .../ToastMessage/ToastMessage.scss | 24 ++++++++++ .../__snapshots__/ToastMessage.test.jsx.snap | 22 ++++++--- 10 files changed, 143 insertions(+), 37 deletions(-) create mode 100644 app/images/hub/toast/toast-alert.svg create mode 100644 app/images/hub/toast/toast-success.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7c87a4a6d..1840ff7de 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1740,6 +1740,9 @@ "hub_create_account_toast_error": { "message": "That email address is already in use. Please choose another." }, + "ghostery_dawn_onboarding_toast_fix_errors": { + "message": "Please fix errors before proceeding." + }, "ghostery_dawn_onboarding_install_heading": { "message": "Ghostery Dawn now installing..." }, @@ -1926,7 +1929,7 @@ "ghostery_dawn_onboarding_info_smart_browsing": { "message": "Smart-browsing adjusts your blocking settings to decrease page breakage and accelerate page loads." }, - "ghostery_browser_hub__onboarding_choose_your_default_search": { + "ghostery_dawn_hub__onboarding_choose_your_default_search": { "message": "Choose your default search" }, "ghostery_dawn_onboarding_pick_a_default_search_engine": { @@ -1959,8 +1962,11 @@ "ghostery_dawn_onboarding_other": { "message": "Other" }, - "ghostery_dawn_onboarding_toast_error": { - "message": "Please answer all questions" + "ghostery_dawn_onboarding_toast_success": { + "message": "Success: " + }, + "ghostery_dawn_onboarding_toast_alert": { + "message": "Error: " }, "enable_when_paused": { "message": "To use this function, Resume Ghostery." @@ -2415,7 +2421,7 @@ "pick_a_default_search_engine": { "message": "Pick a default engine for all your searches." }, - "ghostery_browser_hub_blocking_settings_view_toast_error_message": { - "message": "Error: Please answer all questions" + "ghostery_dawn_hub_blocking_settings_view_toast_error_message": { + "message": "Please answer all questions" } } diff --git a/app/ghostery-browser-hub/Views/AppView/AppView.jsx b/app/ghostery-browser-hub/Views/AppView/AppView.jsx index 108d77346..8978d1cb3 100644 --- a/app/ghostery-browser-hub/Views/AppView/AppView.jsx +++ b/app/ghostery-browser-hub/Views/AppView/AppView.jsx @@ -38,7 +38,7 @@ const AppView = (props) => { return (
    - {} + {} {children}
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx index 0cd3e4fac..de260dd49 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountFormContainer.jsx @@ -144,10 +144,15 @@ class CreateAccountFormContainer extends Component { validateInput: true, }); + const { actions } = this.props; if (!emailIsValid || !confirmIsValid || !legalConsentChecked || !passwordIsValid || confirmPasswordError) { + actions.setToast({ + toastMessage: t('ghostery_dawn_onboarding_toast_fix_errors'), + toastClass: 'alert' + }); return; } - const { actions } = this.props; + actions.setToast({ toastMessage: '', toastClass: '' diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index 1e8242294..330463407 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -66,9 +66,16 @@ class BlockSettingsView extends Component { blockAds, kindsOfTrackers, antiTracking, smartBrowsing } = this.state; + const { actions } = this.props; + const { setToast } = actions; + // Will only change user settings if all questions are answered if (blockAds !== null && kindsOfTrackers !== null && antiTracking !== null && smartBrowsing !== null) { - const { actions } = this.props; + setToast({ + toastMessage: '', + toastClass: '' + }); + const { setAdBlock, setAntiTracking, setSmartBlocking, setBlockingPolicy, setSetupStep } = actions; @@ -93,15 +100,13 @@ class BlockSettingsView extends Component { break; } setBlockingPolicy({ blockingPolicy }); + setSetupStep({ setup_step: CHOOSE_DEFAULT_SEARCH, origin: ONBOARDING }); history.push('/onboarding/3'); } else { - const { actions } = this.props; - const { setToast } = actions; - setToast({ - toastMessage: t('ghostery_browser_hub_blocking_settings_view_toast_error_message'), - toastClass: 'error' + toastMessage: t('ghostery_dawn_hub_blocking_settings_view_toast_error_message'), + toastClass: 'alert' }); } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss index 8482b7f7a..d25ecef48 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss @@ -121,12 +121,16 @@ } .BlockSettingsView_checkbox { - width: 18px; - height: 18px; + width: 20px; + height: 20px; + padding: 0; + margin-right: 14px; } .BlockSettingsView__infoIcon { width: 18px; + height: 25px; + background-position: center; background-repeat: no-repeat; background-image: url('/app/images/hub/setup/info.svg'); margin-left: 8px; diff --git a/app/images/hub/toast/toast-alert.svg b/app/images/hub/toast/toast-alert.svg new file mode 100644 index 000000000..693f1643b --- /dev/null +++ b/app/images/hub/toast/toast-alert.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/images/hub/toast/toast-success.svg b/app/images/hub/toast/toast-success.svg new file mode 100644 index 000000000..7885c19e8 --- /dev/null +++ b/app/images/hub/toast/toast-success.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/shared-components/ToastMessage/ToastMessage.jsx b/app/shared-components/ToastMessage/ToastMessage.jsx index 2cd45cc12..95a092ac7 100644 --- a/app/shared-components/ToastMessage/ToastMessage.jsx +++ b/app/shared-components/ToastMessage/ToastMessage.jsx @@ -19,24 +19,40 @@ import PropTypes from 'prop-types'; * @return {JSX} JSX for rendering a Toast Message * @memberof SharedComponents */ -const ToastMessage = ({ toastText, toastClass, toastExit }) => ( -
    - {toastText && ( -
    -
    -
    -
    - {toastText} +const ToastMessage = ({ + toastText, toastClass, toastExit, dawnHub +}) => { + // These variables will be used to determine whether the toast should display with Dawn + // onboarding styling or the default styling used in GBE + const dawnHubClass = dawnHub ? 'dawn-hub' : ''; + const dawnLayout = dawnHub ? 'align-justify align-middle' : 'align-center-middle'; + + const dawnToastText = dawnHub ? t(`ghostery_dawn_onboarding_toast_${toastClass}`) : ''; + + return ( +
    + {toastText && ( +
    +
    +
    +
    + {dawnHub && ( + + )} +
    + {`${dawnToastText}${toastText}`} +
    +
    + {toastExit && ( +
    + )}
    - {toastExit && ( -
    - )}
    -
    - )} -
    -); + )} +
    + ); +}; // PropTypes ensure we pass required props of the correct type ToastMessage.propTypes = { @@ -46,11 +62,13 @@ ToastMessage.propTypes = { PropTypes.func, PropTypes.bool, ]), + dawnHub: PropTypes.bool, }; // Default props used in the Toast Message ToastMessage.defaultProps = { toastExit: false, + dawnHub: false, }; export default ToastMessage; diff --git a/app/shared-components/ToastMessage/ToastMessage.scss b/app/shared-components/ToastMessage/ToastMessage.scss index 4040110fa..7fa82e84b 100644 --- a/app/shared-components/ToastMessage/ToastMessage.scss +++ b/app/shared-components/ToastMessage/ToastMessage.scss @@ -16,7 +16,31 @@ position: relative; top: 30px; height: 0; + &.dawn-hub { + z-index: 1; + max-width: 700px; + margin: auto; + font-family: Roboto; + font-size: 14px; + font-weight: 500; + .alert { + background-color: #f8e6e6; + .callout-text {color: #9f4948;} + } + .success { + background-color: #dff0d8; + .callout-text {color: #417505;} + } + .ToastMessage__close { + background-image: buildIconX(#000000); + } + } } + +.ToastMessage_classIcon { + margin-right: 20px; +} + .ToastMessage__close { height: 13px; width: 13px; diff --git a/app/shared-components/ToastMessage/__tests__/__snapshots__/ToastMessage.test.jsx.snap b/app/shared-components/ToastMessage/__tests__/__snapshots__/ToastMessage.test.jsx.snap index 0ab06d344..1a5c0fba2 100644 --- a/app/shared-components/ToastMessage/__tests__/__snapshots__/ToastMessage.test.jsx.snap +++ b/app/shared-components/ToastMessage/__tests__/__snapshots__/ToastMessage.test.jsx.snap @@ -2,7 +2,7 @@ exports[`app/shared-components/ToastMessage More Snapshot tests with react-test-renderer, but for edge cases edge case where text is empty string 1`] = `
    @@ -10,7 +10,7 @@ exports[`app/shared-components/ToastMessage More Snapshot tests with react-test- exports[`app/shared-components/ToastMessage Snapshot tests with react-test-renderer toast message is rendered correctly 1`] = `
    - sample text +
    + sample text +
    @@ -34,7 +38,7 @@ exports[`app/shared-components/ToastMessage Snapshot tests with react-test-rende exports[`app/shared-components/ToastMessage Snapshot tests with react-test-renderer toast message is rendered correctly with a close button 1`] = `
    - example text +
    + example text +
    Date: Fri, 22 Jan 2021 08:33:07 -0500 Subject: [PATCH 068/113] Reenable search selection which had been disabled for testing purposes --- .../ChooseDefaultSearchView.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 265c0e916..e49a2a345 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -56,6 +56,8 @@ class ChooseDefaultSearchView extends Component { search: chosenSearch, }; + chrome.runtime.sendMessage('search@ghostery.com', payload, () => {}); + // chrome.runtime.sendMessage('search@ghostery.com', payload, () => { // // TODO handle errors if needed // // TODO save user's search setting to redux / background if needed @@ -112,7 +114,8 @@ class ChooseDefaultSearchView extends Component {
    - {`Just so you know: ${searchBeingConsidered}'s search engine will log your data and use it to serve you targeted ads.`} + {searchBeingConsidered !== SEARCH_GHOSTERY && `Just so you know: ${searchBeingConsidered}'s search engine will log your data and use it to serve you targeted ads.`} + {searchBeingConsidered === SEARCH_GHOSTERY && 'With a Plus subscription, you can access the world’s first and only ad-free private search! We use a special cookie-less login to keep your search absolutely private.'}
    diff --git a/app/images/hub/success/ghostery-suite.png b/app/images/hub/success/ghostery-suite.png index ef30ba38783b891b4661db5227e0e8603bcbbf00..2fabf948e6e5db7128a0e99c3fd46436f649c71d 100644 GIT binary patch literal 215849 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

    mt)cuHQ#bso^V9~Y3s%(`H&*)Fp3Mf86X^@^b9 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

    ^SJF`_=? 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 z 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

    xYyVX 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%0Av@34g}!JwPJT`isJ2HeJD@? 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

    G4sx7*v0}!t)q_oQho~OB?57XuiF2Jq{Lf_79FQ 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 Rx);DlQC3Z+TFN~1{{T_lvQ+>8 From f1ba4f0bd84096f43afb11826e63274f35a3ff30 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Fri, 22 Jan 2021 14:46:47 -0500 Subject: [PATCH 071/113] Ghostery Dawn Miscellaneous Bug fixes (#664) * Bold numbers in step progress bar * Align BlockSettingsView checkbox per vinny, add click handler to checkbox label * Restore already have account and create an account, also get rid of console error * Close tab when clicking CTA button on Success View * Fix terms and agreements label position on Create Account Form * Update snapshots * Clear toasts when clicking skip button --- .../__snapshots__/WelcomeView.test.jsx.snap | 6 +- .../Step1_CreateAccountForm.jsx | 5 +- .../Step1_CreateAccountForm.scss | 3 +- .../Step1_CreateAccountForm.test.jsx.snap | 32 +------ .../Step1_CreateAccountView.jsx | 20 +++-- .../Step1_CreateAccountView.test.jsx.snap | 84 +++++++++++++++---- .../Step1_CreateAccountView/index.js | 2 + .../BlockSettingsView.jsx | 2 +- .../BlockSettingsView.scss | 6 +- .../BlockSettingsView.test.jsx.snap | 27 +++--- .../ChoosePlanView.test.jsx.snap | 40 ++++----- .../Step5_SuccessView/SuccessView.jsx | 8 +- .../__snapshots__/SuccessView.test.jsx.snap | 8 +- .../StepProgressBar.test.jsx.snap | 8 +- .../__snapshots__/AppView.test.jsx.snap | 10 ++- .../hub/step-progress-bar/step-1-current.svg | 9 +- .../hub/step-progress-bar/step-2-current.svg | 9 +- .../step-progress-bar/step-2-incomplete.svg | 9 +- .../hub/step-progress-bar/step-3-current.svg | 9 +- .../step-progress-bar/step-3-incomplete.svg | 2 +- .../hub/step-progress-bar/step-4-current.svg | 9 +- .../step-progress-bar/step-4-incomplete.svg | 9 +- .../hub/step-progress-bar/step-completed.svg | 18 +++- 23 files changed, 219 insertions(+), 116 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/__tests__/__snapshots__/WelcomeView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/__tests__/__snapshots__/WelcomeView.test.jsx.snap index 6d26c9fcc..dbf7b51fe 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/__tests__/__snapshots__/WelcomeView.test.jsx.snap +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/__tests__/__snapshots__/WelcomeView.test.jsx.snap @@ -7,12 +7,12 @@ exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/Welcom

    - ghostery_browser_hub_onboarding_welcome + ghostery_dawn_onboarding_welcome
    - ghostery_browser_hub_onboarding_lets_begin + ghostery_dawn_onboarding_lets_begin
    - ghostery_browser_hub_onboarding_lets_do_this + ghostery_dawn_onboarding_lets_do_this

    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx index fb5634a9d..7a80e9b18 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.jsx @@ -188,8 +188,8 @@ export const Step1_CreateAccountForm = (props) => { )}
    -
    -
    +
    +
    { />
    -
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss index ab27dedf5..43bea6263 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss @@ -53,8 +53,9 @@ } .Step1_CreateAccountForm__legalConsentCheckedLabel { font-size: 14px; + margin-top: 13px; @include breakpoint(small down) { - width: 258px; + width: 290px; } &.error { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/__tests__/__snapshots__/Step1_CreateAccountForm.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/__tests__/__snapshots__/Step1_CreateAccountForm.test.jsx.snap index a03bbdbf6..65c1003cf 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/__tests__/__snapshots__/Step1_CreateAccountForm.test.jsx.snap +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/__tests__/__snapshots__/Step1_CreateAccountForm.test.jsx.snap @@ -140,35 +140,10 @@ exports[`app/hub/Views/Step1_CreateAccountForm component Snapshot tests with rea
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    ( -
    +
    @@ -54,16 +54,24 @@ const renderFAQListItem = (icon, label, description) => ( */ const Step1_CreateAccountView = (props) => { const { user, actions } = props; - const { setSetupStep } = actions; + const { setSetupStep, setToast } = actions; const email = user && user.email; const [view, setView] = useState(CREATE_ACCOUNT); + const handleSkipButton = () => { + setSetupStep({ setup_step: LOGIN, origin: ONBOARDING }); + setToast({ + toastMessage: '', + toastClass: '' + }); + }; + const renderSkipLink = () => (
    - setSetupStep({ setup_step: LOGIN, origin: ONBOARDING })}> + handleSkipButton()}> {t('ghostery_dawn_onboarding_skip')}
    @@ -75,7 +83,7 @@ const Step1_CreateAccountView = (props) => {
    {t('ghostery_dawn_onboarding_you_are_signed_in_as')}
    {email}
    - setSetupStep({ setup_step: LOGIN, origin: ONBOARDING })}> + handleSkipButton()}> {t('next')}
    @@ -91,10 +99,10 @@ const Step1_CreateAccountView = (props) => {
    { t('ghostery_dawn_onboarding_sync_settings') }
    {view === CREATE_ACCOUNT && ( -
    setView(SIGN_IN)}>{t('ghostery_browser_hub_onboarding_already_have_account')}
    +
    setView(SIGN_IN)}>{t('ghostery_dawn_onboarding_already_have_account')}
    )} {view === SIGN_IN && ( -
    setView(CREATE_ACCOUNT)}>{t('ghostery_browser_hub_onboarding_create_an_account')}
    +
    setView(CREATE_ACCOUNT)}>{t('ghostery_dawn_onboarding_create_an_account')}
    )}
    {view === CREATE_ACCOUNT ? ( diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/__snapshots__/Step1_CreateAccountView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/__snapshots__/Step1_CreateAccountView.test.jsx.snap index ef2454236..4d2149cf0 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/__snapshots__/Step1_CreateAccountView.test.jsx.snap +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/__tests__/__snapshots__/Step1_CreateAccountView.test.jsx.snap @@ -7,25 +7,22 @@ exports[`app/hub/Views/Step1_CreateAccountView component Snapshot tests with rea
    - ghostery_browser_hub_onboarding_create_a_ghostery_account + ghostery_dawn_onboarding_create_a_ghostery_account
    - ghostery_browser_hub_onboarding_sync_settings + ghostery_dawn_onboarding_sync_settings
    - ghostery_browser_hub_onboarding_already_have_account + ghostery_dawn_onboarding_already_have_account
    -
    - ghostery_browser_hub_onboarding_skip + ghostery_dawn_onboarding_skip
    - ghostery_browser_hub_onboarding_we_take_your_privacy_very_seriously +
    + +
    +
    +
    + ghostery_dawn_onboarding_private_by_design +
    +
    + ghostery_dawn_onboarding_private_by_design_description +
    +
    +
    +
    +
    + +
    +
    +
    + ghostery_dawn_onboarding_can_i_remove_my_account +
    +
    + ghostery_dawn_onboarding_can_i_remove_my_account_description +
    +
    `; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js index 09b839fa8..0d9bbea15 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/index.js @@ -14,9 +14,11 @@ import { buildReduxHOC } from '../../../../shared-hub/utils'; import Step1_CreateAccountView from './Step1_CreateAccountView'; import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; +import setToast from '../../../../shared-hub/actions/ToastActions'; const actionCreators = { setSetupStep, + setToast }; export default buildReduxHOC(['account'], actionCreators, Step1_CreateAccountView); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index 330463407..0eca9aa0e 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -137,7 +137,7 @@ class BlockSettingsView extends Component { checked={recommendedChoices} onChange={() => this.toggleRecommendedChoices(!recommendedChoices)} /> -
    {t('ghostery_dawn_onboarding_recommended_choices')}
    +
    this.toggleRecommendedChoices(!recommendedChoices)}>{t('ghostery_dawn_onboarding_recommended_choices')}
    1. {t('ghostery_dawn_onboarding_question_block_ads')}
    2. diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss index 8a50b974a..399b6e8cd 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss @@ -128,7 +128,11 @@ width: 20px; height: 20px; padding: 0; - margin-right: 14px; + margin-left: -14px; +} + +.BlockSettingsView_checkboxLabel { + cursor: pointer; } .BlockSettingsView__infoIcon { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap index e4753d50d..823005b73 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap @@ -21,7 +21,7 @@ Array [ - ghostery_browser_hub_onboarding_back + ghostery_dawn_onboarding_back
    @@ -32,12 +32,12 @@ Array [
    - ghostery_browser_hub_onboarding_which_privacy_plan + ghostery_dawn_onboarding_which_privacy_plan
    - ghostery_browser_hub_onboarding_tell_us_your_preferences + ghostery_dawn_onboarding_tell_us_your_preferences
    -
    - ghostery_browser_hub_onboarding_recommended_choices +
    + ghostery_dawn_onboarding_recommended_choices
    1. - ghostery_browser_hub_onboarding_question_block_ads + ghostery_dawn_onboarding_question_block_ads
    2. - ghostery_browser_hub_onboarding_question_kinds_of_trackers + ghostery_dawn_onboarding_question_kinds_of_trackers
      @@ -144,7 +147,7 @@ Array [
      - ghostery_browser_hub_onboarding_kinds_of_trackers_all + ghostery_dawn_onboarding_kinds_of_trackers_all
      - ghostery_browser_hub_onboarding_kinds_of_trackers_ad_and_analytics + ghostery_dawn_onboarding_kinds_of_trackers_ad_and_analytics
      - ghostery_browser_hub_onboarding_kinds_of_trackers_none + ghostery_dawn_onboarding_kinds_of_trackers_none
  • - ghostery_browser_hub_onboarding_question_anti_tracking + ghostery_dawn_onboarding_question_anti_tracking
    @@ -257,7 +260,7 @@ Array [
    - ghostery_browser_hub_onboarding_question_smart_browsing + ghostery_dawn_onboarding_question_smart_browsing
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap index 28e39c2ea..1f22378ab 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap @@ -27,7 +27,7 @@ exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/Cho - ghostery_browser_hub_onboarding_back + ghostery_dawn_onboarding_back
    @@ -38,12 +38,12 @@ exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/Cho
    - ghostery_browser_hub_onboarding_your_privacy_plan + ghostery_dawn_onboarding_your_privacy_plan
    - ghostery_browser_hub_onboarding_choose_an_option + ghostery_dawn_onboarding_choose_an_option
    - ghostery_browser_hub_onboarding_private_search + ghostery_dawn_onboarding_private_search
    - ghostery_browser_hub_onboarding_tracker_protection + ghostery_dawn_onboarding_tracker_protection
    - ghostery_browser_hub_onboarding_speedy_page_loads + ghostery_dawn_onboarding_speedy_page_loads
    - ghostery_browser_hub_onboarding_intelligence_technology + ghostery_dawn_onboarding_intelligence_technology
    @@ -204,7 +204,7 @@ exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/Cho - ghostery_browser_hub_onboarding_private_search + ghostery_dawn_onboarding_private_search
    - ghostery_browser_hub_onboarding_tracker_protection + ghostery_dawn_onboarding_tracker_protection
    - ghostery_browser_hub_onboarding_speedy_page_loads + ghostery_dawn_onboarding_speedy_page_loads
    - ghostery_browser_hub_onboarding_intelligence_technology + ghostery_dawn_onboarding_intelligence_technology
    - ghostery_browser_hub_onboarding_ad_free + ghostery_dawn_onboarding_ad_free
    - ghostery_browser_hub_onboarding_supports_ghosterys_mission + ghostery_dawn_onboarding_supports_ghosterys_mission
  • @@ -316,7 +316,7 @@ exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/Cho - ghostery_browser_hub_onboarding_private_search + ghostery_dawn_onboarding_private_search
    - ghostery_browser_hub_onboarding_tracker_protection + ghostery_dawn_onboarding_tracker_protection
    - ghostery_browser_hub_onboarding_speedy_page_loads + ghostery_dawn_onboarding_speedy_page_loads
    - ghostery_browser_hub_onboarding_intelligence_technology + ghostery_dawn_onboarding_intelligence_technology
    - ghostery_browser_hub_onboarding_ad_free + ghostery_dawn_onboarding_ad_free
    - ghostery_browser_hub_onboarding_supports_ghosterys_mission + ghostery_dawn_onboarding_supports_ghosterys_mission
    - ghostery_browser_hub_onboarding_unlimited_bandwidth + ghostery_dawn_onboarding_unlimited_bandwidth
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx index 3192276c7..fe6f0e4e2 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.jsx @@ -22,6 +22,12 @@ import { NavLink } from 'react-router-dom'; const SuccessView = (props) => { const { actions } = props; const { sendPing } = actions; + + const handleCTAButtonClick = () => { + sendPing({ type: 'gb_onboarding_success' }); + window.close(); + }; + return (
    @@ -37,7 +43,7 @@ const SuccessView = (props) => {
    {`${t('ghostery_dawn_onboarding_surf_with_ease')}`}
    - +
    ); diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap index 6508140e8..2a6ff5651 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap @@ -19,7 +19,7 @@ Array [ - ghostery_browser_hub_onboarding_back + ghostery_dawn_onboarding_back
    @@ -30,12 +30,12 @@ Array [
    - ghostery_browser_hub_onboarding_youve_successfully_set_up_your_browser + ghostery_dawn_onboarding_youve_successfully_set_up_your_browser
    - ghostery_browser_hub_onboarding_surf_with_ease Ghostery + ghostery_dawn_onboarding_surf_with_ease Ghostery
    - ghostery_browser_hub_onboarding_start_browsing + ghostery_dawn_onboarding_start_browsing
    , ] diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap index e389612c4..9b0463a08 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap @@ -4,8 +4,6 @@ exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProg exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 1 1`] = `ShallowWrapper {}`; -exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 2 1`] = `ShallowWrapper {}`; - exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 4 1`] = `ShallowWrapper {}`; exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 5 1`] = `ShallowWrapper {}`; @@ -41,7 +39,7 @@ exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProg
    - ghostery_browser_hub_onboarding_privacy + ghostery_dawn_onboarding_privacy
    - ghostery_browser_hub_onboarding_search + ghostery_dawn_onboarding_search
    - ghostery_browser_hub_onboarding_plan + ghostery_dawn_onboarding_plan
    - Example toast message +
    + Example toast message +
    1 + + + + + 1 + + + diff --git a/app/images/hub/step-progress-bar/step-2-current.svg b/app/images/hub/step-progress-bar/step-2-current.svg index 334222a0f..7b2b0d76f 100644 --- a/app/images/hub/step-progress-bar/step-2-current.svg +++ b/app/images/hub/step-progress-bar/step-2-current.svg @@ -1 +1,8 @@ -2 + + + + + 2 + + + diff --git a/app/images/hub/step-progress-bar/step-2-incomplete.svg b/app/images/hub/step-progress-bar/step-2-incomplete.svg index b85269220..f53cb0b71 100644 --- a/app/images/hub/step-progress-bar/step-2-incomplete.svg +++ b/app/images/hub/step-progress-bar/step-2-incomplete.svg @@ -1 +1,8 @@ -2 + + + + + 2 + + + diff --git a/app/images/hub/step-progress-bar/step-3-current.svg b/app/images/hub/step-progress-bar/step-3-current.svg index 6e8c4a48f..a1e47f728 100644 --- a/app/images/hub/step-progress-bar/step-3-current.svg +++ b/app/images/hub/step-progress-bar/step-3-current.svg @@ -1 +1,8 @@ -3 + + + + + 3 + + + diff --git a/app/images/hub/step-progress-bar/step-3-incomplete.svg b/app/images/hub/step-progress-bar/step-3-incomplete.svg index 3e5ac18a2..3b0ec21f0 100644 --- a/app/images/hub/step-progress-bar/step-3-incomplete.svg +++ b/app/images/hub/step-progress-bar/step-3-incomplete.svg @@ -3,7 +3,7 @@ - + 3 diff --git a/app/images/hub/step-progress-bar/step-4-current.svg b/app/images/hub/step-progress-bar/step-4-current.svg index e558755dc..eeed221c5 100644 --- a/app/images/hub/step-progress-bar/step-4-current.svg +++ b/app/images/hub/step-progress-bar/step-4-current.svg @@ -1 +1,8 @@ -4 + + + + + 4 + + + diff --git a/app/images/hub/step-progress-bar/step-4-incomplete.svg b/app/images/hub/step-progress-bar/step-4-incomplete.svg index ee06f911b..26322cabe 100644 --- a/app/images/hub/step-progress-bar/step-4-incomplete.svg +++ b/app/images/hub/step-progress-bar/step-4-incomplete.svg @@ -1 +1,8 @@ -4 + + + + + 4 + + + diff --git a/app/images/hub/step-progress-bar/step-completed.svg b/app/images/hub/step-progress-bar/step-completed.svg index 0ff6754ad..531837424 100644 --- a/app/images/hub/step-progress-bar/step-completed.svg +++ b/app/images/hub/step-progress-bar/step-completed.svg @@ -1 +1,17 @@ - + + + + + + + + + + + + + + + + + From 4627a05d229d6e337056429187228084278944e3 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Sun, 24 Jan 2021 22:40:57 -0500 Subject: [PATCH 072/113] Update and clean up strings --- _locales/en/messages.json | 24 +++++++++---------- .../ChooseDefaultSearchView.jsx | 13 ++++++---- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1840ff7de..00d74a8db 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1929,12 +1929,6 @@ "ghostery_dawn_onboarding_info_smart_browsing": { "message": "Smart-browsing adjusts your blocking settings to decrease page breakage and accelerate page loads." }, - "ghostery_dawn_hub__onboarding_choose_your_default_search": { - "message": "Choose your default search" - }, - "ghostery_dawn_onboarding_pick_a_default_search_engine": { - "message": "Pick a default search engine for all your searches." - }, "ghostery_dawn_onboarding_ad_free_private_search": { "message": "Ad-free private search" }, @@ -1947,12 +1941,6 @@ "ghostery_dawn_onboarding_type_in_search_domain": { "message": "Type in search domain" }, - "ghostery_dawn_onboarding_just_so_you_know": { - "message": "Just so you know:" - }, - "ghostery_dawn_onboarding_search_engine_will_log_your_data": { - "message": "search engine will log your data and use it to serve you targeted ads." - }, "ghostery_dawn_onboarding_go_back": { "message": "Go Back" }, @@ -2423,5 +2411,17 @@ }, "ghostery_dawn_hub_blocking_settings_view_toast_error_message": { "message": "Please answer all questions" + }, + "ghostery_dawn_onboarding_startpage_warning": { + "message": "Just so you know: Startpage's search engine will log your data and use it to serve you targeted ads." + }, + "ghostery_dawn_onboarding_bing_warning": { + "message": "Just so you know: Bing's search engine will log your data and use it to serve you targeted ads." + }, + "ghostery_dawn_onboarding_yahoo_warning": { + "message": "Just so you know: Yahoo's search engine will log your data and use it to serve you targeted ads." + }, + "ghostery_dawn_onboarding_glow_benefit": { + "message": "With a Plus subscription, you can access the world’s first and only ad-free private search! We use a special cookie-less login to keep your search absolutely private." } } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index e49a2a345..ce766ad22 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -56,7 +56,8 @@ class ChooseDefaultSearchView extends Component { search: chosenSearch, }; - chrome.runtime.sendMessage('search@ghostery.com', payload, () => {}); + // TODO comment this IN for builds for Dawn + // chrome.runtime.sendMessage('search@ghostery.com', payload, () => {}); // chrome.runtime.sendMessage('search@ghostery.com', payload, () => { // // TODO handle errors if needed @@ -114,8 +115,10 @@ class ChooseDefaultSearchView extends Component {
    - {searchBeingConsidered !== SEARCH_GHOSTERY && `Just so you know: ${searchBeingConsidered}'s search engine will log your data and use it to serve you targeted ads.`} - {searchBeingConsidered === SEARCH_GHOSTERY && 'With a Plus subscription, you can access the world’s first and only ad-free private search! We use a special cookie-less login to keep your search absolutely private.'} + {searchBeingConsidered === SEARCH_STARTPAGE && t('ghostery_dawn_onboarding_startpage_warning')} + {searchBeingConsidered === SEARCH_BING && t('ghostery_dawn_onboarding_bing_warning')} + {searchBeingConsidered === SEARCH_YAHOO && t('ghostery_dawn_onboarding_yahoo_warning')} + {searchBeingConsidered === SEARCH_GHOSTERY && t('ghostery_dawn_onboarding_glow_benefit')}
    From 1b02dfebaecbd34473d0b9a8b21ece22e64e3b31 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 25 Jan 2021 00:06:27 -0500 Subject: [PATCH 073/113] Add search dropdown strings --- _locales/en/messages.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5a4194d64..86b2cc44b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2426,5 +2426,14 @@ }, "ghostery_dawn_onboarding_glow_benefit": { "message": "With a Plus subscription, you can access the world’s first and only ad-free private search! We use a special cookie-less login to keep your search absolutely private." + }, + "ghostery_dawn_onboarding_choose_alternate_search": { + "message": "Choose alternate search" + }, + "ghostery_dawn_onboarding_select_option": { + "message": "Select option" + }, + "ghostery_dawn_onboarding_you_have_selected_an_alternate_serach_engine": { + "message": "You have selected an alternate search engine:" } } From f5e9536aa6fc8e61e09459255efcbc6d0972394c Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 25 Jan 2021 09:05:38 -0500 Subject: [PATCH 074/113] Clean uphub templates and on install hub branching code --- .tx/config | 19 ++++++++++--------- app/templates/dawn_onboarding.html | 24 ++---------------------- app/templates/hub.html | 24 ++---------------------- manifest.json | 2 +- src/background.js | 22 +++++++--------------- 5 files changed, 22 insertions(+), 69 deletions(-) diff --git a/.tx/config b/.tx/config index 220c7d6b5..49ca0defa 100644 --- a/.tx/config +++ b/.tx/config @@ -1,9 +1,10 @@ -[main] -host = https://www.transifex.com - -[ghostery-browser-extension.develop-en-messages-json] -file_filter = _locales//messages.json -minimum_perc = 0 -source_file = _locales/en/messages.json -source_lang = en -type = CHROME +[main] +host = https://www.transifex.com + +[ghostery-browser-extension.develop-en-messages-json] +file_filter = _locales//messages.json +minimum_perc = 0 +source_file = _locales/en/messages.json +source_lang = en +type = CHROME + diff --git a/app/templates/dawn_onboarding.html b/app/templates/dawn_onboarding.html index 04b069514..58d7a871a 100644 --- a/app/templates/dawn_onboarding.html +++ b/app/templates/dawn_onboarding.html @@ -17,30 +17,10 @@ +
    - + diff --git a/app/templates/hub.html b/app/templates/hub.html index 04b069514..25a9137b6 100644 --- a/app/templates/hub.html +++ b/app/templates/hub.html @@ -17,30 +17,10 @@ +
    - + diff --git a/manifest.json b/manifest.json index 5acc4cd8e..44389fbce 100644 --- a/manifest.json +++ b/manifest.json @@ -80,7 +80,7 @@ "run_at": "document_start" } ], - "content_security_policy": "script-src 'self' 'sha256-iu+nxHOBLSMqKFuIk9bZIUxScKTqR9rnGxVunOSBJ5k='; object-src 'self'", + "content_security_policy": "script-src 'self'; object-src 'self'", "permissions": [ "webNavigation", "webRequest", diff --git a/src/background.js b/src/background.js index 89dfe57b3..a4e1686a5 100644 --- a/src/background.js +++ b/src/background.js @@ -1638,28 +1638,20 @@ function initializeGhosteryModules() { ]).then(() => { // run scheduledTasks on init scheduledTasks().then(() => { - // Open the Ghostery Hub on install with justInstalled query parameter set to true. - // We need to do this after running scheduledTasks for the first time - // because of an A/B test that determines which promo variant is shown in the Hub on install - if (globals.JUST_INSTALLED && BROWSER_INFO.name !== 'ghostery_desktop') { - const showAlternateHub = conf.hub_layout === 'alternate'; - const route = showAlternateHub ? '#home' : ''; - chrome.tabs.create({ - url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), - active: true - }); - } if (globals.JUST_INSTALLED) { if (BROWSER_INFO.name !== 'ghostery_desktop') { - const showAlternateHub = conf.hub_layout === 'alternate'; - const route = showAlternateHub ? '#home' : ''; chrome.tabs.create({ - url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), + url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html'), active: true }); } else { + // Open the Ghostery Hub on install with justInstalled query parameter set to true. + // We need to do this after running scheduledTasks for the first time + // because of an A/B test that determines which promo variant is shown in the Hub on install + const showAlternateHub = conf.hub_layout === 'alternate'; + const route = showAlternateHub ? '#home' : ''; chrome.tabs.create({ - url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html'), + url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), active: true }); } From 7f9cbf1f443684a51a6b662681ed8dd3808a02db Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 25 Jan 2021 09:46:56 -0500 Subject: [PATCH 075/113] Linter updates and fixes --- .../Views/OnboardingView/OnboardingView.jsx | 1 - .../OnboardingView/OnboardingViewContainer.jsx | 13 ++++++++++--- .../ChooseDefaultSearchView.jsx | 13 +++++++------ .../__snapshots__/SuccessView.test.jsx.snap | 2 +- app/hub/Views/CreateAccountView/index.js | 2 +- src/background.js | 2 +- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx index 220d7aaba..357aa9c2a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx @@ -27,7 +27,6 @@ import { BLOCK_SETTINGS, CHOOSE_DEFAULT_SEARCH, CHOOSE_PLAN } from './Onboarding */ const OnboardingView = (props) => { const { sendMountActions, steps } = props; - console.log('in OnboardingView'); const getScreenContainerClassNames = index => ClassNames('OnboardingView__screenContainer', { step2: index === BLOCK_SETTINGS, diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index afa0afc25..aa1a234e2 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -13,7 +13,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import QueryString from 'query-string'; import OnboardingView from './OnboardingView'; + import { BLOCKING_POLICY_RECOMMENDED } from '../../../shared-hub/constants/BlockingPolicyConstants'; // Component Views @@ -33,6 +35,8 @@ import { SUCCESS } from './OnboardingConstants'; +const justInstalled = (QueryString.parse(window.location.search).justInstalled === 'true') || false; + /** * @class Implement the Onboarding View for the Ghostery Browser Hub * @extends Component @@ -61,12 +65,15 @@ class OnboardingViewContainer extends Component { const { origin, pathname, hash } = window.location; window.history.pushState({}, '', `${origin}${pathname}${hash}`); - // TODO only invoke if there are no existing settings - if (true) { + // Only set settings to defaults if the user has just installed the browser + if (justInstalled) { this.state = { sendMountActions: true }; - actions.setSetupStep({ setup_step: 8, origin: ONBOARDING }); + actions.setSetupStep({ + setup_step: 8, + origin: ONBOARDING + }); actions.setBlockingPolicy({ blockingPolicy: BLOCKING_POLICY_RECOMMENDED }); actions.setAntiTracking({ enable_anti_tracking: true }); // covered actions.setAdBlock({ enable_ad_block: true }); // covered diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index ce766ad22..b991acef6 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -15,7 +15,7 @@ import React, { Component, Fragment } from 'react'; import { NavLink } from 'react-router-dom'; import ClassNames from 'classnames'; import RadioButton from '../../../../shared-components/RadioButton'; -import { ONBOARDING, CHOOSE_PLAN, CHOOSE_DEFAULT_SEARCH } from '../../OnboardingView/OnboardingConstants'; +import { ONBOARDING, CHOOSE_PLAN } from '../../OnboardingView/OnboardingConstants'; import { Modal } from '../../../../shared-components'; const SEARCH_GHOSTERY = 'Ghostery'; @@ -51,12 +51,13 @@ class ChooseDefaultSearchView extends Component { const { actions, history } = this.props; const { setSetupStep } = actions; - const payload = { - type: 'setDefaultSearch', - search: chosenSearch, - }; - // TODO comment this IN for builds for Dawn + // commented out for testing purposes, as trying to message search@ghostery.com + // outside of Dawn causes an error + // const payload = { + // type: 'setDefaultSearch', + // search: chosenSearch, + // }; // chrome.runtime.sendMessage('search@ghostery.com', payload, () => {}); // chrome.runtime.sendMessage('search@ghostery.com', payload, () => { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap index 2a6ff5651..096c1cfa6 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap @@ -35,7 +35,7 @@ Array [
    - ghostery_dawn_onboarding_surf_with_ease Ghostery + ghostery_dawn_onboarding_surf_with_ease
    Date: Mon, 25 Jan 2021 09:53:09 -0500 Subject: [PATCH 076/113] Use correct hub branching logic in background --- src/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/background.js b/src/background.js index a4e561fcb..a36805461 100644 --- a/src/background.js +++ b/src/background.js @@ -1639,7 +1639,7 @@ function initializeGhosteryModules() { // run scheduledTasks on init scheduledTasks().then(() => { if (globals.JUST_INSTALLED) { - if (BROWSER_INFO.name !== 'ghostery_desktop') { + if (BROWSER_INFO.name === 'ghostery_desktop') { chrome.tabs.create({ url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html?justInstalled=true'), active: true From de2e78a04bbb9977e534ab5c579bf65c6c65b9f6 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 25 Jan 2021 14:37:27 -0500 Subject: [PATCH 077/113] Open correct hub from Help view of panel --- src/background.js | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/background.js b/src/background.js index 85e24f18e..e2f9e4062 100644 --- a/src/background.js +++ b/src/background.js @@ -957,7 +957,9 @@ function onMessageHandler(request, sender, callback) { return true; } if (name === 'openHubPage') { - const hubUrl = chrome.runtime.getURL('./app/templates/hub.html'); + const hubUrl = (BROWSER_INFO.name === 'ghostery_desktop') + ? chrome.runtime.getURL('./app/templates/dawn_onboarding.html') + : chrome.runtime.getURL('./app/templates/hub.html'); metrics.ping('intro_hub_click'); utils.openNewTab({ url: hubUrl, become_active: true }); return false; @@ -1614,22 +1616,23 @@ function initializeGhosteryModules() { // run scheduledTasks on init scheduledTasks().then(() => { if (globals.JUST_INSTALLED) { - if (BROWSER_INFO.name === 'ghostery_desktop') { - chrome.tabs.create({ - url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html?justInstalled=true'), - active: true - }); - } else { - // Open the Ghostery Hub on install with justInstalled query parameter set to true. - // We need to do this after running scheduledTasks for the first time - // because of an A/B test that determines which promo variant is shown in the Hub on install - const showAlternateHub = conf.hub_layout === 'alternate'; - const route = showAlternateHub ? '#home' : ''; - chrome.tabs.create({ - url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), - active: true - }); - } + // TODO comment in before 8.5.5 release (commented out to facilitate onboarding testing) + // if (BROWSER_INFO.name === 'ghostery_desktop') { + chrome.tabs.create({ + url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html?justInstalled=true'), + active: true + }); + // } else { + // // Open the Ghostery Hub on install with justInstalled query parameter set to true. + // // We need to do this after running scheduledTasks for the first time + // // because of an A/B test that determines which promo variant is shown in the Hub on install + // const showAlternateHub = conf.hub_layout === 'alternate'; + // const route = showAlternateHub ? '#home' : ''; + // chrome.tabs.create({ + // url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), + // active: true + // }); + // } } }); }); From 0e25788195eefc3a62771b614894bd1deeec8828 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 25 Jan 2021 17:34:23 -0500 Subject: [PATCH 078/113] Show the appropriate upgrade plan view to user based on their default search selection --- .../ChooseDefaultSearchActions.js | 18 +++++++ .../ChooseDefaultSearchConstants.js | 18 +++++++ .../ChooseDefaultSearchReducer.js | 48 +++++++++++++++++++ .../ChooseDefaultSearchView.jsx | 14 +++--- .../Step3_ChooseDefaultSearchView/index.js | 2 + .../Step4_ChoosePlanView/ChoosePlanView.jsx | 16 +++++-- .../__tests__/ChoosePlanView.test.jsx | 2 +- .../Step4_ChoosePlanView/index.jsx | 2 +- app/ghostery-browser-hub/createStore.js | 2 + 9 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchActions.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js create mode 100644 app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchActions.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchActions.js new file mode 100644 index 000000000..40180dec7 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchActions.js @@ -0,0 +1,18 @@ +/** + * Choose Default Search action creators for the Dawn onboarding hub + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { SET_DEFAULT_SEARCH } from './ChooseDefaultSearchConstants'; + +const setDefaultSearch = data => ({ type: SET_DEFAULT_SEARCH, data }); + +export default setDefaultSearch; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js new file mode 100644 index 000000000..0f06774e1 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js @@ -0,0 +1,18 @@ +/** + * Choose Default Search constants for use by the Dawn onboarding hub + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +export const SET_DEFAULT_SEARCH = 'SET_DEFAULT_SEARCH'; +export const SEARCH_GHOSTERY = 'Ghostery'; +export const SEARCH_BING = 'Bing'; +export const SEARCH_YAHOO = 'Yahoo'; +export const SEARCH_STARTPAGE = 'StartPage'; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js new file mode 100644 index 000000000..3125b14a6 --- /dev/null +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js @@ -0,0 +1,48 @@ +/** + * Reducer for the Choose Default Search view in the Dawn onboarding hub + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2021 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { + SET_DEFAULT_SEARCH, + SEARCH_GHOSTERY, + SEARCH_BING, + SEARCH_STARTPAGE, + SEARCH_YAHOO +} from './ChooseDefaultSearchConstants'; + +const initialState = { + defaultSearch: SEARCH_GHOSTERY, +}; + +function ChooseDefaultSearchReducer(state = initialState, action) { + switch (action.type) { + case SET_DEFAULT_SEARCH: { + const newDefault = action.data; + + console.log('in SET_DEFAULT_SEARCH reducer switch case. newDefault:'); + console.log(newDefault); + + if ([SEARCH_GHOSTERY, SEARCH_BING, SEARCH_STARTPAGE, SEARCH_YAHOO].includes(newDefault)) { + return { + ...state, + defaultSearch: newDefault + }; + } + + return state; + } + + default: return state; + } +} + +export default ChooseDefaultSearchReducer; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index b991acef6..5288a70fd 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -16,13 +16,14 @@ import { NavLink } from 'react-router-dom'; import ClassNames from 'classnames'; import RadioButton from '../../../../shared-components/RadioButton'; import { ONBOARDING, CHOOSE_PLAN } from '../../OnboardingView/OnboardingConstants'; +import { + SEARCH_GHOSTERY, + SEARCH_YAHOO, + SEARCH_STARTPAGE, + SEARCH_BING +} from './ChooseDefaultSearchConstants'; import { Modal } from '../../../../shared-components'; -const SEARCH_GHOSTERY = 'Ghostery'; -const SEARCH_BING = 'Bing'; -const SEARCH_YAHOO = 'Yahoo'; -const SEARCH_STARTPAGE = 'StartPage'; - class ChooseDefaultSearchView extends Component { constructor(props) { super(props); @@ -49,7 +50,7 @@ class ChooseDefaultSearchView extends Component { handleSubmit = () => { const { chosenSearch } = this.state; const { actions, history } = this.props; - const { setSetupStep } = actions; + const { setSetupStep, setDefaultSearch } = actions; // TODO comment this IN for builds for Dawn // commented out for testing purposes, as trying to message search@ghostery.com @@ -67,6 +68,7 @@ class ChooseDefaultSearchView extends Component { // history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); // }); + setDefaultSearch(chosenSearch); setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING }); history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js index e7740acf9..a29cba813 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js @@ -14,9 +14,11 @@ import { withRouter } from 'react-router-dom'; import ChooseDefaultSearchView from './ChooseDefaultSearchView'; import { buildReduxHOC } from '../../../../shared-hub/utils'; +import setDefaultSearch from './ChooseDefaultSearchActions'; import { setSetupStep } from '../../../../shared-hub/actions/SetupLifecycleActions'; const actionCreators = { + setDefaultSearch, setSetupStep, }; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 895ac13a4..3873c9d0d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -19,6 +19,7 @@ import RadioButton from '../../../../shared-components/RadioButton'; import globals from '../../../../../src/classes/Globals'; import { BASIC, PLUS, PREMIUM } from '../../../../hub/Views/UpgradePlanView/UpgradePlanViewConstants'; import { CHOOSE_PLAN, ONBOARDING } from '../../OnboardingView/OnboardingConstants'; +import { SEARCH_GHOSTERY } from '../Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants'; const plusCheckoutLink = `${globals.CHECKOUT_BASE_URL}/en/plus`; const premiumCheckoutLink = `${globals.CHECKOUT_BASE_URL}/en/premium`; @@ -280,7 +281,7 @@ class ChoosePlanView extends React.Component { }; render() { - const { user, didNotSelectGhosterySearch, actions } = this.props; + const { user, defaultSearch, actions } = this.props; const { setSetupStep } = actions; const { expanded, selectedPlan } = this.state; @@ -293,6 +294,11 @@ class ChoosePlanView extends React.Component { down: expanded }); + const selectedGhosteryGlow = (defaultSearch === SEARCH_GHOSTERY); + + console.log('this.props in ChoosePlanView#render:'); + console.log(this.props); + return (
    @@ -305,8 +311,8 @@ class ChoosePlanView extends React.Component {
    {this.renderTitleText()}
    -
    {this.renderSubtitleText(didNotSelectGhosterySearch)}
    - {didNotSelectGhosterySearch && isBasic && ( +
    {this.renderSubtitleText(!selectedGhosteryGlow)}
    + {selectedGhosteryGlow && isBasic && ( {searchPromo()} {/* TODO: For the CTA button below, @@ -319,7 +325,7 @@ class ChoosePlanView extends React.Component {
    )} - {((isBasic && !didNotSelectGhosterySearch) || expanded || isPlus || isPremium) && ( + {((isBasic && !selectedGhosteryGlow) || expanded || isPlus || isPremium) && (
    {(isPlus) ? (
    @@ -378,7 +384,7 @@ ChoosePlanView.propTypes = { plusAccess: PropTypes.bool, premiumAccess: PropTypes.bool, }), - didNotSelectGhosterySearch: PropTypes.bool.isRequired, + defaultSearch: PropTypes.bool.isRequired, }; // Default props used in the Plus View diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx index 9457ee6e8..e68caf9c6 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx @@ -22,7 +22,7 @@ const noop = () => {}; describe('app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx', () => { const initialState = { user: null, - didNotSelectGhosterySearch: false, + selectedGhosteryGlow: true, actions: { setSetupStep: noop } diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx index 397a0ab66..939cf7f5a 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/index.jsx @@ -19,4 +19,4 @@ const actionCreators = { setSetupStep, }; -export default buildReduxHOC(['account'], actionCreators, ChoosePlanView); +export default buildReduxHOC(['account', 'defaultSearch'], actionCreators, ChoosePlanView); diff --git a/app/ghostery-browser-hub/createStore.js b/app/ghostery-browser-hub/createStore.js index 44c6d9fcc..92ad86d72 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/ghostery-browser-hub/createStore.js @@ -20,6 +20,7 @@ import { makeStoreCreator } from '../shared-hub/utils/index'; import toast from '../shared-hub/reducers/ToastReducer'; import antiSuite from '../shared-hub/reducers/AntiSuiteReducer'; import blockingPolicy from '../shared-hub/reducers/BlockingPolicyReducer'; +import defaultSearch from './Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer'; import setupLifecycle from '../shared-hub/reducers/SetupLifecycleReducer'; import account from '../Account/AccountReducer'; import settings from '../panel/reducers/settings'; @@ -28,6 +29,7 @@ const reducers = { toast, antiSuite, blockingPolicy, + defaultSearch, setupLifecycle, account, settings From 8053eeab00765e0f5596449302f6899ea09ed553 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 25 Jan 2021 22:28:29 -0500 Subject: [PATCH 079/113] Copy and formatting fixes in the Choose Plan view --- .../Step4_ChoosePlanView/ChoosePlanView.jsx | 10 ++++++---- .../Step4_ChoosePlanView/ChoosePlanView.scss | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 3873c9d0d..525a1af39 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -28,7 +28,9 @@ const searchPromo = () => (
    { t('ghostery_dawn_onboarding_ad_free_with_ghostery_plus_subscription') }
    -
    { t('ghostery_dawn_onboarding_ad_free_promo') }
    +
    + { `(${t('ghostery_dawn_onboarding_ad_free_promo')})` } +
    { t('ghostery_dawn_onboarding_ad_free_promo_description') }
    ); @@ -208,12 +210,12 @@ class ChoosePlanView extends React.Component { return t('ghostery_dawn_onboarding_your_privacy_plan'); }; - renderSubtitleText = (fromSearchSelectionScreen) => { + renderSubtitleText = (selectedGhosteryGlow) => { const { user } = this.props; const isPlus = (user && user.plusAccess) || false; const isPremium = (user && user.premiumAccess) || false; - if (fromSearchSelectionScreen) return t('ghostery_dawn_onboarding_based_on_your_privacy_preferences'); + if (selectedGhosteryGlow) return t('ghostery_dawn_onboarding_based_on_your_privacy_preferences'); if (isPremium) return ''; if (isPlus) return t('ghostery_dawn_onboarding_keep_your_current_plan_or_upgrade'); return t('ghostery_dawn_onboarding_choose_an_option'); @@ -311,7 +313,7 @@ class ChoosePlanView extends React.Component {
    {this.renderTitleText()}
    -
    {this.renderSubtitleText(!selectedGhosteryGlow)}
    +
    {this.renderSubtitleText(selectedGhosteryGlow)}
    {selectedGhosteryGlow && isBasic && ( {searchPromo()} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss index 8a5eb64ec..f077b06e9 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss @@ -48,6 +48,7 @@ $medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow t font-size: 24px; display: flex; justify-content: center; + font-weight: 500; } .ChoosePlanView__subtitle { margin-top: 12px; @@ -63,8 +64,8 @@ $medium-large-breakpoint: 1118px; // Break when 3 cards on the screen overflow t border: solid 4px $ghosty-blue; } .ChoosePlanView__searchLogo { - height: 37px; - width: 193px; + height: 36px; + width: 166px; margin: 65px auto 0 auto; background-image: url('/app/images/hub/ChooseDefaultSearchView/search-engine-logo-ghostery.svg'); } From 67a9bb3e59e8d26bf8b9977bc38ef172165be2aa Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 25 Jan 2021 22:40:20 -0500 Subject: [PATCH 080/113] Fix Startpage search selection confirmation modal copy --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 86b2cc44b..ef2521506 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2416,7 +2416,7 @@ "message": "Please answer all questions" }, "ghostery_dawn_onboarding_startpage_warning": { - "message": "Just so you know: Startpage's search engine will log your data and use it to serve you targeted ads." + "message": "Just so you know: Startpage's search engine does not collect your data. In order to keep privacy protection free though, it will serve you untargeted, private ads." }, "ghostery_dawn_onboarding_bing_warning": { "message": "Just so you know: Bing's search engine will log your data and use it to serve you targeted ads." From 478ea52a9bc81c398df0c629b652c56af2cb329b Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 25 Jan 2021 22:45:51 -0500 Subject: [PATCH 081/113] Do not show progress bar on Sucess view --- .../Views/OnboardingView/OnboardingView.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx index 357aa9c2a..a8e2584fa 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingView.jsx @@ -18,7 +18,12 @@ import { Route } from 'react-router-dom'; import StepProgressBar from '../OnboardingViews/StepProgressBar'; import StepNavigator from '../OnboardingViews/StepNavigator'; -import { BLOCK_SETTINGS, CHOOSE_DEFAULT_SEARCH, CHOOSE_PLAN } from './OnboardingConstants'; +import { + BLOCK_SETTINGS, + CHOOSE_DEFAULT_SEARCH, + CHOOSE_PLAN, + SUCCESS +} from './OnboardingConstants'; /** * A Functional React component for rendering the Onboarding View @@ -43,7 +48,7 @@ const OnboardingView = (props) => { path={step.path} render={() => (
    - + {(step.index !== SUCCESS) && }
    )} From f5d4668319685a560a944682e534eeee5b8484f0 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 25 Jan 2021 22:52:31 -0500 Subject: [PATCH 082/113] Prevent title text on Success view from wrapping to a new line --- .../Views/OnboardingViews/Step5_SuccessView/SuccessView.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss index fbc607f97..053f1c33d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss @@ -51,11 +51,9 @@ .SuccessView__title { margin-top: 125px; font-size: 24px; - font-weight: 600; - max-width: 450px; + font-weight: 500; line-height: 2.33; text-align: center; - max-width: 430px; } .SuccessView__subtitle { From 201f955996ada0dfc6f9770e06cb861f51f04086 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 26 Jan 2021 01:25:29 -0500 Subject: [PATCH 083/113] Implements Start Trial button logic that branches on user login state. Takes signed in user to checkout instead of starting free trial --- _locales/en/messages.json | 6 +++ .../OnboardingViewContainer.jsx | 2 +- .../Step1_CreateAccountView.jsx | 46 ++++++++++++++++--- .../Step1_CreateAccountView.scss | 37 ++++++++++++++- .../ChooseDefaultSearchView.jsx | 10 ++-- .../Step4_ChoosePlanView/ChoosePlanView.jsx | 22 ++++++--- .../StepNavigator/StepNavigator.jsx | 4 +- src/background.js | 32 ++++++------- 8 files changed, 119 insertions(+), 40 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ef2521506..eb5903e37 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2435,5 +2435,11 @@ }, "ghostery_dawn_onboarding_you_have_selected_an_alternate_serach_engine": { "message": "You have selected an alternate search engine:" + }, + "ghostery_dawn_onboarding_create_account_for_trial": { + "message": "To start your Ghostery Search free trial, you must create or log into an account. This allows you to sync your privacy settings across devices." + }, + "ghostery_dawn_onboarding_back_to_search_selection": { + "message": "Back to search selection" } } diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx index aa1a234e2..2d8d855c2 100644 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingViewContainer.jsx @@ -111,7 +111,7 @@ class OnboardingViewContainer extends Component { { index: CHOOSE_PLAN, path: `/${ONBOARDING}/${CHOOSE_PLAN}`, - bodyComponents: [ChoosePlanView], + bodyComponents: [ChoosePlanView, Step1_CreateAccountView], }, { index: SUCCESS, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx index b551bd552..985b346ee 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.jsx @@ -53,7 +53,7 @@ const renderFAQListItem = (icon, label, description) => ( * @memberof GhosteryBrowserHubViews */ const Step1_CreateAccountView = (props) => { - const { user, actions } = props; + const { actions, step, user } = props; const { setSetupStep, setToast } = actions; const email = user && user.email; @@ -67,6 +67,17 @@ const Step1_CreateAccountView = (props) => { }); }; + const handleNextOnSelectPlanStep = () => { + const { prev } = props; + + setToast({ + toastMessage: '', + toastClass: '' + }); + + prev(); + }; + const renderSkipLink = () => (
    @@ -78,25 +89,46 @@ const Step1_CreateAccountView = (props) => {
    ); + const subtitle = (step === LOGIN) + ? t('ghostery_dawn_onboarding_sync_settings') + : t('ghostery_dawn_onboarding_create_account_for_trial'); + return (user ? (
    {t('ghostery_dawn_onboarding_you_are_signed_in_as')}
    {email}
    - handleSkipButton()}> - {t('next')} - + {step === LOGIN && ( + handleSkipButton()}> + {t('next')} + + )} + {step !== LOGIN && ( +
    handleNextOnSelectPlanStep()}> + {t('next')} +
    + )}
    ) : (
    + {step !== LOGIN && ( +
    +
    + + + {t('ghostery_dawn_onboarding_back_to_search_selection')} + +
    +
    + )} {view === CREATE_ACCOUNT && (
    {t('ghostery_dawn_onboarding_create_a_ghostery_account')}
    )} {view === SIGN_IN && (
    {t('sign_in')}
    )} -
    { t('ghostery_dawn_onboarding_sync_settings') }
    +
    {subtitle}
    {view === CREATE_ACCOUNT && (
    setView(SIGN_IN)}>{t('ghostery_dawn_onboarding_already_have_account')}
    @@ -109,7 +141,7 @@ const Step1_CreateAccountView = (props) => { {/* eslint-disable-next-line react/jsx-pascal-case */} - {renderSkipLink()} + {(step === LOGIN) && renderSkipLink()}
    {faqList.map(item => renderFAQListItem(item.icon, item.label, item.description))}
    @@ -123,7 +155,7 @@ const Step1_CreateAccountView = (props) => { {/* eslint-disable-next-line react/jsx-pascal-case */} - {renderSkipLink()} + {(step === LOGIN) && renderSkipLink()} )}
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss index 59be428f7..10396b3d7 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss @@ -18,6 +18,38 @@ $color-create-account-form-error-red: #e74055; padding-top: 80px; padding-bottom: 40px; } + +.CreateAccountView__relativeContainer { + position: relative; + left: 30px; + top: -70px; +} + +.CreateAccountView__caret.left { + margin: 7px auto 0 auto; + height: 10px; + width: 10px; + border-left: 2px solid $tundora; + border-top: 2px solid $tundora; + cursor: pointer; + transform: rotate(-45deg); +} + +.CreateAccountView__backContainer { + position: absolute; + display: flex; + margin-top: 60px; + @include breakpoint(xlarge down) { + margin-top: 22px; + } + .CreateAccountView__back { + margin-top: 8px; + font-size: 16px; + color: $tundora; + text-decoration: underline; + } +} + .Step1_CreateAccountView__alreadySignedIn { margin-top: 245px; display: flex; @@ -73,11 +105,12 @@ $color-create-account-form-error-red: #e74055; } } .Step1_CreateAccountView__subtitle { - margin-top: 10px; + margin: 10px auto 0 auto; display: flex; justify-content: center; font-size: 18px; text-align: center; + max-width: 600px; @include breakpoint(small down) { margin: auto; max-width: 237px; @@ -235,7 +268,7 @@ $color-create-account-form-error-red: #e74055; @media only screen and (max-width: 740px) { .Step1_CreateAccountView { margin-bottom: 40px; - padding-top: 20px; + padding-top: 80px; } .Step1_CreateAccountView__header { flex-direction: column; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 5288a70fd..2574d59c9 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -55,11 +55,11 @@ class ChooseDefaultSearchView extends Component { // TODO comment this IN for builds for Dawn // commented out for testing purposes, as trying to message search@ghostery.com // outside of Dawn causes an error - // const payload = { - // type: 'setDefaultSearch', - // search: chosenSearch, - // }; - // chrome.runtime.sendMessage('search@ghostery.com', payload, () => {}); + const payload = { + type: 'setDefaultSearch', + search: chosenSearch, + }; + chrome.runtime.sendMessage('search@ghostery.com', payload, () => {}); // chrome.runtime.sendMessage('search@ghostery.com', payload, () => { // // TODO handle errors if needed diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 525a1af39..5b4fad8ac 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -283,7 +283,13 @@ class ChoosePlanView extends React.Component { }; render() { - const { user, defaultSearch, actions } = this.props; + const { + actions, + defaultSearch, + loggedIn, + next, + user, + } = this.props; const { setSetupStep } = actions; const { expanded, selectedPlan } = this.state; @@ -298,9 +304,6 @@ class ChoosePlanView extends React.Component { const selectedGhosteryGlow = (defaultSearch === SEARCH_GHOSTERY); - console.log('this.props in ChoosePlanView#render:'); - console.log(this.props); - return (
    @@ -318,11 +321,16 @@ class ChoosePlanView extends React.Component { {searchPromo()} {/* TODO: For the CTA button below, - 1. If user is signed in, activate the user’s 7-day free trial for the Ghostery Search Plus plan + 1. WIP - what 7-day trial? If user is signed in, activate the user’s 7-day free trial for the Ghostery Search Plus plan and move them to Step 5 if signed in - 2. If user is signed out, clicking this should take them to Step 4b (linked) + 2. DONE If user is signed out, clicking this should take them to Step 4b */} -
    {t('ghostery_dawn_onboarding_start_trial')}
    + {loggedIn && ( + {t('ghostery_dawn_onboarding_start_trial')} + )} + {!loggedIn && ( +
    next()}>{t('ghostery_dawn_onboarding_start_trial')}
    + )}
    {t('ghostery_dawn_onboarding_see_all_plans')}
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx index 8d6af03e0..59fdaa3b5 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx @@ -27,12 +27,12 @@ class StepNavigator extends Component { }; } - next() { + next = () => { const { components } = this.props; this.setState(state => ({ screen: (state.screen + 1) % components.length })); } - prev() { + prev = () => { const { components } = this.props; this.setState(state => ({ screen: ((state.screen - 1) + components.length) % components.length })); } diff --git a/src/background.js b/src/background.js index e2f9e4062..97c0ea529 100644 --- a/src/background.js +++ b/src/background.js @@ -1617,22 +1617,22 @@ function initializeGhosteryModules() { scheduledTasks().then(() => { if (globals.JUST_INSTALLED) { // TODO comment in before 8.5.5 release (commented out to facilitate onboarding testing) - // if (BROWSER_INFO.name === 'ghostery_desktop') { - chrome.tabs.create({ - url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html?justInstalled=true'), - active: true - }); - // } else { - // // Open the Ghostery Hub on install with justInstalled query parameter set to true. - // // We need to do this after running scheduledTasks for the first time - // // because of an A/B test that determines which promo variant is shown in the Hub on install - // const showAlternateHub = conf.hub_layout === 'alternate'; - // const route = showAlternateHub ? '#home' : ''; - // chrome.tabs.create({ - // url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), - // active: true - // }); - // } + if (BROWSER_INFO.name === 'ghostery_desktop') { + chrome.tabs.create({ + url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html?justInstalled=true'), + active: true + }); + } else { + // Open the Ghostery Hub on install with justInstalled query parameter set to true. + // We need to do this after running scheduledTasks for the first time + // because of an A/B test that determines which promo variant is shown in the Hub on install + const showAlternateHub = conf.hub_layout === 'alternate'; + const route = showAlternateHub ? '#home' : ''; + chrome.tabs.create({ + url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), + active: true + }); + } } }); }); From ba27815be3dd4aee722016c3426b93466066d415 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 26 Jan 2021 09:22:35 -0500 Subject: [PATCH 084/113] Change StartPage identifier to Startpage --- .../ChooseDefaultSearchConstants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js index 0f06774e1..874dfcb5e 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js @@ -15,4 +15,4 @@ export const SET_DEFAULT_SEARCH = 'SET_DEFAULT_SEARCH'; export const SEARCH_GHOSTERY = 'Ghostery'; export const SEARCH_BING = 'Bing'; export const SEARCH_YAHOO = 'Yahoo'; -export const SEARCH_STARTPAGE = 'StartPage'; +export const SEARCH_STARTPAGE = 'Startpage'; From 09ee4de83c5cc53d980c740f4a782d7489881fcf Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 26 Jan 2021 10:53:34 -0500 Subject: [PATCH 085/113] Modifications to faciliate testing --- .../ChooseDefaultSearchView.jsx | 10 +++--- src/background.js | 32 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 2574d59c9..5288a70fd 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -55,11 +55,11 @@ class ChooseDefaultSearchView extends Component { // TODO comment this IN for builds for Dawn // commented out for testing purposes, as trying to message search@ghostery.com // outside of Dawn causes an error - const payload = { - type: 'setDefaultSearch', - search: chosenSearch, - }; - chrome.runtime.sendMessage('search@ghostery.com', payload, () => {}); + // const payload = { + // type: 'setDefaultSearch', + // search: chosenSearch, + // }; + // chrome.runtime.sendMessage('search@ghostery.com', payload, () => {}); // chrome.runtime.sendMessage('search@ghostery.com', payload, () => { // // TODO handle errors if needed diff --git a/src/background.js b/src/background.js index 97c0ea529..e2f9e4062 100644 --- a/src/background.js +++ b/src/background.js @@ -1617,22 +1617,22 @@ function initializeGhosteryModules() { scheduledTasks().then(() => { if (globals.JUST_INSTALLED) { // TODO comment in before 8.5.5 release (commented out to facilitate onboarding testing) - if (BROWSER_INFO.name === 'ghostery_desktop') { - chrome.tabs.create({ - url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html?justInstalled=true'), - active: true - }); - } else { - // Open the Ghostery Hub on install with justInstalled query parameter set to true. - // We need to do this after running scheduledTasks for the first time - // because of an A/B test that determines which promo variant is shown in the Hub on install - const showAlternateHub = conf.hub_layout === 'alternate'; - const route = showAlternateHub ? '#home' : ''; - chrome.tabs.create({ - url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), - active: true - }); - } + // if (BROWSER_INFO.name === 'ghostery_desktop') { + chrome.tabs.create({ + url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html?justInstalled=true'), + active: true + }); + // } else { + // // Open the Ghostery Hub on install with justInstalled query parameter set to true. + // // We need to do this after running scheduledTasks for the first time + // // because of an A/B test that determines which promo variant is shown in the Hub on install + // const showAlternateHub = conf.hub_layout === 'alternate'; + // const route = showAlternateHub ? '#home' : ''; + // chrome.tabs.create({ + // url: chrome.runtime.getURL(`./app/templates/hub.html?justInstalled=true&ah=${showAlternateHub}${route}`), + // active: true + // }); + // } } }); }); From 6d4d5ca21f3c86099b577b449069e7e296dedb96 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 26 Jan 2021 11:24:36 -0500 Subject: [PATCH 086/113] Update ghostery-common dependency to ^1.2.1 --- package.json | 2 +- yarn.lock | 70 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index c75e4fc76..40bcf8dac 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "classnames": "^2.2.5", "d3": "^5.16.0", "foundation-sites": "^6.6.2", - "ghostery-common": "^1.2.0", + "ghostery-common": "^1.2.1", "history": "^4.10.1", "jquery": "3.5.0", "json-api-normalizer": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index e68aee61c..02e1aeafe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -618,37 +618,47 @@ resolved "https://registry.yarnpkg.com/@cliqz-oss/dexie/-/dexie-2.0.4.tgz#0e710504e2b9198baa9b046abd3a82731b94d56e" integrity sha512-HxMbBQfdy0CehThTFierXbRPI+PHDEucUUriCCzViAKbCWWQIlL6uZcyDaaPRMPWy45v78lezPB4457kfjS72g== -"@cliqz/adblocker-content@^1.18.8": - version "1.18.8" - resolved "https://registry.yarnpkg.com/@cliqz/adblocker-content/-/adblocker-content-1.18.8.tgz#96473f14c098a20091298d34a6addcd430aceebd" - integrity sha512-YZ1xYBVG3LmxsdTYvTs/Bc7pzCw/Dy4HFo6N+oIuGP+Le/0aGSkACUl3ue5I2+Cx0WmL0Z8I4QonTKDc06HR+A== +"@cliqz/adblocker-content@^1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@cliqz/adblocker-content/-/adblocker-content-1.20.0.tgz#fcfa2845a577ba8d9af282afbae2fc437b3f1c70" + integrity sha512-KcokmK2B+tAnVMi7nGHgzXUVf78wAODG1Uk+K3tBPf9VAo3mwldYZ472uTj6LUfZv5oeTwe4PwfmPWXWZy3Eew== + dependencies: + "@cliqz/adblocker-extended-selectors" "^1.20.0" + +"@cliqz/adblocker-extended-selectors@^1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@cliqz/adblocker-extended-selectors/-/adblocker-extended-selectors-1.20.0.tgz#95ede657b670f627b39f92d85a97093cecee6ffe" + integrity sha512-dnBPIngGe1eDWvYX49eP2yyCE2AY1QD5E+8SaXW6lslnjS0GQnkcXCAkkGR2am4Qdk78HAiWTXL65Zt9hdkupA== -"@cliqz/adblocker-webextension-cosmetics@^1.18.8": - version "1.18.8" - resolved "https://registry.yarnpkg.com/@cliqz/adblocker-webextension-cosmetics/-/adblocker-webextension-cosmetics-1.18.8.tgz#a1064393bb19cef2c2d80f0aa2cbbe546eec564a" - integrity sha512-epQLl7POmuH5l1WYXEzR6TYZktgxxJ+UVXEAkN1CX98PJfdEGtbzL7C2gaBdstLSuAIdCZQ9Xrw4BLF2MoFlmw== +"@cliqz/adblocker-webextension-cosmetics@^1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@cliqz/adblocker-webextension-cosmetics/-/adblocker-webextension-cosmetics-1.20.0.tgz#ea35811fe25bd19f16cd9ddccdd818c292025ef0" + integrity sha512-AkfuXHzMw/L9rq9iLzw5bzjU7EgBDpWL6eR/VB9gqrPeprj8NR8jDH74DFIzW6KZ4CGM77q1WEz3LUEG3d3nfA== dependencies: - "@cliqz/adblocker-content" "^1.18.8" + "@cliqz/adblocker-content" "^1.20.0" + "@cliqz/adblocker-extended-selectors" "^1.20.0" -"@cliqz/adblocker-webextension@^1.18.8": - version "1.18.8" - resolved "https://registry.yarnpkg.com/@cliqz/adblocker-webextension/-/adblocker-webextension-1.18.8.tgz#07a3fa5bdb2171bd1f13b84889c33b67232bc192" - integrity sha512-ZdgIEmETSOmg5TEs54KqwsALCB2kQpYN+Tqhelr0T5G2N+YHvZHc8t5V+uit829jEGHKPhHjSBhDkfDcjHK3Jw== +"@cliqz/adblocker-webextension@^1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@cliqz/adblocker-webextension/-/adblocker-webextension-1.20.0.tgz#f914c43067b501797828e84f31b8d05a0cbd2e3a" + integrity sha512-AuIa04SzCZUTphVtZx75tsbejO9k5kiI71rzzTdELt7peW2aNatyXTtzIe2f62HqMKgiMKCTZZiPi6Fak7Jvkg== dependencies: - "@cliqz/adblocker" "^1.18.8" - "@cliqz/adblocker-content" "^1.18.8" + "@cliqz/adblocker" "^1.20.0" + "@cliqz/adblocker-content" "^1.20.0" tldts-experimental "^5.6.21" webextension-polyfill-ts "^0.22.0" -"@cliqz/adblocker@^1.18.8": - version "1.18.8" - resolved "https://registry.yarnpkg.com/@cliqz/adblocker/-/adblocker-1.18.8.tgz#f6e5724fe6573c2e68f2545d90bcce3e1ecfbae9" - integrity sha512-19m0GhlOcdSvQ/BqVuaMgbYkgQ4ys8koBRW4K7Ua4V5fFWL0t8ckdcZ/gBOqwECS2m8agXSpEbbyJjNmHBHpMQ== +"@cliqz/adblocker@^1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@cliqz/adblocker/-/adblocker-1.20.0.tgz#514746e9ee72fcd886f1e2e1aaf13b28fc63f232" + integrity sha512-lkEj0Pj1ikwMURrvoFv0YnLfaXFuJI+jexI7zdh4fDmlwRppzDDgOhPXgCczoAlYacJk5x2mf7pan6JybRD9Kw== dependencies: + "@cliqz/adblocker-content" "^1.20.0" + "@cliqz/adblocker-extended-selectors" "^1.20.0" "@remusao/guess-url-type" "^1.1.2" "@remusao/small" "^1.1.2" "@remusao/smaz" "^1.7.1" - "@types/chrome" "^0.0.126" + "@types/chrome" "^0.0.128" "@types/firefox-webext-browser" "^82.0.0" tldts-experimental "^5.6.21" @@ -1013,10 +1023,10 @@ dependencies: "@babel/types" "^7.3.0" -"@types/chrome@^0.0.126": - version "0.0.126" - resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.126.tgz#f9f3436712f0c7c12ea9798abc9b95575ad7b23a" - integrity sha512-191z7uoyfbGU+z7/m45j9XbWugWqVHVPMM4hJV5cZ+3YzGCT9wFjMUHO3Wr3Xvo8aVodvRNu28u7lvEaAnfbzg== +"@types/chrome@^0.0.128": + version "0.0.128" + resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.128.tgz#5dbd8b2539a367353fbe4386f119b510105f8b6a" + integrity sha512-eGc599TDtersMBW1cSnExHm0IHrXrO5xdk6Sa2Dq30ED+hR1rpT1ez0NNcCgvGO52nmktGfyvd3Uyquzv3LL4g== dependencies: "@types/filesystem" "*" "@types/har-format" "*" @@ -4043,14 +4053,14 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -ghostery-common@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ghostery-common/-/ghostery-common-1.2.0.tgz#84d7bbe7c29fbf488e4668853ffeef76d394fee1" - integrity sha512-J/cSrlgXjrCUTqToroMihWwsKuuKGXhWvLg+n9p5sFr1HHUvOoe7hNRaXfWLzcCfjPIawIvnEf3eJYGlKrleTg== +ghostery-common@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ghostery-common/-/ghostery-common-1.2.1.tgz#ad201b6e959633c3f5bdcababe6ea9ce03e2ae2c" + integrity sha512-BidnDzj/emiR+BUaQ6FwAYYZJda0aqPid/mhbEx5IJGogwHh5lGnmxwADH1DAA442GbW+zgZx2e2cFjC/dJGwA== dependencies: "@cliqz-oss/dexie" "^2.0.4" - "@cliqz/adblocker-webextension" "^1.18.8" - "@cliqz/adblocker-webextension-cosmetics" "^1.18.8" + "@cliqz/adblocker-webextension" "^1.20.0" + "@cliqz/adblocker-webextension-cosmetics" "^1.20.0" "@cliqz/url-parser" "^1.1.4" abortcontroller-polyfill "^1.5.0" anonymous-credentials "https://github.com/cliqz-oss/anonymous-credentials/releases/download/1.0.0/anonymous-credentials-1.0.0.tgz" From f7848dcf250091e382c032501e9a15a9cd9a747b Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 26 Jan 2021 11:27:12 -0500 Subject: [PATCH 087/113] Update Startpage logo --- .../search-engine-logo-startpage.svg | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-startpage.svg b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-startpage.svg index 7ed830f93..96ba1cf85 100644 --- a/app/images/hub/ChooseDefaultSearchView/search-engine-logo-startpage.svg +++ b/app/images/hub/ChooseDefaultSearchView/search-engine-logo-startpage.svg @@ -1,14 +1,9 @@ - - - - - - - - - - - + + + Startpage-Logo + + + - + \ No newline at end of file From f7ce5460b35daefeb380280da355684846e0921a Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 26 Jan 2021 12:08:37 -0500 Subject: [PATCH 088/113] Fix prop type mismatch console warnings on Welcome and Choose Plan screens --- .../OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx | 2 +- .../Views/OnboardingViews/StepNavigator/StepNavigator.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 5b4fad8ac..6b23a5728 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -394,7 +394,7 @@ ChoosePlanView.propTypes = { plusAccess: PropTypes.bool, premiumAccess: PropTypes.bool, }), - defaultSearch: PropTypes.bool.isRequired, + defaultSearch: PropTypes.string.isRequired, }; // Default props used in the Plus View diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx index 59fdaa3b5..08d5580d9 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/StepNavigator/StepNavigator.jsx @@ -51,7 +51,7 @@ class StepNavigator extends Component { // PropTypes ensure we pass required props of the correct type StepNavigator.propTypes = { - step: PropTypes.number.isRequired, + step: PropTypes.string.isRequired, components: PropTypes.arrayOf(PropTypes.elementType.isRequired).isRequired, }; From f92160c0664d3f0b76d5d554c4e65d5406b2f50e Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Tue, 26 Jan 2021 12:13:54 -0500 Subject: [PATCH 089/113] GH-2218: Browser Onboarding Copy Edits (#666) * Add welcome message, startpage message already updated * Fix errors, add max-width to beautify string --- _locales/en/messages.json | 3 +++ .../Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx | 2 +- .../Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index eb5903e37..844613025 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1776,6 +1776,9 @@ "ghostery_dawn_onboarding_lets_begin": { "message": "We've centralized online privacy by integrating our signature as well as novel technologies." }, + "ghostery_dawn_onboarding_welcome_message": { + "message": "Our browser protects your data and blocks bloatware so that you can surf privately and over 2x faster than with Chrome." + }, "ghostery_dawn_onboarding_lets_do_this": { "message": "Set up My Browser" }, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx index 1b25e5772..3f4216740 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.jsx @@ -27,7 +27,7 @@ const WelcomeView = (props) => { return (
    {t('ghostery_dawn_onboarding_welcome')}
    -
    {t('ghostery_dawn_onboarding_lets_begin')}
    +
    {t('ghostery_dawn_onboarding_welcome_message')}
    setSetupStep({ setup_step: LOGIN, origin: WELCOME })}> {t('ghostery_dawn_onboarding_lets_do_this')} diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss index e327db797..f25adcef6 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss @@ -28,7 +28,7 @@ .WelcomeView__subtitle { margin-bottom: 20px; - width: 392px; + max-width: 495px; font-size: 18px; line-height: 2.33; text-align: center; From 172af55fa4d8bd024578db2345d52a758ab82730 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 26 Jan 2021 12:40:59 -0500 Subject: [PATCH 090/113] Fix
  • inside
  • console warning on Block Settings screen --- .../BlockSettingsView.jsx | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx index 0eca9aa0e..c4691c26b 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx +++ b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.jsx @@ -160,65 +160,65 @@ class BlockSettingsView extends Component {
  • -
    -
    - this.handleAnswerChange('kindsOfTrackers', 0)} altDesign /> -
    -
    {t('ghostery_dawn_onboarding_kinds_of_trackers_all')}
    + +
    +
    + this.handleAnswerChange('kindsOfTrackers', 0)} altDesign />
    -
    -
    - this.handleAnswerChange('kindsOfTrackers', 1)} altDesign /> -
    -
    {t('ghostery_dawn_onboarding_kinds_of_trackers_ad_and_analytics')}
    +
    {t('ghostery_dawn_onboarding_kinds_of_trackers_all')}
    +
    +
    +
    + this.handleAnswerChange('kindsOfTrackers', 1)} altDesign />
    -
    -
    - this.handleAnswerChange('kindsOfTrackers', 2)} altDesign /> -
    -
    {t('ghostery_dawn_onboarding_kinds_of_trackers_none')}
    +
    {t('ghostery_dawn_onboarding_kinds_of_trackers_ad_and_analytics')}
    +
    +
    +
    + this.handleAnswerChange('kindsOfTrackers', 2)} altDesign />
    -
  • -
    - {t('ghostery_dawn_onboarding_question_anti_tracking')} -
    - -
    -
    -
  • -
    -
    - this.handleAnswerChange('antiTracking', true)} altDesign /> +
    {t('ghostery_dawn_onboarding_kinds_of_trackers_none')}
    +
    +
  • +
    + {t('ghostery_dawn_onboarding_question_anti_tracking')} +
    +
    -
    {t('hub_setup_modal_button_yes')}
    -
    -
    - this.handleAnswerChange('antiTracking', false)} altDesign /> -
    -
    {t('hub_setup_modal_button_no')}
    +
  • +
    +
    + this.handleAnswerChange('antiTracking', true)} altDesign />
    -
  • -
    - {t('ghostery_dawn_onboarding_question_smart_browsing')} -
    - -
    -
    -
  • -
    -
    - this.handleAnswerChange('smartBrowsing', true)} altDesign /> -
    -
    {t('hub_setup_modal_button_yes')}
    +
    {t('hub_setup_modal_button_yes')}
    +
    +
    +
    + this.handleAnswerChange('antiTracking', false)} altDesign />
    -
    -
    - this.handleAnswerChange('smartBrowsing', false)} altDesign /> +
    {t('hub_setup_modal_button_no')}
    +
    +
  • +
    + {t('ghostery_dawn_onboarding_question_smart_browsing')} +
    +
    -
    {t('hub_setup_modal_button_no')}
  • +
    +
    + this.handleAnswerChange('smartBrowsing', true)} altDesign /> +
    +
    {t('hub_setup_modal_button_yes')}
    +
    +
    +
    + this.handleAnswerChange('smartBrowsing', false)} altDesign /> +
    +
    {t('hub_setup_modal_button_no')}
    +
    1. {t('ghostery_dawn_onboarding_question_block_ads')}
    2. -
      -
      - this.handleAnswerChange('blockAds', true)} altDesign /> -
      -
      {t('hub_setup_modal_button_yes')}
      -
      -
      -
      - this.handleAnswerChange('blockAds', false)} altDesign /> -
      -
      {t('hub_setup_modal_button_no')}
      -
      + {this.renderAnswerBlock((blockAds === true), 'blockAds', true, t('hub_setup_modal_button_yes'))} + {this.renderAnswerBlock((blockAds === false), 'blockAds', false, t('hub_setup_modal_button_no'))}
    3. {t('ghostery_dawn_onboarding_question_kinds_of_trackers')} @@ -161,24 +160,9 @@ class BlockSettingsView extends Component {
    -
    -
    - this.handleAnswerChange('kindsOfTrackers', 0)} altDesign /> -
    -
    {t('ghostery_dawn_onboarding_kinds_of_trackers_all')}
    -
    -
    -
    - this.handleAnswerChange('kindsOfTrackers', 1)} altDesign /> -
    -
    {t('ghostery_dawn_onboarding_kinds_of_trackers_ad_and_analytics')}
    -
    -
    -
    - this.handleAnswerChange('kindsOfTrackers', 2)} altDesign /> -
    -
    {t('ghostery_dawn_onboarding_kinds_of_trackers_none')}
    -
    + {this.renderAnswerBlock((kindsOfTrackers === 0), 'kindsOfTrackers', 0, t('ghostery_dawn_onboarding_kinds_of_trackers_all'))} + {this.renderAnswerBlock((kindsOfTrackers === 1), 'kindsOfTrackers', 1, t('ghostery_dawn_onboarding_kinds_of_trackers_ad_and_analytics'))} + {this.renderAnswerBlock((kindsOfTrackers === 2), 'kindsOfTrackers', 2, t('ghostery_dawn_onboarding_kinds_of_trackers_none'))}
  • {t('ghostery_dawn_onboarding_question_anti_tracking')} @@ -187,18 +171,8 @@ class BlockSettingsView extends Component {
  • -
    -
    - this.handleAnswerChange('antiTracking', true)} altDesign /> -
    -
    {t('hub_setup_modal_button_yes')}
    -
    -
    -
    - this.handleAnswerChange('antiTracking', false)} altDesign /> -
    -
    {t('hub_setup_modal_button_no')}
    -
    + {this.renderAnswerBlock((antiTracking === true), 'antiTracking', true, t('hub_setup_modal_button_yes'))} + {this.renderAnswerBlock((antiTracking === false), 'antiTracking', false, t('hub_setup_modal_button_no'))}
  • {t('ghostery_dawn_onboarding_question_smart_browsing')} @@ -207,18 +181,8 @@ class BlockSettingsView extends Component {
  • -
    -
    - this.handleAnswerChange('smartBrowsing', true)} altDesign /> -
    -
    {t('hub_setup_modal_button_yes')}
    -
    -
    -
    - this.handleAnswerChange('smartBrowsing', false)} altDesign /> -
    -
    {t('hub_setup_modal_button_no')}
    -
    + {this.renderAnswerBlock((smartBrowsing === true), 'smartBrowsing', true, t('hub_setup_modal_button_yes'))} + {this.renderAnswerBlock((smartBrowsing === false), 'smartBrowsing', false, t('hub_setup_modal_button_no'))}
    +
    , +] +`; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js b/app/dawn-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js similarity index 92% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js rename to app/dawn-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js index 1d8b1c048..ec60917d4 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js +++ b/app/dawn-hub/Views/OnboardingViews/Step2_BlockSettingsView/index.js @@ -1,5 +1,5 @@ /** - * Point of entry index.js file for Ghostery Browser Hub Onboarding Block Settings View + * Point of entry index.js file for Dawn Hub onboarding flow Onboarding Block Settings View * * Ghostery Browser Extension * https://www.ghostery.com/ diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchActions.js b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchActions.js similarity index 100% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchActions.js rename to app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchActions.js diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js similarity index 100% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js rename to app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchConstants.js diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js similarity index 100% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js rename to app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx similarity index 98% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx rename to app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index ac0ad7dab..45c2175e1 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -1,10 +1,10 @@ /** - * Ghostery Browser Hub Choose Default Search View Component + * Dawn Hub onboarding flow Choose Default Search View Component * * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss similarity index 100% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss rename to app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js similarity index 84% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js rename to app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js index a29cba813..bd33072be 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js +++ b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/index.js @@ -1,10 +1,10 @@ /** - * Point of entry index.js file for Ghostery Browser Hub Choose Default Search View + * Point of entry index.js file for Dawn Hub onboarding flow Choose Default Search View * * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2021 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx b/app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx similarity index 99% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx rename to app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx index 6b23a5728..6cdcf2fe0 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx +++ b/app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.jsx @@ -1,5 +1,5 @@ /** - * Ghostery Browser Hub Choose Plan View Component + * Dawn Hub onboarding flow Choose Plan View Component * * Ghostery Browser Extension * https://www.ghostery.com/ diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss b/app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss similarity index 100% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss rename to app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx b/app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx similarity index 95% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx rename to app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx index e68caf9c6..8f815686f 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx +++ b/app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/ChoosePlanView.test.jsx @@ -19,7 +19,7 @@ import ChoosePlanView from '../ChoosePlanView'; const noop = () => {}; -describe('app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx', () => { +describe('app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx', () => { const initialState = { user: null, selectedGhosteryGlow: true, diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap b/app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap similarity index 91% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap rename to app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap index 1f22378ab..17346e643 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap +++ b/app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/__tests__/__snapshots__/ChoosePlanView.test.jsx.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with basic user logged in 1`] = `ShallowWrapper {}`; +exports[`app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with basic user logged in 1`] = `ShallowWrapper {}`; -exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with plus user logged in 1`] = `ShallowWrapper {}`; +exports[`app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with plus user logged in 1`] = `ShallowWrapper {}`; -exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with premium user logged in 1`] = `ShallowWrapper {}`; +exports[`app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with premium user logged in 1`] = `ShallowWrapper {}`; -exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with user not logged in 1`] = `ShallowWrapper {}`; +exports[`app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Shallow snapshot tests rendered with Enzyme ChoosePlanView View with user not logged in 1`] = `ShallowWrapper {}`; -exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Snapshot tests with react-test-renderer ChoosePlanView is rendered correctly 1`] = ` +exports[`app/dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.test.jsx Snapshot tests with react-test-renderer ChoosePlanView is rendered correctly 1`] = `
    { const { actions } = props; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss b/app/dawn-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss similarity index 100% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss rename to app/dawn-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/SuccessView.test.jsx b/app/dawn-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/SuccessView.test.jsx similarity index 89% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/SuccessView.test.jsx rename to app/dawn-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/SuccessView.test.jsx index 7136d4ad6..761ef937d 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/SuccessView.test.jsx +++ b/app/dawn-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/SuccessView.test.jsx @@ -18,7 +18,7 @@ import SuccessView from '../SuccessView'; const noop = () => {}; -describe('app/ghostery-browser-hub/Views/OnboardingViews/Step0_SuccessView/SuccessView.test.jsx', () => { +describe('app/dawn-hub/Views/OnboardingViews/Step0_SuccessView/SuccessView.test.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { test('Success View is rendered correctly', () => { const initialState = { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap b/app/dawn-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap similarity index 85% rename from app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap rename to app/dawn-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap index 096c1cfa6..c6b8ecbbe 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap +++ b/app/dawn-hub/Views/OnboardingViews/Step5_SuccessView/__tests__/__snapshots__/SuccessView.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step0_SuccessView/SuccessView.test.jsx Snapshot tests with react-test-renderer Success View is rendered correctly 1`] = ` +exports[`app/dawn-hub/Views/OnboardingViews/Step0_SuccessView/SuccessView.test.jsx Snapshot tests with react-test-renderer Success View is rendered correctly 1`] = ` Array [
    { const { currentStep } = props; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss b/app/dawn-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss similarity index 100% rename from app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss rename to app/dawn-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.scss diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/StepProgressBar.test.jsx b/app/dawn-hub/Views/OnboardingViews/StepProgressBar/__tests__/StepProgressBar.test.jsx similarity index 95% rename from app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/StepProgressBar.test.jsx rename to app/dawn-hub/Views/OnboardingViews/StepProgressBar/__tests__/StepProgressBar.test.jsx index a4320310e..c0f438584 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/StepProgressBar.test.jsx +++ b/app/dawn-hub/Views/OnboardingViews/StepProgressBar/__tests__/StepProgressBar.test.jsx @@ -20,7 +20,7 @@ import { WELCOME, LOGIN, BLOCK_SETTINGS, CHOOSE_DEFAULT_SEARCH, CHOOSE_PLAN, SUC const noop = () => {}; -describe('app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx', () => { +describe('app/dawn-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx', () => { const initialState = { currentStep: LOGIN, actions: { diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap b/app/dawn-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap similarity index 58% rename from app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap rename to app/dawn-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap index 9b0463a08..340afb482 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap +++ b/app/dawn-hub/Views/OnboardingViews/StepProgressBar/__tests__/__snapshots__/StepProgressBar.test.jsx.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 0 1`] = `ShallowWrapper {}`; +exports[`app/dawn-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 0 1`] = `ShallowWrapper {}`; -exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 1 1`] = `ShallowWrapper {}`; +exports[`app/dawn-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 1 1`] = `ShallowWrapper {}`; -exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 4 1`] = `ShallowWrapper {}`; +exports[`app/dawn-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 4 1`] = `ShallowWrapper {}`; -exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 5 1`] = `ShallowWrapper {}`; +exports[`app/dawn-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Shallow snapshot tests rendered with Enzyme StepProgressBar View step 5 1`] = `ShallowWrapper {}`; -exports[`app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Snapshot tests with react-test-renderer StepProgressBar is rendered correctly 1`] = ` +exports[`app/dawn-hub/Views/OnboardingViews/StepProgressBar/StepProgressBar.test.jsx Snapshot tests with react-test-renderer StepProgressBar is rendered correctly 1`] = `
    diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js b/app/dawn-hub/Views/OnboardingViews/StepProgressBar/index.js similarity index 88% rename from app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js rename to app/dawn-hub/Views/OnboardingViews/StepProgressBar/index.js index 1f20a3fc0..e3394b2ef 100644 --- a/app/ghostery-browser-hub/Views/OnboardingViews/StepProgressBar/index.js +++ b/app/dawn-hub/Views/OnboardingViews/StepProgressBar/index.js @@ -1,5 +1,5 @@ /** - * Point of entry index.js file for Ghostery Browser Hub Step Progress Bar + * Point of entry index.js file for Dawn Hub onboarding flow Step Progress Bar * * Ghostery Browser Extension * https://www.ghostery.com/ diff --git a/app/ghostery-browser-hub/createStore.js b/app/dawn-hub/createStore.js similarity index 96% rename from app/ghostery-browser-hub/createStore.js rename to app/dawn-hub/createStore.js index 92ad86d72..e9b289bd4 100644 --- a/app/ghostery-browser-hub/createStore.js +++ b/app/dawn-hub/createStore.js @@ -1,5 +1,5 @@ /** - * Ghostery Browser Hub React Store Init + * Dawn Hub onboarding flow Redux Store Init * * Ghostery Browser Extension * https://www.ghostery.com/ diff --git a/app/ghostery-browser-hub/index.jsx b/app/dawn-hub/index.jsx similarity index 82% rename from app/ghostery-browser-hub/index.jsx rename to app/dawn-hub/index.jsx index 9a9aa4a39..8c256f91a 100644 --- a/app/ghostery-browser-hub/index.jsx +++ b/app/dawn-hub/index.jsx @@ -1,5 +1,5 @@ /** - * Ghostery Ghostery-Browser-Specific Hub React App Init + * Dawn Hub React App Init * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -10,7 +10,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0 * - * @namespace GhosteryBrowserHubComponents + * @namespace DawnHubViews */ import React from 'react'; import ReactDOM from 'react-dom'; @@ -26,8 +26,8 @@ import OnboardingView from './Views/OnboardingView'; const store = createStore(); /** - * Top-Level Component for the Ghostery Browser Hub - * @memberof GhosteryBrowserHubComponents + * Top-Level view for the Dawn Hub + * @memberof DawnHubViews */ const Hub = () => ( diff --git a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingConstants.js b/app/ghostery-browser-hub/Views/OnboardingView/OnboardingConstants.js deleted file mode 100644 index fc325d8fb..000000000 --- a/app/ghostery-browser-hub/Views/OnboardingView/OnboardingConstants.js +++ /dev/null @@ -1,12 +0,0 @@ -/** -Constants for Ghostery Browser Intro Hub Onboarding -These constants map the onboarding step numbers to more memorable names - */ - -export const ONBOARDING = 'onboarding'; -export const WELCOME = '0'; -export const LOGIN = '1'; -export const BLOCK_SETTINGS = '2'; -export const CHOOSE_DEFAULT_SEARCH = '3'; -export const CHOOSE_PLAN = '4'; -export const SUCCESS = '5'; diff --git a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap b/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap deleted file mode 100644 index 823005b73..000000000 --- a/app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/__tests__/__snapshots__/BlockSettingsView.test.jsx.snap +++ /dev/null @@ -1,327 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.test.jsx Shallow snapshot tests rendered with Enzyme BlockSettings View happy path 1`] = `ShallowWrapper {}`; - -exports[`app/ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.test.jsx Snapshot tests with react-test-renderer BlockSettings View is rendered correctly 1`] = ` -Array [ - , -
    -
    - ghostery_dawn_onboarding_which_privacy_plan -
    -
    - ghostery_dawn_onboarding_tell_us_your_preferences -
    -
    -
    -
    - - - -
    -
    - ghostery_dawn_onboarding_recommended_choices -
    -
    -
      -
    1. - ghostery_dawn_onboarding_question_block_ads -
    2. -
      -
      - - - - - -
      -
      - hub_setup_modal_button_yes -
      -
      -
      -
      - - - - - -
      -
      - hub_setup_modal_button_no -
      -
      -
    3. -
      - ghostery_dawn_onboarding_question_kinds_of_trackers -
      -
      -
      -
      - - - - - -
      -
      - ghostery_dawn_onboarding_kinds_of_trackers_all -
      -
      -
      -
      - - - - - -
      -
      - ghostery_dawn_onboarding_kinds_of_trackers_ad_and_analytics -
      -
      -
      -
      - - - - - -
      -
      - ghostery_dawn_onboarding_kinds_of_trackers_none -
      -
      -
    4. -
      - ghostery_dawn_onboarding_question_anti_tracking -
      -
      -
    5. -
      -
      - - - - - -
      -
      - hub_setup_modal_button_yes -
      -
      -
      -
      - - - - - -
      -
      - hub_setup_modal_button_no -
      -
      -
    6. -
      - ghostery_dawn_onboarding_question_smart_browsing -
      -
      -
    7. -
      -
      - - - - - -
      -
      - hub_setup_modal_button_yes -
      -
      -
      -
      - - - - - -
      -
      - hub_setup_modal_button_no -
      -
      - -
    -
    - -
    , -] -`; diff --git a/app/scss/hub_ghostery_browser.scss b/app/scss/dawn_hub.scss similarity index 66% rename from app/scss/hub_ghostery_browser.scss rename to app/scss/dawn_hub.scss index ef1d248c8..9efd2446a 100644 --- a/app/scss/hub_ghostery_browser.scss +++ b/app/scss/dawn_hub.scss @@ -67,16 +67,16 @@ html, body, #root { @import './partials/radio_button'; // Imports from ../ghostery-browser-hub directory -@import '../ghostery-browser-hub/Views/OnboardingView/OnboardingView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/StepProgressbar/StepProgressbar.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss'; -@import '../ghostery-browser-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; +@import '../dawn-hub/Views/OnboardingView/OnboardingView.scss'; +@import '../dawn-hub/Views/OnboardingViews/Step0_WelcomeView/WelcomeView.scss'; +@import '../dawn-hub/Views/OnboardingViews/StepProgressbar/StepProgressbar.scss'; +@import '../dawn-hub/Views/OnboardingViews/Step1_CreateAccountView/Step1_CreateAccountView.scss'; +@import '../dawn-hub/Views/OnboardingViews/Step1_CreateAccountForm/Step1_CreateAccountForm.scss'; +@import '../dawn-hub/Views/OnboardingViews/Step1_LogInForm/Step1_LogInForm.scss'; +@import '../dawn-hub/Views/OnboardingViews/Step2_BlockSettingsView/BlockSettingsView.scss'; +@import '../dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss'; +@import '../dawn-hub/Views/OnboardingViews/Step4_ChoosePlanView/ChoosePlanView.scss'; +@import '../dawn-hub/Views/OnboardingViews/Step5_SuccessView/SuccessView.scss'; // Imports from ../shared-components directory @import '../shared-components/ToastMessage/ToastMessage.scss'; diff --git a/app/templates/dawn_onboarding.html b/app/templates/dawn_hub.html similarity index 80% rename from app/templates/dawn_onboarding.html rename to app/templates/dawn_hub.html index 58d7a871a..c9d4474a3 100644 --- a/app/templates/dawn_onboarding.html +++ b/app/templates/dawn_hub.html @@ -17,10 +17,10 @@ - +
    - + diff --git a/jsdoc.json b/jsdoc.json index 5eeac976d..70a203ed3 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -7,6 +7,7 @@ "app/licenses", "app/panel", "app/hub", + "app/dawn-hub", "app/shared-components" ], "includePattern": ".+\\.js(doc|x)?$", diff --git a/src/background.js b/src/background.js index 7c795d3e7..eef3f5aec 100644 --- a/src/background.js +++ b/src/background.js @@ -958,7 +958,7 @@ function onMessageHandler(request, sender, callback) { } if (name === 'openHubPage') { const hubUrl = (BROWSER_INFO.name === 'ghostery_desktop') - ? chrome.runtime.getURL('./app/templates/dawn_onboarding.html') + ? chrome.runtime.getURL('./app/templates/dawn_hub.html') : chrome.runtime.getURL('./app/templates/hub.html'); metrics.ping('intro_hub_click'); utils.openNewTab({ url: hubUrl, become_active: true }); @@ -1621,7 +1621,7 @@ function initializeGhosteryModules() { // TODO comment in before 8.5.5 release (commented out to facilitate onboarding testing) // if (BROWSER_INFO.name === 'ghostery_desktop') { chrome.tabs.create({ - url: chrome.runtime.getURL('./app/templates/dawn_onboarding.html?justInstalled=true'), + url: chrome.runtime.getURL('./app/templates/dawn_hub.html?justInstalled=true'), active: true }); // } else { diff --git a/webpack.config.js b/webpack.config.js index 137f7da4e..c2f80f823 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,7 +25,7 @@ const SHARED_COMP_DIR = path.resolve(__dirname, 'app/shared-components'); const PANEL_DIR = path.resolve(__dirname, 'app/panel'); const PANEL_ANDROID_DIR = path.resolve(__dirname, 'app/panel-android'); const HUB_DIR = path.resolve(__dirname, 'app/hub'); -const HUB_GHOSTERY_BROWSER_DIR = path.resolve(__dirname, 'app/ghostery-browser-hub'); +const DAWN_HUB_DIR = path.resolve(__dirname, 'app/dawn-hub'); const LICENSES_DIR = path.resolve(__dirname, 'app/licenses'); const SASS_DIR = path.resolve(__dirname, 'app/scss'); const CONTENT_SCRIPTS_DIR = path.resolve(__dirname, 'app/content-scripts'); @@ -51,7 +51,7 @@ module.exports = { click_to_play: [`${CONTENT_SCRIPTS_DIR}/click_to_play.js`], content_script_bundle: [`${CONTENT_SCRIPTS_DIR}/content_script_bundle.js`], hub_react: [`${HUB_DIR}/index.jsx`], - hub_ghostery_browser_react: [`${HUB_GHOSTERY_BROWSER_DIR}/index.jsx`], + dawn_hub_react: [`${DAWN_HUB_DIR}/index.jsx`], licenses_react: [`${LICENSES_DIR}/Licenses.jsx`, `${LICENSES_DIR}/License.jsx`], notifications: [`${CONTENT_SCRIPTS_DIR}/notifications.js`], page_performance: [`${CONTENT_SCRIPTS_DIR}/page_performance.js`], @@ -63,7 +63,7 @@ module.exports = { foundation: [`${SASS_DIR}/vendor/foundation.scss`], foundation_hub: [`${SASS_DIR}/vendor/foundation_hub.scss`], hub: [`${SASS_DIR}/hub.scss`], - hub_ghostery_browser: [`${SASS_DIR}/hub_ghostery_browser.scss`], + dawn_hub: [`${SASS_DIR}/dawn_hub.scss`], licenses: [`${SASS_DIR}/licenses.scss`], panel: [`${SASS_DIR}/panel.scss`], panel_android: [`${SASS_DIR}/panel_android.scss`], @@ -90,7 +90,7 @@ module.exports = { `${RM} ./dist/foundation.js`, `${RM} ./dist/foundation_hub.js`, `${RM} ./dist/hub.js`, - `${RM} ./dist/hub_ghostery_browser.js`, + `${RM} ./dist/dawn_hub.js`, `${RM} ./dist/licenses.js`, `${RM} ./dist/panel.js`, `${RM} ./dist/panel_android.js`, @@ -119,7 +119,7 @@ module.exports = { } }, { test: /\.(js|jsx)$/, - include: [SHARED_COMP_DIR, PANEL_ANDROID_DIR, PANEL_DIR, HUB_DIR, HUB_GHOSTERY_BROWSER_DIR, LICENSES_DIR, CONTENT_SCRIPTS_DIR], + include: [SHARED_COMP_DIR, PANEL_ANDROID_DIR, PANEL_DIR, HUB_DIR, DAWN_HUB_DIR, LICENSES_DIR, CONTENT_SCRIPTS_DIR], exclude: /node_modules/, use: [ { From 895ac092c083ca414b77041d7a05f7bbf80c166d Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Wed, 27 Jan 2021 16:09:59 -0500 Subject: [PATCH 099/113] Implement other search engine option retrieval via browser.search.get --- .../ChooseDefaultSearchReducer.js | 19 ++---- .../ChooseDefaultSearchView.jsx | 63 +++++++++++++------ .../ChooseDefaultSearchView.scss | 2 +- manifest.json | 3 +- 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js index d571c99ae..299aaed39 100644 --- a/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js +++ b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchReducer.js @@ -13,10 +13,7 @@ import { SET_DEFAULT_SEARCH, - SEARCH_GHOSTERY, - SEARCH_BING, - SEARCH_STARTPAGE, - SEARCH_YAHOO + SEARCH_GHOSTERY } from './ChooseDefaultSearchConstants'; const initialState = { @@ -26,16 +23,10 @@ const initialState = { function ChooseDefaultSearchReducer(state = initialState, action) { switch (action.type) { case SET_DEFAULT_SEARCH: { - const newDefault = action.data; - - if ([SEARCH_GHOSTERY, SEARCH_BING, SEARCH_STARTPAGE, SEARCH_YAHOO].includes(newDefault)) { - return { - ...state, - defaultSearch: newDefault - }; - } - - return state; + return { + ...state, + defaultSearch: action.data + }; } default: return state; diff --git a/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 45c2175e1..914f2e2c2 100644 --- a/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -35,11 +35,43 @@ class ChooseDefaultSearchView extends Component { otherSearchSelected: null, otherListOpen: false, modalActive: false, + searchEnginesFetched: false, + searchEngines: [], }; + + this.fetchSearchEnginesAsync = this.fetchSearchEnginesAsync.bind(this); + } + + async fetchSearchEnginesAsync() { + // eslint-disable-next-line no-undef + if (typeof browser === 'undefined') { + this.setState(state => ({ + ...state, + searchEnginesFetched: true, + searchEngines: [] + })); + return; + } + // eslint-disable-next-line no-undef + const response = await browser.search.get(); + console.log('chrome.search.get() response:'); + console.log(response); + + const otherOptions = response + .map(item => item.name) + .filter(name => ![SEARCH_YAHOO, SEARCH_STARTPAGE, SEARCH_BING].includes(name)); + + this.setState(state => ({ + ...state, + searchEnginesFetched: true, + otherSearchOptions: otherOptions + })); } componentDidMount() { document.addEventListener('click', this.handleClickAway); + + this.fetchSearchEnginesAsync(); } componentWillUnmount() { @@ -74,18 +106,18 @@ class ChooseDefaultSearchView extends Component { } handleSubmit = () => { - let { chosenSearch } = this.state; - const { otherSearchSelected } = this.state; + const { chosenSearch, otherSearchSelected } = this.state; const { actions, history } = this.props; const { setSetupStep, setDefaultSearch } = actions; - if (chosenSearch === SEARCH_OTHER && otherSearchSelected) { - chosenSearch = otherSearchSelected; - } + const chosenSearchName = chosenSearch === SEARCH_OTHER + ? otherSearchSelected + : chosenSearch; const payload = { type: 'setDefaultSearch', - search: chosenSearch, + search: chosenSearchName, + isOther: chosenSearch === SEARCH_OTHER, }; // The try/catch wrapper facilitates testing in non-Dawn browsers which have no search@ghostery.com extension @@ -104,7 +136,7 @@ class ChooseDefaultSearchView extends Component { // history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); // }); - setDefaultSearch(chosenSearch); + setDefaultSearch(chosenSearchName); setSetupStep({ setup_step: CHOOSE_PLAN, origin: ONBOARDING }); history.push(`/${ONBOARDING}/${CHOOSE_PLAN}`); } @@ -150,18 +182,7 @@ class ChooseDefaultSearchView extends Component { } renderOtherOptionsList = () => { - const otherSearchOptions = [ - 'DuckDuck Go', - 'Ecosia', - 'Ekoru', - 'Gibiru', - 'Google', - 'OneSearch', - 'Privado', - 'Qwant', - 'Search Encrypt', - 'Tailcat', - ]; + const { otherSearchOptions } = this.state; return ( @@ -307,7 +328,9 @@ class ChooseDefaultSearchView extends Component { } render() { - const { modalActive } = this.state; + const { modalActive, searchEnginesFetched } = this.state; + + if (!searchEnginesFetched) return null; return (
    diff --git a/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss index c0d9011f5..920b87938 100644 --- a/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss +++ b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.scss @@ -192,7 +192,7 @@ .ChooseSearchView__caret { height: 35px; width: 25px; - padding-top: 21px; + padding-top: 9px; padding-right: 0; background-repeat: no-repeat; background-position: center center; diff --git a/manifest.json b/manifest.json index 4700eb994..d281714c9 100644 --- a/manifest.json +++ b/manifest.json @@ -74,7 +74,8 @@ "https://*/*", "ws://*/*", "wss://*/*", - "storage" + "storage", + "search" ], "background": { "scripts": [ From a76110396c51b5fc78c5ee1666c61c16116354ba Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Wed, 27 Jan 2021 16:21:11 -0500 Subject: [PATCH 100/113] Only show other search options dropdown if there are actually other options available --- .../ChooseDefaultSearchView.jsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx index 914f2e2c2..18be23130 100644 --- a/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx +++ b/app/dawn-hub/Views/OnboardingViews/Step3_ChooseDefaultSearchView/ChooseDefaultSearchView.jsx @@ -35,8 +35,8 @@ class ChooseDefaultSearchView extends Component { otherSearchSelected: null, otherListOpen: false, modalActive: false, - searchEnginesFetched: false, - searchEngines: [], + otherSearchOptionsFetched: false, + otherSearchOptions: [], }; this.fetchSearchEnginesAsync = this.fetchSearchEnginesAsync.bind(this); @@ -44,26 +44,27 @@ class ChooseDefaultSearchView extends Component { async fetchSearchEnginesAsync() { // eslint-disable-next-line no-undef - if (typeof browser === 'undefined') { + if (typeof browser === 'undefined') { // we are not in Dawn (or Firefox) this.setState(state => ({ ...state, - searchEnginesFetched: true, - searchEngines: [] + otherSearchOptionsFetched: true, })); return; } // eslint-disable-next-line no-undef - const response = await browser.search.get(); + const response = await browser.search.get(); // we are in Dawn / Firefox, where this API is supported console.log('chrome.search.get() response:'); console.log(response); + // a successful response is guaranteed to be an array of search engine objects + // see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/search/get const otherOptions = response .map(item => item.name) .filter(name => ![SEARCH_YAHOO, SEARCH_STARTPAGE, SEARCH_BING].includes(name)); this.setState(state => ({ ...state, - searchEnginesFetched: true, + otherSearchOptionsFetched: true, otherSearchOptions: otherOptions })); } @@ -293,7 +294,10 @@ class ChooseDefaultSearchView extends Component { } renderSearchOptions = () => { - const { chosenSearch } = this.state; + const { chosenSearch, otherSearchOptions } = this.state; + + // No sense showing dropdown if there are no other options + const showOtherOptionsDropdown = otherSearchOptions.length > 0; return ( @@ -312,7 +316,7 @@ class ChooseDefaultSearchView extends Component { {this.renderOptionContainer(chosenSearch, SEARCH_GHOSTERY)} {this.renderOptionContainer(chosenSearch, SEARCH_STARTPAGE)} {this.renderOptionContainer(chosenSearch, SEARCH_BING)} - {this.renderOptionContainer(chosenSearch, SEARCH_OTHER)} + {showOtherOptionsDropdown && this.renderOptionContainer(chosenSearch, SEARCH_OTHER)} {this.renderOptionContainer(chosenSearch, SEARCH_YAHOO)}