diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 356f3efc8..344314d05 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -526,6 +526,27 @@ "panel_help_setup": { "message": "Set Up Ghostery" }, + "panel_insights_audit_tags": { + "message": "Audit marketing tags on a page" + }, + "panel_insights_promotion_explore_trends": { + "message": "Explore global digital trends" + }, + "panel_insights_promotion_call_to_action": { + "message": "Try for free" + }, + "panel_insights_promotion_header": { + "message": "Try Ghostery Insights" + }, + "panel_insights_promotion_description": { + "message": "Speed up and clean up digital user experience with our professional tag analytics tool." + }, + "panel_insights_promotion_trace_poor_performance": { + "message": "Trace sources of poor performance" + }, + "panel_insights_promotion_watch_pings": { + "message": "Watch pings fire in real-time" + }, "panel_about_panel_header": { "message": "About Ghostery Browser Extension" }, @@ -1786,9 +1807,18 @@ "subscribe_pitch_learn_more": { "message": "Learn more" }, + "subscribe_pitch_button_label": { + "message": "Get Ghostery Plus!" + }, + "subscribe_pitch_no_thanks": { + "message": "No thanks, maybe later" + }, "subscribe_pitch_sign_here": { "message": "Already a subscriber? Sign in here" }, + "subscribe_pitch_sign_in": { + "message": "Already subscribed? Sign in" + }, "subscription_midnight_theme": { "message": "Midnight Theme" }, diff --git a/app/fonts/roboto-all-charsets.woff b/app/fonts/roboto-all-charsets.woff deleted file mode 100644 index 96c1986f0..000000000 Binary files a/app/fonts/roboto-all-charsets.woff and /dev/null differ diff --git a/app/fonts/roboto-condensed-latin-bold-700.woff2 b/app/fonts/roboto-condensed-latin-bold-700.woff2 new file mode 100644 index 000000000..1a32150b5 Binary files /dev/null and b/app/fonts/roboto-condensed-latin-bold-700.woff2 differ diff --git a/app/fonts/roboto-latin-bold-300.woff2 b/app/fonts/roboto-latin-bold-300.woff2 new file mode 100644 index 000000000..ef8c8836b Binary files /dev/null and b/app/fonts/roboto-latin-bold-300.woff2 differ diff --git a/app/fonts/roboto-latin-bold-500.woff2 b/app/fonts/roboto-latin-bold-500.woff2 new file mode 100644 index 000000000..6362d7f64 Binary files /dev/null and b/app/fonts/roboto-latin-bold-500.woff2 differ diff --git a/app/fonts/roboto-latin-bold-700.woff2 b/app/fonts/roboto-latin-bold-700.woff2 new file mode 100644 index 000000000..32b25eee7 Binary files /dev/null and b/app/fonts/roboto-latin-bold-700.woff2 differ diff --git a/app/fonts/roboto-latin-bold-900.woff2 b/app/fonts/roboto-latin-bold-900.woff2 new file mode 100644 index 000000000..802499d3f Binary files /dev/null and b/app/fonts/roboto-latin-bold-900.woff2 differ diff --git a/app/images/panel/checked-circle-icon.svg b/app/images/panel/checked-circle-icon.svg new file mode 100644 index 000000000..493ad4055 --- /dev/null +++ b/app/images/panel/checked-circle-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/panel/insights-ribbon.svg b/app/images/panel/insights-ribbon.svg new file mode 100644 index 000000000..568714725 --- /dev/null +++ b/app/images/panel/insights-ribbon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/panel/actions/PanelActions.js b/app/panel/actions/PanelActions.js index 601f169e9..55618cf28 100644 --- a/app/panel/actions/PanelActions.js +++ b/app/panel/actions/PanelActions.js @@ -17,7 +17,8 @@ import { CLOSE_NOTIFICATION, TOGGLE_EXPERT, SET_THEME, - CLEAR_THEME + CLEAR_THEME, + TOGGLE_INSIGHTS_MODAL } from '../constants/constants'; import { sendMessageInPromise } from '../utils/msg'; @@ -98,3 +99,13 @@ export const getTheme = name => dispatch => ( } }) ); + +/** + * Triggered when the user signs in through the Insights modal into an account that does not have an insights subscription, prompting to re-display the modal, requiring a re-render + * @return {Object} + */ +export function toggleInsightsModal() { + return { + type: TOGGLE_INSIGHTS_MODAL, + }; +} diff --git a/app/panel/components/BuildingBlocks/ModalExitButton.jsx b/app/panel/components/BuildingBlocks/ModalExitButton.jsx new file mode 100644 index 000000000..92dad90e3 --- /dev/null +++ b/app/panel/components/BuildingBlocks/ModalExitButton.jsx @@ -0,0 +1,40 @@ + +/** + * Modal Exit Button 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 Exit Button + * @return {JSX} JSX for rendering a Exit Button + * @memberof SharedComponents + */ +const ModalExitButton = (props) => { + const { + toggleModal + } = props; + + return ( + + ); +}; + +// PropTypes ensure we pass required props of the correct type +ModalExitButton.propTypes = { + toggleModal: PropTypes.func.isRequired +}; + +export default ModalExitButton; diff --git a/app/panel/components/BuildingBlocks/index.js b/app/panel/components/BuildingBlocks/index.js index e689a77de..3cb86a8ca 100644 --- a/app/panel/components/BuildingBlocks/index.js +++ b/app/panel/components/BuildingBlocks/index.js @@ -23,6 +23,7 @@ import PauseButton from './PauseButton'; import ToggleSlider from './ToggleSlider'; import RewardDetail from './RewardDetail'; import RewardListItem from './RewardListItem'; +import ModalExitButton from './ModalExitButton'; export { ClickOutside, @@ -33,5 +34,6 @@ export { PauseButton, ToggleSlider, RewardDetail, - RewardListItem + RewardListItem, + ModalExitButton }; diff --git a/app/panel/components/InsightsPromoModal.jsx b/app/panel/components/InsightsPromoModal.jsx new file mode 100644 index 000000000..f881a8bac --- /dev/null +++ b/app/panel/components/InsightsPromoModal.jsx @@ -0,0 +1,87 @@ +/** + * 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 history from '../utils/history'; +import ModalExitButton from './BuildingBlocks/ModalExitButton'; + +// A Functional React component for a Modal +class InsightsPromoModal extends React.Component { + clickSignIn = () => { + history.push({ + pathname: '/login', + }); + this.props.actions.toggleInsightsModal(); + }; + + 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('subscribe_pitch_sign_in')} + {t('subscribe_pitch_no_thanks')} +
+
+
+ + ); + } +} + +export default InsightsPromoModal; diff --git a/app/panel/components/Login.jsx b/app/panel/components/Login.jsx index d92dbe856..8243b1214 100644 --- a/app/panel/components/Login.jsx +++ b/app/panel/components/Login.jsx @@ -17,6 +17,7 @@ import ClassNames from 'classnames'; import RSVP from 'rsvp'; import { validateEmail } from '../utils/utils'; import { log } from '../../../src/utils/common'; +import history from '../utils/history'; /** * @class Implement Sign In view which opens from 'Sign In' CTA on the Header. @@ -72,7 +73,10 @@ class Login extends React.Component { }) .finally(() => { this.setState({ loading: false }, () => { - this.props.history.push(this.props.is_expert ? '/detail/blocking' : '/'); + this.props.actions.toggleInsightsModal(); + history.push({ + pathname: this.props.is_expert ? '/detail/blocking' : '/' + }); }); }); } else { diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 64a34941f..c835e73d1 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -14,11 +14,11 @@ import React from 'react'; import ClassNames from 'classnames'; import Header from '../containers/HeaderContainer'; -import { PlusPromoModal } from '../../shared-components'; +import { PlusPromoModal, Modal } from '../../shared-components'; +import InsightsPromoModal from '../containers/InsightsPromoModalContainer'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; -import Modal from '../../shared-components/Modal'; /** * @class Implement base view with functionality common to all views. * @memberof PanelClasses @@ -26,15 +26,15 @@ import Modal from '../../shared-components/Modal'; class Panel extends React.Component { constructor(props) { super(props); + this.state = { + insightsPromoModalShown: false, + plusPromoModalShown: false + }; // event bindings this.closeNotification = this.closeNotification.bind(this); this.clickReloadBanner = this.clickReloadBanner.bind(this); this.filterTrackers = this.filterTrackers.bind(this); - - this.state = { - plusPromoModalShown: false, - }; } /** @@ -42,7 +42,6 @@ class Panel extends React.Component { */ componentDidMount() { sendMessage('ping', 'engaged'); - this._dynamicUIDataInitialized = false; this._dynamicUIPort = chrome.runtime.connect({ name: 'dynamicUIPanelPort' }); this._dynamicUIPort.onMessage.addListener((msg) => { @@ -65,6 +64,16 @@ class Panel extends React.Component { this._dynamicUIPort.disconnect(); } + /** + * Reload the current tab + * @param {Object} event + * @todo Why do we need explicit argument here? + */ + clickReloadBanner() { + sendMessage('reloadTab', { tab_id: +this.props.tab_id }); + window.close(); + } + /** * Close banner notification * @param {Object} event @@ -87,16 +96,6 @@ class Panel extends React.Component { }); } - /** - * Reload the current tab - * @param {Object} event - * @todo Why do we need explicit argument here? - */ - clickReloadBanner() { - sendMessage('reloadTab', { tab_id: +this.props.tab_id }); - window.close(); - } - /** * Filter trackers when clicking on compatibility/slow * tracker notifications and trigger appropriate action. @@ -118,12 +117,12 @@ class Panel extends React.Component { this.closeNotification(); } - /** * Dynamic UI data port first payload handling * Called once, when we get the first message from the background through the port * @param {Object} payload the body of the message */ + _initializeData(payload) { this._dynamicUIDataInitialized = true; @@ -155,6 +154,7 @@ class Panel extends React.Component { } } + /** * Helper render function for the notification callout * @return {JSX} JSX for the notification callout @@ -238,8 +238,6 @@ class Panel extends React.Component { if (plusPromoModalShown || !isTimeForAPlusPromo) return null; - const version = haveSeenInitialPlusPromo ? PlusPromoModal.UPGRADE : PlusPromoModal.INITIAL; - if (haveSeenInitialPlusPromo) { return this._renderPlusPromoUpgradeModal(); } return ( @@ -251,6 +249,21 @@ class Panel extends React.Component { ); } + _renderInsightsPromoModal = () => { + const { account, isTimeForInsightsPromo, isInsightsModalHidden } = this.props; + const { insightsPromoModalShown } = this.state; + + if (isInsightsModalHidden) return null; + if (insightsPromoModalShown || !isTimeForInsightsPromo) return null; + if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; + + sendMessage('promoModals.sawInsightsPromo', '', 'metrics'); + + return ( + + ); + } + /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Panel @@ -266,6 +279,7 @@ class Panel extends React.Component { return (
{this._renderPlusPromoModal()} + {this._renderInsightsPromoModal()}
diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index 01cf55b04..81fec0993 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -246,6 +246,7 @@ class Summary extends React.Component { } } + /** * Calculates total tracker latency and sets it to state * @param {Object} props Summary's props, either this.props or nextProps. diff --git a/app/panel/constants/constants.js b/app/panel/constants/constants.js index 7d5a98342..4c4b2c1ef 100644 --- a/app/panel/constants/constants.js +++ b/app/panel/constants/constants.js @@ -18,6 +18,7 @@ export const SHOW_NOTIFICATION = 'SHOW_NOTIFICATION'; export const TOGGLE_CLIQZ_FEATURE = 'TOGGLE_CLIQZ_FEATURE'; export const CLOSE_NOTIFICATION = 'CLOSE_NOTIFICATION'; export const TOGGLE_EXPERT = 'TOGGLE_EXPERT'; +export const TOGGLE_INSIGHTS_MODAL = 'TOGGLE_INSIGHTS_MODAL'; // summary export const UPDATE_SUMMARY_DATA = 'UPDATE_SUMMARY_DATA'; diff --git a/app/panel/containers/InsightsPromoModalContainer.js b/app/panel/containers/InsightsPromoModalContainer.js new file mode 100644 index 000000000..2cfa8de58 --- /dev/null +++ b/app/panel/containers/InsightsPromoModalContainer.js @@ -0,0 +1,35 @@ +/** + * InsightsPromoModal 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 { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import InsightsPromoModal from '../components/InsightsPromoModal'; +import * as actions from '../actions/PanelActions'; // get shared actions from Panel + +/* + * 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(actions), dispatch) }); +/** + * Connects Login view component to the Redux store. + * @memberOf PanelContainers + * @param {function} mapStateToProps maps redux store state properties to Login view component own properties + * @param {function} mapDispatchToProps binds Login view component action creators + * @return {Object} A higher-order React component class that passes state and action + * creators into Login view component. Used by React framework. + */ +export default connect(null, mapDispatchToProps)(InsightsPromoModal); diff --git a/app/panel/containers/PanelContainer.js b/app/panel/containers/PanelContainer.js index ce4a744bb..2a4b9e007 100644 --- a/app/panel/containers/PanelContainer.js +++ b/app/panel/containers/PanelContainer.js @@ -27,7 +27,7 @@ import { updateBlockingData } from '../actions/BlockingActions'; * @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({}, state.panel, state.drawer, { +const mapStateToProps = state => Object.assign({}, state.panel, state.drawer, state.account, { paused_blocking: state.summary.paused_blocking, sitePolicy: state.summary.sitePolicy, trackerCounts: state.summary.trackerCounts, diff --git a/app/panel/reducers/panel.js b/app/panel/reducers/panel.js index b249b6e70..37b6f6e52 100644 --- a/app/panel/reducers/panel.js +++ b/app/panel/reducers/panel.js @@ -26,7 +26,8 @@ import { SET_OFFER_READ, TOGGLE_EXPANDED, SET_THEME, - CLEAR_THEME + CLEAR_THEME, + TOGGLE_INSIGHTS_MODAL } from '../constants/constants'; import { LOGIN_SUCCESS, @@ -60,6 +61,7 @@ const initialState = { email: '', emailValidated: false, current_theme: 'default', + isInsightsModalHidden: false, }; /** * Default export for panel view reducer. Handles actions @@ -260,6 +262,12 @@ export default (state = initialState, action) => { } return state; } + case TOGGLE_INSIGHTS_MODAL: { + return { + ...state, + isInsightsModalHidden: !state.isInsightsModalHidden + }; + } default: return state; } }; diff --git a/app/scss/panel.scss b/app/scss/panel.scss index f7e6e1952..3f6596ed4 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -51,6 +51,7 @@ html body { // Partial View SASS files @import './partials/_svgs'; +@import './partials/_shared_components_svgs'; @import './partials/_header'; @import './partials/_callout'; @import './partials/_summary'; @@ -73,6 +74,8 @@ html body { @import './partials/_subscribe'; @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'; diff --git a/app/scss/partials/_colors.scss b/app/scss/partials/_colors.scss index 213fa9cab..d52e7f0ca 100644 --- a/app/scss/partials/_colors.scss +++ b/app/scss/partials/_colors.scss @@ -37,6 +37,7 @@ $ghosty-blue: #00AEF0; $active-blue: #48ACD3; //top_nav_active_tab $link-blue: #2092BF; //primary-color $button-primary: #3AA2CF; +$dark-cyan-blue: #325e97; //insights modal border /* MARKETING COLORS */ $red: #E74055; diff --git a/app/scss/partials/_fonts.scss b/app/scss/partials/_fonts.scss index 919880895..c048be1ee 100644 --- a/app/scss/partials/_fonts.scss +++ b/app/scss/partials/_fonts.scss @@ -182,8 +182,45 @@ src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans-semibold-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; } - +/* roboto-300 - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: local('Roboto Light'), local('Roboto-Light'), url('../fonts/roboto-latin-bold-300.woff2') format('woff2'), +} +/* roboto-regular - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url('../fonts/roboto-all-charsets.woff2') format('woff2'), +} +/* roboto-500 - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url('../fonts/roboto-latin-bold-500.woff2') format('woff2'), +} +/* roboto-700 - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + src: local('Roboto Bold'), local('Roboto-Bold'), url('../fonts/roboto-latin-bold-700.woff2') format('woff2'), +} +/* roboto-900 - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + src: local('Roboto Black'), local('Roboto-Black'), url('../fonts/roboto-latin-bold-900.woff2') format('woff2'), +} +/* roboto-condensed-700 - latin */ @font-face { - font-family: Roboto; - src: local(Roboto), url(../fonts/roboto-all-charsets.woff2) format('woff2'), url(../fonts/roboto-all-charsets.woff) format('woff'); + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 700; + src: local('Roboto Condensed Bold'), local('RobotoCondensed-Bold'), url('../fonts/roboto-condensed-latin-bold-700.woff2') format('woff2'), /* Super Modern Browsers */ } diff --git a/app/scss/partials/_insights_promo_modal.scss b/app/scss/partials/_insights_promo_modal.scss new file mode 100644 index 000000000..670e9b275 --- /dev/null +++ b/app/scss/partials/_insights_promo_modal.scss @@ -0,0 +1,104 @@ +.InsightsModal__content { + background-color: $alabaster; + position: relative; + width: 518px; + min-height: 437px; + padding-top: 21px; + color: $tundora; + border: 2px solid $dark-cyan-blue; + z-index: 10; +} + +.InsightsModal__image { + height: 94px; + width: 177px; + margin-bottom: 16px; + background-image: url('/app/images/panel/insights-ribbon.svg'); +} + +.InsightsModal__header { + font-family: 'Roboto'; + height: 27.1px; + font-size: 20px; + font-weight: 900; + line-height: 1.35; + color: $tundora; + margin-bottom: 8.9px; +} + +.InsightsModal__description { + width: 372px; + height: 54.2px; + font-size: 18px; + font-weight: 500; + text-align: center; + margin-bottom: 12.8px; + font-family: 'Roboto'; +} + +.InsightsModal__features { + &:nth-child(odd) { + margin-left: 28px; + width: 260px; + } + &:nth-child(even) { + width: 229.8px; + } +} + +.InsightsModal__feature-text { + font-family: 'Roboto'; + font-size: 14px; + line-height: 39px; + color: $medium-gray; +} + +.InsightsModal__checked-circle-icon { + height: 18px; + width: 18px; + margin-right: 8px; + background-image: url('/app/images/panel/checked-circle-icon.svg'); +} + +.InsightsModal__call-to-action-container { + height: 107px; + width: 506px; + margin-top: 10px; + background-color: $mystic; +} + +.InsightsModal__call-to-action { + width: 176px; + height: 36px; + margin-top: 18px; + 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, #070e18, #1678a0); + font-size: 14px; + font-family: 'Roboto Condensed'; + letter-spacing: .5px; + color: $white; + line-height: 36px; + &:hover { + background-image: linear-gradient(101deg, #000004, #02648C); + color: $white; + } +} + +.InsightsModal__link { + font-family: 'Roboto'; + font-size: 15px; + color: $tundora; + &:hover { + cursor: pointer; + color: #090909; + } +} + +.InsightsModal__other-options-container { + margin-top: 23px; + padding: 0 10.5px; + text-decoration: underline; + font-size: 15px; + color: $tundora; +} diff --git a/app/scss/partials/_modal_exit_button.scss b/app/scss/partials/_modal_exit_button.scss new file mode 100644 index 000000000..955fdfaa1 --- /dev/null +++ b/app/scss/partials/_modal_exit_button.scss @@ -0,0 +1,37 @@ +/** + * Exit Button 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 + */ + +// Exit Button +.ModalExitButton__exit { + position: absolute; + top: -10px; + right: -10px; + width: 26px; + height: 26px; + border-radius: 15px; + border: solid 0.8px #325e97; + background-color: #f7f7f7; + @include transition(background-color 0.2s); +} +.ModalExitButton__exit:hover { + background-color: #efefef; + cursor: pointer; +} +.ModalExitButton__exitIcon { + height: 11px; + width: 11px; + margin: 0 auto; + background-repeat: no-repeat; + background-position: center center; + background-image: buildIconX(#4a4a4a); +} diff --git a/src/background.js b/src/background.js index c1f9b4683..0ee925611 100644 --- a/src/background.js +++ b/src/background.js @@ -1082,6 +1082,10 @@ function onMessageHandler(request, sender, callback) { promoModals.recordPlusPromoSighting(); return true; } + if (name === 'promoModals.sawInsightsPromo') { + promoModals.recordInsightsPromoSighting(); + return true; + } } /** diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index b15041a1b..fffaec688 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -115,6 +115,7 @@ class ConfData { _initProperty('hide_alert_trusted', false); _initProperty('ignore_first_party', true); _initProperty('import_callout_dismissed', true); + _initProperty('insights_promo_modal_last_seen', null); _initProperty('install_random_number', 0); _initProperty('install_date', 0); _initProperty('is_expanded', false); diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 145e9239e..71864c1d7 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -748,16 +748,24 @@ class Metrics { */ _recordEngaged() { const engaged_daily_velocity = conf.metrics.engaged_daily_velocity || []; - const today = Math.floor(Number(new Date().getTime()) / 86400000); + const engaged_daily_count = conf.metrics.engaged_daily_count || new Array(engaged_daily_velocity.length).fill(0); + + const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time + engaged_daily_velocity.sort(); if (!engaged_daily_velocity.includes(today)) { engaged_daily_velocity.push(today); + engaged_daily_count.push(1); if (engaged_daily_velocity.length > 7) { + engaged_daily_count.shift(); engaged_daily_velocity.shift(); } + } else { + engaged_daily_count[engaged_daily_velocity.indexOf(today)]++; } - conf.metrics.engaged_daily_velocity = engaged_daily_velocity; + conf.metrics.engaged_daily_count = engaged_daily_count; + conf.metrics.engaged_daily_velocity = engaged_daily_velocity; this._sendReq('engaged', ['daily', 'weekly', 'monthly']); } diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 0419551ba..4e005e1a5 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -366,6 +366,7 @@ class PanelData { is_expert, is_android: globals.BROWSER_INFO.os === 'android', language, + isTimeForInsightsPromo: promoModals.isTimeForInsightsPromo(), isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), haveSeenInitialPlusPromo: promoModals.haveSeenInitialPlusPromo(), reload_banner_status, diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index ddcd04ed2..c068b2c84 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -16,9 +16,11 @@ import globals from './Globals'; const DAYS_BETWEEN_PROMOS = { plus: globals.DEBUG ? 0.00025 : 30, + insights: globals.DEBUG ? 0.00025 : 30 }; 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 INSIGHTS = 'insights'; const PROMO_MODAL_LAST_SEEN = 'promo_modal_last_seen'; /** @@ -33,16 +35,27 @@ class PromoModals { static isTimeForAPlusPromo() { return this._isTimeForAPromo(PLUS); } + static isTimeForInsightsPromo() { return this._isTimeForAPromo(INSIGHTS); } + static recordPlusPromoSighting() { this._recordPromoSighting(PLUS); } - // TODO integrate the Insights promo modal into the "has it been long enough since last modal?" logic here + static recordInsightsPromoSighting() { this._recordPromoSighting(INSIGHTS); } + static _isTimeForAPromo(type) { - const lastSeenTime = conf[`${type}_${PROMO_MODAL_LAST_SEEN}`]; + if (conf.notify_promotions === false) { return false; } + + const lastSeenPlusPromo = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; + const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; + const lastSeenPromo = lastSeenPlusPromo > lastSeenInsightsPromo ? lastSeenPlusPromo : lastSeenInsightsPromo; + + if (lastSeenPromo === null) { return true; } - if (lastSeenTime === null) { return true; } + if (type === INSIGHTS && !this._hasEngagedFrequently()) { + return false; + } return ( - (Date.now() - lastSeenTime) > + (Date.now() - lastSeenPromo) > (MSECS_IN_DAY * DAYS_BETWEEN_PROMOS[type]) ); } @@ -50,6 +63,20 @@ class PromoModals { static _recordPromoSighting(type) { conf[`${type}_${PROMO_MODAL_LAST_SEEN}`] = Date.now(); } + + static _hasEngagedFrequently() { + const { engaged_daily_count } = conf.metrics || []; + const DAILY_TARGET = 3; + const WEEKLY_TARGET = 3; + + let very_engaged_days = 0; + engaged_daily_count.forEach((count) => { + very_engaged_days = count >= DAILY_TARGET ? ++very_engaged_days : very_engaged_days; + }); + if (very_engaged_days >= WEEKLY_TARGET) return true; + + return false; + } } // the class is simply a namespace for some static methods,