diff --git a/_locales/en/messages.json b/_locales/en/messages.json index fcc4118af..da0035b9e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2348,5 +2348,40 @@ }, "settings": { "message": "Settings" + }, + "try_ghostery_midnight": { + "message": "Try Ghostery Midnight" + }, + "seven_day_free_trial": { + "message": "7 Day Free Trial ($$14/mo)", + "description": "Do not localize currency. Use the $14 USD amount. The second $ is needed to escape the special meaning of $" + }, + "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" + }, + "built_in_vpn": { + "message": "Built-in VPN" + }, + "custom_whitelist_options": { + "message": "Custom whitelist options" + }, + "historical_tracking_insights": { + "message": "Historical tracking insights" + }, + "download_for_free": { + "message": "Download for free" + }, + "support_ghostery_for_2_instead": { + "message": "Support Ghostery for $$2/mo instead", + "description": "Do not localize currency. Use the $2 USD amount. The second $ is needed to escape the special meaning of $" + }, + "no_thanks_continue_with_basic": { + "message": "No thanks, continue with basic" + }, + "no_thanks_turn_promos_off": { + "message": "No thanks, turn promos off" } } diff --git a/app/hub/Views/HomeView/HomeViewActions.js b/app/hub/Views/HomeView/HomeViewActions.js index 3de1b1976..026bf2011 100644 --- a/app/hub/Views/HomeView/HomeViewActions.js +++ b/app/hub/Views/HomeView/HomeViewActions.js @@ -12,7 +12,7 @@ */ import { log, sendMessageInPromise } from '../../utils'; -import { GET_HOME_PROPS, MARK_PLUS_PROMO_MODAL_SHOWN, SET_METRICS } from './HomeViewConstants'; +import { GET_HOME_PROPS, MARK_PREMIUM_PROMO_MODAL_SHOWN, SET_METRICS } from './HomeViewConstants'; export function getHomeProps() { return function(dispatch) { @@ -40,8 +40,8 @@ export function setMetrics(actionData) { }; } -export function markPlusPromoModalShown() { +export function markPremiumPromoModalShown() { return { - type: MARK_PLUS_PROMO_MODAL_SHOWN, + type: MARK_PREMIUM_PROMO_MODAL_SHOWN, }; } diff --git a/app/hub/Views/HomeView/HomeViewConstants.js b/app/hub/Views/HomeView/HomeViewConstants.js index 2defff14b..d4040d1eb 100644 --- a/app/hub/Views/HomeView/HomeViewConstants.js +++ b/app/hub/Views/HomeView/HomeViewConstants.js @@ -13,5 +13,5 @@ // Home View export const GET_HOME_PROPS = 'GET_HOME_PROPS'; -export const MARK_PLUS_PROMO_MODAL_SHOWN = 'MARK_PLUS_PROMO_MODAL_SHOWN'; +export const MARK_PREMIUM_PROMO_MODAL_SHOWN = 'MARK_PREMIUM_PROMO_MODAL_SHOWN'; export const SET_METRICS = 'SET_METRICS'; diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index b9edcd753..f7ee124e5 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -15,7 +15,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import QueryString from 'query-string'; import HomeView from './HomeView'; -import { PlusPromoModal } from '../../../shared-components'; +import { PremiumPromoModal } from '../../../shared-components'; import { sendMessage } from '../../utils'; import globals from '../../../../src/classes/Globals'; @@ -41,8 +41,7 @@ class HomeViewContainer extends Component { props.actions.getHomeProps(); - // Prevent flickering in of user's email if getUser() returns after initial render, - // as well as flickering of plus promo modal if user is already a subscriber + // Prevent flickering in of user's email if getUser() returns after initial render props.actions.getUser() .then(() => { this.setState({ @@ -61,29 +60,57 @@ class HomeViewContainer extends Component { } /** + * Handle clicks on premium promo modal button + * @param type 'basic' (default), 'plus', or 'premium' * @private - * Function to handle clicks on Select Basic in the Plus Promo Modal */ - _handlePromoSelectBasicClick = () => { + _handlePremiumPromoModalClick = (type = 'basic') => { // GH-1777 - // we want to show the Plus Promo modal once per Hub visit - this.props.actions.markPlusPromoModalShown(); - - sendMessage('SET_PLUS_PROMO_MODAL_SEEN', {}); + // we want to show the promo modal exactly once per Hub visit + this.props.actions.markPremiumPromoModalShown(); + + sendMessage('SET_PREMIUM_PROMO_MODAL_SEEN', {}); + + switch (type) { + case 'plus': + window.open(`https://checkout.${DOMAIN}.com/plus?utm_source=gbe&utm_campaign=intro_hub`, '_blank'); + break; + case 'premium': + window.open('https://ghostery.com/thanks-for-downloading-midnight', '_blank'); + break; + case 'basic': + default: + break; + } } /** * @private - * Function to handle clicks on 'Select Plus' in the Plus Promo Modal (Choose Your Plan) + * Function to handle clicks on "No thanks, continue with basic" in Premium promo modal */ - _handlePromoSelectPlusClick = () => { - // GH-1777 - // we want to show the Plus Promo modal once per Hub visit - this.props.actions.markPlusPromoModalShown(); + _handleKeepBasicClick = () => { this._handlePremiumPromoModalClick(); } - sendMessage('SET_PLUS_PROMO_MODAL_SEEN', {}); + /** + * @private + * Function to handle clicks on the "Get Plus instead" link in the Premium promo modal + */ + _handleGetPlusClick = () => { this._handlePremiumPromoModalClick('plus'); } + + /** + * @private + * Function to handle clicks on the Midnight download button in the Premium promo modal + */ + _handleTryMidnightClick = () => { this._handlePremiumPromoModalClick('premium'); } + + /** + * @returns {bool} + * @private + * Is the user a Premium subscriber? + */ + _premiumSubscriber = () => { + const { loggedIn, user } = this.props; - window.open(`https://checkout.${DOMAIN}.com/plus?utm_source=gbe&utm_campaign=intro_hub`, '_blank'); + return loggedIn && (user && user.scopes && user.scopes.includes('subscriptions:premium')); } _render() { @@ -91,7 +118,7 @@ class HomeViewContainer extends Component { const { home, user } = this.props; const isPlus = user && user.subscriptionsPlus || false; const { - plus_promo_modal_shown, + premium_promo_modal_shown, setup_complete, tutorial_complete, enable_metrics, @@ -106,15 +133,16 @@ class HomeViewContainer extends Component { isPlus, }; - const showPromoModal = !isPlus && !plus_promo_modal_shown; + const showPromoModal = !premium_promo_modal_shown && !this._premiumSubscriber(); return (
-
@@ -137,7 +165,7 @@ class HomeViewContainer extends Component { HomeViewContainer.propTypes = { home: PropTypes.shape({ enable_metrics: PropTypes.bool, - plus_promo_modal_shown: PropTypes.bool, + premium_promo_modal_shown: PropTypes.bool, setup_complete: PropTypes.bool, tutorial_complete: PropTypes.bool, }), @@ -148,7 +176,7 @@ HomeViewContainer.propTypes = { actions: PropTypes.shape({ getHomeProps: PropTypes.func.isRequired, getUser: PropTypes.func.isRequired, - markPlusPromoModalShown: PropTypes.func.isRequired, + markPremiumPromoModalShown: PropTypes.func.isRequired, setMetrics: PropTypes.func.isRequired, }).isRequired, }; @@ -157,7 +185,7 @@ HomeViewContainer.propTypes = { HomeViewContainer.defaultProps = { home: { enable_metrics: false, - plus_promo_modal_shown: false, + premium_promo_modal_shown: false, setup_complete: false, tutorial_complete: false, }, diff --git a/app/hub/Views/HomeView/HomeViewReducer.js b/app/hub/Views/HomeView/HomeViewReducer.js index f60c13123..1582d7e62 100644 --- a/app/hub/Views/HomeView/HomeViewReducer.js +++ b/app/hub/Views/HomeView/HomeViewReducer.js @@ -11,7 +11,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { GET_HOME_PROPS, MARK_PLUS_PROMO_MODAL_SHOWN, SET_METRICS } from './HomeViewConstants'; +import { GET_HOME_PROPS, MARK_PREMIUM_PROMO_MODAL_SHOWN, SET_METRICS } from './HomeViewConstants'; const initialState = {}; @@ -31,10 +31,10 @@ function HomeViewReducer(state = initialState, action) { }), }); } - case MARK_PLUS_PROMO_MODAL_SHOWN: { + case MARK_PREMIUM_PROMO_MODAL_SHOWN: { return Object.assign({}, state, { home: Object.assign({}, state.home, { - plus_promo_modal_shown: true, + premium_promo_modal_shown: true, }) }); } diff --git a/app/images/panel/midnight-beta-icon.svg b/app/images/panel/midnight-beta-icon.svg new file mode 100644 index 000000000..6621b5a68 --- /dev/null +++ b/app/images/panel/midnight-beta-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/images/panel/midnight-check-icon.svg b/app/images/panel/midnight-check-icon.svg new file mode 100644 index 000000000..ba6251149 --- /dev/null +++ b/app/images/panel/midnight-check-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/images/panel/midnight-logo.svg b/app/images/panel/midnight-logo.svg new file mode 100644 index 000000000..28cba2896 --- /dev/null +++ b/app/images/panel/midnight-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/panel/components/InsightsPromoModal.jsx b/app/panel/components/InsightsPromoModal.jsx index 7936ad4cc..5b1e54b7e 100644 --- a/app/panel/components/InsightsPromoModal.jsx +++ b/app/panel/components/InsightsPromoModal.jsx @@ -22,7 +22,7 @@ const INSIGHTS = 'insights'; * @memberof PanelClasses */ class InsightsPromoModal extends React.Component { - handleNoThanksClick = () => { this.props.handleNoThanksClick(INSIGHTS); } + handleGoAwayClick = () => { this.props.handleGoAwayClick(INSIGHTS); } handleSubscribeClick = () => { this.props.handleSubscribeClick(INSIGHTS); } @@ -78,7 +78,7 @@ class InsightsPromoModal extends React.Component {
{t('subscribe_pitch_sign_in')} - {t('no_thanks_turn_promos_off')} + {t('no_thanks_turn_promos_off')}
diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 575027e6f..d583a2c9f 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -14,9 +14,8 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import Header from '../containers/HeaderContainer'; -import { PlusPromoModal } from '../../shared-components'; +import { PremiumPromoModal } from '../../shared-components'; import InsightsPromoModal from './InsightsPromoModal'; -import PlusUpgradePromoModal from './PlusUpgradePromoModal'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; @@ -200,15 +199,18 @@ class Panel extends React.Component { return false; } - _handlePromoNoThanksClick = (modal) => { + /** + * @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'); - } else if (modal === 'plus_upgrade') { - sendMessage('ping', 'promo_modals_decline_plus_upgrade'); } this.props.actions.showNotification({ @@ -218,6 +220,10 @@ class Panel extends React.Component { }); }; + /** + * @private + * Handle clicks on sign in links in promo modals + */ _handlePromoSignInClick = () => { this.props.actions.togglePromoModal(); history.push({ @@ -225,25 +231,27 @@ class Panel extends React.Component { }); }; - _handlePromoSelectBasicClick = () => { + /** + * @private + * Handle clicks on the download button in the Premium promo modals + */ + _handlePromoTryMidnightClick = () => { this.props.actions.togglePromoModal(); - // we do not mark the choice-required initial plus promo as 'seen' until - // the user has clicked Select Basic or Select Plus - sendMessage('promoModals.sawPlusPromo', {}); - }; + const url = 'https://ghostery.com/thanks-for-downloading-midnight'; + sendMessage('openNewTab', { + url, + become_active: true, + }); + } /** * @private - * Handle clicks on 'Select Plus' from the Plus Promo Modal (Choose Your Plan) + * Handle clicks on the 'Get Plus' option in the Premium modals */ - _handlePromoSelectPlusClick = () => { + _handlePromoGetPlusClick = () => { this.props.actions.togglePromoModal(); - // we do not mark the choice-required initial plus promo as 'seen' until - // the user has clicked Select Basic or Select Plus - sendMessage('promoModals.sawPlusPromo', {}); - const url = `https://checkout.${DOMAIN}.com/plus?utm_source=gbe&utm_campaign=in_app`; sendMessage('openNewTab', { url, @@ -253,8 +261,7 @@ class Panel extends React.Component { /** * @private - * Handle click action when user selects Subscribe button in - * Plus Upgrade or Insights modal + * Handle click action when user selects Subscribe button in the Insights modal * @param {string} modal Modal type (insights or plus) */ _handlePromoSubscribeClick = (modal) => { @@ -265,9 +272,6 @@ class Panel extends React.Component { if (modal === 'insights') { sendMessage('ping', 'promo_modals_insights_upgrade_cta'); url += 'insights?utm_source=gbe&utm_campaign=in_app_upgrade'; - } else if (modal === 'plus_upgrade') { - sendMessage('ping', 'promo_modals_plus_upgrade_cta'); - url += 'plus?utm_source=gbe&utm_campaign=in_app_upgrade'; } sendMessage('openNewTab', { @@ -276,57 +280,68 @@ class Panel extends React.Component { }); }; + /** + * @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'); - } else if (modal === 'plus_upgrade') { - sendMessage('ping', 'promo_modals_decline_plus_upgrade'); } }; - _plusSubscriber = () => { + /** + * @returns {bool} + * @private + * Is the user an Insights subscriber? + */ + _insightsSubscriber = () => { const { loggedIn, user } = this.props; - return loggedIn && (user && user.subscriptionsPlus); + return loggedIn && (user && user.scopes && user.scopes.includes('subscriptions:insights')); } - _insightsSubscriber = () => { + /** + * @returns {bool} + * @private + * Is the user a Premium subscriber? + */ + _premiumSubscriber = () => { const { loggedIn, user } = this.props; - return loggedIn && (user && user.scopes && user.scopes.includes('subscriptions:insights')); + return loggedIn && (user && user.scopes && user.scopes.includes('subscriptions:premium')); } - _renderPlusPromoModal = () => { - if (this._plusSubscriber() || this._insightsSubscriber()) return null; + /** + * @returns {JSX} + * @private + * Renders the Premium promo modal + */ + _renderPremiumPromoModal = () => { + if (this._premiumSubscriber()) return null; - if (this.props.promoModal === 'plus_upgrade') { - // the upgrade promo does not require the user to make a choice, so we mark it as 'seen' immediately - sendMessage('promoModals.sawPlusPromo', {}); - sendMessage('ping', 'promo_modals_show_upgrade_plus'); - return ( - - ); - } + sendMessage('promoModals.sawPremiumPromo', {}); - // promoModal === 'plus_initial' return ( - ); } + /** + * @returns {null|JSX} + * @private + * Renders the Insights promo modal if the user is not already an Insights subscriber + */ _renderInsightsPromoModal = () => { if (this._insightsSubscriber()) return null; @@ -336,7 +351,7 @@ class Panel extends React.Component { return ( { const { promoModal, @@ -356,8 +376,8 @@ class Panel extends React.Component { return this._renderInsightsPromoModal(); } - if (promoModal === 'plus_initial' || promoModal === 'plus_upgrade') { - return this._renderPlusPromoModal(); + if (promoModal === 'premium') { + return this._renderPremiumPromoModal(); } return null; diff --git a/app/panel/components/PlusUpgradePromoModal.jsx b/app/panel/components/PlusUpgradePromoModal.jsx deleted file mode 100644 index e4e5fd456..000000000 --- a/app/panel/components/PlusUpgradePromoModal.jsx +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Plus Upgrade 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 ClassNames from 'classnames'; -import Modal from '../../shared-components/Modal'; -import ModalExitButton from './BuildingBlocks/ModalExitButton'; - -const PLUS_UPGRADE = 'plus_upgrade'; - -/** - * @class Implements the Upgrade variant of the Plus Promo Modal - * @memberof PanelClasses - */ -class PlusUpgradePromoModal extends React.Component { - handleNoThanksClick = () => { this.props.handleNoThanksClick(PLUS_UPGRADE); } - - handleSubscribeClick = () => { this.props.handleSubscribeClick(PLUS_UPGRADE); } - - handleXClick = () => { this.props.handleXClick(PLUS_UPGRADE); } - - render() { - const { loggedIn } = this.props; - - const contentClassNames = ClassNames( - 'PlusPromoModal__content', - 'flex-container', - 'flex-dir-column', - 'align-middle', - 'panel', - 'upgrade' - ); - - // TODO update ModalExitButton class here - // TODO pass down handler for click on ModalExitButton - return ( - -
- - -
- {t('upgrade_your_ghostery_experience')} -
-
-
-
- - {t('upgrade_to_plus')} - -
-
- { - !loggedIn && - ( -
- {t('already_subscribed_sign_in')} -
- ) - } -
- {t('no_thanks_turn_promos_off')} -
-
-
-
- - ); - } -} - -export default PlusUpgradePromoModal; diff --git a/app/scss/hub.scss b/app/scss/hub.scss index a59365f96..9cc1a696b 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/PlusPromoModal/PlusPromoModal.scss'; +@import '../shared-components/PremiumPromoModal/PremiumPromoModal.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 3f6596ed4..eaef66ef7 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -79,4 +79,4 @@ html body { // Imports from ../shared-components directory @import '../shared-components/Modal/Modal.scss'; -@import '../shared-components/PlusPromoModal/PlusPromoModal.scss'; +@import '../shared-components/PremiumPromoModal/PremiumPromoModal.scss'; diff --git a/app/shared-components/PlusPromoModal/PlusPromoModal.jsx b/app/shared-components/PlusPromoModal/PlusPromoModal.jsx deleted file mode 100644 index a85c5bf0a..000000000 --- a/app/shared-components/PlusPromoModal/PlusPromoModal.jsx +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Plus 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'; - -/** - * A functional React component for a Plus Promo Modal that may be displayed in the Hub and/or Panel - * @return {JSX} JSX for rendering a Plus Promo Modal - * @memberof SharedComponents - */ -const PlusPromoModal = (props) => { - const { - show, - location, - handleSelectBasicClick, - handleSelectPlusClick, - handleSignInClick, - } = props; - - const isInHub = location === 'hub'; - const isInPanel = location === 'panel'; - - const locationClassName = { - 'in-hub': isInHub, - 'in-panel': location === 'panel' - }; - const contentClassNames = ClassNames( - 'PlusPromoModal__content', - 'flex-container', - 'flex-dir-column', - 'align-middle', - 'initial', - locationClassName - ); - const optionsContainerClassNames = ClassNames( - 'PlusPromoModal__options-container', - 'full-width', - locationClassName - ); - const optionsDecriptionClassNames = ClassNames( - 'PlusPromoModal__option-description-item', - locationClassName - ); - const chooseYourPlanClassNames = ClassNames( - 'PlusPromoModal__choose-your-plan', - locationClassName - ); - const recommendedBannerClassNames = ClassNames( - 'PlusPromoModal__recommended-banner', - locationClassName - ); - const optionDescriptionBoxClassNames = ClassNames( - 'PlusPromoModal__option-description-box', - locationClassName - ); - const buttonBackgroundClassNames = ClassNames( - 'PlusPromoModal__buttons-background', - 'initial', - locationClassName - ); - - return ( - -
-
- {isInHub && ( -
- {t('ghostery_is_ready')} -
- )} -
- {isInHub ? t('choose_your_privacy_plan') : t('choose_your_ghostery_privacy_plan')} -
-
-
-
-
-
{t('ghostery_basic')}
-
- - 0 - - {t('per_month')} -
-
-
{t('faster_cleaner_browsing')}
-
{t('blocks_ads')}
-
{t('blocks_trackers')}
-
{t('data_protection')}
-
-
-
-
-
-
- -
{t('recommended')}
-
-
{t('ghostery_plus')}
-
- - {t('plus_monthly_subscription_price_number')} - - {t('per_month')} -
-
-
{t('all_basic_features_plus_COLON')}
-
-
- - {t('historical_tracker_stats')} -
-
-
-
- - {t('priority_support')} -
-
-
-
- - {t('new_color_themes')} -
-
-
-
-
-
-
-
- {t('select_basic')} -
-
- {t('select_plus')} -
- {isInPanel && ( -
- {t('already_subscribed_sign_in')} -
- )} -
-
-
- ); -}; - -// PropTypes ensure we pass required props of the correct type -PlusPromoModal.propTypes = { - show: PropTypes.bool.isRequired, - location: PropTypes.string.isRequired, - handleSelectBasicClick: PropTypes.func.isRequired, - handleSelectPlusClick: PropTypes.func.isRequired, - handleSignInClick: PropTypes.func, -}; - -PlusPromoModal.defaultProps = { - handleSignInClick: () => {}, -}; - -export default PlusPromoModal; diff --git a/app/shared-components/PlusPromoModal/PlusPromoModal.scss b/app/shared-components/PlusPromoModal/PlusPromoModal.scss deleted file mode 100644 index 897a73d0d..000000000 --- a/app/shared-components/PlusPromoModal/PlusPromoModal.scss +++ /dev/null @@ -1,320 +0,0 @@ -/** - * 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__gold-ghostie-badge { - margin-top: 21px; -} - -.PlusPromoModal__header { - font-size: 20px; - font-weight: bold; - font-family: $standard-font-family; - padding: 0 15px; - text-align: center; -} - -.PlusPromoModal__description { - width: 350px; - text-align: center; - font-size: 18px; - font-weight: 500; - font-family: $standard-font-family -} - -.PlusPromoModal__content { - position: relative; - background-color: #f7f7f7; - border: 1.9px solid #930194; - justify-content: space-between; - z-index: 10; - &.initial { - &.in-hub { - width: 646px; - height: 553px; - } - &.in-panel { - width: 556px; - height: 471px; - } - } - &.upgrade { - width: 475px; - min-height: 425px; - } -} - -.PlusPromoModal__buttons-background { - background-color: #e7ecee; - margin-bottom: 3px; - width: 99%; - height: 72px; - z-index: -1; - display: flex; - &.initial { - height: 72px; - flex-direction: row; - justify-content: space-around; - align-items: center; - &.in-panel { - height: 80px; - flex-wrap: wrap; - align-content: center; - } - } - &.upgrade { - height: 95px; - flex-direction: column; - justify-content: center; - } -} - -.PlusPromoModal__thanks-for-download { - padding-top: 30px; - font-size: 26px; - font-weight: bold; - text-align: center; -} - -.PlusPromoModal__choose-your-plan { - font-size: 18px; - font-weight: bold; - font-family: $standard-font-family; - color: #4a4a4a; - text-align: center; - &.in-hub {margin-top: 10px;} - &.in-panel { - margin-bottom: 10px; - padding-top: 17px; - } -} - -.PlusPromoModal__options-container { - display: flex; - justify-content: space-around; - &.in-hub {} - &.in-panel {} -} - -.PlusPromoModal__option-container { - display: flex; - align-items: center; - flex-direction: column; -} - -.PlusPromoModal__option-description-box { - border-style: solid; - border-width: 3px; - background-color: #FFFFFF; - padding: 0 15px; - - &.basic { - border-color: #1dafed; - } - - &.plus { - position: relative; // so that the recommended banner is positioned relative to this element - border-image-source: linear-gradient(39deg, #f2daa2, #eab968); - border-image-slice: 1; - } - - &.in-hub { - width: 240px; - flex: 1; - min-height: 275px; - } - - &.in-panel { - width: 221px; - flex: 1; - min-height: 250px; - } -} - -.PlusPromoModal__recommended-banner { - position: absolute; - font-family: $standard-font-family; - - &.in-hub { - left: -30px; - top: -30px; - } - - &.in-panel { - left: -30px; - top: -32px; - } -} - -.PlusPromoModal__recommended-banner-text { - position: relative; - left: 15px; - top: -47px; - transform: rotate(-15deg); - color: white; - font-size: 16px; - font-weight: bold; -} - -.PlusPromoModal__option-header { - margin-top: 20px; - font-family: $condensed-font-family; - font-size: 19px; - font-weight: bold; - text-align: center; - - &.basic { color: #1dafed; } - &.plus { color: #ebbf73; } -} - -.PlusPromoModal__price-text { - line-height: 1.3; - text-align: center; - - &.basic { color: #1dafed; } - &.plus { color: #ebbf73; } -} - -.PlusPromoModal__currency-sign { - font-family: $condensed-font-family; - font-size: 30px; - font-weight: bold; -} - -.PlusPromoModal__amount { - vertical-align: middle; - font-family: $standard-font-family; - font-size: 58px; - font-weight: normal; -} - -.PlusPromoModal__per-month { - vertical-align: sub; - font-family: $condensed-font-family; - font-size: 18px; - font-weight: bold; -} - -.PlusPromoModal__option-description-item { - padding-bottom: 15px; - text-align: center; - font-family: $standard-font-family; - font-size: 16px; - color: #333333; - line-height: 16px; - &.in-panel { - padding-bottom: 10px; - font-size: 15px; - } - &.italic { - font-style: italic; - } -} - -.PlusPromoModal__check-icon { - padding-right: 10px; -} - -.PlusPromoModal__plus-option-description-item-container { - display: flex; - justify-content: center; -} - -.PlusPromoModal__button-container { - display: flex; - margin-bottom: 10px; - align-self: center; -} - -.PlusPromoModal__button { - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - border-radius: 3px; - font-weight: bold; - font-family: $condensed-font-family; - letter-spacing: 0.5px; - text-transform: uppercase; - cursor: pointer; - transition: background-color 0.25s ease-out, color 0.25s ease-out; - -webkit-appearance: none; - - &.basic { - height: 40px; - font-size: 14px; - border: solid 2px #15b4f2; - background-color: white; - color: #2cbcf4; - box-shadow: none; - } - &.basic:hover { - background-color: #2cbcf4; - color: white; - } - - &.plus { - font-size: 14px; - height: 38px; - border: none; - background-image: linear-gradient(to bottom, #2fdbfa, #15b4f2); - color: white; - box-shadow: none; - } - &.plus:hover { - background-image: linear-gradient(to bottom, #1fcbea, #05a4e2); - } - - &.upgrade { - font-size: 13px; - height: 36px; - 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; - color: white; - } - &.upgrade:hover { - background-color: #0698d6; - } - - .side-padded { - padding: 0px 20px 0px; // more localization-proof than hardcoding width - } - - .button-text { - color: white; - text-decoration: none; - } -} - -.PlusPromoModal__text-link-container { - display: flex; - justify-content: space-evenly; -} - -.PlusPromoModal__text-link { - margin-left: 10px; - margin-right: 10px; - font-size: 13px; - font-family: $standard-font-family; - color: #4a4a4a; - text-decoration: underline; - text-align: center; - cursor: pointer; - &.sign-in { - margin-top: 5px; - width: 100%; - } -} diff --git a/app/shared-components/PremiumPromoModal/PremiumPromoModal.jsx b/app/shared-components/PremiumPromoModal/PremiumPromoModal.jsx new file mode 100644 index 000000000..d7163fb3c --- /dev/null +++ b/app/shared-components/PremiumPromoModal/PremiumPromoModal.jsx @@ -0,0 +1,139 @@ +/** + * 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, + 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('seven_day_free_trial')}
+
+
+ {t('full_coverage_protection_promise')} +
+
+
+
+ +
+ {t('system_wide_tracker_and_ad_blocking')} +
+
+
+ +
+ {t('built_in_vpn')} +
+
+
+
+
+ +
+ {t('custom_whitelist_options')} +
+
+
+ +
+ {t('historical_tracking_insights')} +
+
+
+
+
+
+
+ {t('download_for_free')} +
+
+
+
+ {t('support_ghostery_for_2_instead')} +
+ {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, + 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/PremiumPromoModal/PremiumPromoModal.scss b/app/shared-components/PremiumPromoModal/PremiumPromoModal.scss new file mode 100644 index 000000000..f8258d80c --- /dev/null +++ b/app/shared-components/PremiumPromoModal/PremiumPromoModal.scss @@ -0,0 +1,166 @@ +/** + * Premium 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 + */ + +// Premium 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; + +.PremiumPromoModal__content { + position: relative; + z-index: 10; + width: 518px; + height: 437px; + border: solid 1.9px #720174; + background-color: #f7f7f7; +} + +.PremiumPromoModal__midnight-logo { + width: 177px; + height: 106px; + margin: 22px 0px 16px; + background-image: url('/app/images/panel/midnight-logo.svg'); +} + +.PremiumPromoModal__header { + display: flex; + width: 100%; + justify-content: space-evenly; +} + +.PremiumPromoModal__header-text { + text-align: center; + font-size: 20px; + font-weight: bold; + font-family: $standard-font-family; +} + +.PremiumPromoModal__header-beta-icon { + float: right; + margin-left: 5px; + width: 20px; + height: 12px; + background-image: url('/app/images/panel/midnight-beta-icon.svg'); +} + +.PremiumPromoModal__sub-header { + margin: 9px 0px 22px; + width: 438px; + text-align: center; + font-size: 18px; + font-weight: bold; + line-height: 1.5; + color: #4A4A4A; +} + +.PremiumPromoModal__features-container { + display: flex; + width: 100%; + margin-bottom: 28px; +} + +.PremiumPromoModal__feature-column { + &:nth-child(odd) { + margin-left: 20px; + width: 55%; + } + &:nth-child(even) { + margin-left: -12px; + margin-right: 25px; + width: 45%; + } +} + +.PremiumPromoModal__feature { + display: flex; + align-items: center; + &:nth-child(1) { + margin-bottom: 20px; + } +} + +.PremiumPromoModal__feature-text { + font-family: $standard-font-family; + font-size: 14px; + color: $medium-gray; + padding-bottom: 5px; + line-height: 16px; +} + +.PremiumPromoModal__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; +} + +.PremiumPromoModal__buttons-background { + background-color: #e7ecee; + margin-bottom: 3px; + width: 99%; + height: 107px; + z-index: -1; + display: flex; + flex-direction: column; + justify-content: space-evenly +} + +.PremiumPromoModal__button-container { + display: flex; + justify-content: center; +} + +.PremiumPromoModal__download-button { + width: 196px; + height: 36px; + border-radius: 2px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12); + background-image: linear-gradient(101deg, #720174 19%, #00aef0 100%); + + 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; + + &:hover { + background-image: linear-gradient(101deg, #A80AAB 19%, #41BCEA 100%); + } + + text-transform: uppercase; +} + +.PremiumPromoModal__text-link-container { + display: flex; + justify-content: space-between; +} + +.PremiumPromoModal__text-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; +} diff --git a/app/shared-components/PlusPromoModal/index.js b/app/shared-components/PremiumPromoModal/index.js similarity index 68% rename from app/shared-components/PlusPromoModal/index.js rename to app/shared-components/PremiumPromoModal/index.js index da26a8b7e..9d353dc6c 100644 --- a/app/shared-components/PlusPromoModal/index.js +++ b/app/shared-components/PremiumPromoModal/index.js @@ -1,5 +1,5 @@ /** - * Point of entry index.js file for Plus Promo Modal Component + * Point of entry index.js file for Premium Promo Modal Component * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -11,6 +11,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import PlusPromoModal from './PlusPromoModal'; +import PremiumPromoModal from './PremiumPromoModal'; -export default PlusPromoModal; +export default PremiumPromoModal; diff --git a/app/shared-components/index.js b/app/shared-components/index.js index a7fe2f139..8cea5f1ae 100644 --- a/app/shared-components/index.js +++ b/app/shared-components/index.js @@ -17,7 +17,7 @@ import ExitButton from './ExitButton'; import Modal from './Modal'; -import PlusPromoModal from './PlusPromoModal'; +import PremiumPromoModal from './PremiumPromoModal'; import SteppedNavigation from './SteppedNavigation'; import ToastMessage from './ToastMessage'; import ToggleCheckbox from './ToggleCheckbox'; @@ -26,7 +26,7 @@ import ToggleSwitch from './ToggleSwitch'; export { ExitButton, Modal, - PlusPromoModal, + PremiumPromoModal, SteppedNavigation, ToastMessage, ToggleCheckbox, diff --git a/src/background.js b/src/background.js index ef34dda8e..2dcd826b0 100644 --- a/src/background.js +++ b/src/background.js @@ -513,8 +513,8 @@ function handleRewards(name, message, callback) { */ function handleGhosteryHub(name, message, callback) { switch (name) { - case 'SET_PLUS_PROMO_MODAL_SEEN': - promoModals.recordPlusPromoSighting(); + case 'SET_PREMIUM_PROMO_MODAL_SEEN': + promoModals.recordPremiumPromoSighting(); break; case 'SEND_PING': { const { type } = message; @@ -1006,8 +1006,8 @@ function onMessageHandler(request, sender, callback) { }); return true; } - if (name === 'promoModals.sawPlusPromo') { - promoModals.recordPlusPromoSighting(); + if (name === 'promoModals.sawPremiumPromo') { + promoModals.recordPremiumPromoSighting(); return false; } if (name === 'promoModals.sawInsightsPromo') { diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 050e302eb..22d73cba5 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -126,6 +126,7 @@ class ConfData { _initProperty('notify_upgrade_updates', true); _initProperty('paid_subscription', false); _initProperty('plus_promo_modal_last_seen', 0); + _initProperty('premium_promo_modal_last_seen', 0); _initProperty('rewards_accepted', false); _initProperty('rewards_opted_in', false); _initProperty('settings_last_imported', 0); diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index 2e202d80d..f0079429f 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -16,21 +16,19 @@ import globals from './Globals'; import panelData from './PanelData'; const DAYS_BETWEEN_PROMOS = { - plus: globals.DEBUG ? 0.0005 : 30, // 40 seconds on staging + premium: globals.DEBUG ? 0.0005 : 30, // 40 seconds on staging insights: 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; const MSECS_IN_DAY = 86400000; // 1000 msecs-in-sec * 60 secs-in-min * 60 mins-in-hour * 24 hours-in-day -const PLUS = 'plus'; -const PLUS_INITIAL = 'plus_initial'; -const PLUS_UPGRADE = 'plus_upgrade'; +const PREMIUM = 'premium'; const INSIGHTS = 'insights'; const PROMO_MODAL_LAST_SEEN = 'promo_modal_last_seen'; /** - * Static 'namespace' class for handling the business logic for the display of promo modals (Plus, Insights, etc...) + * Static 'namespace' class for handling the business logic for the display of promo modals (Premium, Insights, etc...) * @memberOf BackgroundClasses */ class PromoModals { @@ -41,28 +39,19 @@ class PromoModals { * @return {string} Type of promo to show */ static whichPromoModalShouldWeDisplay() { + // The order is important + // Insights takes priority over Premium if (this._isTimeForAPromo(INSIGHTS)) return INSIGHTS; - - if (this._isTimeForAPromo(PLUS)) { - if (this._haveSeenInitialPlusPromo()) return PLUS_UPGRADE; - - return PLUS_INITIAL; - } - + if (this._isTimeForAPromo(PREMIUM)) return PREMIUM; return null; } - static recordPlusPromoSighting() { this._recordPromoSighting(PLUS); } + static recordPremiumPromoSighting() { this._recordPromoSighting(PREMIUM); } static recordInsightsPromoSighting() { this._recordPromoSighting(INSIGHTS); } static turnOffPromos() { panelData.set({ notify_promotions: false }); } - static _haveSeenInitialPlusPromo() { - const lastSeenTime = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; - return (lastSeenTime !== 0); - } - /** * Check Conf values to determine if the enough time has * passed for `type` modal to be displayed @@ -72,9 +61,9 @@ class PromoModals { static _isTimeForAPromo(type) { if (conf.notify_promotions === false) { return false; } - const lastSeenPlusPromo = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; + const lastSeenPremiumPromo = conf[`${PREMIUM}_${PROMO_MODAL_LAST_SEEN}`]; const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; - const lastSeenPromo = lastSeenPlusPromo > lastSeenInsightsPromo ? lastSeenPlusPromo : lastSeenInsightsPromo; + const lastSeenPromo = Math.max(lastSeenPremiumPromo, lastSeenInsightsPromo); if (type === INSIGHTS && !this._hasEngagedFrequently()) { return false; @@ -107,14 +96,9 @@ class PromoModals { static _hasEngagedFrequently() { const { engaged_daily_count } = conf.metrics || []; - let very_engaged_days = 0; - engaged_daily_count.forEach((count) => { - very_engaged_days = count >= DAILY_INSIGHTS_TARGET ? ++very_engaged_days : very_engaged_days; - }); - - if (very_engaged_days >= WEEKLY_INSIGHTS_TARGET) return true; + const very_engaged_days = engaged_daily_count.reduce((acc, count) => (count >= DAILY_INSIGHTS_TARGET ? acc++ : acc), 0); - return false; + return very_engaged_days >= WEEKLY_INSIGHTS_TARGET; } }