diff --git a/.eslintrc.js b/.eslintrc.js index 1a07c41f3..d1af6c8b8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -83,6 +83,7 @@ module.exports = { 'react/no-access-state-in-setstate': [0], // TODO: enable this check 'react/no-danger': [0], 'react/prop-types': [0], + 'react/jsx-fragments': [1, 'element'], 'react/sort-comp': [2, { order: [ "static-variables", diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 3eee43116..1b545417a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1802,6 +1802,9 @@ "subscribe_pitch": { "message": "While Ghostery is free, you can choose to support us through a small subscription of $2 per month in exchange for cool perks, such as color themes, priority help service, and more. Join our mission and subscribe!" }, + "subscribe_pitch_spring": { + "message": "Support us and unlock a new spring theme, personal tracking insights, and other special perks by upgrading to Ghostery Plus for $2 per month." + }, "subscribe_pitch_learn_more": { "message": "Learn more" }, @@ -2349,6 +2352,9 @@ "message": "Already subscribed? Sign In", "description": "Character limit (including spaces and punctuation): 28." }, + "already_subscribed_to_plus_sign_in": { + "message": "Already a plus subscriber?" + }, "promos_turned_off_notification": { "message": "Promos turned off. You can turn them back on in", "description": "translation must take into account that 'Settings' is appended to this string" @@ -2362,8 +2368,11 @@ "seven_day_free_trial": { "message": "7 Day Free Trial ($14/mo)" }, - "full_coverage_protection_promise": { - "message": "Get full-coverage protection across all browsers & apps on your device" + "spring_has_sprung": { + "message": "Spring has sprung!" + }, + "full_coverage_protection_promise": { + "message": "Get full-coverage protection across all browsers & apps on your device" }, "system_wide_tracker_and_ad_blocking": { "message": "System-wide tracker & ad-blocking" diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index babcbf1c1..c98e55b78 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -15,11 +15,12 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import QueryString from 'query-string'; import HomeView from './HomeView'; -import { PremiumPromoModal } from '../../../shared-components'; +import PromoModal from '../../../shared-components/PromoModal'; import { sendMessage } from '../../utils'; import globals from '../../../../src/classes/Globals'; const DOMAIN = globals.DEBUG ? 'ghosterystage' : 'ghostery'; +const PREMIUM = 'premium'; /** * @class Implement the Home View for the Ghostery Hub @@ -137,7 +138,8 @@ class HomeViewContainer extends Component { return (
- \ No newline at end of file diff --git a/app/panel/components/BuildingBlocks/ModalExitButton.jsx b/app/panel/components/BuildingBlocks/ModalExitButton.jsx index 92dad90e3..50b7a604e 100644 --- a/app/panel/components/BuildingBlocks/ModalExitButton.jsx +++ b/app/panel/components/BuildingBlocks/ModalExitButton.jsx @@ -22,11 +22,14 @@ import PropTypes from 'prop-types'; */ const ModalExitButton = (props) => { const { - toggleModal + toggleModal, + border } = props; + const borderClassNames = `ModalExitButton__exit flex-container align-middle ${border}`; + return ( - ); @@ -34,7 +37,13 @@ const ModalExitButton = (props) => { // PropTypes ensure we pass required props of the correct type ModalExitButton.propTypes = { - toggleModal: PropTypes.func.isRequired + toggleModal: PropTypes.func.isRequired, + border: PropTypes.string, +}; + +// Default props used in the App +ModalExitButton.defaultProps = { + border: 'grey' }; export default ModalExitButton; diff --git a/app/panel/components/InsightsPromoModal.jsx b/app/panel/components/InsightsPromoModal.jsx deleted file mode 100644 index 5b1e54b7e..000000000 --- a/app/panel/components/InsightsPromoModal.jsx +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Insights Promo Modal Component - * - * 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 Modal from '../../shared-components/Modal'; -import ModalExitButton from './BuildingBlocks/ModalExitButton'; - -const INSIGHTS = 'insights'; - -/** - * @class Implements the Insights Promo Modal - * @memberof PanelClasses - */ -class InsightsPromoModal extends React.Component { - handleGoAwayClick = () => { this.props.handleGoAwayClick(INSIGHTS); } - - handleSubscribeClick = () => { this.props.handleSubscribeClick(INSIGHTS); } - - handleXClick = () => { this.props.handleXClick(INSIGHTS); } - - render() { - return ( - -
- -
-
- {t('panel_insights_promotion_header')} -
-
- {t('panel_insights_promotion_description')} -
-
-
-
- -
- { t('panel_insights_audit_tags') } -
-
-
- -
- { t('panel_insights_promotion_trace_poor_performance') } -
-
-
-
-
- -
- { t('panel_insights_promotion_watch_pings') } -
-
-
- -
- { t('panel_insights_promotion_explore_trends') } -
-
-
-
-
-
- - {t('panel_insights_promotion_call_to_action')} - -
-
- {t('subscribe_pitch_sign_in')} - {t('no_thanks_turn_promos_off')} -
-
-
- - ); - } -} - -export default InsightsPromoModal; diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 6ed5e9517..77055f519 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -14,16 +14,15 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import Header from '../containers/HeaderContainer'; -import { PremiumPromoModal } from '../../shared-components'; -import InsightsPromoModal from './InsightsPromoModal'; +import PromoModalContainer from '../../shared-components/PromoModal/PromoModalContainer'; import { ThemeContext } from '../contexts/ThemeContext'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; -import history from '../utils/history'; -import globals from '../../../src/classes/Globals'; -const DOMAIN = globals.DEBUG ? 'ghosterystage' : 'ghostery'; +const INSIGHTS = 'insights'; +const PLUS = 'plus'; +const PREMIUM = 'premium'; /** * @class Implement base view with functionality common to all views. @@ -196,104 +195,9 @@ class Panel extends React.Component { ); } - return false; } - /** - * @param modal 'insights' or 'premium' - * @private - * Handle clicks on the link to turn off promos in the promo modals - */ - _handlePromoGoAwayClick = (modal) => { - this.props.actions.togglePromoModal(); - - sendMessage('promoModals.turnOffPromos', {}); - - if (modal === 'insights') { - sendMessage('ping', 'promo_modals_decline_insights_upgrade'); - } - - this.props.actions.showNotification({ - classes: 'warning', - reload: false, - text: t('promos_turned_off_notification'), - }); - }; - - /** - * @private - * Handle clicks on sign in links in promo modals - */ - _handlePromoSignInClick = () => { - this.props.actions.togglePromoModal(); - history.push({ - pathname: '/login', - }); - }; - - /** - * @private - * Handle clicks on the download button in the Premium promo modals - */ - _handlePromoTryMidnightClick = () => { - this.props.actions.togglePromoModal(); - - const url = 'https://ghostery.com/thanks-for-downloading-midnight?utm_source=gbe&utm_campaign=in_app'; - sendMessage('openNewTab', { - url, - become_active: true, - }); - } - - /** - * @private - * Handle clicks on the 'Get Plus' option in the Premium modals - */ - _handlePromoGetPlusClick = () => { - this.props.actions.togglePromoModal(); - - const url = `https://checkout.${DOMAIN}.com/plus?utm_source=gbe&utm_campaign=in_app`; - sendMessage('openNewTab', { - url, - become_active: true, - }); - }; - - /** - * @private - * Handle click action when user selects Subscribe button in the Insights modal - * @param {string} modal Modal type (insights or plus) - */ - _handlePromoSubscribeClick = (modal) => { - this.props.actions.togglePromoModal(); - - let url = `https://checkout.${DOMAIN}.com/`; - - if (modal === 'insights') { - sendMessage('ping', 'promo_modals_insights_upgrade_cta'); - url += 'insights?utm_source=gbe&utm_campaign=in_app_upgrade'; - } - - sendMessage('openNewTab', { - url, - become_active: true, - }); - }; - - /** - * @param modal 'insights' or 'premium' - * @private - * Handle clicks on the 'X' close icon in promo modals - */ - _handlePromoXClick = (modal) => { - this.props.actions.togglePromoModal(); - - if (modal === 'insights') { - sendMessage('ping', 'promo_modals_decline_insights_upgrade'); - } - }; - /** * @returns {bool} * @private @@ -340,14 +244,11 @@ class Panel extends React.Component { const isPlus = this._plusSubscriber(); return ( - ); } @@ -361,15 +262,32 @@ class Panel extends React.Component { if (this._insightsSubscriber()) return null; sendMessage('promoModals.sawInsightsPromo', {}); - sendMessage('ping', 'promo_modals_show_insights'); return ( - + ); + } + + /** + * @returns {null|JSX} + * @private + * Renders the Plus promo modal if the user is not already an subscriber + */ + _renderPlusPromoModal = () => { + if (this._plusSubscriber() || this._premiumSubscriber()) { return null; } + + sendMessage('promoModals.sawPlusPromo', {}); + + const { loggedIn } = this.props; + return ( + ); } @@ -391,6 +309,10 @@ class Panel extends React.Component { return this._renderInsightsPromoModal(); } + if (promoModal === 'plus') { + return this._renderPlusPromoModal(); + } + if (promoModal === 'premium') { return this._renderPremiumPromoModal(); } diff --git a/app/panel/components/Rewards.jsx b/app/panel/components/Rewards.jsx index 2c628882a..eab30434a 100644 --- a/app/panel/components/Rewards.jsx +++ b/app/panel/components/Rewards.jsx @@ -11,7 +11,7 @@ * 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 { Route } from 'react-router-dom'; import { ToggleSlider } from './BuildingBlocks'; @@ -266,7 +266,7 @@ class Rewards extends React.Component { const src = chrome.runtime.getURL('cliqz/offers-templates/control-center.html?cross-origin'); const text = t(`panel_rewards_view__reward${rewardsCount === 1 ? '' : 's'}`); return ( - <> + {is_expanded && (
{rewardsCount}
@@ -281,7 +281,7 @@ class Rewards extends React.Component { height={iframeHeight} title="myoffrz-rewards" /> - + ); } diff --git a/app/scss/hub.scss b/app/scss/hub.scss index c53e21f19..ad804d16a 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -76,7 +76,7 @@ html, body, #root { // Imports from ../shared-components directory @import '../shared-components/ExitButton/ExitButton.scss'; @import '../shared-components/Modal/Modal.scss'; -@import '../shared-components/PremiumPromoModal/PremiumPromoModal.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'; diff --git a/app/scss/panel.scss b/app/scss/panel.scss index f1b45e2c7..dc511bf62 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -76,9 +76,10 @@ html body { @import './partials/_stats'; @import './partials/_stats_graph'; @import './partials/_modal_exit_button'; -@import './partials/insights_promo_modal.scss'; // Imports from ../shared-components directory @import '../shared-components/Modal/Modal.scss'; -@import '../shared-components/PremiumPromoModal/PremiumPromoModal.scss'; +@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' diff --git a/app/scss/partials/_colors.scss b/app/scss/partials/_colors.scss index d52e7f0ca..4695aab80 100644 --- a/app/scss/partials/_colors.scss +++ b/app/scss/partials/_colors.scss @@ -39,6 +39,9 @@ $link-blue: #2092BF; //primary-color $button-primary: #3AA2CF; $dark-cyan-blue: #325e97; //insights modal border +/* GREENS */ +$spring-green: #6aa103; + /* MARKETING COLORS */ $red: #E74055; $purple: #720174; diff --git a/app/scss/partials/_modal_exit_button.scss b/app/scss/partials/_modal_exit_button.scss index 955fdfaa1..82aade2b4 100644 --- a/app/scss/partials/_modal_exit_button.scss +++ b/app/scss/partials/_modal_exit_button.scss @@ -19,9 +19,14 @@ width: 26px; height: 26px; border-radius: 15px; - border: solid 0.8px #325e97; background-color: #f7f7f7; @include transition(background-color 0.2s); + &.grey { + border: solid 0.8px #325e97; + } + &.green { + border: solid 2px $spring-green; + } } .ModalExitButton__exit:hover { background-color: #efefef; diff --git a/app/shared-components/ModalContent/InsightsPromoModalContent/InsightsPromoModalContent.jsx b/app/shared-components/ModalContent/InsightsPromoModalContent/InsightsPromoModalContent.jsx new file mode 100644 index 000000000..129a88e45 --- /dev/null +++ b/app/shared-components/ModalContent/InsightsPromoModalContent/InsightsPromoModalContent.jsx @@ -0,0 +1,76 @@ +/** + * Insights Promo Modal Component + * + * 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 } from 'react'; + +const InsightsPromoModalContent = (props) => { + const { + handleGoAwayClick, + handleSignInClick, + handleTryInsightsClick, + } = props; + return ( + +
+
+ {t('panel_insights_promotion_header')} +
+
+ {t('panel_insights_promotion_description')} +
+
+
+
+ +
+ {t('panel_insights_audit_tags')} +
+
+
+ +
+ {t('panel_insights_promotion_trace_poor_performance')} +
+
+
+
+
+ +
+ {t('panel_insights_promotion_watch_pings')} +
+
+
+ +
+ {t('panel_insights_promotion_explore_trends')} +
+
+
+
+
+
+ + {t('panel_insights_promotion_call_to_action')} + +
+
+ {t('subscribe_pitch_sign_in')} + {t('no_thanks_turn_promos_off')} +
+
+ + ); +}; + +export default InsightsPromoModalContent; diff --git a/app/scss/partials/_insights_promo_modal.scss b/app/shared-components/ModalContent/InsightsPromoModalContent/InsightsPromoModalContent.scss similarity index 86% rename from app/scss/partials/_insights_promo_modal.scss rename to app/shared-components/ModalContent/InsightsPromoModalContent/InsightsPromoModalContent.scss index 6ef2c2adb..7f1d192bb 100644 --- a/app/scss/partials/_insights_promo_modal.scss +++ b/app/shared-components/ModalContent/InsightsPromoModalContent/InsightsPromoModalContent.scss @@ -15,7 +15,7 @@ $standard-font-family: Roboto, "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; -.InsightsModal__content { +.InsightsPromoModal__content { background-color: $alabaster; position: relative; width: 518px; @@ -26,14 +26,14 @@ $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helveti z-index: 10; } -.InsightsModal__image { +.InsightsPromoModal__image { height: 94px; width: 177px; margin-top: 20px; background-image: url('/app/images/panel/insights-ribbon.svg'); } -.InsightsModal__header { +.InsightsPromoModal__header { font-family: $standard-font-family; height: 27.1px; font-size: 20px; @@ -43,7 +43,7 @@ $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helveti margin-bottom: 10px; } -.InsightsModal__description { +.InsightsPromoModal__description { width: 400px; font-size: 18px; font-weight: 500; @@ -52,7 +52,7 @@ $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helveti font-family: $standard-font-family; } -.InsightsModal__features { +.InsightsPromoModal__features { &:nth-child(odd) { margin-left: 25px; width: 50%; @@ -64,7 +64,7 @@ $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helveti } } -.InsightsModal__feature-text { +.InsightsPromoModal__feature-text { font-family: $standard-font-family; font-size: 14px; color: $medium-gray; @@ -72,7 +72,7 @@ $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helveti line-height: 16px; } -.InsightsModal__checked-circle-icon { +.InsightsPromoModal__checked-circle-icon { flex: none; height: 18px; width: 18px; @@ -81,7 +81,7 @@ $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helveti align-self: flex-start; } -.InsightsModal__call-to-action-container { +.InsightsPromoModal__call-to-action-container { height: 95px; width: 99%; margin-top: 10px; @@ -91,7 +91,7 @@ $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helveti flex-direction: column; } -.InsightsModal__call-to-action { +.InsightsPromoModal__call-to-action { cursor: pointer; display: flex; justify-content: center; @@ -111,7 +111,7 @@ $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helveti } } -.InsightsModal__link { +.InsightsPromoModal__link { font-family: $standard-font-family; font-size: 13px; color: $tundora; @@ -121,7 +121,7 @@ $condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helveti } } -.InsightsModal__other-options-container { +.InsightsPromoModal__other-options-container { margin-top: 10px; padding: 0 10.5px; text-decoration: underline; diff --git a/app/shared-components/ModalContent/InsightsPromoModalContent/index.js b/app/shared-components/ModalContent/InsightsPromoModalContent/index.js new file mode 100644 index 000000000..9708db36b --- /dev/null +++ b/app/shared-components/ModalContent/InsightsPromoModalContent/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for InsightsPromoModalContent Component + * + * 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 InsightsPromoModalContent from './InsightsPromoModalContent'; + +export default InsightsPromoModalContent; diff --git a/app/shared-components/ModalContent/PlusPromoModalContent/PlusPromoModalContent.jsx b/app/shared-components/ModalContent/PlusPromoModalContent/PlusPromoModalContent.jsx new file mode 100644 index 000000000..b938559e5 --- /dev/null +++ b/app/shared-components/ModalContent/PlusPromoModalContent/PlusPromoModalContent.jsx @@ -0,0 +1,69 @@ +/** + * PlusPromoModal Component + * + * 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'; + +/** + * A functional React component for a PlusPromo Modal that may be displayed in the Hub and/or Panel + * @return {JSX} JSX for rendering a PlusPromo Modal + * @memberof SharedComponents + */ +const PlusPromoModalContent = (props) => { + const { + handleGoAwayClick, + handleTryPlusClick, + handleSignInClick, + loggedIn, + } = props; + + return ( +
+
+
+
+
+
{t('spring_has_sprung')}
+
+
+
+
+
+
+
+
+ +
+
+ {!loggedIn && + {t('already_subscribed_to_plus_sign_in')} + } + {t('no_thanks_turn_promos_off')} +
+
+
+ ); +}; + + +// PropTypes ensure we pass required props of the correct type +PlusPromoModalContent.propTypes = { + handleTryPlusClick: PropTypes.func.isRequired, + handleSignInClick: PropTypes.func.isRequired, + handleGoAwayClick: PropTypes.func.isRequired, + loggedIn: PropTypes.bool.isRequired, +}; + +export default PlusPromoModalContent; diff --git a/app/shared-components/ModalContent/PlusPromoModalContent/PlusPromoModalContent.scss b/app/shared-components/ModalContent/PlusPromoModalContent/PlusPromoModalContent.scss new file mode 100644 index 000000000..7d43f0ab1 --- /dev/null +++ b/app/shared-components/ModalContent/PlusPromoModalContent/PlusPromoModalContent.scss @@ -0,0 +1,183 @@ +/** + * Plus Promo Modal 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 + */ + +// Plus Promo Modal +$standard-font-family: Roboto, "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +$condensed-font-family: Roboto Condensed, "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + +.PlusPromoModal__content { + position: relative; + z-index: 10; + width: 456px; + height: 410px; + border: solid 1.9px $spring-green; + background-color: #f7f7f7; +} + +.PlusPromoModal__plus-logo { + width: 216px; + height: 92px; + margin: 22px 0px 6px; + background-image: url('/app/images/panel/spring-plus-logo.svg'); +} + +.PlusPromoModal__main-content-container { + height: 210px; + display: flex; + justify-content: center; +} +.PlusPromoModal__header { + text-align: center; + font-weight: bold; + font-family: $standard-font-family; + .title { + font-size: 20px; + margin-top: 8px; + } + .description { + font-size: 18px; + margin-top: 16px; + font-weight: 500; + color: $tundora; + width: 350px; + } +} + +.PlusPromoModal__header-beta-icon { + float: right; + margin-left: 5px; + width: 20px; + height: 12px; + background-image: url('/app/images/panel/midnight-beta-icon.svg'); +} + +.PlusPromoModal__sub-header { + width: 438px; + margin-bottom: 12px; + text-align: center; + font-size: 18px; + font-weight: bold; + line-height: 1.5; + color: #4A4A4A; +} + +.PlusPromoModal__features-container { + display: flex; + width: 100%; +} + +.PlusPromoModal__feature-column { + &:nth-child(odd) { + margin-left: 20px; + width: 55%; + } + &:nth-child(even) { + margin-right: 25px; + width: 45%; + } +} + +.PlusPromoModal__feature { + display: flex; + align-items: center; + &:nth-child(1) { + margin-bottom: 4px; + } +} + +.PlusPromoModal__feature-text { + font-family: $standard-font-family; + font-size: 14px; + color: $medium-gray; + padding-bottom: 5px; + line-height: 16px; +} + +.PlusPromoModal__checked-circle-icon { + flex: none; + height: 18px; + width: 18px; + margin-right: 8px; + background-image: url('/app/images/panel/midnight-check-icon.svg'); + align-self: flex-start; +} + +.PlusPromoModal__call-to-action-container { + background-color: #e7ecee; + margin-bottom: 3px; + width: 99%; + height: 107px; + z-index: -1; + display: flex; + flex-direction: column; + justify-content: space-around; // Edge does not support space-evenly + position: absolute; + bottom: 1px; +} + +.PlusPromoModal__button-container { + display: flex; + justify-content: center; + margin-top: 10px; +} + +.PlusPromoModal__download-button { + height: 36px; + width: 176px; + border-radius: 2px; + padding: 0px 20px 0px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12); + background-color: #1dafed; + + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + + font-family: $condensed-font-family; + font-size: 14px; + font-weight: bold; + letter-spacing: 0.5px; + text-align: center; + color: #ffffff; + + transition: background-color 0.25s ease-out, color 0.25s ease-out; + + text-transform: uppercase; +} + +.PlusPromoModal__text-link-container { + display: flex; + justify-content: space-around; // Edge does not support space-evenly +} + +.PlusPromoModal__link { + margin-left: 10px; + margin-right: 10px; + font-size: 15px; + font-family: $standard-font-family; + color: #4a4a4a; + text-decoration: underline; + text-align: center; + cursor: pointer; + + &.sign-in { + float: left; + left: 10px; + } + + &.turn-promos-off { + float: right; + right: 10px; + } +} diff --git a/app/shared-components/ModalContent/PlusPromoModalContent/index.js b/app/shared-components/ModalContent/PlusPromoModalContent/index.js new file mode 100644 index 000000000..06264f719 --- /dev/null +++ b/app/shared-components/ModalContent/PlusPromoModalContent/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for PlusPromoModalContent Component + * + * 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 PlusPromoModalContent from './PlusPromoModalContent'; + +export default PlusPromoModalContent; diff --git a/app/shared-components/ModalContent/PremiumPromoModalContent/PremiumPromoModalContent.jsx b/app/shared-components/ModalContent/PremiumPromoModalContent/PremiumPromoModalContent.jsx new file mode 100644 index 000000000..b4f925cfe --- /dev/null +++ b/app/shared-components/ModalContent/PremiumPromoModalContent/PremiumPromoModalContent.jsx @@ -0,0 +1,118 @@ +/** + * Premium Promo Modal Component + * + * 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'; + +/** + * A functional React component for a Premium Promo Modal that may be displayed in the Hub and/or Panel + * @return {JSX} JSX for rendering a Premium Promo Modal + * @memberof SharedComponents + */ +const PremiumPromoModalContent = (props) => { + const { + isPlus, + handleTryMidnightClick, + handleGetPlusClick, + handleKeepBasicClick, + handleGoAwayClick, + location, + } = props; + + const isInHub = location === 'hub'; + const isInPanel = location === 'panel'; + + return ( +
+
+
+
+
+ {t('try_ghostery_midnight')} +
+
+
+
{t('full_coverage_protection_promise')}
+
+
+
+ +
+ {t('system_wide_tracker_and_ad_blocking')} +
+
+
+ +
+ {t('built_in_vpn')} +
+
+
+
+
+ +
+ {t('historical_tracking_insights')} +
+
+
+ +
+
+
+
+
+
+
+
+ {t('download_for_free')} +
+
+
+ {!isPlus && ( +
+ )} + {isInHub && ( +
+ {t('no_thanks_continue_with_basic')} +
+ )} + {isInPanel && ( +
+ {t('no_thanks_turn_promos_off')} +
+ )} +
+
+
+ ); +}; + + +// PropTypes ensure we pass required props of the correct type +PremiumPromoModalContent.propTypes = { + location: PropTypes.string.isRequired, + isPlus: PropTypes.bool.isRequired, + handleTryMidnightClick: PropTypes.func.isRequired, + handleGetPlusClick: PropTypes.func.isRequired, + handleKeepBasicClick: PropTypes.func, + handleGoAwayClick: PropTypes.func, +}; + +const noop = () => { }; +PremiumPromoModalContent.defaultProps = { + handleKeepBasicClick: noop, + handleGoAwayClick: noop, +}; + +export default PremiumPromoModalContent; diff --git a/app/shared-components/PremiumPromoModal/PremiumPromoModal.scss b/app/shared-components/ModalContent/PremiumPromoModalContent/PremiumPromoModalContent.scss similarity index 100% rename from app/shared-components/PremiumPromoModal/PremiumPromoModal.scss rename to app/shared-components/ModalContent/PremiumPromoModalContent/PremiumPromoModalContent.scss diff --git a/app/shared-components/PremiumPromoModal/index.js b/app/shared-components/ModalContent/PremiumPromoModalContent/index.js similarity index 78% rename from app/shared-components/PremiumPromoModal/index.js rename to app/shared-components/ModalContent/PremiumPromoModalContent/index.js index 9d353dc6c..ee9bf24d2 100644 --- a/app/shared-components/PremiumPromoModal/index.js +++ b/app/shared-components/ModalContent/PremiumPromoModalContent/index.js @@ -11,6 +11,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import PremiumPromoModal from './PremiumPromoModal'; +import PremiumPromoModalContent from './PremiumPromoModalContent'; -export default PremiumPromoModal; +export default PremiumPromoModalContent; diff --git a/app/shared-components/PremiumPromoModal/PremiumPromoModal.jsx b/app/shared-components/PremiumPromoModal/PremiumPromoModal.jsx deleted file mode 100644 index b1fa7018f..000000000 --- a/app/shared-components/PremiumPromoModal/PremiumPromoModal.jsx +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Premium Promo Modal Component - * - * 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 Modal from '../Modal/Modal'; -import ModalExitButton from '../../panel/components/BuildingBlocks/ModalExitButton'; - -/** - * A functional React component for a Premium Promo Modal that may be displayed in the Hub and/or Panel - * @return {JSX} JSX for rendering a Premium Promo Modal - * @memberof SharedComponents - */ -const PremiumPromoModal = (props) => { - const { - show, - location, - isPlus, - handleTryMidnightClick, - handleGetPlusClick, - handleKeepBasicClick, - handleGoAwayClick, - handleXClick, - } = props; - - const isInHub = location === 'hub'; - const isInPanel = location === 'panel'; - - const contentClassNames = ClassNames( - 'PremiumPromoModal__content', - 'flex-container', - 'flex-dir-column', - 'align-middle', - ); - - return ( - -
- {isInPanel && ( - - )} -
-
-
-
- {t('try_ghostery_midnight')} -
-
-
-
{t('full_coverage_protection_promise')}
-
-
-
- -
- {t('system_wide_tracker_and_ad_blocking')} -
-
-
- -
- {t('built_in_vpn')} -
-
-
-
-
- -
- {t('historical_tracking_insights')} -
-
-
- -
-
-
-
-
-
-
-
- {t('download_for_free')} -
-
-
- {!isPlus && ( -
- )} - {isInHub && ( -
- {t('no_thanks_continue_with_basic')} -
- )} - {isInPanel && ( -
- {t('no_thanks_turn_promos_off')} -
- )} -
-
-
- - ); -}; - - -// PropTypes ensure we pass required props of the correct type -PremiumPromoModal.propTypes = { - show: PropTypes.bool.isRequired, - location: PropTypes.string.isRequired, - isPlus: PropTypes.bool.isRequired, - handleTryMidnightClick: PropTypes.func.isRequired, - handleGetPlusClick: PropTypes.func.isRequired, - handleKeepBasicClick: PropTypes.func, - handleGoAwayClick: PropTypes.func, - handleXClick: PropTypes.func, -}; - -const noop = () => {}; -PremiumPromoModal.defaultProps = { - handleKeepBasicClick: noop, - handleGoAwayClick: noop, - handleXClick: noop, -}; - -export default PremiumPromoModal; diff --git a/app/shared-components/PromoModal/PromoModal.jsx b/app/shared-components/PromoModal/PromoModal.jsx new file mode 100644 index 000000000..603de8cae --- /dev/null +++ b/app/shared-components/PromoModal/PromoModal.jsx @@ -0,0 +1,196 @@ +/** + * Base component for all of the Promo Modals + * + * 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 Modal from '../Modal'; +import InsightsPromoModalContent from '../ModalContent/InsightsPromoModalContent'; +import PlusPromoModalContent from '../ModalContent/PlusPromoModalContent'; +import PremiumPromoModalContent from '../ModalContent/PremiumPromoModalContent'; +import history from '../../panel/utils/history'; +import { sendMessage } from '../../panel/utils/msg'; +import globals from '../../../src/classes/Globals'; +import ModalExitButton from '../../panel/components/BuildingBlocks/ModalExitButton'; + +const DOMAIN = globals.DEBUG ? 'ghosterystage' : 'ghostery'; +const INSIGHTS = 'insights'; +const PLUS = 'plus'; +const PREMIUM = 'premium'; + +/** + * A base class component for Promo Modals + * @return {JSX} + * @memberof HubComponents + */ + +class PromoModal extends React.Component { + /** + * @param modal 'insights' or 'premium' + * @private + * Handle clicks on the link to turn off promos in the promo modals + */ + _handlePromoGoAwayClick = (modal) => { + this.props.actions.togglePromoModal(); + + sendMessage('promoModals.turnOffPromos', {}); + + if (modal === INSIGHTS + || modal === PLUS) { + sendMessage('ping', `promo_modals_decline_${modal}_upgrade`); + } + + this.props.actions.showNotification({ + classes: 'warning', + reload: false, + text: t('promos_turned_off_notification'), + }); + }; + + /** + * @private + * Handle clicks on the download buttons + */ + _handlePromoTryProductClick = (product, utm_campaign) => { + this.props.actions.togglePromoModal(); + + let url; + switch (product) { + case PLUS: + url = `https://checkout.${DOMAIN}.com/plus?utm_source=gbe&utm_campaign=${utm_campaign}`; + break; + case PREMIUM: + url = `https://ghostery.com/thanks-for-downloading-midnight?utm_source=gbe&utm_campaign=${utm_campaign}`; + break; + case INSIGHTS: + sendMessage('ping', 'promo_modals_insights_upgrade_cta'); + url = `https://checkout.${DOMAIN}.com/insights?utm_source=gbe&utm_campaign=${utm_campaign}`; + break; + default: + } + + sendMessage('openNewTab', { + url, + become_active: true, + }); + } + + /** + * @private + * Handle clicks on sign in links in promo modals + */ + _handlePromoSignInClick = () => { + this.props.actions.togglePromoModal(); + history.push({ + pathname: '/login', + }); + }; + + _handlePromoXClick = (type) => { + this.props.actions.togglePromoModal(); + + if (type === INSIGHTS) { + sendMessage('ping', 'promo_modals_decline_insights_upgrade'); + } else if (type === PLUS) { + sendMessage('ping', 'promo_modals_decline_plus_upgrade'); + } + } + + _renderXButton = (type) => { + const XButtonClass = ClassNames({ PlusPromoModal__exitButton: type === PLUS }); + let border; + if (type === PLUS) { + border = 'green'; + } else if (type === INSIGHTS) { + border = 'grey'; + } + return ( + this._handlePromoXClick(type)} + /> + ); + }; + + renderModalContent() { + const { type, loggedIn } = this.props; + switch (type) { + case INSIGHTS: + return ( + this._handlePromoGoAwayClick(INSIGHTS)} + handleTryInsightsClick={() => this._handlePromoTryProductClick(INSIGHTS, 'in_app_upgrade')} + handleSignInClick={this._handlePromoSignInClick} + {...this.props} + /> + ); + case PLUS: + return ( + this._handlePromoGoAwayClick(PLUS)} + handleTryPlusClick={() => this._handlePromoTryProductClick(PLUS, 'in_app_spring2020')} + handleSignInClick={this._handlePromoSignInClick} + loggedIn={loggedIn} + /> + ); + case PREMIUM: + return ( + this._handlePromoGoAwayClick(PREMIUM)} + handleTryMidnightClick={() => this._handlePromoTryProductClick(PREMIUM, 'in_app')} + handleGetPlusClick={() => this._handlePromoTryProductClick(PLUS, 'in_app')} + {...this.props} + /> + ); + default: + return ; + } + } + + render() { + const { type, show } = this.props; + const modalContentClassNames = ClassNames( + 'flex-container', + 'flex-dir-column', + 'align-middle', + { + InsightsPromoModal__content: type === INSIGHTS, + PlusPromoModal__content: type === PLUS, + PremiumPromoModal__content: type === PREMIUM, + } + ); + return ( + +
+ {this._renderXButton(type)} + {this.renderModalContent()} +
+
+ ); + } +} + +// PropTypes ensure we pass required props of the correct type +PromoModal.propTypes = { + type: PropTypes.string.isRequired, + location: PropTypes.string, + isPlus: PropTypes.bool, +}; + +PromoModal.defaultProps = { + location: 'panel', + isPlus: false, +}; + +export default PromoModal; diff --git a/app/shared-components/PromoModal/PromoModalContainer.js b/app/shared-components/PromoModal/PromoModalContainer.js new file mode 100644 index 000000000..50585c692 --- /dev/null +++ b/app/shared-components/PromoModal/PromoModalContainer.js @@ -0,0 +1,55 @@ +/** + * Promo Modal 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 + */ +/** + * @namespace PanelContainers + */ +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import PromoModal from './PromoModal'; +import { togglePromoModal, showNotification } from '../../panel/actions/PanelActions'; // get shared actions from Panel + +/** + * Map redux store state properties to PromoModal component own properties. + * @memberOf PanelContainers + * @param {Object} state entire Redux store's state + * @param {Object} ownProps props passed to the connected component + * @return {function} this function returns plain object, which will be merged into PromoModal props + * @todo We are not using ownProps, so we better not specify it explicitly, + * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). + */ +// const mapStateToProps = state => Object.assign({}, { +// togglePromoModal: state.panel.togglePromoModal, +// }); + +/** + * Bind Login view component action creators using Redux's bindActionCreators + * @memberOf PanelContainers + * @param {function} dispatch redux store method which dispatches actions + * @param {Object} ownProps Login view component own props + * @return {function} to be used as an argument in redux connect call + */ +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators( + Object.assign({ + togglePromoModal, + showNotification + }), dispatch + ) +}); +/** + * Connects PromoModal component to the Redux store. + * @param {function} mapStateToProps [description] + * + * @return {Object} A higher-order React component class that passes state into PromoModal. Used by React framework. + */ +export default connect(null, mapDispatchToProps)(PromoModal); diff --git a/app/shared-components/PromoModal/index.js b/app/shared-components/PromoModal/index.js new file mode 100644 index 000000000..1cd699cc2 --- /dev/null +++ b/app/shared-components/PromoModal/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for PromoModal + * + * 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 PromoModal from './PromoModal'; + +export default PromoModal; diff --git a/app/shared-components/index.js b/app/shared-components/index.js index dcc550c36..7efe761ea 100644 --- a/app/shared-components/index.js +++ b/app/shared-components/index.js @@ -17,7 +17,9 @@ import ExitButton from './ExitButton'; import Modal from './Modal'; -import PremiumPromoModal from './PremiumPromoModal'; +import PremiumPromoModalContent from './ModalContent/PremiumPromoModalContent'; +import PlusPromoModalContent from './ModalContent/PlusPromoModalContent'; +import InsightsPromoModalContent from './ModalContent/InsightsPromoModalContent'; import SteppedNavigation from './SteppedNavigation'; import ToastMessage from './ToastMessage'; import ToggleCheckbox from './ToggleCheckbox'; @@ -27,7 +29,9 @@ import ForgotPassword from './ForgotPassword/ForgotPasswordContainer'; export { ExitButton, Modal, - PremiumPromoModal, + PremiumPromoModalContent, + PlusPromoModalContent, + InsightsPromoModalContent, SteppedNavigation, ToastMessage, ToggleCheckbox, diff --git a/src/background.js b/src/background.js index 12d0d5e31..e6ef6225e 100644 --- a/src/background.js +++ b/src/background.js @@ -985,6 +985,10 @@ function onMessageHandler(request, sender, callback) { promoModals.recordInsightsPromoSighting(); return false; } + if (name === 'promoModals.sawPlusPromo') { + promoModals.recordPlusPromoSighting(); + return false; + } if (name === 'promoModals.turnOffPromos') { promoModals.turnOffPromos(); return false; diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 600683de0..1ef073f08 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -510,10 +510,16 @@ class Metrics { */ _getThemeValue() { const { current_theme } = conf; - if (current_theme === 'midnight-theme') { - return 1; + switch (current_theme) { + case 'midnight-theme': + return 1; + case 'leaf-theme': + return 2; + case 'palm-theme': + return 3; + default: + return 0; } - return 0; } /** diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index dd9306ec9..2f49320a9 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -17,7 +17,8 @@ import panelData from './PanelData'; const DAYS_BETWEEN_PROMOS = { premium: globals.DEBUG ? 0.0005 : 30, // 40 seconds on staging - insights: globals.DEBUG ? 0.0005 : 30 // 40 seconds on staging + insights: globals.DEBUG ? 0.0005 : 30, // 40 seconds on staging + plus: globals.DEBUG ? 0.0005 : 30 // 40 seconds on staging }; const WEEKLY_INSIGHTS_TARGET = globals.DEBUG ? 1 : 3; const DAILY_INSIGHTS_TARGET = globals.DEBUG ? 7 : 3; @@ -25,6 +26,7 @@ const DAILY_INSIGHTS_TARGET = globals.DEBUG ? 7 : 3; const MSECS_IN_DAY = 86400000; // 1000 msecs-in-sec * 60 secs-in-min * 60 mins-in-hour * 24 hours-in-day const PREMIUM = 'premium'; const INSIGHTS = 'insights'; +const PLUS = 'plus'; const PROMO_MODAL_LAST_SEEN = 'promo_modal_last_seen'; /** @@ -40,9 +42,9 @@ class PromoModals { */ static whichPromoModalShouldWeDisplay() { // The order is important - // Insights takes priority over Premium + // Insights takes priority over Plus if (this._isTimeForAPromo(INSIGHTS)) return INSIGHTS; - if (this._isTimeForAPromo(PREMIUM)) return PREMIUM; + if (this._isTimeForAPromo(PLUS)) return PLUS; return null; } @@ -50,7 +52,11 @@ class PromoModals { static recordInsightsPromoSighting() { this._recordPromoSighting(INSIGHTS); } - static turnOffPromos() { panelData.set({ notify_promotions: false }); } + static recordPlusPromoSighting() { this._recordPromoSighting(PLUS); } + + static turnOffPromos() { + panelData.set({ notify_promotions: false }); + } /** * Check Conf values to determine if the enough time has @@ -61,9 +67,9 @@ class PromoModals { static _isTimeForAPromo(type) { if (conf.notify_promotions === false) { return false; } - const lastSeenPremiumPromo = conf[`${PREMIUM}_${PROMO_MODAL_LAST_SEEN}`]; + const lastSeenPlusPromo = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; - const lastSeenPromo = Math.max(lastSeenPremiumPromo, lastSeenInsightsPromo); + const lastSeenPromo = Math.max(lastSeenPlusPromo, lastSeenInsightsPromo); if (type === INSIGHTS && !this._hasEngagedFrequently()) { return false;