diff --git a/CHANGELOG.md b/CHANGELOG.md index 95582ba5d..d7ed025b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### GHOSTERY 8.4.6 (UNRELEASED) + ++ Modularize Rewards code (#462) + ### GHOSTERY 8.4.5 (November 20, 2019) + Remove unused telemetry from promo modals (#474) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9f34a79b1..8b51e6331 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -782,7 +782,7 @@ "message": "We seem to be having a technical issue. Try again later." }, "email_address_in_use": { - "message": "That email address is already in use. Please choose another. " + "message": "That email address is already in use. Please choose another. " }, "short_description": { "description": "A short description of this extension (Ghostery).", @@ -1659,7 +1659,7 @@ "message": "Account Successfully Created" }, "hub_create_account_toast_error": { - "message": "That email address is already in use. Please choose another." + "message": "That email address is already in use. Please choose another." }, "enable_when_paused": { "message": "To use this function, Resume Ghostery." @@ -1993,6 +1993,168 @@ "enhanced_anti_tracking": { "message": "Enhanced Anti-Tracking" }, + "panel_rewards_view__reward": { + "message": "Reward" + }, + "panel_rewards_view__rewards": { + "message": "Rewards" + }, + "OFFERS_BEGIN": { + "message": "" + }, + "myoffrz_affiliate_link": { + "message": "affiliate link" + }, + "myoffrz_copy_and_go": { + "message": "copy & go" + }, + "myoffrz_feedback_option1": { + "message": "I already used the offer" + }, + "myoffrz_feedback_option2": { + "message": "It's not a good deal" + }, + "myoffrz_feedback_option3": { + "message": "It's not relevant to me" + }, + "myoffrz_feedback_option4": { + "message": "Other reasons" + }, + "myoffrz_feedback_title": { + "message": "Feedback" + }, + "myoffrz_get_code": { + "message": "Get code" + }, + "myoffrz_offer_removed": { + "message": "This offer was removed." + }, + "myoffrz_optional": { + "message": "Optional" + }, + "myoffrz_powered_by_offrz": { + "message": "POWERED BY MyOffrz" + }, + "myoffrz_show_code": { + "message": "Show code" + }, + "myoffrz_show_more": { + "message": "Show more" + }, + "myoffrz_skip": { + "message": "Skip" + }, + "myoffrz_welcome_link": { + "message": "How It Works" + }, + "myoffrz_welcome_text": { + "message": "As you browse the Internet, we are searching for attractive offers based on your interests." + }, + "myoffrz_welcome_title": { + "message": "Hello!" + }, + "myoffrz_why_offers_text": { + "message": "No personal data leaves your device. Your browser automatically identifies what you might be interested in based on your previous use. " + }, + "myoffrz_why_see_these_offers": { + "message": "Why do I see these offers?" + }, + "myoffrz_why_see_this": { + "message": "Why do I see this?" + }, + "myoffrz_badge_text_new": { + "message": "NEW" + }, + "myoffrz_conditions": { + "message": "Conditions" + }, + "myoffrz_turnoff_rewards": { + "message": "Turn off Ghostery Rewards" + }, + "OFFERS_ALREADY_TRANSLATED": { + "message": "" + }, + "myoffrz_help": { + "message": "Help" + }, + "myoffrz_learn_more": { + "message": "Learn more" + }, + "myoffrz_send": { + "message": "Send" + }, + "myoffrz_settings": { + "message": "Settings" + }, + "myoffrz_tooltip_new_offer": { + "message": "New Reward discovered!" + }, + "myoffrz_turnoff_notification": { + "message": "Ghostery Rewards is now off. You can always turn it on in your Settings or the Rewards dashboard." + }, + "myoffrz_optin_description": { + "message": "Would you like to receive offers and discounts from trusted Ghostery partners?" + }, + "myoffrz_optin_title": { + "message": "New Reward discovered!" + }, + "myoffrz_no": { + "message": "no" + }, + "myoffrz_yes": { + "message": "yes" + }, + "myoffrz_rewards_new": { + "message": "New Reward discovered!" + }, + "myoffrz_apply_code": { + "message": "Apply code" + }, + "myoffrz_cancel": { + "message": "Cancel" + }, + "myoffrz_copied": { + "message": "Copied" + }, + "myoffrz_copy_code": { + "message": "Copy code" + }, + "myoffrz_expires_in_day": { + "message": "Available for 1 day" + }, + "myoffrz_expires_in_days": { + "message": "Available for $COUNT$ days", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "myoffrz_expires_in_hour": { + "message": "Available for 1 hour" + }, + "myoffrz_expires_in_hours": { + "message": "Available for $COUNT$ hours", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "myoffrz_expires_in_minute": { + "message": "Available for 1 minute" + }, + "myoffrz_expires_in_minutes": { + "message": "Available for $COUNT$ minutes", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "OFFERS_ENDS": { + "message": "" + }, "unknown": { "message": "Unknown" }, @@ -2183,5 +2345,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/content-scripts/rewards/HotDog.jsx b/app/content-scripts/rewards/HotDog.jsx deleted file mode 100644 index 365462653..000000000 --- a/app/content-scripts/rewards/HotDog.jsx +++ /dev/null @@ -1,84 +0,0 @@ -/** - * HotDog 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, { Component } from 'react'; -import { withRouter } from 'react-router-dom'; - -/** - * @class Create the Rewards "HotDog", aka UI element - * @memberOf RewardsContentScript - */ -class HotDog extends Component { - constructor(props) { - super(props); - this.state = { - closed: false - }; - this.shownSignal = false; - this.iframeEl = window.parent.document.getElementById('ghostery-iframe-container'); - - if (this.iframeEl) { - this.iframeEl.classList = ''; - this.iframeEl.classList.add('hot-dog'); - } - - this.ghostyStar = `url(${chrome.extension.getURL('app/images/rewards/ghosty-star.svg')})`; - this.closeIcon = `url(${chrome.extension.getURL('app/images/rewards/light-x.svg')})`; - this.close = this.close.bind(this); - this.navigate = this.navigate.bind(this); - } - - UNSAFE_componentWillReceiveProps(nextProps) { - if (nextProps.reward && nextProps.reward !== null && !this.shownSignal) { - this.props.actions.sendSignal('offer_notification_hotdog'); - this.shownSignal = true; - } - } - - navigate() { - this.props.actions.sendSignal('offer_click_hotdog'); - if (this.iframeEl) { - this.iframeEl.classList.add('offer-card'); - } - this.props.history.push('/offercard'); - } - - close() { - this.props.actions.sendSignal('offer_closed_hotdog'); - if (this.iframeEl) { - this.iframeEl.classList = ''; - } - this.setState({ - closed: true - }); - } - - render() { - return ( -
- { this.state.closed !== true && ( -
-
-
- {t('rewards_new_text')} -
-
-
-
- )} -
- ); - } -} - -export default withRouter(HotDog); diff --git a/app/content-scripts/rewards/Notification.jsx b/app/content-scripts/rewards/Notification.jsx deleted file mode 100644 index 9aaed0ffe..000000000 --- a/app/content-scripts/rewards/Notification.jsx +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Notification 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, { Component } from 'react'; - -/** - * @class Handles notifications for Rewards - * @memberOf RewardsContentScript - */ -class Notification extends Component { - constructor(props) { - super(props); - this.closeIcon = `url(${chrome.extension.getURL('app/images/rewards/white-x.svg')})`; - this.state = { - closed: false - }; - - this.closeNotification = this.closeNotification.bind(this); - } - - closeNotification(confirm) { - if (typeof this.props.data.closeCallback === 'function') { - this.props.data.closeCallback(confirm); - } - this.setState({ - closed: true - }); - } - - renderOptoutImage() { - return ( -
- -
- ); - } - - renderOptoutLink() { - return ( - this.props.data.textLink.callback()} - > - {this.props.data.textLink.text} - - ); - } - - renderHeadline() { - return ( -
- {t('rewards_first_prompt_headline')} -
- ); - } - - renderLabels() { - return ( -
- - {t('rewards_exclusive')} - - {t('rewards_best_offer')} -
- ); - } - - renderClose() { - return ( -
{ this.closeNotification(); }} - style={{ backgroundImage: this.closeIcon }} - /> - ); - } - - render() { - return ( -
- {!this.state.closed && ( -
-
-
-
- {this.props.data.type === 'first-prompt' && this.renderOptoutImage()} - {this.props.data.type !== 'first-prompt' && this.renderClose()} -
- {this.props.data.type === 'first-prompt' && this.renderLabels()} - {this.props.data.type === 'first-prompt' && this.renderHeadline()} - {this.props.data.message} - {' '} - {this.props.data.type === 'first-prompt' && this.renderOptoutLink()} -
- {this.props.data.buttons && ( -
- - -
- )} - {this.props.data.textLink && this.props.data.type !== 'first-prompt' - && ( - { - if (this.props.data.textLink.callback) { - this.props.data.textLink.callback(); - } - }} - > - {this.props.data.textLink.text} - - ) - } -
-
-
- )} -
- ); - } -} - -export default Notification; diff --git a/app/content-scripts/rewards/OfferCard.jsx b/app/content-scripts/rewards/OfferCard.jsx deleted file mode 100644 index c97e906e9..000000000 --- a/app/content-scripts/rewards/OfferCard.jsx +++ /dev/null @@ -1,361 +0,0 @@ -/** - * Offer Card 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, { Component } from 'react'; -import msgModule from '../utils/msg'; -import { computeTimeDelta } from '../../panel/utils/utils'; -import Notification from './Notification'; -import Settings from './Settings'; -import ClickOutside from '../../panel/components/BuildingBlocks/ClickOutside'; -import Tooltip from '../../panel/components/Tooltip'; - -const msg = msgModule('rewards'); -const { sendMessage } = msg; - -/** - * @class Generate Rewards offer card - * @memberOf RewardsContentScript - */ -class OfferCard extends Component { - constructor(props) { - super(props); - const { - attrs: { isCodeHidden } = {}, - offer_data: offerData = {}, - } = props.reward || {}; - const { ui_info: { template_data: templateData = {} } = {} } = offerData; - this.state = { - code: isCodeHidden ? '*****' : templateData.code, - closed: false, - copyText: t('rewards_copy_code'), - showPrompt: this.props.conf.rewardsPromptAccepted ? false : 1, - showSettings: false, - rewardUI: templateData, - shouldShowCross: this.props.conf.rewardsPromptAccepted, - }; - - this.iframeEl = window.parent.document.getElementById('ghostery-iframe-container'); - if (this.iframeEl) { - this.iframeContentDocument = this.iframeEl.contentDocument; - this.iframeEl.classList = ''; - this.iframeEl.classList.add('offer-card'); - } - this.rewardPictureEl = null; - - this.betaLogo = `url(${chrome.extension.getURL('app/images/rewards/ghostery-rewards-beta.png')})`; - this.closeIcon = `url(${chrome.extension.getURL('app/images/drawer/x.svg')})`; - this.ghostyGrey = `url(${chrome.extension.getURL('app/images/rewards/ghosty-grey.svg')})`; - this.kebabIcon = `url(${chrome.extension.getURL('app/images/rewards/settings-kebab.svg')})`; - this.poweredByMyoffrz = `url(${chrome.extension.getURL('app/images/rewards/powered-by-myoffrz.svg')})`; - - this.closeOfferCard = this.closeOfferCard.bind(this); - this.copyCode = this.copyCode.bind(this); - this.disableRewards = this.disableRewards.bind(this); - this.disableRewardsNotification = this.disableRewardsNotification.bind(this); - this.toggleSettings = this.toggleSettings.bind(this); - this.handleImageLoaded = this.handleImageLoaded.bind(this); - this.handlePrompt = this.handlePrompt.bind(this); - this.redeem = this.redeem.bind(this); - - this.notifications = [ - { - type: 'first-prompt', - buttons: true, - message: t('rewards_first_prompt_extended'), - textLink: { - href: 'https://www.ghostery.com/faqs/what-is-ghostery-rewards/', - text: t('learn_more'), - callback: () => { - this.props.actions.sendSignal('offer_first_learn'); - sendMessage('ping', 'rewards_first_learn_more'); - }, - }, - closeCallback: (option) => { this.handlePrompt(1, option); }, - }, - { - type: 'second-prompt', - buttons: true, - message: t('rewards_second_prompt'), - textLink: {}, - closeCallback: (option) => { this.handlePrompt(2, option); }, - }, - { - type: 'disabled-message', - buttons: false, - message: t('rewards_disable_notification'), - textLink: { - text: t('rewards_disable_confirm'), - callback: this.closeOfferCard, - }, - closeCallback: this.closeOfferCard, - }, - ]; - - const { reward } = props; - this.props.actions.messageBackground('rewardSeen', { - offerId: reward.offer_id - }); - this.props.actions.sendSignal('offer_shown'); - this.props.actions.sendSignal('offer_dsp_session'); - } - - componentDidMount() { - this.props.actions.addRewardSeenListener(); - if (this.state.rewardUI.picture_url) { - const bgImg = new Image(); - bgImg.onload = () => { - this.rewardPictureEl.style.backgroundImage = `url(${bgImg.src})`; - }; - bgImg.src = this.state.rewardUI.picture_url; - } - } - - copyCode() { - this.props.actions.sendSignal('code_copied'); - - // 'copied' feedback for user - this.setState({ - copyText: `${t('rewards_code_copied')}!`, - code: this.state.rewardUI.code, - }, () => { - this.offerCardRef.querySelector('.reward-code-input').select(); - document.execCommand('copy'); - }); - - // prevent multiple clicks - clearTimeout(this.timeout); - this.timeout = setTimeout(() => { - this.setState({ - copyText: t('rewards_copy_code') - }); - }, 3000); - } - - toggleSettings() { - if (!this.state.showSettings) { - this.props.actions.sendSignal('offer_settings'); - } - this.setState({ - showSettings: !this.state.showSettings - }); - } - - disableRewards() { - const signal = { - actionId: 'rewards_off', - origin: 'rewards-hotdog-card', - type: 'action-signal', - }; - sendMessage('setPanelData', { enable_offers: false, signal }); - // TODO catch - sendMessage('ping', 'rewards_off'); - } - - disableRewardsNotification() { - this.disableRewards(); - this.setState({ - showPrompt: 3 - }); - } - - handlePrompt(promptNumber, option) { - const reject = () => { - this.props.actions.sendSignal('offer_first_optout'); - sendMessage('ping', 'rewards_first_reject_optout'); - this.disableRewards(); - this.closeOfferCard(); - }; - if (promptNumber === 1) { - if (!option) { - reject(); - return; - } - this.props.actions.messageBackground('rewardsPromptOptedIn'); - this.props.actions.sendSignal('offer_first_optin'); - sendMessage('ping', 'rewards_first_accept'); - } else if (promptNumber === 2) { - if (option) { - reject(); - return; - } - this.props.actions.sendSignal('offer_first_optlater'); - sendMessage('ping', 'rewards_first_reject_optin'); - this.closeOfferCard(); - } - this.props.actions.messageBackground('rewardsPromptAccepted'); - this.setState({ - showPrompt: false, - shouldShowCross: true, - }); - } - - closeOfferCard() { - this.props.actions.removeFocusListener(); - if (this.iframeEl) { - this.iframeEl.classList = ''; - } - this.setState({ - closed: true - }); - } - - redeem() { - this.setState({ code: this.state.rewardUI.code }); - this.props.actions.sendSignal('offer_ca_action'); - } - - handleImageLoaded(e) { - e.target.classList.remove('hide'); - } - - renderNotification(type) { - const notificationProps = this.notifications[type]; - return ( - - ); - } - - renderExpiresText() { - const { expirationMs } = this.props.reward.offer_data; - const expireDays = Math.round((new Date()).setDate(new Date().getDate() + expirationMs / 1000 / 60 / 60 / 24)); - const delta = computeTimeDelta(new Date(expireDays), new Date()); - const { count, type } = delta; - if (count === 1) { - return t(`rewards_expires_in_${type.slice(0, -1)}`); - } - return t(`rewards_expires_in_${type}`, [count]); - } - - renderCross() { - return ( -
{ this.props.actions.sendSignal('offer_closed_card'); this.closeOfferCard(); }} - style={{ backgroundImage: this.closeIcon }} - /> - ); - } - - render() { - return ( - // @TODO condition for hide class -
{ this.offerCardRef = ref; }} className="ghostery-rewards-component"> - { this.state.closed !== true && ( -
-
-
-
- {this.state.shouldShowCross && this.renderCross()} -
-
-
-
-
- -
-
{ this.kebabRef = node; }} - /> - { this.state.showSettings && ( -
- - { this.props.actions.sendSignal('about_ghostery_rewards', false); }} disable={this.disableRewardsNotification} /> - -
- )} -
- { this.state.rewardUI.picture_url && ( -
-
-
{ this.rewardPictureEl = node; }} /> -
-
- )} -
- {/*
*/} -
- { this.state.rewardUI.benefit } -
- - { this.state.rewardUI.headline } - - - { this.state.rewardUI.desc } - -
-
- { this.state.rewardUI.code && ( -
-
- {this.state.code} - -
- {this.state.copyText} -
- )} -
- - { this.renderExpiresText() } - - {this.state.rewardUI.conditions && ( -
- { t('rewards_terms_conditions') } - -
- )} -
- - {this.state.rewardUI.call_to_action.text} - -
-
-
-
- {this.props.conf.rewardsPromptAccepted && - {t('rewards_disable')} - } -
-
- -
- -
-
- { this.state.showPrompt === 1 && - this.renderNotification(0) - } - { this.state.showPrompt === 2 && - this.renderNotification(1) - } - { this.state.showPrompt === 3 && - this.renderNotification(2) - } -
- )} -
- ); - } -} - -export default OfferCard; diff --git a/app/content-scripts/rewards/Settings.jsx b/app/content-scripts/rewards/Settings.jsx deleted file mode 100644 index 65c9524a6..000000000 --- a/app/content-scripts/rewards/Settings.jsx +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Settings 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, { Component } from 'react'; - -/** - * @class Handles settings for Rewards - * @memberOf RewardsContentScript - */ -class Settings extends Component { - constructor(props) { - super(props); - this.closeIcon = `url(${chrome.extension.getURL('app/images/rewards/white-x.svg')})`; - this.state = { - closed: false - }; - - this.close = this.close.bind(this); - } - - close() { - this.setState({ - closed: true - }); - if (typeof this.props.closeCallback === 'function') { - this.props.closeCallback(); - } - } - - render() { - return ( -
- {!this.state.closed && ( -
-
-
- {t('rewards_about')} -
{ this.close(); this.props.disable(); }}> - {t('rewards_disable')} -
-
- {/*
{t('rewards_settings')}
-
{t('rewards_delete')}
*/} -
-
- )} -
- ); - } -} - -export default Settings; diff --git a/app/content-scripts/rewards/index.jsx b/app/content-scripts/rewards/index.jsx deleted file mode 100644 index 0befce2bb..000000000 --- a/app/content-scripts/rewards/index.jsx +++ /dev/null @@ -1,249 +0,0 @@ -/** - * Ghostery Rewards - * - * 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 RewardsContentScript - */ - -/* eslint no-use-before-define: 0 */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Router, Route } from 'react-router-dom'; -import retargetEvents from 'react-shadow-dom-retarget-events'; -import HotDog from './HotDog'; -import OfferCard from './OfferCard'; -import msgModule from '../utils/msg'; -import history from '../../panel/utils/history'; -import globals from '../../../src/classes/Globals'; - -const msg = msgModule('rewards'); -const { sendMessage } = msg; -const { onMessage } = globals; -const channelsSupported = (typeof chrome.runtime.connect === 'function'); - -/** - * @class Injects Ghostery Rewards components - * @memberOf RewardsContentScript - */ -class RewardsApp { - constructor() { - this.reward = null; - this.conf = null; - this.rewardsContainer = document.createElement('div'); - this.rewardsApp = document.createElement('div'); - this.rewardsIframe = null; - this.iframeStyle = null; - this.port = null; - this.mainView = null; - this.rewardsApp.id = 'ghostery-rewards-app'; - this.rewardsApp.className = 'show'; - - this.handleMessages = this.handleMessages.bind(this); - this.sendSignal = this.sendSignal.bind(this); - this.messageBackground = this.messageBackground.bind(this); - this.removeFocusListener = this.removeFocusListener.bind(this); - this.focusListener = this.focusListener.bind(this); - this.addRewardSeenListener = this.addRewardSeenListener.bind(this); - - this.actions = { - sendSignal: this.sendSignal, - messageBackground: this.messageBackground, - removeFocusListener: this.removeFocusListener, - addRewardSeenListener: this.addRewardSeenListener - }; - } - - init() { - if (document.readyState === 'complete') { - this.start(); - } else { - document.onreadystatechange = () => { - if (document.readyState === 'complete') { - this.start(); - } - }; - } - } - - start() { - if (document.head.createShadowRoot || document.head.attachShadow) { - this.renderShadow(); - } else { - // use iframe to encapsulate CSS - fallback for everything else besides chrome - this.renderIframe(); - } - } - - renderReact() { - const MainView = this.mainView; - ReactDOM.render(, this.rewardsApp); - } - - renderShadow() { - // Use shadowDOM to encapsulate CSS - document.body.appendChild(this.rewardsContainer); - const shadowRoot = this.rewardsContainer.attachShadow({ mode: 'open' }); // Get the shadow root - shadowRoot.appendChild(this.rewardsApp); // Append React root to shadow root - retargetEvents(shadowRoot); // Reattach React click events - - this.mainView = props => ( - - - - } - /> - - } - /> - - } - /> - - ); - this.initListener(); - } - - renderIframe() { - this.rewardsIframe = document.createElement('iframe'); - this.rewardsIframe.id = 'ghostery-iframe-container'; - this.rewardsIframe.onload = () => { - this.iframeStyle = document.createElement('link'); - this.iframeStyle.rel = 'stylesheet'; - this.iframeStyle.type = 'text/css'; - this.iframeStyle.href = chrome.extension.getURL('dist/css/rewards_styles.css'); - - this.rewardsIframe.contentWindow.document.head.appendChild(this.iframeStyle); - this.rewardsContainer = this.rewardsIframe.contentWindow.document.body; - - this.rewardsApp.classList.add('iframe-child'); - this.rewardsContainer.appendChild(this.rewardsApp); - this.mainView = props => ( - -
- - } - /> - - } - /> - - } - /> -
-
- ); - this.initListener(); - }; - document.body.appendChild(this.rewardsIframe); - } - - initListener() { - if (channelsSupported) { - this.port = chrome.runtime.connect({ name: 'rewardsPort' }); - if (this.port) { - this.port.onMessage.addListener(this.handleMessages); - this.port.postMessage({ name: 'rewardsLoaded' }); - } - } else { - // TODO listen for this in background.js - sendMessage('rewardsLoaded'); - onMessage.addListener(this.handleMessages); - } - } - - handleMessages(request) { - if (request.name === 'showOffer' || request.name === 'showHotDog') { - this.reward = request.reward; - this.conf = request.conf; - } - - if (request.name === 'showOffer') { - history.push('/offercard'); - } - - // in FF 61 react sees some elements in iframes as non interactive - // react overwrites the event handlers for these elements with `function noop() {}` - // https://github.com/facebook/react/blob/16.4.2-dev/packages/react-dom/src/client/ReactDOMFiberComponent.js#L235 - // using setTimeout to render react is a temporary workaround - setTimeout(() => { - this.renderReact(); - }); - } - - focusListener() { - this.sendSignal('offer_shown'); - } - - addRewardSeenListener() { - window.addEventListener('focus', this.focusListener); - } - - removeFocusListener() { - window.removeEventListener('focus', this.focusListener); - } - - messageBackground(name, message, signal = false) { - if ( - signal && - this.port && - (message.actionId !== 'rewards_off') && - (message.actionId !== 'rewards_on') - ) { - this.port.postMessage({ - name, - message - }); - } else { - sendMessage(name, message); - } - } - - sendSignal(actionId, offerSignal = true) { - // Cliqz metrics - const offerId = offerSignal ? this.reward.offer_id : null; - const message = { - offerId, - actionId, - origin: 'rewards-hotdog-card', - type: !offerSignal ? 'action-signal' : 'offer-action-signal', - }; - this.messageBackground('rewardSignal', message, true); - } -} - -const rewardsApp = new RewardsApp(); -rewardsApp.init(); 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..45c933b20 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,37 +60,54 @@ 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'); } - window.open(`https://checkout.${DOMAIN}.com/plus?utm_source=gbe&utm_campaign=intro_hub`, '_blank'); - } + /** + * @private + * Function to handle clicks on the Midnight download button in the Premium promo modal + */ + _handleTryMidnightClick = () => { this._handlePremiumPromoModalClick('premium'); } _render() { const { justInstalled } = this.state; 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 +122,14 @@ class HomeViewContainer extends Component { isPlus, }; - const showPromoModal = !isPlus && !plus_promo_modal_shown; - return (
-
@@ -137,7 +152,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 +163,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 +172,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/images/rewards/best-offer.svg b/app/images/rewards/best-offer.svg deleted file mode 100644 index 13a0f4d8d..000000000 --- a/app/images/rewards/best-offer.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - best-offer-icon13x12 - Created with Sketch. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/images/rewards/exclusive.svg b/app/images/rewards/exclusive.svg deleted file mode 100644 index 61bb751d5..000000000 --- a/app/images/rewards/exclusive.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - exclusive-icon13x12 - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/images/rewards/ghostery-rewards-beta.png b/app/images/rewards/ghostery-rewards-beta.png deleted file mode 100644 index 929d71fc8..000000000 Binary files a/app/images/rewards/ghostery-rewards-beta.png and /dev/null differ diff --git a/app/images/rewards/ghostery_O.png b/app/images/rewards/ghostery_O.png deleted file mode 100644 index 40bb85692..000000000 Binary files a/app/images/rewards/ghostery_O.png and /dev/null differ diff --git a/app/images/rewards/light-x.svg b/app/images/rewards/light-x.svg deleted file mode 100644 index c7006483f..000000000 --- a/app/images/rewards/light-x.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/images/rewards/powered-by-myoffrz.svg b/app/images/rewards/powered-by-myoffrz.svg deleted file mode 100644 index 26f202dd8..000000000 --- a/app/images/rewards/powered-by-myoffrz.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - powered_by - Created with Sketch. - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/images/rewards/settings-kebab.svg b/app/images/rewards/settings-kebab.svg deleted file mode 100644 index bc9b11daa..000000000 --- a/app/images/rewards/settings-kebab.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/images/rewards/white-x.svg b/app/images/rewards/white-x.svg deleted file mode 100644 index 9dd5cd1a4..000000000 --- a/app/images/rewards/white-x.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/panel/actions/RewardsActions.js b/app/panel/actions/RewardsActions.js index 1c75bac48..a8d61933d 100644 --- a/app/panel/actions/RewardsActions.js +++ b/app/panel/actions/RewardsActions.js @@ -14,8 +14,6 @@ import { UPDATE_REWARDS_DATA, TOGGLE_OFFERS_ENABLED, - REMOVE_OFFER, - SET_OFFER_READ, SEND_SIGNAL } from '../constants/constants'; @@ -43,29 +41,6 @@ export function toggleOffersEnabled(enabled) { }; } -/** - * Removes a reward from the rewards list - * @param {String} id The ID of the reward we want to remove. - * @return {Object} - */ -export function removeOffer(id) { - return { - type: REMOVE_OFFER, - data: { id } - }; -} - -/** - * Sets the unread status of an offer to false - * @param {String} id the ID of the reward we want to update. - * @return {Object} - */ -export function setOfferRead(id) { - return { - type: SET_OFFER_READ, - data: { id } - }; -} // TODO the reducer calls getRewardMessage // determine whether it would be better to simply call getRewardMessage directly where sendSignal is called diff --git a/app/panel/actions/__tests__/RewardsActions.js b/app/panel/actions/__tests__/RewardsActions.js index b182350a3..eccf4fff9 100644 --- a/app/panel/actions/__tests__/RewardsActions.js +++ b/app/panel/actions/__tests__/RewardsActions.js @@ -18,8 +18,6 @@ import * as rewardsActions from '../RewardsActions'; import { UPDATE_REWARDS_DATA, TOGGLE_OFFERS_ENABLED, - REMOVE_OFFER, - SET_OFFER_READ, SEND_SIGNAL } from '../../constants/constants'; @@ -63,30 +61,6 @@ describe('app/panel/actions/RewardsActions.js', () => { expect(actions).toEqual([expectedPayload]); }); - test('removeOffer action should return correctly', () => { - const initialState = {}; - const store = mockStore(initialState); - - const id = 'test_reward_id'; - const expectedPayload = { data: { id }, type: REMOVE_OFFER }; - store.dispatch(rewardsActions.removeOffer(id)); - - const actions = store.getActions(); - expect(actions).toEqual([expectedPayload]); - }); - - test('setOfferRead action should return correctly', () => { - const initialState = {}; - const store = mockStore(initialState); - - const id = 'test_reward_id'; - const expectedPayload = { data: { id }, type: SET_OFFER_READ }; - store.dispatch(rewardsActions.setOfferRead(id)); - - const actions = store.getActions(); - expect(actions).toEqual([expectedPayload]); - }); - test('sendSignal offer-action-signal should return correctly', () => { const initialState = {}; const store = mockStore(initialState); diff --git a/app/panel/components/BuildingBlocks/RewardDetail.jsx b/app/panel/components/BuildingBlocks/RewardDetail.jsx deleted file mode 100644 index ec9e9b3a6..000000000 --- a/app/panel/components/BuildingBlocks/RewardDetail.jsx +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Reward Detail 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 { computeTimeDelta } from '../../utils/utils'; -import { sendMessage } from '../../utils/msg'; -import Tooltip from '../Tooltip'; - -/** - * @class Implements the details for a single reward for for the Rewards Panel - * @memberof PanelBuildingBlocks - */ -class RewardDetail extends React.Component { - constructor(props) { - super(props); - this.state = { - copyText: t('rewards_copy_code'), - code: props.isCodeHidden ? '*****' : props.code, - }; - - // Event Bindings - this.handleCopyClick = this.handleCopyClick.bind(this); - this.handleRedeemClick = this.handleRedeemClick.bind(this); - } - - /** - * Lifecycle event - */ - UNSAFE_componentWillMount() { - this.props.actions.setOfferRead(this.props.id); - } - - /** - * Handles clicking the copy button - */ - handleCopyClick() { - // Show a toast notification - this.props.actions.showNotification({ - text: t('rewards_code_copied_toast_notification'), - classes: 'purple', - }); - - // Update and reset Copy Code text - this.setState({ - code: this.props.code, - copyText: t('rewards_code_copied'), - }, () => { - // Copy the reward code - this.copyNode.querySelector('input').select(); - document.execCommand('copy'); - }); - setTimeout(() => { - this.setState({ copyText: t('rewards_copy_code') }); - }, 3000); - - // Send a signal to the offers black box - this.props.actions.sendSignal('code_copied', this.props.id); - } - - /** - * Handles clicking the redeem button - * @param {Object} event the event object - */ - handleRedeemClick(event) { - event.preventDefault(); - this.props.actions.sendSignal('offer_ca_action', this.props.id); - sendMessage('openNewTab', { - url: this.props.redeemUrl, - become_active: true, - }); - window.close(); - } - - /** - * Helper render function for the expires text. - * @return {JSX} JSX for the Rewards Detail - */ - renderExpiresText() { - const { expires } = this.props; - const delta = computeTimeDelta(new Date(expires), new Date()); - const { count, type } = delta; - if (count === 1) { - return t(`rewards_expires_in_${type.slice(0, -1)}`); - } - return t(`rewards_expires_in_${type}`, [count]); - } - - /** - * React's required render function. Returns JSX - * @return {JSX} JSX for rendering the details for a Reward - */ - render() { - const { - code, - text, - description, - benefit, - conditions, - pictureUrl, - redeemText - } = this.props; - - return ( -
-
-
-
- {benefit && ( -
- { benefit } -
- )} - {text && ( -
- { text } -
- )} - {description && ( -
- { description } -
- )} - {code && ( -
- { this.copyNode = node; }}> - {this.state.code} - - - - {this.state.copyText} - -
- )} -
-
- { this.renderExpiresText() } -
- {conditions && ( -
- - { t('rewards_terms_conditions') } - - -
- )} -
-
- { redeemText } -
-
- ); - } -} - -export default RewardDetail; diff --git a/app/panel/components/BuildingBlocks/RewardListItem.jsx b/app/panel/components/BuildingBlocks/RewardListItem.jsx deleted file mode 100644 index 398100889..000000000 --- a/app/panel/components/BuildingBlocks/RewardListItem.jsx +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Reward List Item 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 { Link } from 'react-router-dom'; -import ClassNames from 'classnames'; -import { computeTimeDelta } from '../../utils/utils'; - -/** - * @class Implements a single reward in a list for the Rewards Panel - * @memberof PanelBuildingBlocks - */ -class RewardListItem extends React.Component { - constructor(props) { - super(props); - this.state = {}; - - // Event Bindings - this.handleClick = this.handleClick.bind(this); - this.clickCloseButton = this.clickCloseButton.bind(this); - - // Send Signal - this.props.actions.sendSignal('offer_shown', this.props.id); - } - - /** - * Handle the click event: everything normal unless it is disabled - * @param {Object} event the click event - */ - handleClick(event) { - const { disabled, unread, id } = this.props; - if (disabled) { - event.preventDefault(); - } else { - if (unread) { - this.props.actions.sendSignal('offer_click_specific_new', id); - } - this.props.actions.sendSignal('offer_click_specific', id); - this.props.actions.sendSignal('offer_dsp_session', id); - } - } - - /** - * Handle clicking on the close button - * @param {Object} event the click event - */ - clickCloseButton(event) { - // Prevent the event from propagating and linking to the Reward Detail - event.preventDefault(); - event.stopPropagation(); - - const { id } = this.props; - this.props.actions.removeOffer(id); - this.props.actions.sendSignal('remove_offer_link', id); - } - - /** - * Helper render function for the expires text. - * @return {JSX} JSX for the Rewards Items List - */ - renderExpiresText() { - const { expires } = this.props; - const delta = computeTimeDelta(new Date(expires), new Date()); - const { count, type } = delta; - if (count === 1) { - return t(`rewards_expires_in_${type.slice(0, -1)}`); - } - return t(`rewards_expires_in_${type}`, [count]); - } - - /** - * React's required render function. Returns JSX - * @return {JSX} JSX for rendering a Reward within the Rewards List - */ - render() { - const { - id, - isLong, - text, - benefit, - unread, - disabled, - logoUrl, - } = this.props; - const itemClassName = ClassNames('RewardListItem', 'row', { - 'RewardListItem--greyscale': disabled, - 'RewardListItem--unread': unread, - 'RewardListItem--elongated': isLong, - 'not-clickable': disabled, - clickable: !disabled, - }); - - return ( - -
-
- -
-
-
{ benefit }
-
{ text }
-
{ this.renderExpiresText() }
-
-
-
- - - -
-
- - - -
-
-
- - ); - } -} - -export default RewardListItem; diff --git a/app/panel/components/BuildingBlocks/index.js b/app/panel/components/BuildingBlocks/index.js index 3cb86a8ca..edc925cbc 100644 --- a/app/panel/components/BuildingBlocks/index.js +++ b/app/panel/components/BuildingBlocks/index.js @@ -21,8 +21,6 @@ import GhosteryFeature from './GhosteryFeature'; import NotScanned from './NotScanned'; import PauseButton from './PauseButton'; import ToggleSlider from './ToggleSlider'; -import RewardDetail from './RewardDetail'; -import RewardListItem from './RewardListItem'; import ModalExitButton from './ModalExitButton'; export { @@ -33,7 +31,5 @@ export { NotScanned, PauseButton, ToggleSlider, - RewardDetail, - RewardListItem, ModalExitButton }; diff --git a/app/panel/components/Detail.jsx b/app/panel/components/Detail.jsx index 662cead77..dfb9bf494 100644 --- a/app/panel/components/Detail.jsx +++ b/app/panel/components/Detail.jsx @@ -63,20 +63,23 @@ class Detail extends React.Component { const condensedToggleClassNames = ClassNames('condensed-toggle', { condensed: this.props.is_expanded, }); - const { enable_offers, unread_offer_ids } = this.props; const activeTab = this.props.history.location.pathname.includes('rewards') ? 'rewards' : 'blocking'; + const contentDetailsClassNames = ClassNames({ + expanded: this.props.is_expanded, + rewardsView: activeTab === 'rewards', + }); return (
-
+
0} + hasReward={false} subscriptionsPlus={this.props.user && this.props.user.subscriptionsPlus} activeTab={activeTab} /> 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 45f5de0e4..97e67e271 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'; @@ -150,10 +149,6 @@ class Panel extends React.Component { reload: true }); } - - if (panel.enable_offers && panel.unread_offer_ids.length > 0) { - sendMessage('ping', 'engaged_offer'); - } } /** @@ -204,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({ @@ -222,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({ @@ -229,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, @@ -257,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) => { @@ -269,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', { @@ -280,57 +280,55 @@ 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 = () => { - const { loggedIn, user } = this.props; - - return loggedIn && (user && user.subscriptionsPlus); - } - + /** + * @returns {bool} + * @private + * Is the user an Insights subscriber? + */ _insightsSubscriber = () => { const { loggedIn, user } = this.props; return loggedIn && (user && user.scopes && user.scopes.includes('subscriptions:insights')); } - _renderPlusPromoModal = () => { - if (this._plusSubscriber() || this._insightsSubscriber()) 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 ( - - ); - } + /** + * @returns {JSX} + * @private + * Renders the Premium promo modal + */ + _renderPremiumPromoModal = () => { + 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; @@ -340,7 +338,7 @@ class Panel extends React.Component { return ( { const { promoModal, isPromoModalHidden, } = this.props; + console.error(`promoModal: ${promoModal}`); + console.error(`isPromoModalHidden: ${isPromoModalHidden}`); + if (isPromoModalHidden) return null; if (promoModal === 'insights') { 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/panel/components/Rewards.jsx b/app/panel/components/Rewards.jsx index 8ba483f27..3a36932bf 100644 --- a/app/panel/components/Rewards.jsx +++ b/app/panel/components/Rewards.jsx @@ -13,11 +13,12 @@ import React from 'react'; import ClassNames from 'classnames'; -import { Link, Route } from 'react-router-dom'; -import { ToggleSlider, RewardListItem, RewardDetail } from './BuildingBlocks'; +import { Route } from 'react-router-dom'; +import { ToggleSlider } from './BuildingBlocks'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import globals from '../../../src/classes/Globals'; +import { log } from '../../../src/utils/common'; const IS_CLIQZ = (globals.BROWSER_INFO.name === 'cliqz'); @@ -33,7 +34,10 @@ class Rewards extends React.Component { constructor(props) { super(props); this.state = { - rewardsArray: null, + iframeWidth: 0, + iframeHeight: 0, + shouldHideRewards: false, + rewardsCount: 0, }; // event bindings @@ -41,10 +45,13 @@ class Rewards extends React.Component { // helper render functions this.renderRewardListComponent = this.renderRewardListComponent.bind(this); - this.renderRewardDetailComponent = this.renderRewardDetailComponent.bind(this); - this.handleBackClick = this.handleBackClick.bind(this); this.handleFaqClick = this.handleFaqClick.bind(this); this.handlePortMessage = this.handlePortMessage.bind(this); + + + // myoffrz + this.iframe = React.createRef(); + this.handleMyoffrzMessage = this.handleMyoffrzMessage.bind(this); } /** @@ -53,66 +60,79 @@ class Rewards extends React.Component { componentDidMount() { this._dynamicUIPort = this.context; this._dynamicUIPort.onMessage.addListener(this.handlePortMessage); - this._dynamicUIPort.postMessage({ name: 'RewardsComponentDidMount' }); + window.addEventListener('message', this.handleMyoffrzMessage); + this._dynamicUIPort.postMessage({ name: 'RewardsComponentDidMount' }); this.props.actions.sendSignal('hub_open'); } - /** - * Lifecycle event - */ - UNSAFE_componentWillReceiveProps(nextProps) { - let rewardsArray = null; - if (nextProps.rewards) { - rewardsArray = Object.keys(nextProps.rewards).map((key) => { - const reward = nextProps.rewards[key].offer_data; - const { isCodeHidden } = nextProps.rewards[key].attrs || {}; - const createdTS = nextProps.rewards[key].createdTs; - return { - id: reward.offer_id, - unread: nextProps.unread_offer_ids.indexOf(reward.offer_id) !== -1, - code: reward.ui_info.template_data.code, - isCodeHidden, - text: reward.ui_info.template_data.title, - description: reward.ui_info.template_data.desc, - benefit: reward.ui_info.template_data.benefit, - conditions: reward.ui_info.template_data.conditions, - logo_url: reward.ui_info.template_data.logo_url, - picture_url: reward.ui_info.template_data.picture_url, - redeem_url: reward.ui_info.template_data.call_to_action.url, - redeem_text: reward.ui_info.template_data.call_to_action.text, - expires: new Date(createdTS + reward.expirationMs), - }; - }).filter(reward => reward.expires > Date.now()); - } - this.setState({ rewardsArray }); - } - /** * Lifecycle event */ componentWillUnmount() { /* @TODO send message to background to remove port onDisconnect event */ this.props.actions.sendSignal('hub_closed'); - this._dynamicUIPort.postMessage({ name: 'RewardsComponentWillUnmount' }); this._dynamicUIPort.onMessage.removeListener(this.handlePortMessage); + window.removeEventListener('message', this.handleMyoffrzMessage); } /** - * Handles message from the dynamic UI port to background - */ + * Handles message from the dynamic UI port to background + */ handlePortMessage(msg) { if (msg.to !== 'rewards' || !msg.body) { return; } + // msg.body can contain enable_offers prop this.props.actions.updateRewardsData(msg.body); } - /** - * Handles clicking the back button - */ - handleBackClick(offerId) { - this.props.actions.sendSignal('offer_return_hub', offerId); + iframeResize(data = {}) { + const { width = 0, height = 0 } = data; + this.setState({ iframeWidth: width, iframeHeight: height }); + } + + sendToIframe(message) { + this.iframe.current.contentWindow.postMessage(JSON.stringify({ + target: 'cliqz-offers-cc', + origin: 'window', + message, + }), '*'); + } + + myoffrzSendRuntimeMessage({ message, target }) { + chrome.runtime.sendMessage({ message, target }, (result = {}) => { + if (chrome.runtime.lastError) { + log('myoffrzSendRuntimeMessage, runtime.lastError', chrome.runtime.lastError); + return; + } + if (result.action !== 'pushData' || !this.iframe.current) { return; } + const { data: { vouchers = [] } = {} } = result; + const rewardsCount = vouchers.length; + this.setState({ shouldHideRewards: rewardsCount === 0, rewardsCount }); + this.iframe.current.frameBorder = 0; + this.sendToIframe(result); + }); + } + + handleMyoffrzMessage(msg = {}) { + let target; + let message; + try { + const parsedData = JSON.parse(msg.data || '{}'); + target = parsedData.target; + message = parsedData.message || {}; + } catch (e) { + // just silent return + return; + } + + if (target !== 'cliqz-offers-cc') { return; } + if (message.action === 'resize') { + this.iframeResize(message.data); + } else { + this.myoffrzSendRuntimeMessage({ message, target }); + } } /** @@ -150,33 +170,15 @@ class Rewards extends React.Component { * Helper render function for the Rewards Header * @return {JSX} JSX for the Rewards Header */ - renderRewardsHeader = (routeProps) => { - let reward; - const { id } = routeProps.match.params; - if (id && this.state.rewardsArray) { - reward = this.state.rewardsArray.find(el => el.id === id); - } - const { enable_offers, location } = this.props; - const showBack = location.pathname.indexOf('/detail/rewards/detail') !== -1; - const showToggle = location.pathname === '/detail/rewards/list'; - const headerClassNames = ClassNames('RewardsPanel__header', 'flex-container', 'align-middle', { - 'align-justify': !showBack, - }); - const headerTitleClassNames = ClassNames('RewardsPanel__title', { - 'RewardsPanel--left-padding': showBack, - }); + renderRewardsHeader = () => { + const { enable_offers } = this.props; + const headerClassNames = ClassNames('RewardsPanel__header', 'flex-container', 'align-middle', 'align-justify'); + const headerTitleClassNames = ClassNames('RewardsPanel__title'); return (
- {showBack && ( - { this.handleBackClick(id); }}> - - - - - )} { t('ghostery_rewards') } - {showToggle && !IS_CLIQZ && ( + {!IS_CLIQZ && ( {enable_offers ? t('rewards_on') : t('rewards_off')} @@ -188,9 +190,6 @@ class Rewards extends React.Component { /> )} - {!showToggle && reward && - - }
); } @@ -209,92 +208,76 @@ class Rewards extends React.Component { ); } + renderCLIQZtext() { + return ( +
+ { this.renderRewardSvg() } +
{ t('panel_detail_rewards_cliqz_text') }
+
+
+ { t('panel_detail_learn_more') } +
+
+ ); + } + + renderRewardsTurnoffText() { + return ( +
+ { this.renderRewardSvg() } +
{ t('panel_detail_rewards_off') }
+
+ ); + } + + renderRewardsNoneFoundText() { + return ( +
+ { this.renderRewardSvg() } +
{ t('panel_detail_rewards_none_found') }
+
+ ); + } + /** * Helper render function for the list of Rewards Items * @return {JSX} JSX for the Rewards Items List */ renderRewardListComponent() { - const { actions, enable_offers, is_expanded } = this.props; - const { rewardsArray } = this.state; + if (IS_CLIQZ) { return this.renderCLIQZtext(); } + const { enable_offers, is_expanded } = this.props; + if (!enable_offers) { return this.renderRewardsTurnoffText(); } - if (IS_CLIQZ) { - return ( -
- { this.renderRewardSvg() } -
{ t('panel_detail_rewards_cliqz_text') }
-
-
- { t('learn_more') } -
-
- ); - } - if (enable_offers && !rewardsArray) { - return ( -
- { this.renderRewardSvg() } -
{ t('panel_detail_rewards_loading') }
-
- ); - } - if (enable_offers && rewardsArray.length === 0) { - return ( -
- { this.renderRewardSvg() } -
{ t('panel_detail_rewards_none_found') }
-
- ); - } - if (!enable_offers && (!rewardsArray || rewardsArray.length === 0)) { - return ( -
- { this.renderRewardSvg() } -
{ t('panel_detail_rewards_off') }
-
- ); - } - - const rewardsList = rewardsArray.map((reward, index) => ( - - )); - return
{ rewardsList }
; - } + const { + shouldHideRewards, + iframeWidth, + iframeHeight, + rewardsCount, + } = this.state; + if (shouldHideRewards) { return this.renderRewardsNoneFoundText(); } - /** - * Helper render function for an individual Reward Item - * @return {JSX} JSX for the Rewards Detail Item - */ - renderRewardDetailComponent(routeProps) { - const { id } = routeProps.match.params; - const reward = this.state.rewardsArray.find(el => el.id === id); + const src = chrome.runtime.getURL('cliqz/offers-cc/index.html?cross-origin'); + const text = t(`panel_rewards_view__reward${rewardsCount === 1 ? '' : 's'}`); return ( - + <> + {is_expanded && ( +
+
{rewardsCount}
+
{text}
+
+ )} +