From 7a1ca5548fc7fef38dd1586714976b87bcc499de Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Fri, 31 Jul 2020 11:20:20 -0400 Subject: [PATCH 01/35] bump browser core --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 56eb33e5d..d8264c86d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "dependencies": { "@cliqz/adblocker-circumvention": "^1.12.2", "@cliqz/url-parser": "^1.1.3", - "browser-core": "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.1/browser-core-7.47.1.tgz", + "browser-core": "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.2/browser-core-7.47.2.tgz", "classnames": "^2.2.5", "d3": "^5.16.0", "foundation-sites": "^6.6.2", diff --git a/yarn.lock b/yarn.lock index fb45e82a7..73390ca35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1741,9 +1741,9 @@ brorand@^1.0.1: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= -"browser-core@https://github.com/cliqz-oss/browser-core/releases/download/v7.47.1/browser-core-7.47.1.tgz": - version "7.47.1" - resolved "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.1/browser-core-7.47.1.tgz#283601947231aed807dd4cd40366c552c0d9ccfe" +"browser-core@https://github.com/cliqz-oss/browser-core/releases/download/v7.47.2/browser-core-7.47.2.tgz": + version "7.47.2" + resolved "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.2/browser-core-7.47.2.tgz#cdd67904a05d06e0fec314dc1f758602ad56085e" dependencies: "@cliqz-oss/dexie" "^2.0.4" "@cliqz/adblocker-webextension" "^1.14.2" From 4b3e1920ed71387fdc85153487b8dafeb73fc230 Mon Sep 17 00:00:00 2001 From: Christopher Tino <4699516+christophertino@users.noreply.github.com> Date: Mon, 10 Aug 2020 12:38:10 -0400 Subject: [PATCH 02/35] Updated Firefox Android extension (#587) * add linting for no-param-reassign and fix resulting linting errors * add linting for prefer-object-spread and fix resulting linting errors * add linting for no-restricted-syntax and fix 1/2 of resulting errors * add linting for no-prototype-builtins and fix resulting linting errors * add linting for class-methods-use-this and fix most resulting errors. Add /tools and /tests to linter. Update height in UpgradeBanner * finish linting for class-methods-use-this * add linting for no-mixed-operators and fix resulting linting errors * add linting for import/prefer-default-export and fix resulting linting errors * add linting for react/no-access-state-in-setstate and fix resulting linting errors * add linting for react/jsx-props-no-spreading and fix resulting linting errors * finish linting errors for no-restricted-syntax. 1 remains: couldn't resolve removing iterator loops for urlSearchParams * Fix linting errors resulting from the merge with develop * Refactor UNSAFE_componentWillMount into either constructor or componentDidMount, leave notes for how decision was made. * Refactor UNSAFE_componentWillReceiveProps to componentDidUpdate or getDerivedStateFromProps * re-enable lint exception for no-prototype-builtins and revert calls back to hasOwnProperty * add single line exception for no-restricted-syntax linting rule * add linting for react/destructuring-assignment and fix errors. ToDo: test code and check for errors * Fix minor bugs * Fix General Settings last updated text * rework linting rule no-param-reassign to have more exceptions and param object destructuring * Remove file and line linting exceptions. * re-add linting rule react/sort-comp and fix resulting errors * remove added linting exception consistent-return and fix resulting errors * remove added linting expression no-use-before-define and fix resulting errors. fix BugDB.js bug. * Fix linting error * fix minor bugs * Code cleanup: fix PromoModal imports * remove unnecessary hasOwnProperty calls after refactored for...in loops * Fix missing strings bug * Fix last remaining string bug * Begin work on a global object for debugging. * Add accountEvents array to Ghostery Debug for logging login/out events * Add way to output window.GHOSTERY for the user * prevent error when not signed in * Promises don't reject, just give important data to user * Download DebugInfo as a JSON file. * Update panel so it can be opened in a seperate tab * Update panel-android Tabs. Non-happy path tests needed. * Refactor the Panel Android React app * Panel Android OverviewTab more complete * More work on PanelAndroid I * More work on PanelAndroid II * More work on PanelAndroid III * More work on PanelAndroid IV * More work on PanelAndroid: BlockingTracker and BlockingCategory need final touches. More tests needed. * PanelAndroid update all components complete. Need to fix bugs and add new features * Clean up PanelAndroid * Add Account and Settings route to PanelAndroid * Add Account and Settings to PanelAndroid. * Rework OverviewTab and add tests * Add actions to PanelAndroid Settings * Update BlockingTracker to render in a List compoent to better performance * Update PausedButton actions to work with dropdown * Update path to cliqz trackerdb. Update compyright year for touched files in this PR * Fix linting error * Add CliqzFeatures to PanelAndroid TrackerList * Fix linting and testing errors * Add unknown category in tracker list * sort unknown trackers and hide category when there are no trackers * Add Help and About to PanelAndroid Settings Menu. UI Tweak for Settings and make clicking RadioButton Labels switch Radios. * Hide account icon, try to unhide tab label * Update tests * fix android icons and bump FF min version * remove console debugging code * clean up hub components on Android * fixing issues with unknown trackers on android * update snapshots * Fix broken tests * update hub tutorial images for android * Remove PanelAndroid account code. Replace with Hub/Account-Web * Update tests * add support for new Ghostery android UA string * Add Import/Export to PanelAndroid. Need actions implemented * wire up unknown tracker whitelisting * fix issue where block/allow unknown trackers did not persist on android * Hide ImportExport, add tests for some Settings * Add NeedsReload message to PanelAndroid * Fix bug on BlockingTracker * Add tests for components used in PanelAndroid Settings * fix linting error * Update GeneralSettings test to not depend on MomentJS * fix linting error * Implement Import/Export feature * fix android UA detection * fix hw opt-in for android browser * fix humanweb opt-in for hub on android * clean up annotation and naming for android * refactor getBrowserInfo check * ternary cleanup Co-authored-by: Caleb Richelson --- _locales/en/messages.json | 45 ++ app/content-scripts/notifications.js | 13 +- app/hub/Views/HomeView/HomeView.jsx | 9 +- app/hub/Views/SetupView/SetupView.jsx | 2 +- .../Views/SetupView/SetupViewContainer.jsx | 5 +- .../__snapshots__/SetupView.test.jsx.snap | 10 +- .../SetupViewContainer.test.jsx.snap | 10 +- .../SetupAntiSuiteViewContainer.jsx | 5 +- .../SideNavigationViewContainer.jsx | 7 +- app/hub/Views/TutorialView/TutorialView.jsx | 6 +- app/hub/Views/TutorialView/TutorialView.scss | 11 +- .../TutorialView/TutorialViewContainer.jsx | 6 +- .../__snapshots__/TutorialView.test.jsx.snap | 2 +- .../TutorialViewContainer.test.jsx.snap | 12 +- .../TutorialAntiSuiteView.jsx | 29 +- .../TutorialAntiSuiteViewContainer.jsx | 3 +- .../__test__/TutorialAntiSuiteView.test.jsx | 4 +- .../TutorialAntiSuiteView.test.jsx.snap | 20 +- .../TutorialBlockingView.jsx | 29 +- .../TutorialBlockingViewContainer.jsx | 3 +- .../__tests__/TutorialBlockingView.test.jsx | 4 +- .../TutorialBlockingView.test.jsx.snap | 20 +- .../TutorialLayoutView/TutorialLayoutView.jsx | 13 +- .../TutorialLayoutViewContainer.jsx | 3 +- .../__tests__/TutorialLayoutView.test.jsx | 4 +- .../TutorialLayoutView.test.jsx.snap | 2 +- .../TutorialTrackerListView.jsx | 9 +- .../TutorialTrackerListViewContainer.jsx | 4 +- .../TutorialTrackerListView.test.jsx | 4 +- .../TutorialTrustView/TutorialTrustView.jsx | 29 +- .../TutorialTrustViewContainer.jsx | 3 +- .../__tests__/TutorialTrustView.test.jsx | 4 +- .../TutorialTrustView.test.jsx.snap | 20 +- .../UpgradePlanView/UpgradePlanView.scss | 1 + .../hub/tutorial/antisuite-simple-android.png | Bin 0 -> 41151 bytes .../tutorial/blocking-detailed-android.png | Bin 0 -> 57832 bytes .../hub/tutorial/layout-detailed-android.png | Bin 0 -> 53753 bytes .../hub/tutorial/layout-simple-android.png | Bin 0 -> 40643 bytes .../hub/tutorial/tracker-list-android.png | Bin 0 -> 55296 bytes .../tutorial/trustrestrict-simple-android.png | Bin 0 -> 41104 bytes .../panel-android/categories/unknown.svg | 7 + app/images/panel/checkbox-checked.svg | 2 +- app/images/panel/checkbox-disabled.svg | 2 +- app/images/panel/checkbox.svg | 2 +- .../{trackerActions.js => blockingActions.js} | 68 ++- app/panel-android/actions/handler.js | 45 +- app/panel-android/actions/settingsActions.js | 186 +++++++ app/panel-android/actions/summaryActions.js | 67 ++- .../components/GlobalTrackers.jsx | 89 --- app/panel-android/components/Overview.jsx | 144 ----- app/panel-android/components/Panel.jsx | 190 ------- app/panel-android/components/PanelAndroid.jsx | 288 ++++++++++ app/panel-android/components/SiteTrackers.jsx | 75 --- .../components/content/Accordion.jsx | 269 --------- .../components/content/Accordions.jsx | 83 --- .../components/content/BlockingCategories.jsx | 97 ++++ .../components/content/BlockingCategory.jsx | 292 ++++++++++ .../components/content/BlockingTab.jsx | 134 +++++ .../components/content/BlockingTracker.jsx | 421 +++++++++++++++ .../components/content/ChartSVG.jsx | 77 --- .../components/content/DotsMenu.jsx | 56 +- .../components/content/FixedMenu.jsx | 147 ----- .../components/content/MenuItem.jsx | 105 ---- .../components/content/OverviewTab.jsx | 431 +++++++++++++++ app/panel-android/components/content/Path.jsx | 98 ---- .../components/content/Settings.jsx | 324 +++++++++++ app/panel-android/components/content/Tab.jsx | 26 +- app/panel-android/components/content/Tabs.jsx | 40 +- .../components/content/TrackerItem.jsx | 194 ------- .../components/content/TrackersChart.jsx | 57 -- .../content/__tests__/BlockingCategories.jsx | 253 +++++++++ .../content/__tests__/BlockingCategory.jsx | 368 +++++++++++++ .../content/__tests__/BlockingTab.jsx | 102 ++++ .../content/__tests__/BlockingTracker.jsx | 282 ++++++++++ .../components/content/__tests__/DotsMenu.jsx | 104 ++++ .../content/__tests__/OverviewTab.jsx | 155 ++++++ .../components/content/__tests__/Tabs.jsx | 92 ++++ .../__snapshots__/BlockingCategories.jsx.snap | 243 +++++++++ .../__snapshots__/BlockingCategory.jsx.snap | 451 ++++++++++++++++ .../__snapshots__/BlockingTab.jsx.snap | 396 ++++++++++++++ .../__snapshots__/BlockingTracker.jsx.snap | 320 +++++++++++ .../__tests__/__snapshots__/DotsMenu.jsx.snap | 65 +++ .../__snapshots__/OverviewTab.jsx.snap | 509 ++++++++++++++++++ .../__tests__/__snapshots__/Tabs.jsx.snap | 51 ++ app/panel-android/index.jsx | 8 +- app/panel-android/utils/chart.js | 46 -- app/panel-android/utils/tracker-info.js | 15 +- .../BuildingBlocks/CliqzFeature.jsx | 30 +- .../components/BuildingBlocks/DonutGraph.jsx | 19 +- .../BuildingBlocks/GhosteryFeature.jsx | 18 +- .../components/BuildingBlocks/NotScanned.jsx | 11 +- .../components/BuildingBlocks/PauseButton.jsx | 21 +- .../BuildingBlocks/RadioButtonGroup.jsx | 8 +- .../BuildingBlocks/__tests__/CliqzFeature.jsx | 171 ++++++ .../BuildingBlocks/__tests__/DonutGraph.jsx | 137 +++++ .../__tests__/GhosteryFeature.jsx | 195 +++++++ .../BuildingBlocks/__tests__/NotScanned.jsx | 39 ++ .../__tests__/PauseButton.jsx | 42 +- .../__snapshots__/CliqzFeature.jsx.snap | 178 ++++++ .../__snapshots__/DonutGraph.jsx.snap | 132 +++++ .../__snapshots__/GhosteryFeature.jsx.snap | 100 ++++ .../__snapshots__/NotScanned.jsx.snap | 45 ++ .../__snapshots__/PauseButton.jsx.snap | 0 app/panel/components/Header.jsx | 10 +- app/panel/components/Panel.jsx | 7 +- app/panel/components/Settings/Account.jsx | 49 +- app/panel/components/Settings/AdBlocker.jsx | 9 +- .../components/Settings/GeneralSettings.jsx | 23 +- .../components/Settings/ImportExport.jsx | 86 +++ .../components/Settings/Notifications.jsx | 16 +- app/panel/components/Settings/OptIn.jsx | 17 +- .../components/Settings/TrustAndRestrict.jsx | 25 +- .../Settings/__tests__/AdBlocker.jsx | 95 ++++ .../Settings/__tests__/GeneralSettings.jsx | 113 ++++ .../Settings/__tests__/ImportExport.jsx | 148 +++++ .../Settings/__tests__/Notifications.jsx | 93 ++++ .../components/Settings/__tests__/OptIn.jsx | 77 +++ .../Settings/__tests__/TrustAndRestrict.jsx | 8 +- .../__snapshots__/AdBlocker.jsx.snap | 280 ++++++++++ .../__snapshots__/GeneralSettings.jsx.snap | 462 ++++++++++++++++ .../__snapshots__/ImportExport.jsx.snap | 184 +++++++ .../__snapshots__/Notifications.jsx.snap | 355 ++++++++++++ .../__tests__/__snapshots__/OptIn.jsx.snap | 235 ++++++++ .../__snapshots__/TrustAndRestrict.jsx.snap | 36 +- app/panel/components/__tests__/About.jsx | 27 + app/panel/components/__tests__/Help.jsx | 32 ++ .../__tests__/__snapshots__/About.jsx.snap | 74 +++ .../__tests__/__snapshots__/Help.jsx.snap | 69 +++ app/panel/utils/msg.js | 10 + app/scss/android/_blocking_tab.scss | 381 +++++++++++++ app/scss/android/_dots_menu.scss | 45 ++ app/scss/android/_landscape.scss | 16 + app/scss/android/_mixins.scss | 180 +++++++ .../android/{content => }/_normalize.scss | 6 +- app/scss/android/_overview.scss | 104 ---- app/scss/android/_overview_tab.scss | 136 +++++ app/scss/android/_settings.scss | 159 ++++++ app/scss/android/_tabs.scss | 48 ++ app/scss/android/_variables.scss | 33 ++ app/scss/android/content/_accordions.scss | 303 ----------- app/scss/android/content/_dots-menu.scss | 63 --- app/scss/android/content/_fixed-menu.scss | 212 -------- app/scss/android/content/_landscape.scss | 63 --- app/scss/android/content/_mixins.scss | 69 --- app/scss/android/content/_tabs.scss | 58 -- app/scss/android/content/_trackers-chart.scss | 106 ---- app/scss/android/content/_variables.scss | 31 -- app/scss/hub.scss | 1 + app/scss/panel.scss | 4 +- app/scss/panel_android.scss | 64 ++- app/scss/partials/_header.scss | 12 +- app/scss/partials/_pause_button.scss | 3 +- app/scss/partials/_settings.scss | 4 +- app/scss/partials/_subscribe.scss | 5 +- app/scss/partials/_summary.scss | 4 +- .../ForgotPassword/ForgotPassword.jsx | 88 +-- .../ForgotPassword/ForgotPassword.scss | 6 +- manifest.json | 6 +- package.json | 1 + src/background.js | 66 ++- src/classes/Account.js | 2 +- src/classes/BrowserButton.js | 4 +- src/classes/Cliqz.js | 3 +- src/classes/ConfData.js | 5 +- src/classes/Globals.js | 23 + src/classes/PanelData.js | 27 +- src/utils/cliqzModulesData.js | 4 +- webpack.config.js | 2 +- yarn.lock | 20 + 169 files changed, 10891 insertions(+), 2989 deletions(-) create mode 100644 app/images/hub/tutorial/antisuite-simple-android.png create mode 100644 app/images/hub/tutorial/blocking-detailed-android.png create mode 100644 app/images/hub/tutorial/layout-detailed-android.png create mode 100644 app/images/hub/tutorial/layout-simple-android.png create mode 100644 app/images/hub/tutorial/tracker-list-android.png create mode 100644 app/images/hub/tutorial/trustrestrict-simple-android.png create mode 100644 app/images/panel-android/categories/unknown.svg rename app/panel-android/actions/{trackerActions.js => blockingActions.js} (84%) create mode 100644 app/panel-android/actions/settingsActions.js delete mode 100644 app/panel-android/components/GlobalTrackers.jsx delete mode 100644 app/panel-android/components/Overview.jsx delete mode 100644 app/panel-android/components/Panel.jsx create mode 100644 app/panel-android/components/PanelAndroid.jsx delete mode 100644 app/panel-android/components/SiteTrackers.jsx delete mode 100644 app/panel-android/components/content/Accordion.jsx delete mode 100644 app/panel-android/components/content/Accordions.jsx create mode 100644 app/panel-android/components/content/BlockingCategories.jsx create mode 100644 app/panel-android/components/content/BlockingCategory.jsx create mode 100644 app/panel-android/components/content/BlockingTab.jsx create mode 100644 app/panel-android/components/content/BlockingTracker.jsx delete mode 100644 app/panel-android/components/content/ChartSVG.jsx delete mode 100644 app/panel-android/components/content/FixedMenu.jsx delete mode 100644 app/panel-android/components/content/MenuItem.jsx create mode 100644 app/panel-android/components/content/OverviewTab.jsx delete mode 100644 app/panel-android/components/content/Path.jsx create mode 100644 app/panel-android/components/content/Settings.jsx delete mode 100644 app/panel-android/components/content/TrackerItem.jsx delete mode 100644 app/panel-android/components/content/TrackersChart.jsx create mode 100644 app/panel-android/components/content/__tests__/BlockingCategories.jsx create mode 100644 app/panel-android/components/content/__tests__/BlockingCategory.jsx create mode 100644 app/panel-android/components/content/__tests__/BlockingTab.jsx create mode 100644 app/panel-android/components/content/__tests__/BlockingTracker.jsx create mode 100644 app/panel-android/components/content/__tests__/DotsMenu.jsx create mode 100644 app/panel-android/components/content/__tests__/OverviewTab.jsx create mode 100644 app/panel-android/components/content/__tests__/Tabs.jsx create mode 100644 app/panel-android/components/content/__tests__/__snapshots__/BlockingCategories.jsx.snap create mode 100644 app/panel-android/components/content/__tests__/__snapshots__/BlockingCategory.jsx.snap create mode 100644 app/panel-android/components/content/__tests__/__snapshots__/BlockingTab.jsx.snap create mode 100644 app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap create mode 100644 app/panel-android/components/content/__tests__/__snapshots__/DotsMenu.jsx.snap create mode 100644 app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap create mode 100644 app/panel-android/components/content/__tests__/__snapshots__/Tabs.jsx.snap delete mode 100644 app/panel-android/utils/chart.js create mode 100644 app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx create mode 100644 app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx create mode 100644 app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx create mode 100644 app/panel/components/BuildingBlocks/__tests__/NotScanned.jsx rename app/panel/components/{ => BuildingBlocks}/__tests__/PauseButton.jsx (82%) create mode 100644 app/panel/components/BuildingBlocks/__tests__/__snapshots__/CliqzFeature.jsx.snap create mode 100644 app/panel/components/BuildingBlocks/__tests__/__snapshots__/DonutGraph.jsx.snap create mode 100644 app/panel/components/BuildingBlocks/__tests__/__snapshots__/GhosteryFeature.jsx.snap create mode 100644 app/panel/components/BuildingBlocks/__tests__/__snapshots__/NotScanned.jsx.snap rename app/panel/components/{ => BuildingBlocks}/__tests__/__snapshots__/PauseButton.jsx.snap (100%) create mode 100644 app/panel/components/Settings/ImportExport.jsx create mode 100644 app/panel/components/Settings/__tests__/AdBlocker.jsx create mode 100644 app/panel/components/Settings/__tests__/GeneralSettings.jsx create mode 100644 app/panel/components/Settings/__tests__/ImportExport.jsx create mode 100644 app/panel/components/Settings/__tests__/Notifications.jsx create mode 100644 app/panel/components/Settings/__tests__/OptIn.jsx create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/AdBlocker.jsx.snap create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/ImportExport.jsx.snap create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/Notifications.jsx.snap create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap create mode 100644 app/panel/components/__tests__/About.jsx create mode 100644 app/panel/components/__tests__/Help.jsx create mode 100644 app/panel/components/__tests__/__snapshots__/About.jsx.snap create mode 100644 app/panel/components/__tests__/__snapshots__/Help.jsx.snap create mode 100644 app/scss/android/_blocking_tab.scss create mode 100644 app/scss/android/_dots_menu.scss create mode 100644 app/scss/android/_landscape.scss create mode 100644 app/scss/android/_mixins.scss rename app/scss/android/{content => }/_normalize.scss (79%) delete mode 100644 app/scss/android/_overview.scss create mode 100644 app/scss/android/_overview_tab.scss create mode 100644 app/scss/android/_settings.scss create mode 100644 app/scss/android/_tabs.scss create mode 100644 app/scss/android/_variables.scss delete mode 100644 app/scss/android/content/_accordions.scss delete mode 100644 app/scss/android/content/_dots-menu.scss delete mode 100644 app/scss/android/content/_fixed-menu.scss delete mode 100644 app/scss/android/content/_landscape.scss delete mode 100644 app/scss/android/content/_mixins.scss delete mode 100644 app/scss/android/content/_tabs.scss delete mode 100644 app/scss/android/content/_trackers-chart.scss delete mode 100644 app/scss/android/content/_variables.scss diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b3f1807f1..25a272d39 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Account" }, + "settings_import_export": { + "message": "Import & Export Settings" + }, "settings_trackers": { "message": "Trackers" }, @@ -1234,6 +1237,48 @@ } } }, + "android_tab_overview": { + "message": "Overview" + }, + "android_tab_site_blocking": { + "message": "Site Blocking" + }, + "android_tab_global_blocking": { + "message": "Global Blocking" + }, + "android_site_blocking_header": { + "message": "Trackers on this site" + }, + "android_global_blocking_header": { + "message": "Global Tracking" + }, + "android_blocking_reset": { + "message": "Reset Settings" + }, + "android_block": { + "message": "Block" + }, + "android_unblock": { + "message": "Unblock" + }, + "android_restrict": { + "message": "Restrict" + }, + "android_unrestrict": { + "message": "Undo" + }, + "android_trust": { + "message": "Trust" + }, + "android_untrust": { + "message": "Undo" + }, + "android_anonymize": { + "message": "Anonymize" + }, + "android_anonymized": { + "message": "Anonymized" + }, "hub_side_navigation_home": { "message": "Home" }, diff --git a/app/content-scripts/notifications.js b/app/content-scripts/notifications.js index 375e28caa..c9ecc7c56 100644 --- a/app/content-scripts/notifications.js +++ b/app/content-scripts/notifications.js @@ -1,5 +1,5 @@ /** - * Ghostery NotificationsContentScript + * Ghostery Notifications Content Script * * This file provides notification alerts for the CMP, update dialogs * and import/export functionality @@ -7,7 +7,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -607,10 +607,12 @@ const NotificationsContentScript = (function(win, doc) { * @memberOf NotificationsContentScript * @package */ - const exportFile = function(content) { + const exportFile = function(content, type) { const textFileAsBlob = new Blob([content], { type: 'text/plain' }); + const ext = type === 'Ghostery-Backup' ? 'ghost' : 'json'; const d = new Date(); - const fileNameToSaveAs = `Ghostery-Backup-${d.getMonth() + 1}-${d.getDate()}-${d.getFullYear()}.ghost`; + const dStr = `${d.getMonth() + 1}-${d.getDate()}-${d.getFullYear()}`; + const fileNameToSaveAs = `${type}-${dStr}.${ext}`; let url = ''; if (window.URL) { url = window.URL.createObjectURL(textFileAsBlob); @@ -764,7 +766,8 @@ const NotificationsContentScript = (function(win, doc) { } else if (name === 'onFileImported') { updateBrowseWindow(message); } else if (name === 'exportFile') { - exportFile(message); + const { content, type } = message; + exportFile(content, type); } // trigger a response callback to src/background so that we can handle errors properly diff --git a/app/hub/Views/HomeView/HomeView.jsx b/app/hub/Views/HomeView/HomeView.jsx index 91727d0b1..8f911fd88 100644 --- a/app/hub/Views/HomeView/HomeView.jsx +++ b/app/hub/Views/HomeView/HomeView.jsx @@ -18,8 +18,7 @@ import { NavLink } from 'react-router-dom'; import globals from '../../../../src/classes/Globals'; import { ToggleCheckbox } from '../../../shared-components'; -const { IS_CLIQZ } = globals; -const IS_FIREFOX = (globals.BROWSER_INFO.name === 'firefox'); +const { IS_CLIQZ, BROWSER_INFO } = globals; /** * A Functional React component for rendering the Home View @@ -40,10 +39,10 @@ const HomeView = (props) => { const accountHref = globals.ACCOUNT_BASE_URL; let headerInfoText = t('hub_home_header_info'); - if (globals.BROWSER_INFO) { - if (IS_FIREFOX) { + if (BROWSER_INFO) { + if (BROWSER_INFO.name === 'firefox') { headerInfoText = t('hub_home_header_info_opted_out'); - } else if (IS_CLIQZ) { + } else if (IS_CLIQZ || BROWSER_INFO.name === 'ghostery_android') { headerInfoText = t('hub_home_header_info_cliqz'); } } diff --git a/app/hub/Views/SetupView/SetupView.jsx b/app/hub/Views/SetupView/SetupView.jsx index 8db394a37..f70cecabd 100644 --- a/app/hub/Views/SetupView/SetupView.jsx +++ b/app/hub/Views/SetupView/SetupView.jsx @@ -28,7 +28,7 @@ const SetupView = (props) => { const { extraRoutes, sendMountActions, steps } = props; return ( -
+
{steps.map(step => (
{ - const { sendMountActions, steps } = props; + const { sendMountActions, steps, isAndroid } = props; return ( -
+
{steps.map(step => ( } + render={() => } /> ))}
diff --git a/app/hub/Views/TutorialView/TutorialView.scss b/app/hub/Views/TutorialView/TutorialView.scss index ae47ef2ec..eebb3226e 100644 --- a/app/hub/Views/TutorialView/TutorialView.scss +++ b/app/hub/Views/TutorialView/TutorialView.scss @@ -149,8 +149,17 @@ left: 1%; padding-right: 12%; margin-bottom: 30px; + @media only screen and (max-width: 740px) { + left: auto; + padding-right: 0; + } + } + &.layout-detailed { + left: -11%; + @media only screen and (max-width: 740px) { + left: auto; + } } - &.layout-detailed { left: -11%; } } @media only screen and (max-width: 960px) { .TutorialLayoutView .TutorialView__tagline { diff --git a/app/hub/Views/TutorialView/TutorialViewContainer.jsx b/app/hub/Views/TutorialView/TutorialViewContainer.jsx index f42b3985f..db683ea04 100644 --- a/app/hub/Views/TutorialView/TutorialViewContainer.jsx +++ b/app/hub/Views/TutorialView/TutorialViewContainer.jsx @@ -14,6 +14,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import TutorialView from './TutorialView'; +import globals from '../../../../src/classes/Globals'; // Component Views import TutorialVideoView from '../TutorialViews/TutorialVideoView'; @@ -23,6 +24,9 @@ import TutorialLayoutView from '../TutorialViews/TutorialLayoutView'; import TutorialTrustView from '../TutorialViews/TutorialTrustView'; import TutorialAntiSuiteView from '../TutorialViews/TutorialAntiSuiteView'; +const { BROWSER_INFO } = globals; +const IS_ANDROID = (BROWSER_INFO.os === 'android'); + /** * @class Implement the Tutorial View for the Ghostery Hub * @extends Component @@ -88,7 +92,7 @@ class TutorialViewContainer extends Component { }, ]; - return ; + return ; } } diff --git a/app/hub/Views/TutorialView/__tests__/__snapshots__/TutorialView.test.jsx.snap b/app/hub/Views/TutorialView/__tests__/__snapshots__/TutorialView.test.jsx.snap index 99bd3af03..b26c5aca7 100644 --- a/app/hub/Views/TutorialView/__tests__/__snapshots__/TutorialView.test.jsx.snap +++ b/app/hub/Views/TutorialView/__tests__/__snapshots__/TutorialView.test.jsx.snap @@ -2,7 +2,7 @@ exports[`app/hub/Views/TutorialView component Snapshot tests with react-test-renderer tutorial view is rendered correctly 1`] = `
( +const TutorialAntiSuiteView = ({ isAndroid }) => (
@@ -26,17 +27,21 @@ const TutorialAntiSuiteView = () => (
{t('simple_view')} -
- {t('detailed_view')} -
- {t('detailed_view')} + { !isAndroid && ( +
+
+ {t('detailed_view')} +
+ {t('detailed_view')} +
+ )}
@@ -82,6 +87,8 @@ const TutorialAntiSuiteView = () => (
); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialAntiSuiteView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialAntiSuiteView; diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx index 5313f9b07..0f322785c 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx @@ -51,7 +51,8 @@ class TutorialAntiSuiteViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Anti Suite View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/TutorialAntiSuiteView.test.jsx b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/TutorialAntiSuiteView.test.jsx index 6976c7b7d..caeb77ac8 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/TutorialAntiSuiteView.test.jsx +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/TutorialAntiSuiteView.test.jsx @@ -19,14 +19,14 @@ import TutorialAntiSuiteView from '../TutorialAntiSuiteView'; describe('app/hub/Views/TutorialViews/TutorialAntiSuiteView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('tutorial anti-suite view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialAntiSuiteView').length).toBe(1); expect(component.find('img').length).toBe(2); expect(component.find('.TutorialView__keyItem').length).toBe(3); diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap index 83a6fdc3e..bed91e7ce 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap @@ -17,16 +17,18 @@ exports[`app/hub/Views/TutorialViews/TutorialAntiSuiteView component Snapshot te className="TutorialAntiSuiteView__image antisuite-simple" src="/app/images/hub/tutorial/antisuite-simple.png" /> -
- detailed_view +
+
+ detailed_view +
+ detailed_view
- detailed_view
( +const TutorialBlockingView = ({ isAndroid }) => (
@@ -26,17 +27,21 @@ const TutorialBlockingView = () => (
{t('detailed_view')} -
- {t('hub_tutorial_detailed_expanded_view')} -
- {t('hub_tutorial_detailed_expanded_view')} + { !isAndroid && ( +
+
+ {t('hub_tutorial_detailed_expanded_view')} +
+ {t('hub_tutorial_detailed_expanded_view')} +
+ )}
@@ -60,6 +65,8 @@ const TutorialBlockingView = () => (
); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialBlockingView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialBlockingView; diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx index 5210ca461..856309f8a 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx @@ -44,7 +44,8 @@ class TutorialBlockingViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Blocking View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/TutorialBlockingView.test.jsx b/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/TutorialBlockingView.test.jsx index f15ea7611..5720f6348 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/TutorialBlockingView.test.jsx +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/TutorialBlockingView.test.jsx @@ -19,14 +19,14 @@ import TutorialBlockingView from '../TutorialBlockingView'; describe('app/hub/Views/TutorialViews/TutorialBlockingView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('tutorial blocking view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialBlockingView').length).toBe(1); expect(component.find('.TutorialBlockingView__image').length).toBe(2); expect(component.find('.TutorialView__keyText').length).toBe(3); diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap index f314105f1..6a430821b 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap @@ -17,16 +17,18 @@ exports[`app/hub/Views/TutorialViews/TutorialBlockingView component Snapshot tes className="TutorialBlockingView__image blocking-detailed" src="/app/images/hub/tutorial/blocking-detailed.png" /> -
- hub_tutorial_detailed_expanded_view +
+
+ hub_tutorial_detailed_expanded_view +
+ hub_tutorial_detailed_expanded_view
- hub_tutorial_detailed_expanded_view
( +const TutorialLayoutView = ({ isAndroid }) => (
-
+
{t('simple_view')} {t('detailed_view')}
@@ -43,6 +44,8 @@ const TutorialLayoutView = () => (
); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialLayoutView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialLayoutView; diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx index 59c5f0208..384bcfaa5 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx @@ -44,7 +44,8 @@ class TutorialLayoutViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Layout View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/TutorialLayoutView.test.jsx b/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/TutorialLayoutView.test.jsx index ea3bf45f5..af2a562ca 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/TutorialLayoutView.test.jsx +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/TutorialLayoutView.test.jsx @@ -19,14 +19,14 @@ import TutorialLayoutView from '../TutorialLayoutView'; describe('app/hub/Views/TutorialViews/TutorialLayoutView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('tutorial layout view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialLayoutView').length).toBe(1); expect(component.find('.TutorialLayoutView__image').length).toBe(2); }); diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/__snapshots__/TutorialLayoutView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/__snapshots__/TutorialLayoutView.test.jsx.snap index 1b7dcb048..48c74fe4e 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/__snapshots__/TutorialLayoutView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/__snapshots__/TutorialLayoutView.test.jsx.snap @@ -5,7 +5,7 @@ exports[`app/hub/Views/TutorialViews/TutorialLayoutView component Snapshot tests className="TutorialLayoutView TutorialView--mediumFlexColumn row align-center-middle flex-container" >
simple_view ( +const TutorialTrackerListView = ({ isAndroid }) => (
{t('hub_tutorial_trackerlist_title')}
@@ -38,6 +39,8 @@ const TutorialTrackerListView = () => (
); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialTrackerListView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialTrackerListView; diff --git a/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx index 9aeab240b..dfe809712 100644 --- a/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx @@ -44,13 +44,15 @@ class TutorialTrackerListViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Tracker List View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } // PropTypes ensure we pass required props of the correct type TutorialTrackerListViewContainer.propTypes = { index: PropTypes.number.isRequired, + isAndroid: PropTypes.bool.isRequired, actions: PropTypes.shape({ setTutorialNavigation: PropTypes.func.isRequired, }).isRequired, diff --git a/app/hub/Views/TutorialViews/TutorialTrackerListView/__tests__/TutorialTrackerListView.test.jsx b/app/hub/Views/TutorialViews/TutorialTrackerListView/__tests__/TutorialTrackerListView.test.jsx index 93662c432..c31fde285 100644 --- a/app/hub/Views/TutorialViews/TutorialTrackerListView/__tests__/TutorialTrackerListView.test.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrackerListView/__tests__/TutorialTrackerListView.test.jsx @@ -19,14 +19,14 @@ import TutorialTrackerListView from '../TutorialTrackerListView'; describe('app/hub/Views/TutorialViews/TutorialTrackerListView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('tutorial tracker list view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialTrackerListView').length).toBe(1); expect(component.find('.TutorialTrackerListView__image').length).toBe(1); }); diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx index 605aaa9cb..d477d66fb 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx @@ -12,13 +12,14 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; /** * A Functional React component for rendering the Tutorial Trust and Restrict View * @return {JSX} JSX for rendering the Tutorial Trust and Restrict View of the Hub app * @memberof HubComponents */ -const TutorialTrustView = () => ( +const TutorialTrustView = ({ isAndroid }) => (
@@ -26,17 +27,21 @@ const TutorialTrustView = () => (
{t('simple_view')} -
- {t('detailed_view')} -
- {t('detailed_view')} + { !isAndroid && ( +
+
+ {t('detailed_view')} +
+ {t('detailed_view')} +
+ )}
@@ -63,6 +68,8 @@ const TutorialTrustView = () => (
); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialTrustView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialTrustView; diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx index 42d8e341e..949d361af 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx @@ -44,7 +44,8 @@ class TutorialTrustViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Trust View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/TutorialTrustView.test.jsx b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/TutorialTrustView.test.jsx index 92c050bfa..2cf8e90ec 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/TutorialTrustView.test.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/TutorialTrustView.test.jsx @@ -19,14 +19,14 @@ import TutorialTrustView from '../TutorialTrustView'; describe('app/hub/Views/TutorialViews/TutorialTrustView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('tutorial trust view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialTrustView').length).toBe(1); expect(component.find('.TutorialTrustView__image').length).toBe(2); expect(component.find('.TutorialTrustView__key').length).toBe(1); diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap index 27e9a3973..f34f2f7cd 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap @@ -17,16 +17,18 @@ exports[`app/hub/Views/TutorialViews/TutorialTrustView component Snapshot tests className="TutorialTrustView__image trustrestrict-simple" src="/app/images/hub/tutorial/trustrestrict-simple.png" /> -
- detailed_view +
+
+ detailed_view +
+ detailed_view
- detailed_view
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

k|J?LYIKG=CntU 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 literal 0 HcmV?d00001 diff --git a/app/images/hub/tutorial/blocking-detailed-android.png b/app/images/hub/tutorial/blocking-detailed-android.png new file mode 100644 index 0000000000000000000000000000000000000000..c4b03659a401292152caf385e6b3ebff799dbdf8 GIT binary patch literal 57832 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

k|J?LYIKG=CntU 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𝔡jM$ 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

Ns<1aELA2<%{9E<3%*Y3=C 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 + + + + + + diff --git a/app/images/panel/checkbox-checked.svg b/app/images/panel/checkbox-checked.svg index cc7188ab5..c2f21d1f5 100644 --- a/app/images/panel/checkbox-checked.svg +++ b/app/images/panel/checkbox-checked.svg @@ -1,4 +1,4 @@ - + diff --git a/app/images/panel/checkbox-disabled.svg b/app/images/panel/checkbox-disabled.svg index c3ca2c2a9..7817ebb24 100644 --- a/app/images/panel/checkbox-disabled.svg +++ b/app/images/panel/checkbox-disabled.svg @@ -1,4 +1,4 @@ - + diff --git a/app/images/panel/checkbox.svg b/app/images/panel/checkbox.svg index c1dd4321a..9dd8b477f 100644 --- a/app/images/panel/checkbox.svg +++ b/app/images/panel/checkbox.svg @@ -1,4 +1,4 @@ - + diff --git a/app/panel-android/actions/trackerActions.js b/app/panel-android/actions/blockingActions.js similarity index 84% rename from app/panel-android/actions/trackerActions.js rename to app/panel-android/actions/blockingActions.js index f5ea7cf84..4ff46c5fe 100644 --- a/app/panel-android/actions/trackerActions.js +++ b/app/panel-android/actions/blockingActions.js @@ -1,10 +1,10 @@ /** - * Tracker Action creators + * Blocking Action creators * * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -68,6 +68,70 @@ function calculateDelta(oldState, newState) { return 0; } +// Modified version of _updateCliqzModuleWhitelist from app/panel/reducers/blocking.js +export function anonymizeSiteTracker({ actionData, state }) { + const updatedcliqzModuleData = JSON.parse(JSON.stringify(state.cliqzModuleData)); + const { antiTracking, adBlock } = state.cliqzModuleData; + const whitelistedUrls = { ...antiTracking.whitelistedUrls, ...adBlock.whitelistedUrls }; + const { unknownTracker } = actionData; + const { pageHost } = state.summary; + + const addToWhitelist = () => { + unknownTracker.sources.forEach((domain) => { + if (whitelistedUrls.hasOwnProperty(domain)) { + whitelistedUrls[domain].name = unknownTracker.name; + whitelistedUrls[domain].hosts.push(pageHost); + } else { + whitelistedUrls[domain] = { + name: unknownTracker.name, + hosts: [pageHost], + }; + } + }); + }; + + const removeFromWhitelist = (domain) => { + if (!whitelistedUrls[domain]) { return; } + + whitelistedUrls[domain].hosts = whitelistedUrls[domain].hosts.filter(hostUrl => ( + hostUrl !== pageHost + )); + + if (whitelistedUrls[domain].hosts.length === 0) { + delete whitelistedUrls[domain]; + } + }; + + if (unknownTracker.whitelisted) { + unknownTracker.sources.forEach(removeFromWhitelist); + + Object.keys(whitelistedUrls).forEach((domain) => { + if (whitelistedUrls[domain].name === unknownTracker.name) { + removeFromWhitelist(domain); + } + }); + } else { + addToWhitelist(); + } + + // Update Ad Blocking trackers + updatedcliqzModuleData.adBlock.unknownTrackers.forEach((trackerEl) => { + if (trackerEl.name === unknownTracker.name) { + trackerEl.whitelisted = !trackerEl.whitelisted; + } + }); + // Update Anti-Tracking trackers + updatedcliqzModuleData.antiTracking.unknownTrackers.forEach((trackerEl) => { + if (trackerEl.name === unknownTracker.name) { + trackerEl.whitelisted = !trackerEl.whitelisted; + } + }); + sendMessage('setPanelData', { cliqz_module_whitelist: whitelistedUrls }); + return { + cliqzModuleData: updatedcliqzModuleData, + }; +} + export function trustRestrictBlockSiteTracker({ actionData, state }) { const { blocking, summary, settings } = state; const { pageHost } = summary; diff --git a/app/panel-android/actions/handler.js b/app/panel-android/actions/handler.js index f04da2a7a..09d1aca99 100644 --- a/app/panel-android/actions/handler.js +++ b/app/panel-android/actions/handler.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,11 +12,14 @@ */ import { - handleTrustButtonClick, handleRestrictButtonClick, handlePauseButtonClick, cliqzFeatureToggle + updateSitePolicy, handleTrustButtonClick, handleRestrictButtonClick, handlePauseButtonClick, cliqzFeatureToggle } from './summaryActions'; import { - trustRestrictBlockSiteTracker, blockUnblockGlobalTracker, blockUnBlockAllTrackers, resetSettings -} from './trackerActions'; + anonymizeSiteTracker, trustRestrictBlockSiteTracker, blockUnblockGlobalTracker, blockUnBlockAllTrackers, resetSettings +} from './blockingActions'; +import { + updateDatabase, updateSettingCheckbox, selectItem, exportSettings, importSettingsDialog, importSettingsNative +} from './settingsActions'; // Handle all actions in Panel.jsx export default function handleAllActions({ actionName, actionData, state }) { @@ -32,7 +35,7 @@ export default function handleAllActions({ actionName, actionData, state }) { break; case 'handlePauseButtonClick': - updated = handlePauseButtonClick({ state }); + updated = handlePauseButtonClick({ actionData, state }); break; case 'cliqzFeatureToggle': @@ -43,6 +46,10 @@ export default function handleAllActions({ actionName, actionData, state }) { updated = trustRestrictBlockSiteTracker({ actionData, state }); break; + case 'anonymizeSiteTracker': + updated = anonymizeSiteTracker({ actionData, state }); + break; + case 'blockUnblockGlobalTracker': updated = blockUnblockGlobalTracker({ actionData, state }); break; @@ -55,6 +62,34 @@ export default function handleAllActions({ actionName, actionData, state }) { updated = resetSettings({ state }); break; + case 'updateSitePolicy': + updated = updateSitePolicy({ actionData, state }); + break; + + case 'updateDatabase': + updated = updateDatabase({ actionData, state }); + break; + + case 'updateSettingCheckbox': + updated = updateSettingCheckbox({ actionData, state }); + break; + + case 'selectItem': + updated = selectItem({ actionData, state }); + break; + + case 'exportSettings': + updated = exportSettings({ actionData, state }); + break; + + case 'importSettingsDialog': + updated = importSettingsDialog({ actionData, state }); + break; + + case 'importSettingsNative': + updated = importSettingsNative({ actionData, state }); + break; + default: updated = {}; } diff --git a/app/panel-android/actions/settingsActions.js b/app/panel-android/actions/settingsActions.js new file mode 100644 index 000000000..c850cc94e --- /dev/null +++ b/app/panel-android/actions/settingsActions.js @@ -0,0 +1,186 @@ +/** + * Tracker Action creators + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import moment from 'moment/min/moment-with-locales.min'; +import { sendMessage, sendMessageInPromise } from '../../panel/utils/msg'; +import { hashCode } from '../../../src/utils/common'; + +// Function taken from app/content-scripts/notifications.js +function _saveToFile({ content, type }) { + const textFileAsBlob = new Blob([content], { type: 'text/plain' }); + const ext = type === 'Ghostery-Backup' ? 'ghost' : 'json'; + const d = new Date(); + const dStr = `${d.getMonth() + 1}-${d.getDate()}-${d.getFullYear()}`; + const fileNameToSaveAs = `${type}-${dStr}.${ext}`; + let url = ''; + if (window.URL) { + url = window.URL.createObjectURL(textFileAsBlob); + } else { + url = window.webkitURL.createObjectURL(textFileAsBlob); + } + + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', fileNameToSaveAs); + document.body.appendChild(link); + link.click(); +} + +function _importFromFile(fileToLoad) { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.onload = (fileLoadedEvent) => { + try { + const backup = JSON.parse(fileLoadedEvent.target.result); + if (backup.hash !== hashCode(JSON.stringify(backup.settings))) { + throw new Error('Invalid hash'); + } + const settings = (backup.settings || {}).conf || {}; + resolve(settings); + } catch (err) { + reject(err); + } + }; + fileReader.readAsText(fileToLoad, 'UTF-8'); + }); +} + +function _chooseFile() { + return new Promise((resolve) => { + const inputEl = document.createElement('input'); + inputEl.type = 'file'; + inputEl.addEventListener('change', () => { + resolve(inputEl.files[0]); + }); + inputEl.click(); + }); +} + +export function updateDatabase() { + // Send Message to Background + return sendMessageInPromise('update_database').then((result) => { + let resultText; + if (result && result.success === true) { + if (result.updated === true) { + resultText = t('settings_update_success'); + } else { + resultText = t('settings_update_up_to_date'); + } + } else { + resultText = t('settings_update_failed'); + } + + // Update State for PanelAndroid UI + return { + settings: { + dbUpdateText: resultText, + ...result.confData, + } + }; + }); +} + +export function updateSettingCheckbox({ actionData }) { + const { name, checked } = actionData; + const updatedState = {}; + + if (name === 'trackers_banner_status' || name === 'reload_banner_status') { + updatedState.panel = { [name]: checked }; + } else if (name === 'toggle_individual_trackers') { + updatedState.blocking = { [name]: checked }; + updatedState.settings = { [name]: checked }; + } else { + updatedState.settings = { [name]: checked }; + } + + // Send Message to Background + sendMessage('setPanelData', { [name]: checked }); + + // Update State for PanelAndroid UI + return updatedState; +} + +export function selectItem({ actionData }) { + const { event, value } = actionData; + + // Send Message to Background + sendMessage('setPanelData', { [event]: value }); + + // Update State for PanelAndroid UI + return { + settings: { + [event]: value, + }, + }; +} + +export function exportSettings({ state }) { + return sendMessageInPromise('getAndroidSettingsForExport').then((result) => { + const { needsReload } = state; + const settings_last_exported = Number((new Date()).getTime()); + const exportResultText = `${t('settings_export_success')} ${moment(settings_last_exported).format('LLL')}`; + + _saveToFile(result); + + return { + needsReload, + settings: { + actionSuccess: true, + settings_last_exported, + exportResultText, + } + }; + }); +} + +export function importSettingsNative({ actionData, state }) { + return new Promise((resolve) => { + const { needsReload } = state; + const settings_last_imported = Number((new Date()).getTime()); + const importResultText = `${t('settings_import_success')} ${moment(settings_last_imported).format('LLL')}`; + + _importFromFile(actionData).then((settings) => { + // Taken from panel/reducers/settings.js + const imported_settings = { ...settings }; + if (imported_settings.hasOwnProperty('alert_bubble_timeout')) { + imported_settings.alert_bubble_timeout = Math.min(30, imported_settings.alert_bubble_timeout); + } + + imported_settings.settings_last_imported = Number((new Date()).getTime()); + imported_settings.importResultText = `${t('settings_import_success')} ${moment(imported_settings.settings_last_imported).format('LLL')}`; + imported_settings.actionSuccess = true; + + // persist to background + sendMessage('setPanelData', imported_settings); + + return resolve({ + needsReload: true, + settings: { + actionSuccess: true, + settings_last_imported, + importResultText, + } + }); + }).catch(() => resolve({ + needsReload, + settings: { + actionSuccess: false, + importResultText: t('settings_import_file_error'), + } + })); + }); +} + +export function importSettingsDialog({ state }) { + return _chooseFile().then(fileToLoad => importSettingsNative({ actionData: fileToLoad, state })); +} diff --git a/app/panel-android/actions/summaryActions.js b/app/panel-android/actions/summaryActions.js index 401c8bb2a..fbf2103ad 100644 --- a/app/panel-android/actions/summaryActions.js +++ b/app/panel-android/actions/summaryActions.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -18,6 +18,57 @@ function getPageHostFromSummary(summary) { return summary.pageHost.toLowerCase().replace(/^(http[s]?:\/\/)?(www\.)?/, ''); } +export function updateSitePolicy({ actionData, state }) { + const { type, pageHost } = actionData; + const { summary } = state; + const { site_blacklist, site_whitelist } = summary; + + const host = pageHost.replace(/^www\./, ''); + + let updated_blacklist = site_blacklist.slice(0); + let updated_whitelist = site_whitelist.slice(0); + + if (type === 'whitelist') { + if (site_blacklist.includes(host)) { + // remove from backlist if site is whitelisted + updated_blacklist = removeFromArray(site_blacklist, site_blacklist.indexOf(host)); + } + if (!site_whitelist.includes(host)) { + // add to whitelist + updated_whitelist = addToArray(site_whitelist, host); + } else { + // remove from whitelist + updated_whitelist = removeFromArray(site_whitelist, site_whitelist.indexOf(host)); + } + } else { + if (site_whitelist.includes(host)) { + // remove from whitelisted if site is blacklisted + updated_whitelist = removeFromArray(site_whitelist, site_whitelist.indexOf(host)); + } + if (!site_blacklist.includes(host)) { + // add to blacklist + updated_blacklist = addToArray(site_blacklist, host); + } else { + // remove from blacklist + updated_blacklist = removeFromArray(site_blacklist, site_blacklist.indexOf(host)); + } + } + + // Send Message to Background + sendMessage('setPanelData', { + site_whitelist: updated_whitelist, + site_blacklist: updated_blacklist, + }); + + // Update State for PanelAndroid UI + return { + summary: { + site_whitelist: updated_whitelist, + site_blacklist: updated_blacklist, + }, + }; +} + export function handleTrustButtonClick({ state }) { const { summary } = state; // This pageHost has to be cleaned. @@ -101,24 +152,24 @@ export function handleRestrictButtonClick({ state }) { }; } -export function handlePauseButtonClick({ state }) { - const { summary } = state; - const currentState = summary.paused_blocking; +export function handlePauseButtonClick({ actionData }) { + const { paused_blocking, time } = actionData; sendMessage('setPanelData', { - paused_blocking: !currentState, + paused_blocking: time || paused_blocking, }); return { summary: { - paused_blocking: !currentState, - } + paused_blocking, + paused_blocking_timeout: time, + }, }; } export function cliqzFeatureToggle({ actionData }) { const { currentState, type } = actionData; - const key = `enable_${type}`; + const key = type; sendMessage('setPanelData', { [key]: !currentState, diff --git a/app/panel-android/components/GlobalTrackers.jsx b/app/panel-android/components/GlobalTrackers.jsx deleted file mode 100644 index e2f3e034f..000000000 --- a/app/panel-android/components/GlobalTrackers.jsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Global Trackers 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 Accordions from './content/Accordions'; -import DotsMenu from './content/DotsMenu'; - -export default class GlobalTrackers extends React.Component { - actions = [ - { - id: 'blockAllGlobal', - name: 'Block All', - callback: () => { - const { callGlobalAction } = this.context; - callGlobalAction({ - actionName: 'blockUnBlockAllTrackers', - actionData: { - block: true, - type: 'global', - } - }); - }, - }, - { - id: 'unblockAllGlobal', - name: 'Unblock All', - callback: () => { - const { callGlobalAction } = this.context; - callGlobalAction({ - actionName: 'blockUnBlockAllTrackers', - actionData: { - block: false, - type: 'global', - } - }); - }, - }, - { - id: 'resetSettings', - name: 'Reset Settings', - callback: () => { - const { callGlobalAction } = this.context; - callGlobalAction({ - actionName: 'resetSettings', - }); - }, - } - ]; - - get categories() { - const { categories } = this.props; - return categories; - } - - render() { - return ( -

- ); - } -} - -GlobalTrackers.propTypes = { - categories: PropTypes.arrayOf(PropTypes.object), -}; - -GlobalTrackers.defaultProps = { - categories: [], -}; - -GlobalTrackers.contextTypes = { - callGlobalAction: PropTypes.func, -}; diff --git a/app/panel-android/components/Overview.jsx b/app/panel-android/components/Overview.jsx deleted file mode 100644 index bb5aea0dd..000000000 --- a/app/panel-android/components/Overview.jsx +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Overview 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 TrackersChart from './content/TrackersChart'; -import fromTrackersToChartData from '../utils/chart'; - -export default class Overview extends React.Component { - get isTrusted() { - const { siteProps } = this.context; - return siteProps.isTrusted; - } - - get isRestricted() { - const { siteProps } = this.context; - return siteProps.isRestricted; - } - - get isPaused() { - const { siteProps } = this.context; - return siteProps.isPaused; - } - - get categories() { - const { categories } = this.props; - return categories || []; - } - - get chartData() { - const trackers = this.categories.map(category => ({ - id: category.id, - numTotal: category.num_total, - })); - - return fromTrackersToChartData(trackers); - } - - get hostName() { - const { siteProps } = this.context; - return siteProps.hostName; - } - - get nTrackersBlocked() { - const { siteProps } = this.context; - return siteProps.nTrackersBlocked; - } - - handleTrustButtonClick = () => { - const { callGlobalAction } = this.context; - callGlobalAction({ - actionName: 'handleTrustButtonClick', - }); - } - - handleRestrictButtonClick = () => { - const { callGlobalAction } = this.context; - callGlobalAction({ - actionName: 'handleRestrictButtonClick', - }); - } - - handlePauseButtonClick = () => { - const { callGlobalAction } = this.context; - callGlobalAction({ - actionName: 'handlePauseButtonClick', - }); - } - - render() { - return ( -
-
- -

{this.hostName}

-

- - {this.nTrackersBlocked} - {' '} - - Trackers blocked -

-
- -
-
- -
-
- -
-
- -
-
-
- ); - } -} - -Overview.propTypes = { - categories: PropTypes.arrayOf(PropTypes.shape), -}; - -Overview.defaultProps = { - categories: [], -}; - -Overview.contextTypes = { - siteProps: PropTypes.shape, - callGlobalAction: PropTypes.func, -}; diff --git a/app/panel-android/components/Panel.jsx b/app/panel-android/components/Panel.jsx deleted file mode 100644 index 93d408c6e..000000000 --- a/app/panel-android/components/Panel.jsx +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Panel 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 Tabs from './content/Tabs'; -import Tab from './content/Tab'; -import Overview from './Overview'; -import FixedMenu from './content/FixedMenu'; -import SiteTrackers from './SiteTrackers'; -import GlobalTrackers from './GlobalTrackers'; -import TrackersChart from './content/TrackersChart'; -import { - getPanelData, getSummaryData, getSettingsData, getBlockingData -} from '../actions/panelActions'; -import getCliqzModuleData from '../actions/cliqzActions'; -import handleAllActions from '../actions/handler'; -import fromTrackersToChartData from '../utils/chart'; - -export default class Panel extends React.Component { - constructor(props) { - super(props); - this.state = { - panel: {}, - summary: {}, - settings: {}, - blocking: {}, - cliqzModuleData: {}, - }; - } - - getChildContext = () => ({ - siteProps: this.siteProps, - callGlobalAction: this.callGlobalAction, - }); - - componentDidMount() { - const tabId = new URLSearchParams(window.location.search).get('tabId'); - this.setPanelState(tabId); - this.setSummaryState(tabId); - this.setSettingsState(); - this.setBlockingState(tabId); - this.setCliqzDataState(tabId); - } - - get siteCategories() { - const { blocking } = this.state; - return blocking.categories || []; - } - - get globalCategories() { - const { settings } = this.state; - return settings.categories || []; - } - - get chartData() { - const trackers = this.siteCategories.map(category => ({ - id: category.id, - numTotal: category.num_total, - })); - - return fromTrackersToChartData(trackers); - } - - get siteProps() { - const { summary } = this.state; - const hostName = summary.pageHost || ''; - const pageHost = hostName.toLowerCase().replace(/^(http[s]?:\/\/)?(www\.)?/, ''); - - const siteWhitelist = summary.site_whitelist || []; - const siteBlacklist = summary.site_blacklist || []; - - const isTrusted = siteWhitelist.indexOf(pageHost) !== -1; - const isRestricted = siteBlacklist.indexOf(pageHost) !== -1; - const isPaused = summary.paused_blocking; - - const nTrackersBlocked = (summary.trackerCounts || {}).blocked || 0; - - return { - hostName, pageHost, isTrusted, isRestricted, isPaused, nTrackersBlocked - }; - } - - setPanelState = (tabId) => { - getPanelData(tabId).then((data) => { - this.setState({ - panel: data.panel, - }); - }); - } - - setSummaryState = (tabId) => { - getSummaryData(tabId).then((data) => { - this.setState({ - summary: data, - }); - }); - } - - setSettingsState = () => { - getSettingsData().then((data) => { - this.setState({ - settings: data, - }); - }); - } - - setBlockingState = (tabId) => { - getBlockingData(tabId).then((data) => { - this.setState({ - blocking: data, - }); - }); - } - - setCliqzDataState = (tabId) => { - getCliqzModuleData(tabId).then((data) => { - this.setState({ - cliqzModuleData: data, - }); - }); - } - - setGlobalState = (updated) => { - const newState = {}; - Object.keys(updated).forEach((key) => { - newState[key] = { ...this.state[key], ...updated[key] }; // eslint-disable-line react/destructuring-assignment - }); - - this.setState(newState); - } - - callGlobalAction = ({ actionName, actionData = {} }) => { - const updated = handleAllActions({ actionName, actionData, state: this.state }); - if (Object.keys(updated).length !== 0) { - this.setGlobalState(updated); - } - } - - render() { - const { panel, cliqzModuleData } = this.state; - return ( -
-
- -

{this.siteProps.hostName}

-

- - {this.siteProps.nTrackersBlocked} - {' '} - - Trackers blocked -

-
- - - - - - - - - - - - - - -
- ); - } -} - -Panel.childContextTypes = { - siteProps: PropTypes.shape, - callGlobalAction: PropTypes.func, -}; diff --git a/app/panel-android/components/PanelAndroid.jsx b/app/panel-android/components/PanelAndroid.jsx new file mode 100644 index 000000000..0dbd1bfa2 --- /dev/null +++ b/app/panel-android/components/PanelAndroid.jsx @@ -0,0 +1,288 @@ +/** + * Panel Android Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import ClassNames from 'classnames'; +import Settings from './content/Settings'; +import Tabs from './content/Tabs'; +import Tab from './content/Tab'; +import OverviewTab from './content/OverviewTab'; +import BlockingTab from './content/BlockingTab'; +import { + getPanelData, getSummaryData, getSettingsData, getBlockingData +} from '../actions/panelActions'; +import getCliqzModuleData from '../actions/cliqzActions'; +import handleAllActions from '../actions/handler'; +import { sendMessage, openAccountPageAndroid } from '../../panel/utils/msg'; + +class PanelAndroid extends React.Component { + constructor(props) { + super(props); + + this.state = { + needsReload: false, + view: 'overview', + panel: { + enable_ad_block: false, + enable_anti_tracking: false, + enable_smart_block: false, + smartBlock: { blocked: {}, unblocked: {} }, + }, + summary: { + categories: [], + trackerCounts: { + allowed: 0, + blocked: 0, + }, + sitePolicy: false, + paused_blocking: false, + }, + settings: {}, + blocking: { + siteNotScanned: false, + pageUrl: '', + categories: [], + }, + cliqzModuleData: { + adBlock: { trackerCount: 0, unknownTrackers: [] }, + antiTracking: { trackerCount: 0, unknownTrackers: [] }, + }, + }; + } + + componentDidMount() { + const tabId = new URLSearchParams(window.location.search).get('tabId'); + this.setPanelState(tabId); + this.setSummaryState(tabId); + this.setSettingsState(); + this.setBlockingState(tabId); + this.setCliqzDataState(tabId); + } + + get siteProps() { + const { summary } = this.state; + const hostName = summary.pageHost || ''; + const pageHost = hostName.toLowerCase().replace(/^(http[s]?:\/\/)?(www\.)?/, ''); + + const { + site_whitelist = [], + site_blacklist = [], + trackerCounts = {} + } = summary; + + const isTrusted = site_whitelist.indexOf(pageHost) !== -1; + const isRestricted = site_blacklist.indexOf(pageHost) !== -1; + const isPaused = summary.paused_blocking; + + const nTrackersBlocked = trackerCounts.blocked || 0; + + return { + hostName, pageHost, isTrusted, isRestricted, isPaused, nTrackersBlocked + }; + } + + setPanelState = (tabId) => { + getPanelData(tabId).then((data) => { + this.setState(prevState => ({ + panel: data.panel, + settings: { + ...prevState.settings, + reload_banner_status: data.panel.reload_banner_status, + trackers_banner_status: data.panel.trackers_banner_status, + } + })); + }); + } + + setSummaryState = (tabId) => { + getSummaryData(tabId).then((data) => { + this.setState({ summary: data }); + }); + } + + setSettingsState = () => { + getSettingsData().then((data) => { + this.setState(prevState => ({ + settings: { + ...prevState.settings, + ...data, + dbUpdateText: t('settings_update_now'), + } + })); + }); + } + + setBlockingState = (tabId) => { + getBlockingData(tabId).then((data) => { + this.setState({ blocking: data }); + }); + } + + setCliqzDataState = (tabId) => { + getCliqzModuleData(tabId).then((data) => { + this.setState({ cliqzModuleData: data }); + }); + } + + setGlobalState = (updated) => { + const newState = { needsReload: true }; + Object.keys(updated).forEach((key) => { + newState[key] = { ...this.state[key], ...updated[key] }; // eslint-disable-line react/destructuring-assignment + }); + + if (updated.needsReload === false) { + newState.needsReload = false; + } + + this.setState(newState); + } + + callGlobalAction = ({ actionName, actionData = {} }) => { + const updated = handleAllActions({ actionName, actionData, state: this.state }); + if (updated instanceof Promise) { + updated.then((result) => { + if (Object.keys(result).length !== 0) { + this.setGlobalState(result); + } + }); + } else if (Object.keys(updated).length !== 0) { + this.setGlobalState(updated); + } + } + + changeView = (newView) => { + this.setState({ view: newView }); + } + + massageCliqzTrackers = tracker => ({ + id: tracker.name, + catId: tracker.type, + cliqzAdCount: tracker.ads, + cliqzCookieCount: tracker.cookies, + cliqzFingerprintCount: tracker.fingerprints, + name: tracker.name, + sources: tracker.domains, + whitelisted: tracker.whitelisted, + blocked: false, // To appease BlockingTracker PropTypes + wtm: tracker.wtm, + }) + + reloadTab = () => { + const { panel } = this.state; + sendMessage('reloadTab', { tab_id: +panel.tab_id }); + window.close(); + } + + _renderSettings() { + const { summary, settings } = this.state; + + return ( + { this.changeView('overview'); }} + callGlobalAction={this.callGlobalAction} + /> + ); + } + + _renderOverview() { + const { + panel, + blocking, + summary, + settings, + cliqzModuleData, + } = this.state; + const { categories, toggle_individual_trackers } = blocking; + const { adBlock, antiTracking } = cliqzModuleData; + + const unknownTrackers = Array.from(new Set([ + ...antiTracking.unknownTrackers.map(this.massageCliqzTrackers), + ...adBlock.unknownTrackers.map(this.massageCliqzTrackers), + ])).sort((a, b) => { + const nameA = a.name.toLowerCase(); + const nameB = b.name.toLowerCase(); + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + return 0; + }); + const unknownCategory = { + id: 'unknown', + name: t('unknown'), + description: t('unknown_description'), + img_name: 'unknown', + num_total: unknownTrackers.length, + num_blocked: 0, // We don't want to see the Trackers Blocked text + trackers: unknownTrackers, + }; + + return ( + + + { this.changeView('settings'); }} + callGlobalAction={this.callGlobalAction} + /> + + + + + + + + + + + ); + } + + render() { + const { needsReload, view } = this.state; + const needsReloadClassNames = ClassNames('NeedsReload flex-container align-center-middle', { + 'NeedsReload--show': needsReload, + }); + + return ( +
+
+ {t('alert_reload')} +
+ {view === 'settings' && this._renderSettings()} + {view === 'overview' && this._renderOverview()} +
+ ); + } +} + +export default PanelAndroid; diff --git a/app/panel-android/components/SiteTrackers.jsx b/app/panel-android/components/SiteTrackers.jsx deleted file mode 100644 index be4937e1a..000000000 --- a/app/panel-android/components/SiteTrackers.jsx +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Site Trackers 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 Accordions from './content/Accordions'; -import DotsMenu from './content/DotsMenu'; - -export default class SiteTrackers extends React.Component { - actions = [ - { - id: 'blockAllSite', - name: 'Block All', - callback: () => { - const { callGlobalAction } = this.context; - callGlobalAction({ - actionName: 'blockUnBlockAllTrackers', - actionData: { - block: true, - type: 'site', - } - }); - }, - }, - { - id: 'unblockAllSite', - name: 'Unblock All', - callback: () => { - const { callGlobalAction } = this.context; - callGlobalAction({ - actionName: 'blockUnBlockAllTrackers', - actionData: { - block: false, - type: 'site', - } - }); - }, - } - ] - - render() { - const { categories } = this.props; - return ( -
-
-

Trackers on this site

- -
- -
- ); - } -} - -SiteTrackers.propTypes = { - categories: PropTypes.arrayOf(PropTypes.object), -}; - -SiteTrackers.defaultProps = { - categories: [], -}; - -SiteTrackers.contextTypes = { - callGlobalAction: PropTypes.func, -}; diff --git a/app/panel-android/components/content/Accordion.jsx b/app/panel-android/components/content/Accordion.jsx deleted file mode 100644 index aaa463d6a..000000000 --- a/app/panel-android/components/content/Accordion.jsx +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Accordion 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 TrackerItem from './TrackerItem'; - -export default class Accordion extends React.Component { - itemHeight = 50; - - nExtraItems = 40; - - headerheight = 32; - - constructor(props) { - super(props); - this.myRef = React.createRef(); - - this.state = { - isActive: false, - openMenuIndex: -1, - currentItemsLength: 0, - }; - - this.isWaiting = false; - this.unMounted = false; - } - - componentDidMount() { - window.addEventListener('scroll', this.handleScroll); - } - - componentWillUnmount() { - this.unMounted = true; - window.removeEventListener('scroll', this.handleScroll); - } - - get blockingStatus() { - const { type, numBlocked, numTotal } = this.props; - const { siteProps } = this.context; - if (type === 'site-trackers') { - if (siteProps.isTrusted) { - return 'trusted'; - } - - if (siteProps.isRestricted) { - return 'restricted'; - } - - const trackers = this.getTrackers(true); - if (trackers.every(tracker => tracker.ss_allowed)) { - return 'trusted'; - } - - if (trackers.every(tracker => tracker.ss_blocked)) { - return 'restricted'; - } - - if (trackers.some(tracker => tracker.ss_allowed || tracker.ss_blocked)) { - return 'mixed'; - } - } - - if (numBlocked === numTotal) { - return 'blocked'; - } - - return ''; - } - - getTrackers = (force = false) => { - const { id, getTrackersFromCategory } = this.props; - const { isActive } = this.state; - if (!isActive && !force) { - return []; - } - - return getTrackersFromCategory(id); - } - - getMenuOpenStatus = (index) => { - const { openMenuIndex } = this.state; - return index === openMenuIndex; - } - - checkAndUpdateData = () => { - const { numTotal } = this.props; - const { isActive, currentItemsLength } = this.state; - if (this.unMounted || !isActive || currentItemsLength >= numTotal) { - return; - } - - const needToUpdateHeight = this.nExtraItems * this.itemHeight; // Update even before the bottom is visible - - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - const accordionContentNode = this.myRef.current; - const boundingRect = accordionContentNode.getBoundingClientRect(); - // Try lo load more when needed - if (scrollTop + window.innerHeight - (accordionContentNode.offsetTop + boundingRect.height) > -needToUpdateHeight) { - this.setState((prevState) => { - const itemsLength = Math.min(prevState.currentItemsLength + this.nExtraItems, numTotal); - return { currentItemsLength: itemsLength }; - }); - } - } - - toggleMenu = (index) => { - const { openMenuIndex } = this.state; - if (openMenuIndex === index) { - this.setState({ openMenuIndex: -1 }); - } else { - this.setState({ openMenuIndex: index }); - } - } - - handleScroll = () => { - // Don't call the checkAndUpdateData function so many times. Use throttle - if (this.isWaiting) { - return; - } - - this.isWaiting = true; - - setTimeout(() => { - this.isWaiting = false; - this.checkAndUpdateData(); - }, 200); - } - - toggleContent = () => { - const { index, toggleAccordion, numTotal } = this.props; - const { isActive } = this.state; - toggleAccordion(index); - - // Show some trackers when this category is expanded - const currentState = isActive; - const itemsLength = Math.min(this.nExtraItems, numTotal); - this.setState({ - isActive: !currentState, - currentItemsLength: itemsLength, - }); - } - - handleCategoryClicked = () => { - const { id, type } = this.props; - const { callGlobalAction } = this.context; - if (!this.blockingStatus) { - const blockingType = type === 'site-trackers' ? 'site' : 'global'; - callGlobalAction({ - actionName: 'blockUnBlockAllTrackers', - actionData: { - block: true, - type: blockingType, - categoryId: id, - } - }); - } else if (this.blockingStatus === 'blocked') { - const blockingType = type === 'site-trackers' ? 'site' : 'global'; - callGlobalAction({ - actionName: 'blockUnBlockAllTrackers', - actionData: { - block: false, - type: blockingType, - categoryId: id, - } - }); - } - } - - render() { - const { - index, - open, - numBlocked, - name, - numTotal, - logo, - id, - type - } = this.props; - const { isActive, currentItemsLength } = this.state; - const titleStyle = { backgroundImage: `url(/app/images/panel-android/categories/${logo}.svg)` }; - const contentStyle = { '--trackers-length': `${open ? (currentItemsLength * this.itemHeight) + this.headerheight : 0}px` }; - - return ( -
- -
-

{name}

-

- - {numTotal} - {' '} - TRACKERS - - {!!numBlocked && ( - - {numBlocked} - {' '} - Blocked - - )} -

-

- On this site -

-
-
-

- TRACKERS - Blocked -

-
    - {this.getTrackers(true).slice(0, currentItemsLength).map((tracker, ind) => ( - - ))} -
-
-
- ); - } -} - -Accordion.propTypes = { - toggleAccordion: PropTypes.func.isRequired, - index: PropTypes.number.isRequired, - getTrackersFromCategory: PropTypes.func.isRequired, - open: PropTypes.bool, - numBlocked: PropTypes.number, - name: PropTypes.string, - numTotal: PropTypes.number, - logo: PropTypes.string, - id: PropTypes.string, - type: PropTypes.string, -}; - -Accordion.defaultProps = { - open: false, - numBlocked: 0, - name: '', - numTotal: 0, - logo: '', - id: '', - type: '', -}; - -Accordion.contextTypes = { - siteProps: PropTypes.shape, - callGlobalAction: PropTypes.func, -}; diff --git a/app/panel-android/components/content/Accordions.jsx b/app/panel-android/components/content/Accordions.jsx deleted file mode 100644 index 99e4b7347..000000000 --- a/app/panel-android/components/content/Accordions.jsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Accordions 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 Accordion from './Accordion'; - -class Accordions extends React.Component { - constructor(props) { - super(props); - this.state = { - openAccordionIndex: -1, - }; - } - - getOpenStatus = (index) => { - const { openAccordionIndex } = this.state; - return index === openAccordionIndex; - } - - getTrackersFromCategory = (categoryId) => { - const { categories } = this.props; - const category = categories[categories.findIndex(cat => cat.id === categoryId)]; - return category.trackers; - } - - toggleAccordion = (index) => { - const { openAccordionIndex } = this.state; - if (openAccordionIndex === index) { - this.setState({ openAccordionIndex: -1 }); - } else { - this.setState({ openAccordionIndex: index }); - } - } - - render() { - const { categories, type } = this.props; - return ( -
- { - categories.map((category, index) => ( - - )) - } -
- ); - } -} - -Accordions.propTypes = { - categories: PropTypes.arrayOf(PropTypes.object), - type: PropTypes.string, -}; - -Accordions.defaultProps = { - categories: [], - type: '', -}; - -export default Accordions; diff --git a/app/panel-android/components/content/BlockingCategories.jsx b/app/panel-android/components/content/BlockingCategories.jsx new file mode 100644 index 000000000..f367b8dcf --- /dev/null +++ b/app/panel-android/components/content/BlockingCategories.jsx @@ -0,0 +1,97 @@ +/** + * Blocking Categories Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import BlockingCategory from './BlockingCategory'; + +class BlockingCategories extends React.Component { + constructor(props) { + super(props); + + this.state = { + openCategoryIndex: -1, + blockingType: props.type, + }; + } + + static getDerivedStateFromProps(props, state) { + const { type } = props; + const { blockingType } = state; + + if (type !== blockingType) { + return { + openCategoryIndex: -1, + blockingType: type, + }; + } + return null; + } + + getOpenStatus = (index) => { + const { openCategoryIndex } = this.state; + return index === openCategoryIndex; + } + + toggleCategoryOpen = (index) => { + const { openCategoryIndex } = this.state; + if (openCategoryIndex === index) { + this.setState({ openCategoryIndex: -1 }); + } else { + this.setState({ openCategoryIndex: index }); + } + } + + render() { + const { + categories, + type, + siteProps, + settings, + callGlobalAction, + } = this.props; + + return ( +
+ { + categories.map((category, index) => ( + + )) + } +
+ ); + } +} + +BlockingCategories.propTypes = { + categories: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + type: PropTypes.oneOf([ + 'site', + 'global', + ]).isRequired, + siteProps: PropTypes.shape({}).isRequired, + callGlobalAction: PropTypes.func.isRequired, + settings: PropTypes.shape({}).isRequired, +}; + +export default BlockingCategories; diff --git a/app/panel-android/components/content/BlockingCategory.jsx b/app/panel-android/components/content/BlockingCategory.jsx new file mode 100644 index 000000000..c0b0b7dac --- /dev/null +++ b/app/panel-android/components/content/BlockingCategory.jsx @@ -0,0 +1,292 @@ +/** + * Blocking Category Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import ClassNames from 'classnames'; +import { FixedSizeList as List } from 'react-window'; +import BlockingTracker from './BlockingTracker'; + +class BlockingCategory extends React.Component { + constructor(props) { + super(props); + + this.state = { + openTrackerIndex: -1, + }; + + this.heightTracker = 50; + this.heightListHeader = 30; + this.maxListHeight = 750; + } + + getListHeight(count) { + return Math.min(this.maxListHeight, count * this.heightTracker); + } + + getListHeightWithHeader(count) { + return this.heightListHeader + this.getListHeight(count); + } + + get categorySelectStatus() { + const { type, siteProps, category } = this.props; + const { trackers, num_total, num_blocked } = category; + + if (type === 'site') { + if (siteProps.isTrusted) { + return 'trusted'; + } + + if (siteProps.isRestricted) { + return 'restricted'; + } + + if (category.id === 'unknown') { + return 'unknown'; + } + + if (trackers.every(tracker => tracker.ss_allowed)) { + return 'trusted'; + } + + if (trackers.every(tracker => tracker.ss_blocked)) { + return 'restricted'; + } + + if (trackers.some(tracker => tracker.ss_allowed || tracker.ss_blocked)) { + return 'ss_mixed'; + } + } + + if (num_blocked && num_blocked === num_total) { + return 'blocked'; + } + + if (num_blocked && num_blocked !== num_total) { + return 'mixed'; + } + + return ''; + } + + get numTrackersText() { + const { category } = this.props; + const { num_total } = category; + + return `${num_total} ${(num_total === 1) ? t('blocking_category_tracker') : t('blocking_category_trackers')}`; + } + + get numBlockedText() { + const { category } = this.props; + const { num_blocked } = category; + + return `${num_blocked} ${t('blocking_category_blocked')}`; + } + + getTrackerOpenStatus = (index) => { + const { openTrackerIndex } = this.state; + return index === openTrackerIndex; + } + + toggleTrackerSelectOpen = (index) => { + const { openTrackerIndex } = this.state; + if (openTrackerIndex === index) { + this.setState({ openTrackerIndex: -1 }); + } else { + this.setState({ openTrackerIndex: index }); + } + } + + clickCategorySelect = (event) => { + event.stopPropagation(); + const { category, type, callGlobalAction } = this.props; + const { id } = category; + const selectStatus = this.categorySelectStatus; + + if (selectStatus === '' || selectStatus === 'mixed') { + callGlobalAction({ + actionName: 'blockUnBlockAllTrackers', + actionData: { + block: true, + categoryId: id, + type, + } + }); + } else if (selectStatus === 'blocked') { + callGlobalAction({ + actionName: 'blockUnBlockAllTrackers', + actionData: { + block: false, + categoryId: id, + type, + } + }); + } + } + + renderCategorySelect() { + const categorySelect = this.categorySelectStatus; + // Hide category blocking for Unknown trackers + if (categorySelect === 'unknown') { + return false; + } + const categorySelectClassNames = ClassNames('BlockingSelectButton', { + BlockingSelectButton__mixed: categorySelect === 'mixed' || categorySelect === 'ss_mixed', + BlockingSelectButton__blocked: categorySelect === 'blocked', + BlockingSelectButton__trusted: categorySelect === 'trusted', + BlockingSelectButton__restricted: categorySelect === 'restricted', + }); + + return ( +
+
+
+ ); + } + + renderToggleArrow() { + const { open } = this.props; + const toggleClassNames = ClassNames('BlockingCategory__toggle', { + 'BlockingCategory--open': open, + }); + + return ( +
+ ); + } + + renderBlockingTracker = ({ index, style }) => { + const { + category, + type, + siteProps, + settings, + callGlobalAction, + } = this.props; + const { id, trackers } = category; + const tracker = trackers[index]; + + return ( +
+ { this.toggleTrackerSelectOpen(tracker.id); }} + open={this.getTrackerOpenStatus(tracker.id)} + siteProps={siteProps} + settings={settings} + callGlobalAction={callGlobalAction} + /> +
+
+ ); + } + + render() { + const { openTrackerIndex } = this.state; + const { + index, + category, + open, + toggleCategoryOpen, + } = this.props; + const { + id, + name, + img_name, + num_total, + num_blocked, + } = category; + const categoryImage = `/app/images/panel-android/categories/${img_name}.svg`; + + const categoryClassNames = ClassNames('BlockingCategory', { + BlockingCategory__unknown: id === 'unknown', + }); + + return ( +
+
{ toggleCategoryOpen(index); }}> + +
+

{name}

+
+ {this.numTrackersText} + { !!num_blocked && ( + {this.numBlockedText} + )} +
+
+
+ {this.renderCategorySelect()} + {this.renderToggleArrow()} +
+
+
+ {open && ( +
+
+ {t('blocking_category_trackers')} + {category.id === 'unknown' ? t('android_anonymized') : t('blocking_category_blocked')} +
+ + {this.renderBlockingTracker} + +
+ )} +
+
+ ); + } +} + +BlockingCategory.propTypes = { + index: PropTypes.number.isRequired, + category: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + num_total: PropTypes.number.isRequired, + num_blocked: PropTypes.number.isRequired, + trackers: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + ss_allowed: PropTypes.bool, + ss_blocked: PropTypes.bool, + })).isRequired, + img_name: PropTypes.string.isRequired, + }).isRequired, + open: PropTypes.bool.isRequired, + toggleCategoryOpen: PropTypes.func.isRequired, + type: PropTypes.oneOf([ + 'site', + 'global', + ]).isRequired, + siteProps: PropTypes.shape({ + isTrusted: PropTypes.bool.isRequired, + isRestricted: PropTypes.bool.isRequired, + isPaused: PropTypes.bool.isRequired, + }).isRequired, + settings: PropTypes.shape({}).isRequired, + callGlobalAction: PropTypes.func.isRequired, +}; + +export default BlockingCategory; diff --git a/app/panel-android/components/content/BlockingTab.jsx b/app/panel-android/components/content/BlockingTab.jsx new file mode 100644 index 000000000..a60566d59 --- /dev/null +++ b/app/panel-android/components/content/BlockingTab.jsx @@ -0,0 +1,134 @@ +/** + * Blocking Tab Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import DotsMenu from './DotsMenu'; +import BlockingCategories from './BlockingCategories'; + +class BlockingTab extends React.Component { + constructor(props) { + super(props); + + const { callGlobalAction } = props; + this.siteActions = [ + { + id: 'blockAllSite', + name: t('blocking_block_all'), + callback: () => { + callGlobalAction({ + actionName: 'blockUnBlockAllTrackers', + actionData: { block: true, type: 'site' } + }); + } + }, { + id: 'unblockAllSite', + name: t('blocking_unblock_all'), + callback: () => { + callGlobalAction({ + actionName: 'blockUnBlockAllTrackers', + actionData: { block: false, type: 'site' } + }); + } + } + ]; + this.globalActions = [ + { + id: 'blockAllGlobal', + name: t('blocking_block_all'), + callback: () => { + callGlobalAction({ + actionName: 'blockUnBlockAllTrackers', + actionData: { block: true, type: 'global' } + }); + } + }, { + id: 'unblockAllGlobal', + name: t('blocking_unblock_all'), + callback: () => { + callGlobalAction({ + actionName: 'blockUnBlockAllTrackers', + actionData: { block: false, type: 'global' } + }); + } + }, { + id: 'resetSettings', + name: t('android_blocking_reset'), + callback: () => { + callGlobalAction({ + actionName: 'resetSettings', + }); + } + } + ]; + } + + get actions() { + const { type } = this.props; + if (type === 'site') { + return this.siteActions; + } + return this.globalActions; + } + + get headerText() { + const { type } = this.props; + return (type === 'site') ? + t('android_site_blocking_header') : + t('android_global_blocking_header'); + } + + render() { + const { + type, + categories, + settings, + siteProps, + callGlobalAction, + } = this.props; + + return ( +
+
+

{this.headerText}

+ +
+ +
+ ); + } +} + +BlockingTab.propTypes = { + type: PropTypes.oneOf([ + 'site', + 'global', + ]).isRequired, + callGlobalAction: PropTypes.func.isRequired, + siteProps: PropTypes.shape({}).isRequired, + categories: PropTypes.arrayOf(PropTypes.shape({})), + settings: PropTypes.shape({}), +}; + +BlockingTab.defaultProps = { + categories: [], + settings: {}, +}; + +export default BlockingTab; diff --git a/app/panel-android/components/content/BlockingTracker.jsx b/app/panel-android/components/content/BlockingTracker.jsx new file mode 100644 index 000000000..501607a36 --- /dev/null +++ b/app/panel-android/components/content/BlockingTracker.jsx @@ -0,0 +1,421 @@ +/** + * Blocking Tracker Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import ClassNames from 'classnames'; +import getSlugFromTrackerId from '../../utils/tracker-info'; + +class BlockingTracker extends React.Component { + get trackerSelectStatus() { + const { type, siteProps, tracker } = this.props; + const { isTrusted, isRestricted } = siteProps; + const { + blocked, + catId = '', + ss_allowed = false, + ss_blocked = false, + warningSmartBlock = false, + } = tracker; + + if (type === 'site') { + if (warningSmartBlock) { + return 'override-sb'; + } + + if (isTrusted) { + return 'trusted'; + } + + if (isRestricted) { + return 'restricted'; + } + + if (ss_allowed) { + return 'trusted'; + } + + if (ss_blocked) { + return 'restricted'; + } + + if (blocked) { + return 'blocked'; + } + + if (catId !== '') { + return catId; + } + } + + if (blocked) { + return 'blocked'; + } + + return ''; + } + + get selectDisabled() { + const { type, siteProps } = this.props; + const { isTrusted, isRestricted, isPaused } = siteProps; + + if (type === 'site') { + return isTrusted || isRestricted || isPaused; + } + + return false; + } + + get selectBlockDisabled() { + const { tracker } = this.props; + const { ss_allowed = false, ss_blocked = false } = tracker; + + return this.selectDisabled || ss_allowed || ss_blocked; + } + + openTrackerInfoLink = (event) => { + event.stopPropagation(); + const { tracker } = this.props; + const slug = (tracker.wtm) ? tracker.wtm : getSlugFromTrackerId(tracker.id); + const tab = window.open(`https://whotracks.me/trackers/${slug}.html`, '_blank'); + tab.focus(); + } + + clickBlock = () => { + const { + type, + tracker, + categoryId, + callGlobalAction, + } = this.props; + const { id, blocked } = tracker; + + if (this.selectBlockDisabled) { + return; + } + + if (type === 'site') { + callGlobalAction({ + actionName: 'trustRestrictBlockSiteTracker', + actionData: { + app_id: id, + cat_id: categoryId, + block: !blocked, + trust: false, + restrict: false, + } + }); + } else if (type === 'global') { + callGlobalAction({ + actionName: 'blockUnblockGlobalTracker', + actionData: { + app_id: id, + cat_id: categoryId, + block: !blocked, + } + }); + } + } + + clickRestrict = () => { + const { tracker, categoryId, callGlobalAction } = this.props; + const { id, blocked, ss_blocked = false } = tracker; + + if (this.selectDisabled) { + return; + } + + callGlobalAction({ + actionName: 'trustRestrictBlockSiteTracker', + actionData: { + app_id: id, + cat_id: categoryId, + restrict: !ss_blocked, + trust: false, + block: blocked, // Keep blocking + } + }); + } + + clickTrust = () => { + const { tracker, categoryId, callGlobalAction } = this.props; + const { id, blocked, ss_allowed = false } = tracker; + + if (this.selectDisabled) { + return; + } + + callGlobalAction({ + actionName: 'trustRestrictBlockSiteTracker', + actionData: { + app_id: id, + cat_id: categoryId, + restrict: false, + trust: !ss_allowed, + block: blocked, // Keep blocking + } + }); + } + + clickAnonymize = () => { + const { tracker, callGlobalAction } = this.props; + + if (this.selectDisabled) { + return; + } + + callGlobalAction({ + actionName: 'anonymizeSiteTracker', + actionData: { + unknownTracker: tracker, + } + }); + } + + renderTrackerModified() { + const { type, tracker } = this.props; + const { cliqzAdCount, cliqzCookieCount, cliqzFingerprintCount } = tracker; + + if (type === 'global') { + return null; + } + + return ( +
+ {cliqzAdCount > 0 && ( + + {`${cliqzAdCount} ${cliqzAdCount === 1 ? t('ad') : t('ads')}`} + + )} + {cliqzCookieCount > 0 && ( + + {`${cliqzCookieCount} ${cliqzCookieCount === 1 ? t('cookie') : t('cookies')}`} + + )} + {cliqzFingerprintCount > 0 && ( + + {`${cliqzFingerprintCount} ${cliqzFingerprintCount === 1 ? t('fingerprint') : t('fingerprints')}`} + + )} +
+ ); + } + + renderTrackerStatus() { + const trackerSelect = this.trackerSelectStatus; + const trackerSelectClassNames = ClassNames({ + OverrideSmartBlock: trackerSelect === 'override-sb', + BlockingSelectButton: trackerSelect.indexOf('override-') === -1, + BlockingSelectButton__blocked: trackerSelect === 'blocked', + BlockingSelectButton__trusted: trackerSelect === 'trusted', + BlockingSelectButton__restricted: trackerSelect === 'restricted', + }); + + return ( +
+ ); + } + + renderUnknownTrackerStatus() { + const { siteProps, tracker } = this.props; + const trackerSelect = this.trackerSelectStatus; + const svgContainerClasses = ClassNames('UnknownSVGContainer', { + whitelisted: tracker.whitelisted && !siteProps.isRestricted, + siteRestricted: siteProps.isRestricted, + }); + const borderClassNames = ClassNames('border', { + protected: trackerSelect === 'antiTracking', + restricted: trackerSelect !== 'antiTracking', + }); + const backgroundClassNames = ClassNames('background', { + protected: trackerSelect === 'antiTracking', + restricted: trackerSelect !== 'antiTracking', + }); + + return ( +
+ + + + + + + + + + + + + + {trackerSelect === 'antiTracking' ? ( + + + + + + ) : ( + + + + + + + )} + + +
+ ); + } + + renderSmartBlockOverflow() { + const { open, tracker } = this.props; + const { warningSmartBlock } = tracker; + const selectGroupClassNames = ClassNames('OverrideText full-height', + 'flex-container align-center-middle', { + 'OverrideText--open': open, + }); + const text = (warningSmartBlock && warningSmartBlock === 'blocked') ? + t('panel_tracker_warning_smartblock_tooltip') : + t('panel_tracker_warning_smartunblock_tooltip'); + + return ( +
+ {text} +
+ ); + } + + renderBlockingOverflow() { + const { + type, + open, + tracker, + settings, + } = this.props; + const { ss_allowed = false, ss_blocked = false, blocked } = tracker; + const { toggle_individual_trackers = false } = settings; + + const selectGroupClassNames = ClassNames('BlockingSelectGroup full-height', + 'flex-container flex-dir-row-reverse', { + 'BlockingSelectGroup--open': open, + 'BlockingSelectGroup--wide': type === 'site' && toggle_individual_trackers, + 'BlockingSelectGroup--disabled': this.selectDisabled, + }); + const selectBlockClassNames = ClassNames('BlockingSelect BlockingSelect__block', + 'full-height flex-child-grow', { + 'BlockingSelect--disabled': this.selectBlockDisabled, + }); + + return ( +
+
+ {blocked ? t('android_unblock') : t('android_block')} +
+ {type === 'site' && toggle_individual_trackers && ( +
+ {ss_blocked ? t('android_unrestrict') : t('android_restrict')} +
+ )} + {type === 'site' && toggle_individual_trackers && ( +
+ {ss_allowed ? t('android_untrust') : t('android_trust')} +
+ )} +
+ ); + } + + renderUnknownOverflow() { + const { + open, + tracker, + } = this.props; + const { whitelisted } = tracker; + + const selectGroupClassNames = ClassNames('BlockingSelectGroup full-height', + 'flex-container flex-dir-row-reverse', { + 'BlockingSelectGroup--open': open, + 'BlockingSelectGroup--disabled': this.selectDisabled, + }); + + return ( +
+
+ {whitelisted ? t('android_anonymize') : t('android_trust')} +
+
+ ); + } + + renderTrackerOverflow() { + const trackerSelect = this.trackerSelectStatus; + if (trackerSelect === 'antiTracking' || trackerSelect === 'adBlock') { + return this.renderUnknownOverflow(); + } + if (trackerSelect === 'override-sb') { + return this.renderSmartBlockOverflow(); + } + + return this.renderBlockingOverflow(); + } + + render() { + const trackerSelect = this.trackerSelectStatus; + const { index, tracker, toggleTrackerSelectOpen } = this.props; + const { name } = tracker; + + return ( +
{ toggleTrackerSelectOpen(index); }}> +
+
+
+
+
{name}
+ {this.renderTrackerModified()} +
+ {(trackerSelect === 'antiTracking' || trackerSelect === 'adBlock') ? this.renderUnknownTrackerStatus() : this.renderTrackerStatus()} + {this.renderTrackerOverflow()} +
+ ); + } +} + +BlockingTracker.propTypes = { + index: PropTypes.number.isRequired, + tracker: PropTypes.shape({ + id: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + name: PropTypes.string.isRequired, + ss_allowed: PropTypes.bool, + ss_blocked: PropTypes.bool, + blocked: PropTypes.bool.isRequired, + }).isRequired, + categoryId: PropTypes.string.isRequired, + type: PropTypes.oneOf([ + 'site', + 'global', + ]).isRequired, + toggleTrackerSelectOpen: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + siteProps: PropTypes.shape({ + isTrusted: PropTypes.bool.isRequired, + isRestricted: PropTypes.bool.isRequired, + isPaused: PropTypes.bool.isRequired, + }).isRequired, + settings: PropTypes.shape({}).isRequired, + callGlobalAction: PropTypes.func.isRequired, +}; + +export default BlockingTracker; diff --git a/app/panel-android/components/content/ChartSVG.jsx b/app/panel-android/components/content/ChartSVG.jsx deleted file mode 100644 index ffff1c305..000000000 --- a/app/panel-android/components/content/ChartSVG.jsx +++ /dev/null @@ -1,77 +0,0 @@ -/** - * ChartSVG 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 Path from './Path'; - -export default class ChartSVG extends React.Component { - constructor(props) { - super(props); - this.state = { - nItem: 1, - }; - } - - increaseN = () => { - this.setState((prevState) => { - const { paths } = this.props; - if (prevState.nItem < paths.length) { - return { nItem: prevState.nItem + 1 }; - } - return null; - }); - } - - render() { - const { paths, radius } = this.props; - const { nItem } = this.state; - let computedPaths = paths.slice(0, nItem).map(element => ( - - )); - - if (computedPaths.length === 0) { - // When there is no tracker - const defaultElement = { - start: 0, - end: 360, - category: 'default', - }; - - computedPaths = ( - - ); - } - - return ( - - - {computedPaths} - - - ); - } -} - -ChartSVG.propTypes = { - paths: PropTypes.arrayOf(PropTypes.object), - radius: PropTypes.number.isRequired, -}; - -ChartSVG.defaultProps = { - paths: [], -}; diff --git a/app/panel-android/components/content/DotsMenu.jsx b/app/panel-android/components/content/DotsMenu.jsx index a612ea2ab..ae468053c 100644 --- a/app/panel-android/components/content/DotsMenu.jsx +++ b/app/panel-android/components/content/DotsMenu.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -13,50 +13,52 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ClassNames from 'classnames'; -export default class DotsMenu extends React.Component { +class DotsMenu extends React.Component { constructor(props) { super(props); this.state = { - opening: false, + open: false, + unmounted: false, }; } - componentDidMount() { - window.addEventListener('click', this.handleClick, false); - } - componentWillUnmount() { - window.removeEventListener('click', this.handleClick, false); + this.setState({ unmounted: true }); + window.removeEventListener('click', this.closeDotsMenu); } - /* Close the menu if user clicks anywhere on the window */ - handleClick = (event) => { - const { opening } = this.state; - if (opening && event.target.className.indexOf('dots-menu-btn') === -1) { - this.setState({ - opening: false, - }); + closeDotsMenu = () => { + window.removeEventListener('click', this.closeDotsMenu); + const { unmounted } = this.state; + if (!unmounted) { // Can I remove this and still have no React Warning? + this.setState({ open: false }); } } - /* Toggle menu */ - dotsButtonClicked = () => { - this.setState(prevState => ({ opening: !prevState.opening })); + clickDotsMenu = (event) => { + event.stopPropagation(); + window.addEventListener('click', this.closeDotsMenu); + this.setState(prevState => ({ open: !prevState.open })); } render() { const { actions } = this.props; - const { opening } = this.state; + const { open } = this.state; + const menuContentClassNames = ClassNames('DotsMenu__content', { + DotsMenu__open: open, + }); + return ( -
- + ))} @@ -67,5 +69,11 @@ export default class DotsMenu extends React.Component { } DotsMenu.propTypes = { - actions: PropTypes.arrayOf(PropTypes.object).isRequired, + actions: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + callback: PropTypes.func.isRequired, + })).isRequired, }; + +export default DotsMenu; diff --git a/app/panel-android/components/content/FixedMenu.jsx b/app/panel-android/components/content/FixedMenu.jsx deleted file mode 100644 index 3301bf4ff..000000000 --- a/app/panel-android/components/content/FixedMenu.jsx +++ /dev/null @@ -1,147 +0,0 @@ -/** - * FixedMenu 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 MenuItem from './MenuItem'; - -export default class FixedMenu extends React.Component { - constructor(props) { - super(props); - this.state = { - open: false, - currentMenuItemText: FixedMenu.defaultHeaderText, - }; - } - - static get defaultHeaderText() { - return 'Enhanced Options'; - } - - get cliqzModuleData() { - const { cliqzModuleData } = this.props; - return cliqzModuleData || {}; - } - - get antiTrackingData() { - return this.cliqzModuleData.antitracking || {}; - } - - get adBlockData() { - return this.cliqzModuleData.adblock || {}; - } - - get smartBlockData() { - const { panel } = this.props; - return panel.smartBlock || {}; - } - - getCount = (type) => { - let total = 0; - switch (type) { - case 'enable_anti_tracking': { - const categories = Object.keys(this.antiTrackingData); - categories.forEach((category) => { - const apps = Object.keys(this.antiTrackingData[category]); - apps.forEach((app) => { - if (this.antiTrackingData[category][app] === 'unsafe') { - total++; - } - }); - }); - return total; - } - case 'enable_ad_block': - return (this.adBlockData && this.adBlockData.totalCount) || 0; - case 'enable_smart_block': - Object.keys(this.smartBlockData.blocked || {}).forEach(() => { - total++; - }); - Object.keys(this.smartBlockData.unblocked || {}).forEach(() => { - total++; - }); - return total; - default: - return 0; - } - } - - toggleMenu = () => { - this.setState(prevState => ({ open: !prevState.open })); - } - - updateHeaderText = (text) => { - const textToShow = text || FixedMenu.defaultHeaderText; - - this.setState({ - currentMenuItemText: textToShow, - }); - } - - render() { - const { panel } = this.props; - const { open, currentMenuItemText } = this.state; - return ( -
-
-

{currentMenuItemText}

-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- ); - } -} - -FixedMenu.propTypes = { - panel: PropTypes.shape, - cliqzModuleData: PropTypes.shape, -}; - -FixedMenu.defaultProps = { - panel: {}, - cliqzModuleData: {}, -}; diff --git a/app/panel-android/components/content/MenuItem.jsx b/app/panel-android/components/content/MenuItem.jsx deleted file mode 100644 index 9bc4e765e..000000000 --- a/app/panel-android/components/content/MenuItem.jsx +++ /dev/null @@ -1,105 +0,0 @@ -/** - * MenuItem 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'; - -export default class MenuItem extends React.Component { - constructor(props) { - super(props); - - this.state = { - opening: false, - }; - } - - menuItemClicked = () => { - const { updateHeaderText, title } = this.props; - this.setState({ - opening: true, - }); - - updateHeaderText(title); - } - - closeButtonClicked = () => { - const { updateHeaderText } = this.props; - this.setState({ - opening: false, - }); - - updateHeaderText(''); - } - - switcherClicked = () => { - const { active, type } = this.props; - const { callGlobalAction } = this.context; - callGlobalAction({ - actionName: 'cliqzFeatureToggle', - actionData: { - currentState: active, - type, - }, - }); - } - - render() { - const { - type, - numData, - title, - description, - active, - headline, - } = this.props; - const { opening } = this.state; - return ( -
-
- {numData} - {title} -

{description}

-
- -
- {numData} -

{headline}

-

{description}

-
-
- ); - } -} - -MenuItem.propTypes = { - active: PropTypes.bool, - type: PropTypes.string, - title: PropTypes.string, - numData: PropTypes.number, - headline: PropTypes.string, - description: PropTypes.string, -}; - -MenuItem.defaultProps = { - active: false, - type: '', - title: '', - numData: 0, - headline: '', - description: '', -}; - -MenuItem.contextTypes = { - callGlobalAction: PropTypes.func, -}; diff --git a/app/panel-android/components/content/OverviewTab.jsx b/app/panel-android/components/content/OverviewTab.jsx new file mode 100644 index 000000000..07a67b41f --- /dev/null +++ b/app/panel-android/components/content/OverviewTab.jsx @@ -0,0 +1,431 @@ +/** + * Overview Tab Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import ClassNames from 'classnames'; +import PropTypes from 'prop-types'; +import { + NotScanned, + DonutGraph, + GhosteryFeature, + PauseButton, + CliqzFeature +} from '../../../panel/components/BuildingBlocks'; +import globals from '../../../../src/classes/Globals'; + +const { + IS_CLIQZ, + WHITELISTED, BLACKLISTED +} = globals; + +class OverviewTab extends React.Component { + constructor(props) { + super(props); + + this.pauseOptions = [ + { name: t('pause_30_min'), val: 30 }, + { name: t('pause_1_hour'), val: 60 }, + { name: t('pause_24_hours'), val: 1440 }, + ]; + } + + get siteNotScanned() { + const { blocking, summary } = this.props; + const { siteNotScanned, pageUrl } = blocking; + const { categories } = summary; + const searchRegEx = /http|chrome-extension|moz-extension|ms-browser-extension|newtab|chrome:\/\/startpage\//; + + if (siteNotScanned || !categories || pageUrl.search(searchRegEx) === -1) { + return true; + } + return false; + } + + get adBlockBlocked() { + const { panel, cliqzModuleData } = this.props; + const { enable_ad_block } = panel; + const { adBlock } = cliqzModuleData; + + return (enable_ad_block && adBlock.trackerCount) || 0; + } + + get antiTrackUnsafe() { + const { panel, cliqzModuleData } = this.props; + const { enable_anti_tracking } = panel; + const { antiTracking } = cliqzModuleData; + + return (enable_anti_tracking && antiTracking.trackerCount) || 0; + } + + get trackersFound() { + const { summary } = this.props; + const { trackerCounts } = summary; + + return (trackerCounts && (trackerCounts.allowed + trackerCounts.blocked)) || 0; + } + + get smartBlockBlocked() { + const { panel, summary } = this.props; + const { smartBlock } = panel; + const { trackerCounts } = summary; + + let sbBlocked = (smartBlock && smartBlock.blocked && Object.keys(smartBlock.blocked).length) || 0; + if (sbBlocked === trackerCounts.sbBlocked) { + sbBlocked = 0; + } + + return sbBlocked; + } + + get smartBlockAllowed() { + const { panel, summary } = this.props; + const { smartBlock } = panel; + const { trackerCounts } = summary; + + let sbAllowed = (smartBlock && smartBlock.unblocked && Object.keys(smartBlock.unblocked).length) || 0; + if (sbAllowed === trackerCounts.sbAllowed) { + sbAllowed = 0; + } + + return sbAllowed; + } + + get smartBlockAdjust() { + const { panel } = this.props; + const { enable_smart_block } = panel; + + return enable_smart_block && ((this.smartBlockBlocked - this.smartBlockAllowed) || 0); + } + + get trackersBlockedCount() { + const { summary } = this.props; + const { paused_blocking, sitePolicy, trackerCounts } = summary; + + let totalTrackersBlockedCount; + if (paused_blocking || sitePolicy === WHITELISTED) { + totalTrackersBlockedCount = 0; + } else if (sitePolicy === BLACKLISTED) { + totalTrackersBlockedCount = trackerCounts.blocked + trackerCounts.allowed || 0; + } else { + totalTrackersBlockedCount = trackerCounts.blocked + this.smartBlockAdjust || 0; + } + + return totalTrackersBlockedCount; + } + + get requestsModifiedCount() { + return this.adBlockBlocked + this.antiTrackUnsafe; + } + + handleTrustButtonClick = () => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'handleTrustButtonClick', + }); + } + + handleRestrictButtonClick = () => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'handleRestrictButtonClick', + }); + } + + handlePauseButtonClick = (time) => { + const { summary, callGlobalAction } = this.props; + const { paused_blocking } = summary; + + callGlobalAction({ + actionName: 'handlePauseButtonClick', + actionData: { + paused_blocking: (typeof time === 'number' ? true : !paused_blocking), + time: typeof time === 'number' ? time * 60000 : 0, + }, + }); + } + + handleCliqzFeatureClick = ({ feature, status }) => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'cliqzFeatureToggle', + actionData: { + currentState: status, + type: feature, + }, + }); + } + + _renderNavigationLinks() { + const { clickAccount, clickSettings } = this.props; + const accountIcon = ( + + + + + + + + + ); + + const settingsIcon = ( + + + + + + ); + + return ( +
+
+
+ {accountIcon} +
+
+ {settingsIcon} +
+
+
+ ); + } + + _renderDonut() { + const { + blocking, + cliqzModuleData, + summary, + } = this.props; + const { categories } = blocking; + const { adBlock, antiTracking } = cliqzModuleData; + const { sitePolicy, paused_blocking } = summary; + + return ( + + ); + } + + _renderPageHost() { + const { summary } = this.props; + const { pageHost = 'page_host' } = summary; + const pageHostClassNames = ClassNames('OverviewTab__PageHostText', { + invisible: (pageHost.split('.').length < 2), + }); + + return ( + {pageHost} + ); + } + + _renderTotalTrackersBlocked() { + return ( +
+ + {t('trackers_blocked')} + {' '} + + + {this.trackersBlockedCount} + +
+ ); + } + + _renderTotalRequestsModified() { + return ( +
+ + {t('requests_modified')} + {' '} + + + {this.requestsModifiedCount} + +
+ ); + } + + _renderGhosteryFeatures() { + const { summary } = this.props; + const { paused_blocking, paused_blocking_timeout, sitePolicy } = summary; + const disableBlocking = this.siteNotScanned; + + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); + } + + _renderCliqzFeatures() { + const { panel, summary } = this.props; + const { enable_anti_tracking, enable_ad_block, enable_smart_block } = panel; + const { paused_blocking, sitePolicy } = summary; + const disableBlocking = this.siteNotScanned; + + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); + } + + render() { + return ( +
+ {this._renderNavigationLinks()} + + {this.siteNotScanned && ( +
+ +
+ )} + + {!this.siteNotScanned && ( +
+
+ {this._renderDonut()} +
+
+ {this._renderPageHost()} +
+
+ {this._renderTotalTrackersBlocked()} + {this._renderTotalRequestsModified()} +
+
+ )} + +
+ {this._renderGhosteryFeatures()} +
+ +
+ {this._renderCliqzFeatures()} +
+
+ ); + } +} + +OverviewTab.propTypes = { + panel: PropTypes.shape({ + enable_ad_block: PropTypes.bool.isRequired, + enable_anti_tracking: PropTypes.bool.isRequired, + enable_smart_block: PropTypes.bool.isRequired, + smartBlock: PropTypes.shape({ + blocked: PropTypes.shape({}).isRequired, + unblocked: PropTypes.shape({}).isRequired, + }).isRequired, + }).isRequired, + summary: PropTypes.shape({ + categories: PropTypes.array.isRequired, + trackerCounts: PropTypes.shape({ + allowed: PropTypes.number.isRequired, + blocked: PropTypes.number.isRequired, + }).isRequired, + sitePolicy: PropTypes.oneOf([ + false, + WHITELISTED, + BLACKLISTED, + ]).isRequired, + paused_blocking: PropTypes.bool.isRequired, + }).isRequired, + blocking: PropTypes.shape({ + siteNotScanned: PropTypes.bool.isRequired, + pageUrl: PropTypes.string.isRequired, + }).isRequired, + cliqzModuleData: PropTypes.shape({ + adBlock: PropTypes.shape({ + trackerCount: PropTypes.number.isRequired, + }).isRequired, + antiTracking: PropTypes.shape({ + trackerCount: PropTypes.number.isRequired, + }).isRequired, + }).isRequired, + clickAccount: PropTypes.func.isRequired, + clickSettings: PropTypes.func.isRequired, + callGlobalAction: PropTypes.func.isRequired, +}; + +export default OverviewTab; diff --git a/app/panel-android/components/content/Path.jsx b/app/panel-android/components/content/Path.jsx deleted file mode 100644 index 8f37a2d4d..000000000 --- a/app/panel-android/components/content/Path.jsx +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Path 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'; - -const INTERVAL = 1000; // Define the maximum rendering time for this path - -export default class Path extends React.Component { - constructor(props) { - super(props); - this.myRef = React.createRef(); - this.timer = null; - } - - static polarToCartesian(centerX, centerY, radius, angleInDegrees) { - const angleInRadians = (angleInDegrees - 90) * (Math.PI / 180.0); - - return { - x: centerX + (radius * Math.cos(angleInRadians)), - y: centerY + (radius * Math.sin(angleInRadians)) - }; - } - - static describeArc(x, y, radius, startAngle, endAngle) { - const start = Path.polarToCartesian(x, y, radius, startAngle); - const end = Path.polarToCartesian(x, y, radius, endAngle); - - const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1'; - - const d = [ - 'M', start.x, start.y, - 'A', radius, radius, 0, largeArcFlag, 1, end.x, end.y - ].join(' '); - - return d; - } - - componentDidMount() { - const node = this.myRef.current; - node.style.setProperty('--stroke-length', `${node.getTotalLength()}`); - // Check and call props.handler() if the animationEnd event doesn't get fired somehow - this.timer = setInterval(() => { - clearInterval(this.timer); // Run this only once - const { handler } = this.props; - handler(); - }, INTERVAL); - } - - componentWillUnmount() { - clearInterval(this.timer); - } - - onAnimationEndHandler = () => { - clearInterval(this.timer); - const { handler } = this.props; - handler(); - } - - render() { - const { radius, path } = this.props; - const { start, category } = path; - // Fix error for single path - const end = path.end === 360 ? 359.9999 : path.end; - - const d = Path.describeArc(0, 0, radius, start, end); - - return ( - - ); - } -} - -Path.propTypes = { - radius: PropTypes.number.isRequired, - path: PropTypes.shape, - handler: PropTypes.func.isRequired, -}; - -Path.defaultProps = { - path: {}, -}; diff --git a/app/panel-android/components/content/Settings.jsx b/app/panel-android/components/content/Settings.jsx new file mode 100644 index 000000000..38698a3cb --- /dev/null +++ b/app/panel-android/components/content/Settings.jsx @@ -0,0 +1,324 @@ +/** + * Settings Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { ReactSVG } from 'react-svg'; +import TrustAndRestrict from '../../../panel/components/Settings/TrustAndRestrict'; +import GeneralSettings from '../../../panel/components/Settings/GeneralSettings'; +import AdBlocker from '../../../panel/components/Settings/AdBlocker'; +import Notifications from '../../../panel/components/Settings/Notifications'; +import OptIn from '../../../panel/components/Settings/OptIn'; +import ImportExport from '../../../panel/components/Settings/ImportExport'; +import Help from '../../../panel/components/Help'; +import About from '../../../panel/components/About'; +import globals from '../../../../src/classes/Globals'; + +const { IS_CLIQZ } = globals; + +class Settings extends React.Component { + constructor(props) { + super(props); + + this.state = { + view: 'settings-home', + }; + } + + clickBack = () => { + const { clickHome } = this.props; + const { view } = this.state; + + if (view === 'settings-home') { + clickHome(); + } else { + this.setState({ view: 'settings-home' }); + } + } + + updateSitePolicy = ({ type, pageHost }) => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'updateSitePolicy', + actionData: { type, pageHost }, + }); + } + + updateDatabase = () => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'updateDatabase', + }); + } + + toggleCheckbox = (event) => { + const { callGlobalAction } = this.props; + const { name, checked } = event.currentTarget; + + callGlobalAction({ + actionName: 'updateSettingCheckbox', + actionData: { name, checked }, + }); + } + + selectItem = ({ event, value }) => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'selectItem', + actionData: { event, value }, + }); + } + + exportSettings = () => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'exportSettings', + }); + } + + importSettingsDialog = () => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'importSettingsDialog', + }); + } + + importSettingsNative = (importFile) => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'importSettingsNative', + actionData: importFile, + }); + } + + _renderSettingsHeader() { + const { view } = this.state; + + let headerText; + switch (view) { + case 'settings-home': + headerText = t('panel_menu_settings'); + break; + case 'settings-trust-restrict': + headerText = t('settings_trust_and_restrict'); + break; + case 'settings-general': + headerText = t('settings_general_settings'); + break; + case 'settings-adblocker': + headerText = t('settings_adblocker'); + break; + case 'settings-notifications': + headerText = t('settings_notifications'); + break; + case 'settings-opt-in': + headerText = t('settings_opt_in'); + break; + case 'settings-import-export': + headerText = t('settings_import_export'); + break; + case 'settings-help': + headerText = t('panel_menu_help'); + break; + case 'settings-about': + headerText = t('panel_menu_about'); + break; + default: + headerText = ''; + } + + return ( +
+ + {headerText} +
+ ); + } + + _renderSettingsHome() { + return ( +
+
+
{ this.setState({ view: 'settings-trust-restrict' }); }}> + { t('settings_trust_and_restrict') } +
+
{ this.setState({ view: 'settings-general' }); }}> + { t('settings_general_settings') } +
+ {!IS_CLIQZ && ( +
{ this.setState({ view: 'settings-adblocker' }); }}> + { t('settings_adblocker') } +
+ )} +
{ this.setState({ view: 'settings-notifications' }); }}> + { t('settings_notifications') } +
+
{ this.setState({ view: 'settings-opt-in' }); }}> + { t('settings_opt_in') } +
+
{ this.setState({ view: 'settings-import-export' }); }}> + { t('settings_import_export') } +
+
{ this.setState({ view: 'settings-help' }); }}> + { t('panel_menu_help') } +
+
{ this.setState({ view: 'settings-about' }); }}> + { t('panel_menu_about') } +
+
+
+ ); + } + + _renderSettingsTrustRestrict() { + const { summary } = this.props; + const { site_whitelist, site_blacklist } = summary; + const actions = { + updateSitePolicy: this.updateSitePolicy, + }; + + return ( + + ); + } + + _renderSettingsGeneral() { + const { settings } = this.props; + const actions = { + updateDatabase: this.updateDatabase, + }; + + return ( + + ); + } + + _renderSettingsAdBlocker() { + const { settings } = this.props; + const actions = { + selectItem: this.selectItem, + }; + + return ( + + ); + } + + _renderSettingsNotification() { + const { settings } = this.props; + + return ( + + ); + } + + _renderSettingsOptIn() { + const { settings } = this.props; + + return ( + + ); + } + + _renderSettingsImportExport() { + const { summary, settings } = this.props; + const { pageUrl = '' } = summary; + const { + exportResultText = '', + importResultText = '', + actionSuccess = false, + } = settings; + const settingsData = { + pageUrl, + exportResultText, + importResultText, + actionSuccess, + }; + const actions = { + exportSettings: this.exportSettings, + importSettingsDialog: this.importSettingsDialog, + importSettingsNative: this.importSettingsNative, + }; + + return ( +
+
+
+ +
+
+
+ ); + } + + render() { + const { view } = this.state; + + return ( +
+ {this._renderSettingsHeader()} + {view === 'settings-home' && this._renderSettingsHome()} + {view === 'settings-trust-restrict' && this._renderSettingsTrustRestrict()} + {view === 'settings-general' && this._renderSettingsGeneral()} + {view === 'settings-adblocker' && this._renderSettingsAdBlocker()} + {view === 'settings-notifications' && this._renderSettingsNotification()} + {view === 'settings-opt-in' && this._renderSettingsOptIn()} + {view === 'settings-import-export' && this._renderSettingsImportExport()} + {view === 'settings-help' && ()} + {view === 'settings-about' && ()} +
+ ); + } +} + +Settings.propTypes = { + summary: PropTypes.shape({ + site_whitelist: PropTypes.arrayOf(PropTypes.string).isRequired, + site_blacklist: PropTypes.arrayOf(PropTypes.string).isRequired, + }).isRequired, + settings: PropTypes.shape({}).isRequired, + clickHome: PropTypes.func.isRequired, + callGlobalAction: PropTypes.func.isRequired, +}; + +export default Settings; diff --git a/app/panel-android/components/content/Tab.jsx b/app/panel-android/components/content/Tab.jsx index 07ee41c0c..686139544 100644 --- a/app/panel-android/components/content/Tab.jsx +++ b/app/panel-android/components/content/Tab.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -13,8 +13,9 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ClassNames from 'classnames'; -export default class Tab extends React.Component { +class Tab extends React.Component { handleTabClick = (event) => { event.preventDefault(); const { onClick, tabIndex } = this.props; @@ -23,12 +24,16 @@ export default class Tab extends React.Component { render() { const { isActive, tabLabel, linkClassName } = this.props; + const tabClassNames = ClassNames('Tab__navigation_item flex-container align-center-middle', { + 'Tab--active': isActive, + }); + const tabLinkClassNames = ClassNames('Tab__navigation_link', linkClassName, { + 'Tab--active': isActive, + }); + return ( -
  • - +
  • + {tabLabel}
  • @@ -41,14 +46,13 @@ Tab.propTypes = { tabIndex: PropTypes.number, isActive: PropTypes.bool, tabLabel: PropTypes.string.isRequired, - linkClassName: PropTypes.string.isRequired + linkClassName: PropTypes.string.isRequired, }; Tab.defaultProps = { onClick: () => null, tabIndex: -1, -}; - -Tab.defaultProps = { isActive: false, }; + +export default Tab; diff --git a/app/panel-android/components/content/Tabs.jsx b/app/panel-android/components/content/Tabs.jsx index 50c0a52fb..ec4c9de95 100644 --- a/app/panel-android/components/content/Tabs.jsx +++ b/app/panel-android/components/content/Tabs.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,13 +12,14 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; -export default class Tabs extends React.Component { +class Tabs extends React.Component { constructor(props) { super(props); this.state = { - activeTabIndex: 0 + activeTabIndex: 0, }; } @@ -29,17 +30,17 @@ export default class Tabs extends React.Component { } this.setState({ - activeTabIndex: tabIndex + activeTabIndex: tabIndex, }); } - renderTabsNav = () => { + renderTabsNavigation = () => { const { children } = this.props; const { activeTabIndex } = this.state; return React.Children.map(children, (child, index) => React.cloneElement(child, { onClick: this.handleTabClick, tabIndex: index, - isActive: index === activeTabIndex + isActive: index === activeTabIndex, })); } @@ -54,14 +55,33 @@ export default class Tabs extends React.Component { render() { return ( -
    -
      - {this.renderTabsNav()} +
      +
        + {this.renderTabsNavigation()}
      -
      +
      {this.renderActiveTabContent()}
      ); } } + +// ToDo: Validate that Tabs Children is Tab. +// Tried: +// children: PropTypes.oneOfType([ +// PropTypes.shape({ +// type: Tab +// }), +// PropTypes.arrayOf( +// PropTypes.shape({ +// type: Tab +// }) +// ) +// ]).isRequired, +// But failed because of this: https://github.com/vadimdemedes/ink/issues/37 +Tabs.propTypes = { + children: PropTypes.node.isRequired, +}; + +export default Tabs; diff --git a/app/panel-android/components/content/TrackerItem.jsx b/app/panel-android/components/content/TrackerItem.jsx deleted file mode 100644 index 329ad1fde..000000000 --- a/app/panel-android/components/content/TrackerItem.jsx +++ /dev/null @@ -1,194 +0,0 @@ -/** - * TrackerItem 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 getUrlFromTrackerId from '../../utils/tracker-info'; - -export default class TrackerItem extends React.Component { - get trackerSelectStatus() { - const { type, tracker } = this.props; - const { siteProps } = this.context; - // Only for site trackers - if (type === 'site-trackers') { - if (siteProps.isTrusted) { - return 'trusted'; - } - - if (siteProps.isRestricted) { - return 'restricted'; - } - } - - if (tracker.ss_allowed) { - return 'trusted'; - } - - if (tracker.ss_blocked) { - return 'restricted'; - } - - if (tracker.blocked) { - return 'blocked'; - } - - return ''; - } - - get showMenu() { - const { showMenu } = this.props; - return showMenu; - } - - get disabledStatus() { - return ['trusted', 'restricted'].includes(this.trackerSelectStatus) ? 'disabled' : ''; - } - - clickButtonTrust = () => { - const { - tracker, categoryId, index, toggleMenu - } = this.props; - const { callGlobalAction } = this.context; - const ss_allowed = !tracker.ss_allowed; - - callGlobalAction({ - actionName: 'trustRestrictBlockSiteTracker', - actionData: { - app_id: tracker.id, - cat_id: categoryId, - trust: ss_allowed, - restrict: false, - block: tracker.blocked, // Keep blocking - } - }); - toggleMenu(index); // Hide menu - } - - clickButtonRestrict = () => { - const { - tracker, categoryId, index, toggleMenu - } = this.props; - const { callGlobalAction } = this.context; - const ss_blocked = !tracker.ss_blocked; - callGlobalAction({ - actionName: 'trustRestrictBlockSiteTracker', - actionData: { - app_id: tracker.id, - cat_id: categoryId, - restrict: ss_blocked, - trust: false, - block: tracker.blocked, // Keep blocking - } - }); - toggleMenu(index); - } - - clickButtonBlock = (hideMenu = true) => { - // onClick={(e) => { e.stopPropagation(); this.clickButtonBlock(false); }} - const { - tracker, type, categoryId, index, toggleMenu - } = this.props; - const { callGlobalAction } = this.context; - if (this.disabledStatus) { - return; - } - - const blocked = !tracker.blocked; - - if (type === 'site-trackers') { - callGlobalAction({ - actionName: 'trustRestrictBlockSiteTracker', - actionData: { - app_id: tracker.id, - cat_id: categoryId, - block: blocked, - trust: false, - restrict: false, - } - }); - } else { - callGlobalAction({ - actionName: 'blockUnblockGlobalTracker', - actionData: { - app_id: tracker.id, - cat_id: categoryId, - block: blocked, - } - }); - } - - if (hideMenu) { - toggleMenu(index); - } - } - - openTrackerLink = () => { - const { tracker } = this.props; - const url = getUrlFromTrackerId(tracker.id); - const win = window.open(url, '_blank'); - win.focus(); - } - - toggleMenu = () => { - const { index, toggleMenu } = this.props; - toggleMenu(index); - } - - render() { - const { tracker, type } = this.props; - return ( -
    • -
      - - - -
      -
    • - - ); - } -} - -TrackerItem.propTypes = { - toggleMenu: PropTypes.func.isRequired, - index: PropTypes.number.isRequired, - showMenu: PropTypes.bool, - tracker: PropTypes.shape, - categoryId: PropTypes.string, - type: PropTypes.string, -}; - -TrackerItem.defaultProps = { - showMenu: false, - tracker: {}, - categoryId: '', - type: '', -}; - -TrackerItem.contextTypes = { - callGlobalAction: PropTypes.func, - siteProps: PropTypes.shape, -}; diff --git a/app/panel-android/components/content/TrackersChart.jsx b/app/panel-android/components/content/TrackersChart.jsx deleted file mode 100644 index 8781aa20f..000000000 --- a/app/panel-android/components/content/TrackersChart.jsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * TrackersChart 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 ChartSVG from './ChartSVG'; - -class TrackersChart extends React.Component { - constructor(props) { - super(props); - - this.state = { - config: { - radius: 100, - } - }; - } - - render() { - const { num, paths } = this.props; - const { config } = this.state; - return ( -
      - -

      - - {num} - {' '} - - Trackers found -

      -
      - ); - } -} - -TrackersChart.propTypes = { - paths: PropTypes.arrayOf(PropTypes.object), - num: PropTypes.number, -}; - -TrackersChart.defaultProps = { - paths: [], - num: 0, -}; - -export default TrackersChart; diff --git a/app/panel-android/components/content/__tests__/BlockingCategories.jsx b/app/panel-android/components/content/__tests__/BlockingCategories.jsx new file mode 100644 index 000000000..8496a46af --- /dev/null +++ b/app/panel-android/components/content/__tests__/BlockingCategories.jsx @@ -0,0 +1,253 @@ +/** + * BlockingCategories Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import BlockingCategories from '../BlockingCategories'; + +describe('app/panel-android/components/content/BlockingCategories.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('BlockingCategories component as site', () => { + const categories = [ + { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 1, + trackers: [ + { + id: 1, + name: 'Tracker 1', + ss_allowed: false, + ss_blocked: false, + blocked: true, + }, + ], + img_name: 'category-1-image-url', + }, + { + id: 'cat-2', + name: 'Category-2', + num_total: 5, + num_blocked: 3, + trackers: [ + { + id: 2, + name: 'Tracker 2', + ss_allowed: false, + ss_blocked: false, + blocked: true, + }, + { + id: 3, + name: 'Tracker 3', + ss_allowed: false, + ss_blocked: false, + blocked: true, + }, + { + id: 4, + name: 'Tracker 4', + ss_allowed: false, + ss_blocked: false, + blocked: true, + }, + { + id: 5, + name: 'Tracker 5', + ss_allowed: false, + ss_blocked: false, + blocked: false, + }, + { + id: 6, + name: 'Tracker 6', + ss_allowed: false, + ss_blocked: false, + blocked: false, + }, + ], + img_name: 'category-2-image-url', + }, + ]; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingCategories component as global', () => { + const categories = [ + { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 1, + trackers: [ + { + id: '1', + name: 'Tracker 1', + blocked: true, + }, + ], + img_name: 'category-1-image-url', + }, + { + id: 'cat-2', + name: 'Category-2', + num_total: 5, + num_blocked: 3, + trackers: [ + { + id: '2', + name: 'Tracker 2', + blocked: true, + }, + { + id: '3', + name: 'Tracker 3', + blocked: true, + }, + { + id: '4', + name: 'Tracker 4', + blocked: true, + }, + { + id: '5', + name: 'Tracker 5', + blocked: false, + }, + { + id: '6', + name: 'Tracker 6', + blocked: false, + }, + ], + img_name: 'category-2-image-url', + }, + ]; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Functionality tests shallow mounted with Enzyme', () => { + test('BlockingCategories component toggle category clicks work', () => { + const categories = [ + { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 1, + trackers: [ + { + id: '1', + name: 'Tracker 1', + blocked: true, + }, + ], + img_name: 'category-1-image-url', + }, + { + id: 'cat-2', + name: 'Category-2', + num_total: 5, + num_blocked: 3, + trackers: [ + { + id: '2', + name: 'Tracker 2', + blocked: true, + }, + { + id: '3', + name: 'Tracker 3', + blocked: true, + }, + { + id: '4', + name: 'Tracker 4', + blocked: true, + }, + { + id: '5', + name: 'Tracker 5', + blocked: false, + }, + { + id: '6', + name: 'Tracker 6', + blocked: false, + }, + ], + img_name: 'category-2-image-url', + }, + ]; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = shallow( + {}} + /> + ); + const instance = component.instance(); + + expect(component.state('openCategoryIndex')).toBe(-1); + expect(instance.getOpenStatus(0)).toBe(false); + + instance.toggleCategoryOpen(0); + expect(component.state('openCategoryIndex')).toBe(0); + expect(instance.getOpenStatus(0)).toBe(true); + + component.setProps({ type: 'global' }); + expect(component.state('openCategoryIndex')).toBe(-1); + expect(instance.getOpenStatus(0)).toBe(false); + }); + }); +}); diff --git a/app/panel-android/components/content/__tests__/BlockingCategory.jsx b/app/panel-android/components/content/__tests__/BlockingCategory.jsx new file mode 100644 index 000000000..05571129f --- /dev/null +++ b/app/panel-android/components/content/__tests__/BlockingCategory.jsx @@ -0,0 +1,368 @@ +/** + * BlockingCategory Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import BlockingCategory from '../BlockingCategory'; + +describe('app/panel-android/components/content/BlockingCategory.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('BlockingCategory component when sitePolicy Restricted', () => { + const category = { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 0, + trackers: [], + img_name: 'category-1-image-url', + }; + const siteProps = { + isTrusted: false, + isRestricted: true, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + type="site" + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingCategory component when sitePolicy Trusted', () => { + const category = { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 0, + trackers: [], + img_name: 'category-1-image-url', + }; + const siteProps = { + isTrusted: true, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + type="site" + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingCategory component when sitePolicy Trusted & Paused', () => { + const category = { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 0, + trackers: [], + img_name: 'category-1-image-url', + }; + const siteProps = { + isTrusted: true, + isRestricted: false, + isPaused: true, + }; + + const component = renderer.create( + {}} + open={false} + type="site" + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingCategory component as site with all trackers ss_blocked', () => { + const category = { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 0, + trackers: [ + { + id: '1', + name: 'Tracker 1', + blocked: false, + ss_allowed: false, + ss_blocked: true, + }, + ], + img_name: 'category-1-image-url', + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + type="site" + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingCategory component as site with all trackers ss_allowed', () => { + const category = { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 0, + trackers: [ + { + id: '1', + name: 'Tracker 1', + blocked: false, + ss_allowed: true, + ss_blocked: true, + }, + ], + img_name: 'category-1-image-url', + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + type="site" + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingCategory component as global with no trackers blocked', () => { + const category = { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 0, + trackers: [ + { + id: '1', + name: 'Tracker 1', + blocked: false, + }, + ], + img_name: 'category-1-image-url', + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + type="global" + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingCategory component as global with all trackers blocked', () => { + const category = { + id: 'cat-1', + name: 'Category-1', + num_total: 1, + num_blocked: 1, + trackers: [ + { + id: '1', + name: 'Tracker 1', + blocked: true, + }, + ], + img_name: 'category-1-image-url', + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + type="global" + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingCategory component as global with mixed trackers blocked', () => { + const category = { + id: 'cat-1', + name: 'Category-1', + num_total: 2, + num_blocked: 1, + trackers: [ + { + id: '1', + name: 'Tracker 1', + blocked: false, + }, + { + id: '2', + name: 'Tracker 2', + blocked: true, + }, + ], + img_name: 'category-1-image-url', + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + type="global" + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Functionality tests shallow mounted with Enzyme', () => { + test('BlockingCategory component category click works', () => { + const category = { + id: 'cat-1', + name: 'Category-1', + num_total: 2, + num_blocked: 1, + trackers: [ + { + id: '1', + name: 'Tracker 1', + blocked: false, + }, + { + id: '2', + name: 'Tracker 2', + blocked: true, + }, + ], + img_name: 'category-1-image-url', + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const toggleCategoryOpen = jest.fn(); + const callGlobalAction = jest.fn(); + + const component = shallow( + + ); + + expect(component.find('.BlockingCategory__listHeader').length).toBe(0); + component.find('.BlockingCategory__details').simulate('click'); + component.setProps({ open: true }); + expect(toggleCategoryOpen.mock.calls.length).toBe(1); + expect(component.find('.BlockingCategory__listHeader').length).toBe(1); + + expect(callGlobalAction.mock.calls.length).toBe(0); + component.find('.BlockingSelectButton').simulate('click', { stopPropagation: () => {} }); + component.setProps({ type: 'global' }); + component.find('.BlockingSelectButton').simulate('click', { stopPropagation: () => {} }); + + expect(callGlobalAction.mock.calls[0][0].actionData.type).toBe('site'); + expect(callGlobalAction.mock.calls[1][0].actionData.type).toBe('global'); + }); + }); +}); diff --git a/app/panel-android/components/content/__tests__/BlockingTab.jsx b/app/panel-android/components/content/__tests__/BlockingTab.jsx new file mode 100644 index 000000000..3d1fc940e --- /dev/null +++ b/app/panel-android/components/content/__tests__/BlockingTab.jsx @@ -0,0 +1,102 @@ +/** + * BlockingTab Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import BlockingTab from '../BlockingTab'; + +describe('app/panel-android/components/content/BlockingTab.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('BlockingTab component as site with falsy props', () => { + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingTab component as global with falsy props', () => { + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingTab component as site with tracker falsy props', () => { + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingTab component as global with tracker truthy props', () => { + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/panel-android/components/content/__tests__/BlockingTracker.jsx b/app/panel-android/components/content/__tests__/BlockingTracker.jsx new file mode 100644 index 000000000..97bb43659 --- /dev/null +++ b/app/panel-android/components/content/__tests__/BlockingTracker.jsx @@ -0,0 +1,282 @@ +/** + * BlockingTracker Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import BlockingTracker from '../BlockingTracker'; + +describe('app/panel-android/components/content/BlockingTracker.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('BlockingTracker component with falsy props', () => { + const tracker = { + id: 1, + name: 'Tracker 1', + ss_allowed: false, + ss_blocked: false, + blocked: false, + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + settings={{}} + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingTracker component when tracker blocked', () => { + const tracker = { + id: 1, + name: 'Tracker 1', + ss_allowed: false, + ss_blocked: false, + blocked: true, + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + settings={{ toggle_individual_trackers: false }} + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingTracker component when tracker allowed', () => { + const tracker = { + id: 1, + name: 'Tracker 1', + ss_allowed: true, + ss_blocked: false, + blocked: false, + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + settings={{ toggle_individual_trackers: true }} + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingTracker component when tracker restricted', () => { + const tracker = { + id: 1, + name: 'Tracker 1', + ss_allowed: false, + ss_blocked: true, + blocked: false, + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + settings={{ toggle_individual_trackers: true }} + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingTracker component when site Trusted', () => { + const tracker = { + id: 1, + name: 'Tracker 1', + ss_allowed: false, + ss_blocked: false, + blocked: false, + }; + const siteProps = { + isTrusted: true, + isRestricted: false, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + settings={{ toggle_individual_trackers: true }} + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingTracker component when site Restricted', () => { + const tracker = { + id: 1, + name: 'Tracker 1', + ss_allowed: false, + ss_blocked: false, + blocked: false, + }; + const siteProps = { + isTrusted: false, + isRestricted: true, + isPaused: false, + }; + + const component = renderer.create( + {}} + open={false} + settings={{ toggle_individual_trackers: true }} + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('BlockingTracker component when site Paused', () => { + const tracker = { + id: 1, + name: 'Tracker 1', + ss_allowed: false, + ss_blocked: false, + blocked: false, + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: true, + }; + + const component = renderer.create( + {}} + open={false} + settings={{ toggle_individual_trackers: true }} + siteProps={siteProps} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Functionality tests shallow mounted with Enzyme', () => { + test('BlockingTracker component clicks work', () => { + const tracker = { + id: 1, + name: 'Tracker 1', + ss_allowed: false, + ss_blocked: false, + blocked: false, + }; + const siteProps = { + isTrusted: false, + isRestricted: false, + isPaused: false, + }; + + const toggleTrackerSelectOpen = jest.fn(); + const callGlobalAction = jest.fn(); + + const component = shallow( + + ); + + expect(component.find('.BlockingSelectGroup.BlockingSelectGroup--open').length).toBe(0); + component.find('.BlockingTracker').simulate('click'); + component.setProps({ open: true }); + expect(toggleTrackerSelectOpen.mock.calls.length).toBe(1); + expect(component.find('.BlockingSelectGroup.BlockingSelectGroup--open').length).toBe(1); + + expect(callGlobalAction.mock.calls.length).toBe(0); + component.find('.BlockingSelect__block').simulate('click'); + component.setProps({ type: 'site' }); + component.find('.BlockingSelect__block').simulate('click'); + component.find('.BlockingSelect__restrict').simulate('click'); + component.find('.BlockingSelect__trust').simulate('click'); + expect(callGlobalAction.mock.calls[0][0].actionName).toBe('blockUnblockGlobalTracker'); + expect(callGlobalAction.mock.calls[1][0].actionName).toBe('trustRestrictBlockSiteTracker'); + expect(callGlobalAction.mock.calls[2][0].actionData.restrict).toBe(true); + expect(callGlobalAction.mock.calls[3][0].actionData.trust).toBe(true); + }); + }); +}); diff --git a/app/panel-android/components/content/__tests__/DotsMenu.jsx b/app/panel-android/components/content/__tests__/DotsMenu.jsx new file mode 100644 index 000000000..da4545ab3 --- /dev/null +++ b/app/panel-android/components/content/__tests__/DotsMenu.jsx @@ -0,0 +1,104 @@ +/** + * DotsMenu Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import DotsMenu from '../DotsMenu'; + +describe('app/panel-android/components/content/DotsMenu.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('DotsMenu component with 0 actions', () => { + const actions = []; + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('DotsMenu component with 3 actions', () => { + const actions = [ + { + id: 'action-1', + name: 'Action One', + callback: () => {}, + }, + { + id: 'action-2', + name: 'Action Two', + callback: () => {}, + }, + { + id: 'action-3', + name: 'Action Three', + callback: () => {}, + } + ]; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Functionality tests mounted with Enzyme', () => { + test('DotsMenu component with 3 actions happy path', () => { + const actions = [ + { + id: 'action-1', + name: 'Action One', + callback: jest.fn(), + }, + { + id: 'action-2', + name: 'Action Two', + callback: jest.fn(), + }, + { + id: 'action-3', + name: 'Action Three', + callback: jest.fn(), + } + ]; + + const component = mount( + + ); + expect(component.find('.DotsMenu').length).toBe(1); + expect(component.find('.DotsMenu__button').length).toBe(1); + expect(component.find('.DotsMenu__content').length).toBe(1); + expect(component.find('.DotsMenu__content.DotsMenu__open').length).toBe(0); + expect(component.find('.DotsMenu__item').length).toBe(3); + + component.setState({ open: true }); + expect(component.find('.DotsMenu__content.DotsMenu__open').length).toBe(1); + expect(component.find('.DotsMenu__item').length).toBe(3); + expect(actions[0].callback.mock.calls.length).toBe(0); + expect(actions[1].callback.mock.calls.length).toBe(0); + expect(actions[2].callback.mock.calls.length).toBe(0); + component.find('.DotsMenu__item').at(0).simulate('click'); + expect(actions[0].callback.mock.calls.length).toBe(1); + component.find('.DotsMenu__item').at(1).simulate('click'); + expect(actions[1].callback.mock.calls.length).toBe(1); + component.find('.DotsMenu__item').at(2).simulate('click'); + expect(actions[0].callback.mock.calls.length).toBe(1); + expect(actions[1].callback.mock.calls.length).toBe(1); + expect(actions[2].callback.mock.calls.length).toBe(1); + + component.setState({ open: false }); + expect(component.find('.DotsMenu__content.DotsMenu__open').length).toBe(0); + expect(component.find('.DotsMenu__item').length).toBe(3); + }); + }); +}); diff --git a/app/panel-android/components/content/__tests__/OverviewTab.jsx b/app/panel-android/components/content/__tests__/OverviewTab.jsx new file mode 100644 index 000000000..4fec3f938 --- /dev/null +++ b/app/panel-android/components/content/__tests__/OverviewTab.jsx @@ -0,0 +1,155 @@ +/** + * OverviewTab Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import OverviewTab from '../OverviewTab'; + +jest.mock('../../../../panel/components/Tooltip'); + +describe('app/panel-android/components/content/OverviewTab.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('OverviewTab component with falsy props and SiteNotScanned', () => { + const panel = { + enable_ad_block: false, + enable_anti_tracking: false, + enable_smart_block: false, + smartBlock: { blocked: {}, unblocked: {} }, + }; + const summary = { + categories: [], + trackerCounts: { + allowed: 0, + blocked: 0, + }, + sitePolicy: false, + paused_blocking: false, + }; + const blocking = { + siteNotScanned: true, + pageUrl: '', + }; + const cliqzModuleData = { + adBlock: { trackerCount: 0 }, + antiTracking: { trackerCount: 0 }, + }; + + const component = renderer.create( + {}} + clickSettings={() => {}} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('OverviewTab component with truthy props and no SiteNotScanned', () => { + const panel = { + enable_ad_block: true, + enable_anti_tracking: true, + enable_smart_block: true, + smartBlock: { blocked: { 1: true }, unblocked: { 2: true, 3: true } }, + }; + const summary = { + categories: ['ads', 'trackers'], + trackerCounts: { + allowed: 3, + blocked: 5, + }, + sitePolicy: false, + paused_blocking: true, + }; + const blocking = { + siteNotScanned: false, + pageUrl: 'http://example.com', + }; + const cliqzModuleData = { + adBlock: { trackerCount: 8 }, + antiTracking: { trackerCount: 13 }, + }; + + const component = renderer.create( + {}} + clickSettings={() => {}} + callGlobalAction={() => {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Functionality tests mounted with Enzyme', () => { + test('OverviewTab component clicks work', () => { + const panel = { + enable_ad_block: false, + enable_anti_tracking: false, + enable_smart_block: false, + smartBlock: { blocked: {}, unblocked: {} }, + }; + const summary = { + categories: [], + trackerCounts: { + allowed: 0, + blocked: 0, + }, + sitePolicy: false, + paused_blocking: false, + }; + const blocking = { + siteNotScanned: true, + pageUrl: '', + }; + const cliqzModuleData = { + adBlock: { trackerCount: 0 }, + antiTracking: { trackerCount: 0 }, + }; + + const clickAccount = jest.fn(); + const clickSettings = jest.fn(); + + const component = mount( + {}} + /> + ); + expect(clickAccount.mock.calls.length).toBe(0); + expect(clickSettings.mock.calls.length).toBe(0); + expect(component.find('.OverviewTab__NavigationLink').length).toBe(2); + + component.find('.OverviewTab__NavigationLink').at(0).simulate('click'); + expect(clickAccount.mock.calls.length).toBe(1); + expect(clickSettings.mock.calls.length).toBe(0); + + component.find('.OverviewTab__NavigationLink').at(1).simulate('click'); + expect(clickAccount.mock.calls.length).toBe(1); + expect(clickSettings.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/app/panel-android/components/content/__tests__/Tabs.jsx b/app/panel-android/components/content/__tests__/Tabs.jsx new file mode 100644 index 000000000..30ed48514 --- /dev/null +++ b/app/panel-android/components/content/__tests__/Tabs.jsx @@ -0,0 +1,92 @@ +/** + * Tabs Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import Tabs from '../Tabs'; +import Tab from '../Tab'; + +describe('app/panel-android/components/content/Tabs.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('Tabs component with 3 Tab children components', () => { + const component = renderer.create( + + +
      Tab 1 Content
      +
      + + +
      Tab 2 Content
      +
      + + +
      Tab 3 Content Part I
      +
      Tab 3 Content Part II
      +
      Tab 3 Content Part III
      +
      +
      + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Functionality tests mounted with Enzyme', () => { + test('Tabs component with 3 Tab children components happy path', () => { + const component = mount( + + +
      Tab 1 Content
      +
      + + +
      Tab 2 Content
      +
      + + +
      Tab 3 Content Part I
      +
      Tab 3 Content Part II
      +
      Tab 3 Content Part III
      +
      +
      + ); + expect(component.find('.Tabs__component').length).toBe(1); + expect(component.find('.Tabs__navigation').length).toBe(1); + expect(component.find('.Tab__navigation_item').length).toBe(3); + expect(component.find('.Tab__navigation_item.tab-1-class').length).toBe(0); + expect(component.find('.Tab__navigation_item.Tab--active').length).toBe(1); + expect(component.find('.Tab__navigation_link').length).toBe(3); + expect(component.find('.Tab__navigation_link.Tab--active').length).toBe(1); + expect(component.find('.Tab__navigation_link.Tab--active.tab-1-class').length).toBe(1); + expect(component.find('.Tab__navigation_link.Tab--active.tab-2-class').length).toBe(0); + expect(component.find('.Tab__navigation_link.Tab--active.tab-3-class').length).toBe(0); + expect(component.find('.Tabs__active_content').length).toBe(1); + expect(component.find('.Tabs__active_content .tab-1-content').length).toBe(1); + expect(component.find('.Tabs__active_content .tab-2-content').length).toBe(0); + + component.setState({ activeTabIndex: 1 }); + expect(component.find('.Tab__navigation_link.Tab--active.tab-1-class').length).toBe(0); + expect(component.find('.Tab__navigation_link.Tab--active.tab-2-class').length).toBe(1); + expect(component.find('.Tab__navigation_link.Tab--active.tab-3-class').length).toBe(0); + expect(component.find('.Tabs__active_content .tab-1-content').length).toBe(0); + expect(component.find('.Tabs__active_content .tab-2-content').length).toBe(1); + + component.setState({ activeTabIndex: 2 }); + expect(component.find('.Tab__navigation_link.Tab--active.tab-1-class').length).toBe(0); + expect(component.find('.Tab__navigation_link.Tab--active.tab-2-class').length).toBe(0); + expect(component.find('.Tab__navigation_link.Tab--active.tab-3-class').length).toBe(1); + expect(component.find('.Tabs__active_content .tab-1-content').length).toBe(0); + expect(component.find('.Tabs__active_content .tab-2-content').length).toBe(0); + }); + }); +}); diff --git a/app/panel-android/components/content/__tests__/__snapshots__/BlockingCategories.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/BlockingCategories.jsx.snap new file mode 100644 index 000000000..3e2a53b13 --- /dev/null +++ b/app/panel-android/components/content/__tests__/__snapshots__/BlockingCategories.jsx.snap @@ -0,0 +1,243 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel-android/components/content/BlockingCategories.jsx Snapshot tests with react-test-renderer BlockingCategories component as global 1`] = ` +
      +
      +
      + +
      +

      + Category-1 +

      +
      + + 1 blocking_category_tracker + + + 1 blocking_category_blocked + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + +
      +

      + Category-2 +

      +
      + + 5 blocking_category_trackers + + + 3 blocking_category_blocked + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; + +exports[`app/panel-android/components/content/BlockingCategories.jsx Snapshot tests with react-test-renderer BlockingCategories component as site 1`] = ` +
      +
      +
      + +
      +

      + Category-1 +

      +
      + + 1 blocking_category_tracker + + + 1 blocking_category_blocked + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + +
      +

      + Category-2 +

      +
      + + 5 blocking_category_trackers + + + 3 blocking_category_blocked + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; diff --git a/app/panel-android/components/content/__tests__/__snapshots__/BlockingCategory.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/BlockingCategory.jsx.snap new file mode 100644 index 000000000..b46e865f0 --- /dev/null +++ b/app/panel-android/components/content/__tests__/__snapshots__/BlockingCategory.jsx.snap @@ -0,0 +1,451 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel-android/components/content/BlockingCategory.jsx Snapshot tests with react-test-renderer BlockingCategory component as global with all trackers blocked 1`] = ` +
      +
      + +
      +

      + Category-1 +

      +
      + + 1 blocking_category_tracker + + + 1 blocking_category_blocked + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; + +exports[`app/panel-android/components/content/BlockingCategory.jsx Snapshot tests with react-test-renderer BlockingCategory component as global with mixed trackers blocked 1`] = ` +
      +
      + +
      +

      + Category-1 +

      +
      + + 2 blocking_category_trackers + + + 1 blocking_category_blocked + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; + +exports[`app/panel-android/components/content/BlockingCategory.jsx Snapshot tests with react-test-renderer BlockingCategory component as global with no trackers blocked 1`] = ` +
      +
      + +
      +

      + Category-1 +

      +
      + + 1 blocking_category_tracker + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; + +exports[`app/panel-android/components/content/BlockingCategory.jsx Snapshot tests with react-test-renderer BlockingCategory component as site with all trackers ss_allowed 1`] = ` +
      +
      + +
      +

      + Category-1 +

      +
      + + 1 blocking_category_tracker + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; + +exports[`app/panel-android/components/content/BlockingCategory.jsx Snapshot tests with react-test-renderer BlockingCategory component as site with all trackers ss_blocked 1`] = ` +
      +
      + +
      +

      + Category-1 +

      +
      + + 1 blocking_category_tracker + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; + +exports[`app/panel-android/components/content/BlockingCategory.jsx Snapshot tests with react-test-renderer BlockingCategory component when sitePolicy Restricted 1`] = ` +
      +
      + +
      +

      + Category-1 +

      +
      + + 1 blocking_category_tracker + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; + +exports[`app/panel-android/components/content/BlockingCategory.jsx Snapshot tests with react-test-renderer BlockingCategory component when sitePolicy Trusted & Paused 1`] = ` +
      +
      + +
      +

      + Category-1 +

      +
      + + 1 blocking_category_tracker + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; + +exports[`app/panel-android/components/content/BlockingCategory.jsx Snapshot tests with react-test-renderer BlockingCategory component when sitePolicy Trusted 1`] = ` +
      +
      + +
      +

      + Category-1 +

      +
      + + 1 blocking_category_tracker + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +`; diff --git a/app/panel-android/components/content/__tests__/__snapshots__/BlockingTab.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/BlockingTab.jsx.snap new file mode 100644 index 000000000..4d2206593 --- /dev/null +++ b/app/panel-android/components/content/__tests__/__snapshots__/BlockingTab.jsx.snap @@ -0,0 +1,396 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel-android/components/content/BlockingTab.jsx Snapshot tests with react-test-renderer BlockingTab component as global with falsy props 1`] = ` +
      +
      +

      + android_global_blocking_header +

      +
      + + +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTab.jsx Snapshot tests with react-test-renderer BlockingTab component as global with tracker truthy props 1`] = ` +
    +
    +

    + android_global_blocking_header +

    +
    + + +
  • + +
  • +
  • + +
  • + +
    +
    +
    +
    +
    +
    + +
    +

    + Test1 +

    +
    + + 1 blocking_category_tracker + + + 1 blocking_category_blocked + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +

    + Test2 +

    +
    + + 5 blocking_category_trackers + + + 3 blocking_category_blocked + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTab.jsx Snapshot tests with react-test-renderer BlockingTab component as site with falsy props 1`] = ` +
    +
    +

    + android_site_blocking_header +

    +
    + + +
  • + +
  • + +
    +
    +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTab.jsx Snapshot tests with react-test-renderer BlockingTab component as site with tracker falsy props 1`] = ` +
    +
    +

    + android_site_blocking_header +

    +
    + + +
  • + +
  • + +
    +
    +
    +
    +
    +
    + +
    +

    + Test1 +

    +
    + + 1 blocking_category_tracker + + + 1 blocking_category_blocked + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap new file mode 100644 index 000000000..9b60d4d84 --- /dev/null +++ b/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap @@ -0,0 +1,320 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when site Paused 1`] = ` +
    +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    +
    +
    + android_block +
    +
    + android_restrict +
    +
    + android_trust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when site Restricted 1`] = ` +
    +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    +
    +
    + android_block +
    +
    + android_restrict +
    +
    + android_trust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when site Trusted 1`] = ` +
    +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    +
    +
    + android_block +
    +
    + android_restrict +
    +
    + android_trust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when tracker allowed 1`] = ` +
    +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    +
    +
    + android_block +
    +
    + android_restrict +
    +
    + android_untrust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when tracker blocked 1`] = ` +
    +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    +
    +
    + android_unblock +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when tracker restricted 1`] = ` +
    +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    +
    +
    + android_block +
    +
    + android_unrestrict +
    +
    + android_trust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component with falsy props 1`] = ` +
    +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    +
    +
    + android_block +
    +
    +
    +`; diff --git a/app/panel-android/components/content/__tests__/__snapshots__/DotsMenu.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/DotsMenu.jsx.snap new file mode 100644 index 000000000..330edae49 --- /dev/null +++ b/app/panel-android/components/content/__tests__/__snapshots__/DotsMenu.jsx.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel-android/components/content/DotsMenu.jsx Snapshot tests with react-test-renderer DotsMenu component with 0 actions 1`] = ` +
    +
    +`; + +exports[`app/panel-android/components/content/DotsMenu.jsx Snapshot tests with react-test-renderer DotsMenu component with 3 actions 1`] = ` +
    + + +
  • + +
  • +
  • + +
  • + +
    +
    +`; diff --git a/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap new file mode 100644 index 000000000..d345de9e4 --- /dev/null +++ b/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap @@ -0,0 +1,509 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel-android/components/content/OverviewTab.jsx Snapshot tests with react-test-renderer OverviewTab component with falsy props and SiteNotScanned 1`] = ` +
    +
    +
    +
    + + + + + + + + +
    +
    + + + + + +
    +
    +
    +
    +
    +
    + summary_page_not_scanned +
    +
    + summary_description_not_scanned_1 +
    +
    + summary_description_not_scanned_2 +
    +
    +
    +
    +
    +
    +
    + + + summary_trust_site + + +
    +
    +
    +
    + + + summary_restrict_site + + +
    +
    +
    +
    +
    +
    + + + summary_pause_ghostery + + +
    +
    + + summary_show_menu + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + off +
    +
    +
    + enhanced_anti_tracking +
    +
    +
    +
    +
    +
    + off +
    +
    +
    + enhanced_ad_blocking +
    +
    +
    +
    +
    +
    + off +
    +
    +
    + smart_blocking +
    +
    +
    +
    +
    +
    +`; + +exports[`app/panel-android/components/content/OverviewTab.jsx Snapshot tests with react-test-renderer OverviewTab component with truthy props and no SiteNotScanned 1`] = ` +
    +
    +
    +
    + + + + + + + + +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + 29 +
    +
    +
    +
    +
    + + page_host + +
    +
    +
    + + trackers_blocked + + + + 0 + +
    +
    + + requests_modified + + + + 21 + +
    +
    +
    +
    +
    +
    +
    + + + summary_trust_site + + +
    +
    +
    +
    + + + summary_restrict_site + + +
    +
    +
    +
    +
    +
    + + + summary_resume_ghostery + + +
    +
    + + summary_show_menu + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + on +
    +
    +
    + enhanced_anti_tracking +
    +
    +
    +
    +
    +
    + on +
    +
    +
    + enhanced_ad_blocking +
    +
    +
    +
    +
    +
    + on +
    +
    +
    + smart_blocking +
    +
    +
    +
    +
    +
    +`; diff --git a/app/panel-android/components/content/__tests__/__snapshots__/Tabs.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/Tabs.jsx.snap new file mode 100644 index 000000000..4033ce6f3 --- /dev/null +++ b/app/panel-android/components/content/__tests__/__snapshots__/Tabs.jsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel-android/components/content/Tabs.jsx Snapshot tests with react-test-renderer Tabs component with 3 Tab children components 1`] = ` +
    + +
    +
    + Tab 1 Content +
    +
    +
    +`; diff --git a/app/panel-android/index.jsx b/app/panel-android/index.jsx index 48861700b..90422a32f 100644 --- a/app/panel-android/index.jsx +++ b/app/panel-android/index.jsx @@ -4,21 +4,23 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0 */ + /** * @namespace PanelAndroidClasses */ + import React from 'react'; import ReactDOM from 'react-dom'; -import Panel from './components/Panel'; +import PanelAndroid from './components/PanelAndroid'; ReactDOM.render( ( - + ), document.getElementById('ghostery-content'), ); diff --git a/app/panel-android/utils/chart.js b/app/panel-android/utils/chart.js deleted file mode 100644 index 18b314b98..000000000 --- a/app/panel-android/utils/chart.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Chart Utilities - * - * 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 PanelAndroidUtils - */ - -export default function fromTrackersToChartData(trackers) { - if (trackers.length < 1) { - return { - sum: 0, - arcs: [], - }; - } - - const arcs = []; - let startAngle = 0; - - const sum = trackers.map(tracker => tracker.numTotal).reduce((a, b) => a + b, 0); - - for (let i = 0; i < trackers.length; i += 1) { - const endAngle = startAngle + (trackers[i].numTotal * (360 / sum)); - - arcs.push({ - start: startAngle, - end: endAngle, - category: trackers[i].id, - }); - - startAngle = endAngle; - } - - return { - sum, - arcs, - }; -} diff --git a/app/panel-android/utils/tracker-info.js b/app/panel-android/utils/tracker-info.js index 9749513fc..df8e1a3a2 100644 --- a/app/panel-android/utils/tracker-info.js +++ b/app/panel-android/utils/tracker-info.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -16,10 +16,13 @@ import { apps } from '../../../cliqz/core/tracker_db_v2.json'; -// Link to whotracks.me website -export default function getUrlFromTrackerId(id) { - const trackerName = apps[id].name; +/** + * Look up WhoTracksMe url slug + * @param {Int} id Ghostery tracker ID + * @return {String} WTM slug + */ +export default function getSlugFromTrackerId(id) { + const trackerName = apps[id] && apps[id].name; const trackerWtm = (Object.values(apps).find(app => app.wtm && app.name === trackerName) || {}).wtm; - const slug = trackerWtm || '../tracker-not-found'; - return `https://whotracks.me/trackers/${slug}.html`; + return trackerWtm || '../tracker-not-found'; } diff --git a/app/panel/components/BuildingBlocks/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/CliqzFeature.jsx index 91e01e40d..3abc03dc9 100644 --- a/app/panel/components/BuildingBlocks/CliqzFeature.jsx +++ b/app/panel/components/BuildingBlocks/CliqzFeature.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,6 +12,7 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import ClassNames from 'classnames'; import Tooltip from '../Tooltip'; @@ -162,4 +163,31 @@ class CliqzFeature extends React.Component { } } +CliqzFeature.propTypes = { + clickButton: PropTypes.func.isRequired, + type: PropTypes.oneOf([ + 'anti_track', + 'ad_block', + 'smart_block', + ]).isRequired, + active: PropTypes.bool, + cliqzInactive: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.number, + ]).isRequired, + isSmaller: PropTypes.bool.isRequired, + isCondensed: PropTypes.bool, + isTooltipHeader: PropTypes.bool, + isTooltipBody: PropTypes.bool, + tooltipPosition: PropTypes.string, +}; + +CliqzFeature.defaultProps = { + active: true, + isCondensed: false, + isTooltipHeader: false, + isTooltipBody: false, + tooltipPosition: '', +}; + export default CliqzFeature; diff --git a/app/panel/components/BuildingBlocks/DonutGraph.jsx b/app/panel/components/BuildingBlocks/DonutGraph.jsx index d48b9a0df..9cab95c7b 100644 --- a/app/panel/components/BuildingBlocks/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/DonutGraph.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -13,6 +13,7 @@ import { throttle } from 'underscore'; import React from 'react'; +import PropTypes from 'prop-types'; import ClassNames from 'classnames'; import { arc, @@ -407,8 +408,24 @@ class DonutGraph extends React.Component { } } +DonutGraph.propTypes = { + categories: PropTypes.arrayOf(PropTypes.object), + adBlock: PropTypes.shape({}), + antiTracking: PropTypes.shape({}), + renderRedscale: PropTypes.bool.isRequired, + renderGreyscale: PropTypes.bool.isRequired, + totalCount: PropTypes.number.isRequired, + ghosteryFeatureSelect: PropTypes.oneOf([false, 1, 2]).isRequired, + isSmall: PropTypes.bool, + clickDonut: PropTypes.func, +}; + DonutGraph.defaultProps = { categories: [], + adBlock: { unknownTrackerCount: 0 }, + antiTracking: { unknownTrackerCount: 0 }, + clickDonut: () => {}, + isSmall: false, }; export default DonutGraph; diff --git a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx index b4980cf54..d66e943bd 100644 --- a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx +++ b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,6 +12,7 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import ClassNames from 'classnames'; import Tooltip from '../Tooltip'; import globals from '../../../../src/classes/Globals'; @@ -130,4 +131,19 @@ class GhosteryFeature extends React.Component { } } +GhosteryFeature.propTypes = { + handleClick: PropTypes.func.isRequired, + type: PropTypes.oneOf(['trust', 'restrict']).isRequired, + sitePolicy: PropTypes.oneOf([false, 1, 2]), + blockingPausedOrDisabled: PropTypes.bool.isRequired, + showText: PropTypes.bool.isRequired, + tooltipPosition: PropTypes.string.isRequired, + short: PropTypes.bool.isRequired, + narrow: PropTypes.bool.isRequired, +}; + +GhosteryFeature.defaultProps = { + sitePolicy: false, +}; + export default GhosteryFeature; diff --git a/app/panel/components/BuildingBlocks/NotScanned.jsx b/app/panel/components/BuildingBlocks/NotScanned.jsx index 0d1e36b64..74eb6dd34 100644 --- a/app/panel/components/BuildingBlocks/NotScanned.jsx +++ b/app/panel/components/BuildingBlocks/NotScanned.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,6 +12,7 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import ClassNames from 'classnames'; /** @@ -39,4 +40,12 @@ const NotScanned = ({ isSmall }) => { ); }; +NotScanned.propTypes = { + isSmall: PropTypes.bool, +}; + +NotScanned.defaultProps = { + isSmall: false, +}; + export default NotScanned; diff --git a/app/panel/components/BuildingBlocks/PauseButton.jsx b/app/panel/components/BuildingBlocks/PauseButton.jsx index 217588c87..d7ab0e287 100644 --- a/app/panel/components/BuildingBlocks/PauseButton.jsx +++ b/app/panel/components/BuildingBlocks/PauseButton.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,6 +12,7 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import ClassNames from 'classnames'; import Tooltip from '../Tooltip'; @@ -197,4 +198,22 @@ class PauseButton extends React.Component { } } +PauseButton.propTypes = { + isPaused: PropTypes.bool, + isPausedTimeout: PropTypes.number, + clickPause: PropTypes.func.isRequired, + dropdownItems: PropTypes.arrayOf(PropTypes.shape({ + val: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + name_condensed: PropTypes.string, + })).isRequired, + isCentered: PropTypes.bool.isRequired, + isCondensed: PropTypes.bool.isRequired, +}; + +PauseButton.defaultProps = { + isPaused: false, + isPausedTimeout: 0, +}; + export default PauseButton; diff --git a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx b/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx index 376544392..2722ceddc 100644 --- a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx +++ b/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx @@ -20,8 +20,12 @@ import RadioButton from './RadioButton'; * @memberof PanelBuildingBlocks */ const RadioButtonGroup = ({ indexClicked, handleItemClick, labels }) => { - const labelsEl = labels.map(label => ( -
    + const labelsEl = labels.map((label, index) => ( +
    handleItemClick(index)} + > {t(label)}
    )); diff --git a/app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx new file mode 100644 index 000000000..cba12eac1 --- /dev/null +++ b/app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx @@ -0,0 +1,171 @@ +/** + * Cliqz Feature Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import CliqzFeature from '../CliqzFeature'; + +// Fake the translation function to only return the translation key +global.t = function(str) { + return str; +}; + +// Fake the Tooltip implementation +jest.mock('../../Tooltip'); + +describe('app/panel/components/CliqzFeature.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('CliqzFeature is rendered correctly with falsy props', () => { + const component = renderer.create( +
    + {}} + type="anti_track" + active={false} + cliqzInactive={false} + isSmaller={false} + /> + {}} + type="ad_block" + active={false} + cliqzInactive={false} + isSmaller={false} + isCondensed={false} + isTooltipHeader={false} + isTooltipBody={false} + tooltipPosition="" + /> + {}} + type="smart_block" + active={false} + cliqzInactive={false} + isSmaller={false} + isCondensed={false} + isTooltipHeader={false} + isTooltipBody={false} + tooltipPosition="" + /> +
    + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('CliqzFeature is rendered correctly with some truthy props', () => { + const component = renderer.create( +
    + {}} + type="anti_track" + active + cliqzInactive={false} + isSmaller={false} + /> + {}} + type="ad_block" + active + cliqzInactive + isSmaller={false} + isCondensed={false} + isTooltipHeader={false} + isTooltipBody={false} + tooltipPosition="" + /> + {}} + type="smart_block" + active + cliqzInactive + isSmaller={false} + isCondensed + isTooltipHeader + isTooltipBody + tooltipPosition="top" + /> +
    + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('CliqzFeature is rendered correctly with all truthy props', () => { + const component = renderer.create( +
    + {}} + type="anti_track" + active + cliqzInactive + isSmaller + isCondensed + isTooltipHeader + isTooltipBody + tooltipPosition="right" + /> + {}} + type="ad_block" + active + cliqzInactive + isSmaller + isCondensed + isTooltipHeader + isTooltipBody + tooltipPosition="top" + /> + {}} + type="smart_block" + active + cliqzInactive + isSmaller + isCondensed + isTooltipHeader + isTooltipBody + tooltipPosition="top" + /> +
    + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('CliqzFeature handles clicks correctly', () => { + const clickButton = jest.fn(); + const component = shallow( + + ); + expect(clickButton.mock.calls.length).toBe(0); + component.find('.CliqzFeature').simulate('click'); + expect(clickButton.mock.calls.length).toBe(0); + + component.setProps({ cliqzInactive: false }); + component.find('.CliqzFeature').simulate('click'); + + component.setProps({ active: false }); + component.find('.CliqzFeature').simulate('click'); + expect(clickButton.mock.calls.length).toBe(2); + expect(clickButton.mock.calls[0][0].status).toBe(true); + expect(clickButton.mock.calls[1][0].status).toBe(false); + }); + }); +}); diff --git a/app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx b/app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx new file mode 100644 index 000000000..29d3d9311 --- /dev/null +++ b/app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx @@ -0,0 +1,137 @@ +/** + * Donut Graph Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import DonutGraph from '../DonutGraph'; + +// Fake the translation function to only return the translation key +global.t = function(str) { + return str; +}; + +// Fake the Tooltip implementation +jest.mock('../../Tooltip'); + +describe('app/panel/components/DonutGraph.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('DonutGraph is rendered correctly when props are falsy', () => { + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('DonutGraph is rendered correctly when some props are truthy', () => { + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('DonutGraph is rendered correctly when all props are truthy', () => { + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('DonutGraph handles clicks correctly', () => { + const clickDonut = jest.fn(); + const component = shallow( + + ); + expect(clickDonut.mock.calls.length).toBe(0); + component.find('.DonutGraph__textCountContainer').simulate('click'); + expect(clickDonut.mock.calls.length).toBe(1); + expect(clickDonut.mock.calls[0][0].type).toBe('trackers'); + expect(clickDonut.mock.calls[0][0].name).toBe('all'); + }); + }); +}); diff --git a/app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx b/app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx new file mode 100644 index 000000000..e8516f147 --- /dev/null +++ b/app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx @@ -0,0 +1,195 @@ +/** + * Ghostery Feature Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; +import GhosteryFeature from '../GhosteryFeature'; + +// Fake the translation function to only return the translation key +global.t = function(str) { + return str; +}; + +// Fake the Tooltip implementation +jest.mock('../../Tooltip'); + +describe('app/panel/components/GhosteryFeature.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('GhosteryFeature is rendered correctly props are falsy', () => { + const component = renderer.create( +
    + {}} + type="trust" + sitePolicy={false} + blockingPausedOrDisabled={false} + showText={false} + tooltipPosition="" + short={false} + narrow={false} + /> + {}} + type="restrict" + sitePolicy={false} + blockingPausedOrDisabled={false} + showText={false} + tooltipPosition="" + short={false} + narrow={false} + /> +
    + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('GhosteryFeature is rendered correctly some props are truthy', () => { + const component = renderer.create( +
    + {}} + type="trust" + sitePolicy={2} + blockingPausedOrDisabled={false} + showText + tooltipPosition="right" + short + narrow={false} + /> + {}} + type="restrict" + sitePolicy={2} + blockingPausedOrDisabled={false} + showText + tooltipPosition="right" + short + narrow={false} + /> +
    + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('GhosteryFeature is rendered correctly props are truthy', () => { + const component = renderer.create( +
    + {}} + type="trust" + sitePolicy={1} + blockingPausedOrDisabled + showText + tooltipPosition="top" + short + narrow + /> + {}} + type="restrict" + sitePolicy={1} + blockingPausedOrDisabled + showText + tooltipPosition="top" + short + narrow + /> +
    + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('GhosteryFeature has the correct class names', () => { + const component = shallow( + {}} + type="restrict" + sitePolicy={false} + blockingPausedOrDisabled={false} + showText={false} + tooltipPosition="" + short={false} + narrow={false} + /> + ); + expect(component.find('.GhosteryFeatureButton.restrict').length).toBe(1); + expect(component.find('.GhosteryFeatureButton.trust').length).toBe(0); + expect(component.find('.GhosteryFeatureButton.not-clickable').length).toBe(0); + expect(component.find('.GhosteryFeatureButton.clickable').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--normal').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--short').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--narrow').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--inactive').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--active').length).toBe(0); + + component.setProps({ narrow: true, sitePolicy: 1 }); + expect(component.find('.GhosteryFeatureButton--normal').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--short').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--narrow').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--inactive').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--active').length).toBe(1); + + component.setProps({ short: true, type: 'trust' }); + expect(component.find('.GhosteryFeatureButton.restrict').length).toBe(0); + expect(component.find('.GhosteryFeatureButton.trust').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--normal').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--short').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--narrow').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--inactive').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--active').length).toBe(0); + + component.setProps({ narrow: false, sitePolicy: 2 }); + expect(component.find('.GhosteryFeatureButton--normal').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--short').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--narrow').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--inactive').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--active').length).toBe(1); + + component.setProps({ short: false, blockingPausedOrDisabled: true }); + expect(component.find('.GhosteryFeatureButton.not-clickable').length).toBe(1); + expect(component.find('.GhosteryFeatureButton.clickable').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--normal').length).toBe(1); + expect(component.find('.GhosteryFeatureButton--short').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--narrow').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--inactive').length).toBe(0); + expect(component.find('.GhosteryFeatureButton--active').length).toBe(1); + }); + + test('GhosteryFeature handles clicks correctly', () => { + const handleClick = jest.fn(); + const component = shallow( + + ); + + expect(handleClick.mock.calls.length).toBe(0); + component.find('.GhosteryFeatureButton').simulate('click'); + expect(handleClick.mock.calls.length).toBe(0); + + component.setProps({ blockingPausedOrDisabled: false }); + component.find('.GhosteryFeatureButton').simulate('click'); + expect(handleClick.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/app/panel/components/BuildingBlocks/__tests__/NotScanned.jsx b/app/panel/components/BuildingBlocks/__tests__/NotScanned.jsx new file mode 100644 index 000000000..ac445df7b --- /dev/null +++ b/app/panel/components/BuildingBlocks/__tests__/NotScanned.jsx @@ -0,0 +1,39 @@ +/** + * Not Scanned Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import NotScanned from '../NotScanned'; + +// Fake the translation function to only return the translation key +global.t = function(str) { + return str; +}; + +describe('app/panel/components/NotScanned.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('NotScanned is rendered correctly when no props are passed', () => { + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('NotScanned is rendered correctly when small', () => { + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/panel/components/__tests__/PauseButton.jsx b/app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx similarity index 82% rename from app/panel/components/__tests__/PauseButton.jsx rename to app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx index 4d89ba7b0..37ba604ee 100644 --- a/app/panel/components/__tests__/PauseButton.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -14,7 +14,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; -import PauseButton from '../BuildingBlocks/PauseButton'; +import PauseButton from '../PauseButton'; // Fake the translation function to only return the translation key global.t = function(str) { @@ -22,7 +22,7 @@ global.t = function(str) { }; // Fake the Tooltip implementation -jest.mock('../Tooltip'); +jest.mock('../../Tooltip'); describe('app/panel/components/BuildingBlocks/PauseButton.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { @@ -170,5 +170,41 @@ describe('app/panel/components/BuildingBlocks/PauseButton.jsx', () => { expect(component.find('.button.button-pause.smaller').length).toBe(0); expect(component.find('.button.button-pause.smallest').length).toBe(1); }); + + test('the pause button correctly handles clicks', () => { + const clickPause = jest.fn(); + const dropdownItems = [ + { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, + { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, + { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, + ]; + const component = shallow( + + ); + expect(clickPause.mock.calls.length).toBe(0); + component.find('.button-pause').simulate('click'); + + component.setState({ showDropdown: true }); + component.find('.dropdown').childAt(0).simulate('click'); + + component.setState({ showDropdown: true }); + component.find('.dropdown').childAt(1).simulate('click'); + + component.setState({ showDropdown: true }); + component.find('.dropdown').childAt(2).simulate('click'); + + expect(clickPause.mock.calls.length).toBe(4); + expect(clickPause.mock.calls[0][0]).toBeFalsy(); + expect(clickPause.mock.calls[1][0]).toBe(30); + expect(clickPause.mock.calls[2][0]).toBe(60); + expect(clickPause.mock.calls[3][0]).toBe(1440); + }); }); }); diff --git a/app/panel/components/BuildingBlocks/__tests__/__snapshots__/CliqzFeature.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/CliqzFeature.jsx.snap new file mode 100644 index 000000000..16ba9dffd --- /dev/null +++ b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/CliqzFeature.jsx.snap @@ -0,0 +1,178 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/components/CliqzFeature.jsx Snapshot tests with react-test-renderer CliqzFeature is rendered correctly with all truthy props 1`] = ` +
    +
    +
    + on +
    +
    +
    + enhanced_anti_tracking +
    +
    +
    +
    + on +
    +
    +
    + enhanced_ad_blocking +
    +
    +
    +
    + on +
    +
    +
    + smart_blocking +
    +
    +
    +`; + +exports[`app/panel/components/CliqzFeature.jsx Snapshot tests with react-test-renderer CliqzFeature is rendered correctly with falsy props 1`] = ` +
    +
    +
    + off +
    +
    +
    + enhanced_anti_tracking +
    +
    +
    +
    + off +
    +
    +
    + enhanced_ad_blocking +
    +
    +
    +
    + off +
    +
    +
    + smart_blocking +
    +
    +
    +`; + +exports[`app/panel/components/CliqzFeature.jsx Snapshot tests with react-test-renderer CliqzFeature is rendered correctly with some truthy props 1`] = ` +
    +
    +
    + on +
    +
    +
    + enhanced_anti_tracking +
    +
    +
    +
    + on +
    +
    +
    + enhanced_ad_blocking +
    +
    +
    +
    + on +
    +
    +
    + smart_blocking +
    +
    +
    +`; diff --git a/app/panel/components/BuildingBlocks/__tests__/__snapshots__/DonutGraph.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/DonutGraph.jsx.snap new file mode 100644 index 000000000..b960d1f0e --- /dev/null +++ b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/DonutGraph.jsx.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/components/DonutGraph.jsx Snapshot tests with react-test-renderer DonutGraph is rendered correctly when all props are truthy 1`] = ` +
    +
    + + category-1 + + + category-2 + + + category-3 + + + category-4 + + + unknown + +
    +
    +
    +
    + 38 +
    +
    +
    +`; + +exports[`app/panel/components/DonutGraph.jsx Snapshot tests with react-test-renderer DonutGraph is rendered correctly when props are falsy 1`] = ` +
    +
    +
    +
    +
    + 0 +
    +
    +
    +`; + +exports[`app/panel/components/DonutGraph.jsx Snapshot tests with react-test-renderer DonutGraph is rendered correctly when some props are truthy 1`] = ` +
    +
    + + category-1 + + + category-2 + + + category-3 + + + category-4 + + + unknown + +
    +
    +
    +
    + 8 +
    +
    +
    +`; diff --git a/app/panel/components/BuildingBlocks/__tests__/__snapshots__/GhosteryFeature.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/GhosteryFeature.jsx.snap new file mode 100644 index 000000000..b1c00c3ea --- /dev/null +++ b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/GhosteryFeature.jsx.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/components/GhosteryFeature.jsx Snapshot tests with react-test-renderer GhosteryFeature is rendered correctly props are falsy 1`] = ` +
    +
    + + + + + +
    +
    + + + + + +
    +
    +`; + +exports[`app/panel/components/GhosteryFeature.jsx Snapshot tests with react-test-renderer GhosteryFeature is rendered correctly props are truthy 1`] = ` +
    +
    + + + summary_trust_site + + +
    +
    + + + summary_restrict_site_active + + +
    +
    +`; + +exports[`app/panel/components/GhosteryFeature.jsx Snapshot tests with react-test-renderer GhosteryFeature is rendered correctly some props are truthy 1`] = ` +
    +
    + + + summary_trust_site_active + + +
    +
    + + + summary_restrict_site + + +
    +
    +`; diff --git a/app/panel/components/BuildingBlocks/__tests__/__snapshots__/NotScanned.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/NotScanned.jsx.snap new file mode 100644 index 000000000..eebea9ac8 --- /dev/null +++ b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/NotScanned.jsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/components/NotScanned.jsx Snapshot tests with react-test-renderer NotScanned is rendered correctly when no props are passed 1`] = ` +
    +
    + summary_page_not_scanned +
    +
    + summary_description_not_scanned_1 +
    +
    + summary_description_not_scanned_2 +
    +
    +`; + +exports[`app/panel/components/NotScanned.jsx Snapshot tests with react-test-renderer NotScanned is rendered correctly when small 1`] = ` +
    +
    + summary_page_not_scanned +
    +
    + summary_description_not_scanned_1 +
    +
    + summary_description_not_scanned_2 +
    +
    +`; diff --git a/app/panel/components/__tests__/__snapshots__/PauseButton.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/PauseButton.jsx.snap similarity index 100% rename from app/panel/components/__tests__/__snapshots__/PauseButton.jsx.snap rename to app/panel/components/BuildingBlocks/__tests__/__snapshots__/PauseButton.jsx.snap diff --git a/app/panel/components/Header.jsx b/app/panel/components/Header.jsx index ee3770b4f..f20bb7ac5 100644 --- a/app/panel/components/Header.jsx +++ b/app/panel/components/Header.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -213,9 +213,11 @@ class Header extends React.Component { ); const tabs = ( -
    - {simpleTab} - {detailedTab} +
    +
    + {simpleTab} + {detailedTab} +
    ); diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 7aae2e9fb..01ca9aab6 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -19,6 +19,7 @@ import ThemeContext from '../contexts/ThemeContext'; import DynamicUIPortContext from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; +import { log } from '../../../src/utils/common'; const INSIGHTS = 'insights'; const PLUS = 'plus'; @@ -51,7 +52,9 @@ class Panel extends React.Component { const { body } = msg; - if (body.panel) { + if (body.error) { + log(`Error: ${body.error}`); + } else if (body.panel) { this._initializeData(body); } else if (this._dynamicUIDataInitialized) { actions.updatePanelData(body); diff --git a/app/panel/components/Settings/Account.jsx b/app/panel/components/Settings/Account.jsx index 15273c354..52d4e2e20 100644 --- a/app/panel/components/Settings/Account.jsx +++ b/app/panel/components/Settings/Account.jsx @@ -14,6 +14,7 @@ * @namespace SettingsComponents */ import React from 'react'; +import ImportExport from './ImportExport'; import { sendMessage } from '../../utils/msg'; import globals from '../../../../src/classes/Globals'; /** @@ -44,43 +45,12 @@ class Account extends React.Component { window.close(); } - /** - * Trigger action to export settings in JSON format and save it to a file. - */ - clickExportSettings = () => { - const { actions, settingsData } = this.props; - actions.exportSettings(settingsData.pageUrl); - } - - /** - * Trigger custom Import dialog or a native Open File dialog depending on browser. - */ - clickImportSettings = () => { - const { actions, settingsData } = this.props; - const browserName = globals.BROWSER_INFO.name; - if (browserName === 'firefox') { - // show ghostery dialog window for import - actions.importSettingsDialog(settingsData.pageUrl); - } else { - // for chrome and opera, use the native File Dialog - this.selectedFile.click(); - } - } - - /** - * Parse settings file imported via native browser window. Called via input#select-file onChange. - */ - validateImportFile = () => { - const { actions } = this.props; - actions.importSettingsNative(this.selectedFile.files[0]); - } - /** * Render Account subview. * @return {ReactComponent} ReactComponent instance */ render() { - const { settingsData } = this.props; + const { settingsData, actions } = this.props; const { email, firstName, lastName } = settingsData.user ? settingsData.user : {}; const accountName = (firstName || lastName) ? (firstName ? (`${firstName} ${lastName}`) : lastName) : ''; return ( @@ -116,17 +86,10 @@ class Account extends React.Component {

    { t('settings_edit_account') }

    -
    -

    { t('settings_export_header') }

    -

    { t('settings_export_text') }

    -

    { settingsData.exportResultText }

    -
    -

    { t('settings_import_header') }

    -

    { t('settings_import_text') }

    -

    { t('settings_import_warning') }

    -

    { settingsData.importResultText }

    - { this.selectedFile = input; }} type="file" id="select-file" name="select-file" onChange={this.validateImportFile} /> -
    +
    diff --git a/app/panel/components/Settings/AdBlocker.jsx b/app/panel/components/Settings/AdBlocker.jsx index 90e44a6c4..2a5466602 100644 --- a/app/panel/components/Settings/AdBlocker.jsx +++ b/app/panel/components/Settings/AdBlocker.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -49,13 +49,14 @@ const AdBlocker = (props) => { }; AdBlocker.propTypes = { - settingsData: PropTypes.shape({ - cliqz_adb_mode: PropTypes.number, - }), actions: PropTypes.shape({ selectItem: PropTypes.func.isRequired, }).isRequired, + settingsData: PropTypes.shape({ + cliqz_adb_mode: PropTypes.number, + }), }; + AdBlocker.defaultProps = { settingsData: { cliqz_adb_mode: 0, diff --git a/app/panel/components/Settings/GeneralSettings.jsx b/app/panel/components/Settings/GeneralSettings.jsx index 1381b34e2..02e5704b0 100644 --- a/app/panel/components/Settings/GeneralSettings.jsx +++ b/app/panel/components/Settings/GeneralSettings.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,6 +12,7 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import moment from 'moment/min/moment-with-locales.min'; /** * @class Implement General Settings subview. The view opens from the @@ -88,6 +89,7 @@ class GeneralSettings extends React.Component { render() { const { settingsData, toggleCheckbox } = this.props; const { dbLastUpdated } = this.state; + return (
    @@ -179,4 +181,23 @@ class GeneralSettings extends React.Component { } } +GeneralSettings.propTypes = { + actions: PropTypes.shape({ + updateDatabase: PropTypes.func.isRequired, + }).isRequired, + toggleCheckbox: PropTypes.func.isRequired, + settingsData: PropTypes.shape({ + language: PropTypes.string.isRequired, + bugs_last_checked: PropTypes.number.isRequired, + enable_autoupdate: PropTypes.bool.isRequired, + dbUpdateText: PropTypes.string, + show_tracker_urls: PropTypes.bool.isRequired, + enable_click2play: PropTypes.bool.isRequired, + enable_click2play_social: PropTypes.bool.isRequired, + toggle_individual_trackers: PropTypes.bool.isRequired, + ignore_first_party: PropTypes.bool.isRequired, + block_by_default: PropTypes.bool.isRequired, + }).isRequired, +}; + export default GeneralSettings; diff --git a/app/panel/components/Settings/ImportExport.jsx b/app/panel/components/Settings/ImportExport.jsx new file mode 100644 index 000000000..f781200b0 --- /dev/null +++ b/app/panel/components/Settings/ImportExport.jsx @@ -0,0 +1,86 @@ +/** + * Account Settings Import/Export Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import globals from '../../../../src/classes/Globals'; + +class ImportExport extends React.Component { + /** + * Trigger action to export settings in JSON format and save it to a file. + */ + clickExportSettings = () => { + const { actions, settingsData } = this.props; + actions.exportSettings(settingsData.pageUrl); + } + + /** + * Trigger custom Import dialog or a native Open File dialog depending on browser. + */ + clickImportSettings = () => { + const { actions, settingsData } = this.props; + const browserName = globals.BROWSER_INFO.name; + if (browserName === 'firefox') { + // show ghostery dialog window for import + actions.importSettingsDialog(settingsData.pageUrl); + } else { + // for chrome and opera, use the native File Dialog + this.selectedFile.click(); + } + } + + /** + * Parse settings file imported via native browser window. Called via input#select-file onChange. + */ + validateImportFile = () => { + const { actions } = this.props; + actions.importSettingsNative(this.selectedFile.files[0]); + } + + /** + * Render Account subview. + * @return {ReactComponent} ReactComponent instance + */ + render() { + const { settingsData } = this.props; + return ( +
    +

    { t('settings_export_header') }

    +

    { t('settings_export_text') }

    +

    { settingsData.exportResultText }

    +
    +

    { t('settings_import_header') }

    +

    { t('settings_import_text') }

    +

    { t('settings_import_warning') }

    +

    { settingsData.importResultText }

    + { this.selectedFile = input; }} type="file" id="select-file" name="select-file" onChange={this.validateImportFile} /> +
    + ); + } +} + +ImportExport.propTypes = { + settingsData: PropTypes.shape({ + pageUrl: PropTypes.string.isRequired, + exportResultText: PropTypes.string.isRequired, + importResultText: PropTypes.string.isRequired, + actionSuccess: PropTypes.bool.isRequired, + }).isRequired, + actions: PropTypes.shape({ + exportSettings: PropTypes.func.isRequired, + importSettingsDialog: PropTypes.func.isRequired, + importSettingsNative: PropTypes.func.isRequired, + }).isRequired, +}; + +export default ImportExport; diff --git a/app/panel/components/Settings/Notifications.jsx b/app/panel/components/Settings/Notifications.jsx index b8047f727..9efbef082 100644 --- a/app/panel/components/Settings/Notifications.jsx +++ b/app/panel/components/Settings/Notifications.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,6 +12,7 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; /** * @class Implement Notification subview as a React component. * The view opens from the left-side menu of the main @@ -74,4 +75,17 @@ const Notifications = ({ settingsData, toggleCheckbox }) => (
    ); +Notifications.propTypes = { + toggleCheckbox: PropTypes.func.isRequired, + settingsData: PropTypes.shape({ + show_cmp: PropTypes.bool.isRequired, + notify_upgrade_updates: PropTypes.bool.isRequired, + notify_promotions: PropTypes.bool.isRequired, + notify_library_updates: PropTypes.bool.isRequired, + reload_banner_status: PropTypes.bool, + trackers_banner_status: PropTypes.bool, + show_badge: PropTypes.bool.isRequired, + }).isRequired, +}; + export default Notifications; diff --git a/app/panel/components/Settings/OptIn.jsx b/app/panel/components/Settings/OptIn.jsx index 5f7fbd80a..f74262c0b 100644 --- a/app/panel/components/Settings/OptIn.jsx +++ b/app/panel/components/Settings/OptIn.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,9 +12,11 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import globals from '../../../../src/classes/Globals'; -const { IS_CLIQZ } = globals; +const { IS_CLIQZ, BROWSER_INFO } = globals; +const IS_ANDROID = (BROWSER_INFO.os === 'android'); /** * @class Implement Opt In subview as a React component. @@ -55,7 +57,7 @@ const OptIn = ({ settingsData, toggleCheckbox }) => (
    )} - {!IS_CLIQZ && ( + {!IS_CLIQZ && !IS_ANDROID && (
    @@ -73,4 +75,13 @@ const OptIn = ({ settingsData, toggleCheckbox }) => (
    ); +OptIn.propTypes = { + toggleCheckbox: PropTypes.func.isRequired, + settingsData: PropTypes.shape({ + enable_metrics: PropTypes.bool.isRequired, + enable_human_web: PropTypes.bool.isRequired, + enable_offers: PropTypes.bool.isRequired, + }).isRequired, +}; + export default OptIn; diff --git a/app/panel/components/Settings/TrustAndRestrict.jsx b/app/panel/components/Settings/TrustAndRestrict.jsx index 620fc3faf..03ed86e6b 100644 --- a/app/panel/components/Settings/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/TrustAndRestrict.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,6 +12,7 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import Sites from './Sites'; /** * @class Implement Trust and Restrict subview presenting the lists @@ -191,12 +192,14 @@ class TrustAndRestrict extends React.Component {

    { t('settings_trusted_restricted_sites') }

    -
    -
    - {t('settings_trusted_sites')} -
    -
    - {t('settings_restricted_sites')} +
    +
    +
    + {t('settings_trusted_sites')} +
    +
    + {t('settings_restricted_sites')} +
    @@ -234,4 +237,12 @@ class TrustAndRestrict extends React.Component { } } +TrustAndRestrict.propTypes = { + actions: PropTypes.shape({ + updateSitePolicy: PropTypes.func.isRequired, + }).isRequired, + site_whitelist: PropTypes.arrayOf(PropTypes.string).isRequired, + site_blacklist: PropTypes.arrayOf(PropTypes.string).isRequired, +}; + export default TrustAndRestrict; diff --git a/app/panel/components/Settings/__tests__/AdBlocker.jsx b/app/panel/components/Settings/__tests__/AdBlocker.jsx new file mode 100644 index 000000000..8ef332c2e --- /dev/null +++ b/app/panel/components/Settings/__tests__/AdBlocker.jsx @@ -0,0 +1,95 @@ +/** + * AdBlocker Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import AdBlocker from '../AdBlocker'; + +describe('app/panel/Settings/AdBlocker.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('AdBlocker is rendered correctly with AdOnly checked', () => { + const settingsData = { + cliqz_adb_mode: 0, + }; + const actions = { + selectItem: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('AdBlocker is rendered correctly with Ads & Trackers checked', () => { + const settingsData = { + cliqz_adb_mode: 1, + }; + const actions = { + selectItem: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('AdBlocker is rendered correctly with Ads, Trackers, & Annoyances checked', () => { + const settingsData = { + cliqz_adb_mode: 2, + }; + const actions = { + selectItem: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('AdBlocker functions correctly', () => { + const settingsData = { + cliqz_adb_mode: 0, + }; + const actions = { + selectItem: jest.fn(), + }; + + const component = mount( + + ); + + expect(actions.selectItem.mock.calls.length).toBe(0); + component.find('.RadioButtonGroup__label').at(0).simulate('click'); + component.find('.RadioButton__outerCircle').at(2).simulate('click'); + expect(actions.selectItem.mock.calls.length).toBe(2); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/GeneralSettings.jsx b/app/panel/components/Settings/__tests__/GeneralSettings.jsx new file mode 100644 index 000000000..d67ab814e --- /dev/null +++ b/app/panel/components/Settings/__tests__/GeneralSettings.jsx @@ -0,0 +1,113 @@ +/** + * GeneralSettings Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import GeneralSettings from '../GeneralSettings'; + +jest.spyOn(GeneralSettings, 'getDbLastUpdated').mockImplementation(settingsData => settingsData.bugs_last_checked); + +describe('app/panel/Settings/GeneralSettings.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('GeneralSettings is rendered correctly with falsy props', () => { + const settingsData = { + language: 'en', + bugs_last_checked: 0, + enable_autoupdate: false, + show_tracker_urls: false, + enable_click2play: false, + enable_click2play_social: false, + toggle_individual_trackers: false, + ignore_first_party: false, + block_by_default: false, + }; + const actions = { + updateDatabase: () => {}, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('GeneralSettings is rendered correctly with truthy props', () => { + const settingsData = { + language: 'en', + bugs_last_checked: 1000000, + enable_autoupdate: true, + dbUpdateText: 'database-updated-text', + show_tracker_urls: true, + enable_click2play: true, + enable_click2play_social: true, + toggle_individual_trackers: true, + ignore_first_party: true, + block_by_default: true, + }; + const actions = { + updateDatabase: () => {}, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('GeneralSettings functions correctly', () => { + const settingsData = { + language: 'en', + bugs_last_checked: 0, + enable_autoupdate: false, + show_tracker_urls: false, + enable_click2play: false, + enable_click2play_social: false, + toggle_individual_trackers: false, + ignore_first_party: false, + block_by_default: false, + }; + const actions = { + updateDatabase: jest.fn(), + }; + const toggleCheckbox = jest.fn(); + + const component = mount( + + ); + + expect(actions.updateDatabase.mock.calls.length).toBe(0); + component.find('#update-now-span').simulate('click'); + expect(actions.updateDatabase.mock.calls.length).toBe(1); + + expect(toggleCheckbox.mock.calls.length).toBe(0); + component.find('input[type="checkbox"]').at(0).simulate('click'); + component.find('#settings-allow-trackers').simulate('click'); + expect(toggleCheckbox.mock.calls.length).toBe(2); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/ImportExport.jsx b/app/panel/components/Settings/__tests__/ImportExport.jsx new file mode 100644 index 000000000..a09516e4e --- /dev/null +++ b/app/panel/components/Settings/__tests__/ImportExport.jsx @@ -0,0 +1,148 @@ +/** + * Import Export Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import ImportExport from '../ImportExport'; + +jest.mock('../../../../../src/classes/Globals', () => ({ + BROWSER_INFO: { + name: 'firefox', + } +})); + +describe('app/panel/Settings/ImportExport.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('ImportExport is rendered correctly with baseline props', () => { + const settingsData = { + pageUrl: '', + exportResultText: '', + importResultText: '', + actionSuccess: false, + }; + const actions = { + exportSettings: () => {}, + importSettingsDialog: () => {}, + importSettingsNative: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('ImportExport is rendered correctly with happy-path props', () => { + const settingsData = { + pageUrl: 'https://example.com', + exportResultText: 'Your settings have been successfully exported to your downloads folder on August 8, 2020 6:08 PM', + importResultText: 'Your settings have been successfully imported on September 12, 2020 6:08 PM', + actionSuccess: true, + }; + const actions = { + exportSettings: () => {}, + importSettingsDialog: () => {}, + importSettingsNative: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('ImportExport is rendered correctly with unhappy-path props', () => { + const settingsData = { + pageUrl: 'chrome://extensions', + exportResultText: 'Ghostery cannot export settings when the current page is one of the reserved browser pages. Please navigate to a different page and try again.', + importResultText: 'That is the incorrect file type. Please choose a .ghost file and try again.', + actionSuccess: false, + }; + const actions = { + exportSettings: () => {}, + importSettingsDialog: () => {}, + importSettingsNative: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('ImportExport functions correctly', () => { + const settingsData = { + pageUrl: '', + exportResultText: '', + importResultText: '', + actionSuccess: false, + }; + const actions = { + exportSettings: jest.fn(), + importSettingsDialog: jest.fn(), + importSettingsNative: jest.fn(), + }; + const component = mount( + + ); + + expect(actions.exportSettings.mock.calls.length).toBe(0); + expect(actions.importSettingsDialog.mock.calls.length).toBe(0); + expect(actions.importSettingsNative.mock.calls.length).toBe(0); + expect(component.find('.export-result').text()).toBe(''); + expect(component.find('.import-result').text()).toBe(''); + + component.find('.export').simulate('click'); + component.setProps({ + settingsData: { + pageUrl: '', + exportResultText: 'export-result-text', + importResultText: '', + actionSuccess: true, + } + }); + expect(actions.exportSettings.mock.calls.length).toBe(1); + expect(component.find('.export-result').text()).toBe('export-result-text'); + + component.find('.import').simulate('click'); + component.setProps({ + settingsData: { + pageUrl: '', + exportResultText: '', + importResultText: 'import-result-text', + actionSuccess: true, + } + }); + expect(actions.importSettingsDialog.mock.calls.length).toBe(1); + expect(component.find('.import-result').text()).toBe('import-result-text'); + + component.find('#select-file').simulate('change'); + expect(actions.importSettingsNative.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/Notifications.jsx b/app/panel/components/Settings/__tests__/Notifications.jsx new file mode 100644 index 000000000..32ea8e74e --- /dev/null +++ b/app/panel/components/Settings/__tests__/Notifications.jsx @@ -0,0 +1,93 @@ +/** + * Notifications Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import Notifications from '../Notifications'; + +describe('app/panel/Settings/Notifications.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('Notifications is rendered correctly with falsy props', () => { + const settingsData = { + show_cmp: false, + notify_upgrade_updates: false, + notify_promotions: false, + notify_library_updates: false, + reload_banner_status: false, + trackers_banner_status: false, + show_badge: false, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('Notifications is rendered correctly with truthy props', () => { + const settingsData = { + show_cmp: true, + notify_upgrade_updates: true, + notify_promotions: true, + notify_library_updates: true, + reload_banner_status: true, + trackers_banner_status: true, + show_badge: true, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('Notifications functions correctly', () => { + const settingsData = { + show_cmp: false, + notify_upgrade_updates: false, + notify_promotions: false, + notify_library_updates: false, + reload_banner_status: false, + trackers_banner_status: false, + show_badge: false, + }; + const toggleCheckbox = jest.fn(); + + const component = mount( + + ); + + expect(toggleCheckbox.mock.calls.length).toBe(0); + component.find('#settings-announcements').simulate('click'); + component.find('#settings-new-features').simulate('click'); + component.find('#settings-new-promotions').simulate('click'); + component.find('#settings-new-trackers').simulate('click'); + component.find('#settings-show-reload-banner').simulate('click'); + component.find('#settings-show-trackers-banner').simulate('click'); + component.find('#settings-show-count-badge').simulate('click'); + expect(toggleCheckbox.mock.calls.length).toBe(7); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/OptIn.jsx b/app/panel/components/Settings/__tests__/OptIn.jsx new file mode 100644 index 000000000..083c43a4e --- /dev/null +++ b/app/panel/components/Settings/__tests__/OptIn.jsx @@ -0,0 +1,77 @@ +/** + * OptIn Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import OptIn from '../OptIn'; + +describe('app/panel/Settings/OptIn.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('OptIn is rendered correctly with falsy props', () => { + const settingsData = { + enable_metrics: false, + enable_human_web: false, + enable_offers: false, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('OptIn is rendered correctly with truthy props', () => { + const settingsData = { + enable_metrics: true, + enable_human_web: true, + enable_offers: true, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('OptIn functions correctly', () => { + const settingsData = { + enable_metrics: true, + enable_human_web: true, + enable_offers: true, + }; + const toggleCheckbox = jest.fn(); + + const component = mount( + + ); + + expect(toggleCheckbox.mock.calls.length).toBe(0); + component.find('#settings-share-usage').simulate('click'); + component.find('#settings-share-human-web').simulate('click'); + component.find('#settings-allow-offers').simulate('click'); + expect(toggleCheckbox.mock.calls.length).toBe(3); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx b/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx index eb70ac5bd..823ae3848 100644 --- a/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -20,7 +20,11 @@ describe('app/panel/components/Settings/TrustAndRestrict', () => { describe('Snapshot test with react-test-renderer', () => { test('Testing TrustAndRestrict is rendering', () => { const wrapper = renderer.create( - + {} }} + site_whitelist={[]} + site_blacklist={[]} + /> ).toJSON(); expect(wrapper).toMatchSnapshot(); }); diff --git a/app/panel/components/Settings/__tests__/__snapshots__/AdBlocker.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/AdBlocker.jsx.snap new file mode 100644 index 000000000..72a961de8 --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/AdBlocker.jsx.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/AdBlocker.jsx Snapshot tests with react-test-renderer AdBlocker is rendered correctly with AdOnly checked 1`] = ` +
    +
    +
    +

    + settings_adblocker +

    +
    + settings_adblocker_lists +
    +
    +
    +
    + settings_adblocker_list_1 +
    +
    + settings_adblocker_list_2 +
    +
    + settings_adblocker_list_3 +
    +
    +
    +
    + + + + + +
    +
    + + + + + +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/AdBlocker.jsx Snapshot tests with react-test-renderer AdBlocker is rendered correctly with Ads & Trackers checked 1`] = ` +
    +
    +
    +

    + settings_adblocker +

    +
    + settings_adblocker_lists +
    +
    +
    +
    + settings_adblocker_list_1 +
    +
    + settings_adblocker_list_2 +
    +
    + settings_adblocker_list_3 +
    +
    +
    +
    + + + + + +
    +
    + + + + + +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/AdBlocker.jsx Snapshot tests with react-test-renderer AdBlocker is rendered correctly with Ads, Trackers, & Annoyances checked 1`] = ` +
    +
    +
    +

    + settings_adblocker +

    +
    + settings_adblocker_lists +
    +
    +
    +
    + settings_adblocker_list_1 +
    +
    + settings_adblocker_list_2 +
    +
    + settings_adblocker_list_3 +
    +
    +
    +
    + + + + + +
    +
    + + + + + +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +`; diff --git a/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap new file mode 100644 index 000000000..22e43b4f7 --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap @@ -0,0 +1,462 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/GeneralSettings.jsx Snapshot tests with react-test-renderer GeneralSettings is rendered correctly with falsy props 1`] = ` +
    +
    +
    +

    + settings_trackers +

    +
    +
    + + +
    + + settings_last_update + + + + + + + + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +

    + settings_highlight_trackers +

    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +

    + settings_blocking +

    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/GeneralSettings.jsx Snapshot tests with react-test-renderer GeneralSettings is rendered correctly with truthy props 1`] = ` +
    +
    +
    +

    + settings_trackers +

    +
    +
    + + +
    + + settings_last_update + + + + 1000000 + + + + database-updated-text + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +

    + settings_highlight_trackers +

    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +

    + settings_blocking +

    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +`; diff --git a/app/panel/components/Settings/__tests__/__snapshots__/ImportExport.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/ImportExport.jsx.snap new file mode 100644 index 000000000..fae790743 --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/ImportExport.jsx.snap @@ -0,0 +1,184 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/ImportExport.jsx Snapshot tests with react-test-renderer ImportExport is rendered correctly with baseline props 1`] = ` +
    +

    + settings_export_header +

    +

    + settings_export_text +

    +

    + +

    +
    +

    + settings_import_header +

    +

    + settings_import_text +

    +

    + settings_import_warning +

    +

    + +

    + +
    +`; + +exports[`app/panel/Settings/ImportExport.jsx Snapshot tests with react-test-renderer ImportExport is rendered correctly with happy-path props 1`] = ` +
    +

    + settings_export_header +

    +

    + settings_export_text +

    +

    + Your settings have been successfully exported to your downloads folder on August 8, 2020 6:08 PM +

    +
    +

    + settings_import_header +

    +

    + settings_import_text +

    +

    + settings_import_warning +

    +

    + Your settings have been successfully imported on September 12, 2020 6:08 PM +

    + +
    +`; + +exports[`app/panel/Settings/ImportExport.jsx Snapshot tests with react-test-renderer ImportExport is rendered correctly with unhappy-path props 1`] = ` +
    +

    + settings_export_header +

    +

    + settings_export_text +

    +

    + Ghostery cannot export settings when the current page is one of the reserved browser pages. Please navigate to a different page and try again. +

    +
    +

    + settings_import_header +

    +

    + settings_import_text +

    +

    + settings_import_warning +

    +

    + That is the incorrect file type. Please choose a .ghost file and try again. +

    + +
    +`; diff --git a/app/panel/components/Settings/__tests__/__snapshots__/Notifications.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/Notifications.jsx.snap new file mode 100644 index 000000000..8ffa3a822 --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/Notifications.jsx.snap @@ -0,0 +1,355 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/Notifications.jsx Snapshot tests with react-test-renderer Notifications is rendered correctly with falsy props 1`] = ` +
    +
    +
    +

    + settings_notifications +

    +
    + settings_notify_me +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/Notifications.jsx Snapshot tests with react-test-renderer Notifications is rendered correctly with truthy props 1`] = ` +
    +
    +
    +

    + settings_notifications +

    +
    + settings_notify_me +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +`; diff --git a/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap new file mode 100644 index 000000000..3953f909d --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap @@ -0,0 +1,235 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/OptIn.jsx Snapshot tests with react-test-renderer OptIn is rendered correctly with falsy props 1`] = ` +
    +
    +
    +

    + settings_support_ghostery +

    +
    + settings_support_ghostery_by + : +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/OptIn.jsx Snapshot tests with react-test-renderer OptIn is rendered correctly with truthy props 1`] = ` +
    +
    +
    +

    + settings_support_ghostery +

    +
    + settings_support_ghostery_by + : +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +`; diff --git a/app/panel/components/Settings/__tests__/__snapshots__/TrustAndRestrict.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/TrustAndRestrict.jsx.snap index 0403041cf..96eddffb5 100644 --- a/app/panel/components/Settings/__tests__/__snapshots__/TrustAndRestrict.jsx.snap +++ b/app/panel/components/Settings/__tests__/__snapshots__/TrustAndRestrict.jsx.snap @@ -16,25 +16,29 @@ exports[`app/panel/components/Settings/TrustAndRestrict Snapshot test with react
    - - settings_trusted_sites - -
    -
    - - settings_restricted_sites - +
    + + settings_trusted_sites + +
    +
    + + settings_restricted_sites + +
    { + describe('Snapshot tests with react-test-renderer', () => { + test('About panel is rendered correctly', () => { + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/panel/components/__tests__/Help.jsx b/app/panel/components/__tests__/Help.jsx new file mode 100644 index 000000000..bfd93a890 --- /dev/null +++ b/app/panel/components/__tests__/Help.jsx @@ -0,0 +1,32 @@ +/** + * Help Test 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 renderer from 'react-test-renderer'; +import Help from '../Help'; + +jest.mock('../../utils/msg', () => ({ + openSupportPage: () => {}, + openHubPage: () => {}, +})); + +describe('app/panel/components/Help.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('Help panel is rendered correctly', () => { + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/panel/components/__tests__/__snapshots__/About.jsx.snap b/app/panel/components/__tests__/__snapshots__/About.jsx.snap new file mode 100644 index 000000000..7d943a683 --- /dev/null +++ b/app/panel/components/__tests__/__snapshots__/About.jsx.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/components/About.jsx Snapshot tests with react-test-renderer About panel is rendered correctly 1`] = ` + +`; diff --git a/app/panel/components/__tests__/__snapshots__/Help.jsx.snap b/app/panel/components/__tests__/__snapshots__/Help.jsx.snap new file mode 100644 index 000000000..6043581f6 --- /dev/null +++ b/app/panel/components/__tests__/__snapshots__/Help.jsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/components/Help.jsx Snapshot tests with react-test-renderer Help panel is rendered correctly 1`] = ` +
    +
    +
    +

    + panel_help_panel_header +

    + +
    +

    + panel_help_questions_header +

    + + panel_help_faq + + + panel_help_feedback + + + support + +
    +
    +

    + panel_help_contact_header +

    + + info@ghostery.com + +
    +
    +
    +
    +`; diff --git a/app/panel/utils/msg.js b/app/panel/utils/msg.js index c966af4f1..8582ab152 100644 --- a/app/panel/utils/msg.js +++ b/app/panel/utils/msg.js @@ -150,3 +150,13 @@ export function openHubPage(e) { sendMessage('openHubPage'); window.close(); } + +/** + * Send a message to open Account-Web if signed in or the Account Hub page if not + * This should be used for messages that don't require a callback. + * @memberOf PanelUtils + */ +export function openAccountPageAndroid(e) { + e.preventDefault(); + sendMessage('openAccountAndroid'); +} diff --git a/app/scss/android/_blocking_tab.scss b/app/scss/android/_blocking_tab.scss new file mode 100644 index 000000000..d876f5b93 --- /dev/null +++ b/app/scss/android/_blocking_tab.scss @@ -0,0 +1,381 @@ +/** + * Blocking Tabs Component Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +.BlockingHeader { + .BlockingHeader__text { + position: relative; + + h2 { + font-weight: 500; + padding: 12px 0 25px 10px; + font-size: 24px; + } + } +} + +.BlockingCategories { + .BlockingCategory { + &:not(:last-child) { + border-bottom: 1px solid #E0E0E0; + } + } +} + +.BlockingCategory { + color: $tundora; + font-weight: 500; + + + .BlockingCategory--noPointer { + cursor: text; + } + + .BlockingCategory--uppercase { + text-transform: uppercase; + } + + &.BlockingCategory__unknown:before { + content: ""; + position: relative; + top: -1px; + display: block; + width: 100%; + height: 5px; + border-top: 1px solid $ghosty-blue; + border-bottom: 1px solid $ghosty-blue; + } + + .BlockingCategory__details { + min-height: 80px; + padding: 15px 12px; + cursor: pointer; + } + + .BlockingCategory__image { + padding: 1px 12px 0 0; + width: 47px; + height: 43px; + } + + .BlockingCategory__name { + font-size: 18px; + } + + .BlockingCategory__numTrackers { + text-transform: uppercase; + } + + .BlockingCategory__numBlocked { + text-transform: uppercase; + margin-left: 5px; + color: $button-block; + } + + .BlockingCategory__buttons { + padding: 0 5px; + } + + .BlockingCategory__toggle { + height: 20px; + width: 20px; + background-image: url('/app/images/panel-android/down.svg'); + background-repeat: no-repeat; + background-position: center; + @include prefix('transition', 'background-image 300ms ease'); + + &.BlockingCategory--open { + background-image: url('/app/images/panel-android/up.svg'); + } + } + + .BlockingCategory__listHeader { + color: $tundora; + font-size: 12px; + font-weight: 500; + margin: 0 10px; + padding-bottom: 5px; + border-bottom: 1px solid #E0E0E0; + } + + .BlockingCategory__list { + overflow: hidden; + @include prefix('transition', 'height 300ms ease'); + } + + .BlockingCategory__tracker { + cursor: pointer; + padding: 0 10px; + + &:not(:last-child) .BlockingCategory__trackerBottom { + border-bottom: 1px solid #E0E0E0; + } + } +} + +.BlockingTracker { + position: relative; + + .BlockingTracker--noPointer { + cursor: text; + } + + .BlockingTracker__info { + height: 28px; + width: 28px; + background-image: url('/app/images/panel-android/icon-information-blue.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: 18px; + cursor: pointer; + } + + .BlockingTracker__name { + padding: 0 6px; + font-size: 14px; + font-style: italic; + font-weight: 400; + } + + .BlockingSelectButton { + margin-right: 7px; + } + + .BlockingSelectGroup { + position: absolute; + overflow: hidden; + word-break: break-all; + right: 0px; + width: 0px; + margin-right: -10px; + text-align: center; + @include prefix('transition', 'width 300ms ease'); + + &.BlockingSelectGroup--open.BlockingSelectGroup--wide { + width: 180px; + } + + &.BlockingSelectGroup--open:not(.BlockingSelectGroup--wide) { + width: 60px; + } + + &.BlockingSelectGroup--disabled { + .BlockingSelect__block, + .BlockingSelect__restrict, + .BlockingSelect__trust, + .BlockingSelect__anonymize { + background-color: #C6C6C6; + } + } + + .BlockingSelect { + flex-basis: 0; + padding-top: 30px; + color: $white; + font-size: 11px; + background-repeat: no-repeat; + background-position: center 10px; + } + + .BlockingSelect__block { + background-color: $button-block; + background-image: selectBlocked($white); + + &.BlockingSelect--disabled { + background-color: #C6C6C6; + } + } + + .BlockingSelect__restrict { + background-color: $button-restrict; + background-image: buildIconRestrict($white); + } + + .BlockingSelect__trust { + background-color: $button-trust; + background-image: buildIconTrust($white); + } + + .BlockingSelect__anonymize { + background-color: $ghosty-blue; + background-image: buildIconTrust($white); + } + } + + .OverrideSmartBlock { + position: relative; + cursor: pointer; + height: 25px; + width: 25px; + margin-right: 5px; + background-image: buildIconSmartBlocking($ghosty-blue); + background-size: 40px 40px; + background-position: center; + } + + .OverrideText { + position: absolute; + overflow: hidden; + word-break: keep-all; + right: 0px; + width: 0px; + margin-right: -10px; + color: $white; + background-color: $ghosty-blue; + font-size: 13px; + text-align: center; + @include prefix('transition', 'width 300ms ease'); + + &.OverrideText--open { + width: 250px; + } + + span { + min-width: 250px; + } + } + + .RequestModified { + color: $ghosty-blue; + text-transform: capitalize; + font-size: 11px; + font-style: normal; + padding-right: 5px; + + &:before { + content: ""; + position: relative; + display: inline-block; + top: 2px; + height: 13px; + width: 13px; + margin-right: 2px; + background-size: 21px 21px; + background-position: center; + background-repeat: no-repeat; + + } + &.RequestModified--ad:before { + background-image: buildIconAdBlocking($ghosty-blue); + } + &.RequestModified--cookie:before, + &.RequestModified--fingerprint:before { + background-image: buildIconAntiTracking($ghosty-blue); + } + } +} + +.BlockingSelectButton { + position: relative; + cursor: pointer; + height: 20px; + width: 20px; + border: 1px solid #cccccc; + background-color: #eeeeee; + + &.BlockingSelectButton__mixed::before { + content: " "; + position: absolute; + top: 8px; + left: 2px; + height: 2px; + width: 14px; + background-color: #a4a4a4; + } + + &.BlockingSelectButton__blocked, + &.BlockingSelectButton__trusted, + &.BlockingSelectButton__restricted { + border: none; + background-position: center; + background-repeat: no-repeat; + + &::before { + content: " "; + position: absolute; + top: 1px; + left: 1px; + width: 18px; + height: 18px; + border: 1px solid $white; + } + } + + &.BlockingSelectButton__blocked { + background-color: $button-block; + background-image: selectBlocked($white); + } + + &.BlockingSelectButton__trusted { + background-color: $button-trust; + background-image: buildIconTrust($white); + } + + &.BlockingSelectButton__restricted { + background-color: $button-restrict; + background-image: buildIconRestrict($white); + } +} + +.UnknownSVGContainer { + position: relative; + margin-right: 7px; + display: flex; + align-items: center; + justify-content: space-between; + &:not(.whitelisted) { + .cliqz-tracker-trust { + visibility: hidden; + cursor: pointer; + .border { stroke: #d8d8d8; } + .background { fill: #f7f7f7; } + .trust-circle { stroke: #9B9B9B; } + } + .cliqz-tracker-trust > g > path:nth-child(1) { + stroke: #d8d8d8; + } + .cliqz-tracker-trust > g > path:nth-child(2) { + fill: #f7f7f7; + } + .cliqz-tracker-scrub > g > .border { + fill: #FFF; + stroke: #00AEF0; + } + .cliqz-tracker-scrub > g > .background { + fill: #00AEF0; + stroke: #FFF; + } + .cliqz-tracker-scrub { + pointer-events: none; + .background.protected { + fill: #00AEF0; + } + .background.restricted { + fill: #00AEF0; + } + } + } + &.whitelisted { + flex-direction: row-reverse; + .cliqz-tracker-trust { + pointer-events: none; + } + .cliqz-tracker-scrub { + visibility: hidden; + pointer-events: auto; + cursor: pointer; + .border { stroke: #d8d8d8; } + .background { fill: #f7f7f7; } + .shield { stroke: #9B9B9B; } + } + } +} diff --git a/app/scss/android/_dots_menu.scss b/app/scss/android/_dots_menu.scss new file mode 100644 index 000000000..e51c2e407 --- /dev/null +++ b/app/scss/android/_dots_menu.scss @@ -0,0 +1,45 @@ +/** + * Dots Menu Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +.DotsMenu { + position: absolute; + top: 20px; + right: 5px; + z-index: 2; + text-align: right; + + .DotsMenu__button { + background-image: url(../../app/images/panel-android/kebab-menu-black.svg); + background-size: 4px 16px; + background-position: center; + background-repeat: no-repeat; + height: 16px; + width: 34px; + } + + .DotsMenu__content { + background-color: $white; + border-radius: 2px; + display: none; + @include prefix('box-shadow', '3px 2px 10px rgba(0,0,0,0.25)'); + + &.DotsMenu__open { display: block; } + ul { list-style: none; } + } + + .DotsMenu__item { + padding: 10px 20px; + width: 100%; + text-align: left; + } +} diff --git a/app/scss/android/_landscape.scss b/app/scss/android/_landscape.scss new file mode 100644 index 000000000..10533dfe4 --- /dev/null +++ b/app/scss/android/_landscape.scss @@ -0,0 +1,16 @@ +/** + * Landscape Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +@media screen and (orientation: landscape) { + //ToDo +} diff --git a/app/scss/android/_mixins.scss b/app/scss/android/_mixins.scss new file mode 100644 index 000000000..ee11a347f --- /dev/null +++ b/app/scss/android/_mixins.scss @@ -0,0 +1,180 @@ +/** + * Mixins + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +// Used in Tabs, Dots Menu, and Accordions +@mixin prefix($name, $value) { + @each $vendor in ('-webkit-', '-moz-', '-ms-', '-o-', '') { + #{$vendor}#{$name}: #{$value}; + } +} + +.s-search-box { + position: relative; + width: auto; + padding: 0; + background-color: $whisper; + .s-search-icon { + display: inline-block; + position: absolute; + width: 14.5px; + height: 14.5px; + top: 7px; + left: 7px; + &::before { + content: url('../../app/images/panel/search.svg'); + } + } + input { + margin: 0; + padding: 0; + font-family: $body-font-family; + font-style: normal; + font-weight: normal; + color: #333333; + font-size: 11px; + line-height: 15px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + padding-left: 28px; + height: 29px; + border: 1px solid #E7ECEE; + border-radius: 3px; + } +} + +%pointer { + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + &.disabled { + cursor: not-allowed; + } +} + +@keyframes g-tooltip-animation { + from {opacity: 0;} + to {opacity: 1;} +} + +%g-tooltip-animation { + animation-name: g-tooltip-animation; + animation-duration: 0.2s; +} + +%g-tooltip-body { + @extend %g-tooltip-animation; + pointer-events:none; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-size: 12px; + font-weight: 400; + -webkit-font-smoothing: antialiased; + text-align: center; + white-space: nowrap; + background: #333; + background: rgba(0,0,0,.8); + border-radius: 1px; + color: #fff; + padding: 3px 5px; + position: absolute; + z-index: 10; + font-size: 12px; + -webkit-box-shadow: 0px 2px 2px -1px rgba(#020202, 0.75); + -moz-box-shadow: 0px 2px 2px -1px rgba(#020202, 0.75); + box-shadow: 0px 2px 2px -1px rgba(#020202, 0.75); +} + +%g-tip-body { + @extend %g-tooltip-animation; + border-style: solid; + border-color: #333 transparent; + content: ""; + position: absolute; + z-index: 11; +} + +%g-tooltip { + display: inline; + position: relative; + + &:hover:before { + @extend %g-tooltip-body; + content: attr(data-g-tooltip); + } + &:hover:after { + @extend %g-tip-body; + } +} + +%g-tooltip-up-left{ + @extend %g-tooltip; + &:hover:before { + right: -15px; + bottom: 26px; + } + &:hover:after { + border-width: 6px 6px 0 6px; + left: 20%; + bottom: 20px; + } +} + +%g-tooltip-down-left { + @extend %g-tooltip; + &:hover:before { + @extend %g-tooltip:hover:before; + right: -100%; + top: 20px; + } + &:hover:after { + @extend %g-tooltip:hover:after; + border-width: 0 6px 6px 6px; + left: 20%; + top: 14px; + } +} + +%g-tooltip-down-right { + @extend %g-tooltip; + &:hover:before { + @extend %g-tooltip:hover:before; + left: -100%; + top: 20px; + } + &:hover:after { + @extend %g-tooltip:hover:after; + border-width: 0 6px 6px 6px; + left: 20%; + top: 14px; + } +} + +// Function helper with color variables +@function url-friendly-colour($colour) { + @return '%23' + str-slice('#{$colour}', 2, -1); +} + +// Used in Tabs +@keyframes tab--background-animation { + from { background-color: $ghosty-blue--lighter; } + to { background-color: $ghosty-blue; } +} + +// Panel Android SVGs +// Used in BlockingCategory & BlockingTracker +@function selectBlocked($colour) { + @return url('data:image/svg+xml;charset%3dUS-ASCII,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2050%2050%22%20xmlns%3D%22http://www.w3.org/2000/svg%22%3E%3Cg%20stroke%3D%22#{url-friendly-colour($colour)}%22%20stroke-width%3D%224%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M9%209%2041%2041%22%20/%3E%3Cpath%20d%3D%22M41%209%209%2041%22%20/%3E%3C/g%3E%3C/svg%3E'); +} diff --git a/app/scss/android/content/_normalize.scss b/app/scss/android/_normalize.scss similarity index 79% rename from app/scss/android/content/_normalize.scss rename to app/scss/android/_normalize.scss index 7d4f0e775..6b0095bd6 100644 --- a/app/scss/android/content/_normalize.scss +++ b/app/scss/android/_normalize.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -26,9 +26,9 @@ button { html, body { @include prefix('user-select', 'none'); - font-family: Roboto, "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; } -body, .accordion { +body { background-color:rgba(255, 255, 255, 0.7); } diff --git a/app/scss/android/_overview.scss b/app/scss/android/_overview.scss deleted file mode 100644 index f66745248..000000000 --- a/app/scss/android/_overview.scss +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Overview Component 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 - */ - -.overview { - padding-top: 20px; - padding-bottom: 50px; - text-align: center; - - .buttons-wrapper { - padding-top: 65px; - - .button { - min-width: 180px; - background-color: $white; - border: 1px solid #ccc; - - & > span { - padding-left: 20px; - background-repeat: no-repeat; - background-position: left center; - color: $tundora; - } - - &.trust-site-btn { - - & > span { - background-image: buildIconTrust($button-dark-grey); - } - - &.changed:not(.paused) { - background-color: #9ecc42; - border-color: #9ecc42; - - & > span { - color: $white; - background-image: buildIconTrust($button-white); - } - } - } - - &.restrict-site-btn { - - & > span { - background-image: buildIconRestrict($button-dark-grey); - } - - &.changed:not(.paused) { - background-color: #e74055; - border-color: #e74055; - - & > span { - color: $white; - background-image: buildIconRestrict($button-white); - } - } - } - - &.pause-resume-btn { - - & > span { - background-image: url(../../app/images/panel-android/pause.svg); - - &:last-child { - display: none; - } - } - - &.changed { - background-color: $cliqz-blue; - border-color: $cliqz-blue; - - & > span { - background-image: url(../../app/images/panel-android/play.svg); - - &:first-child { - display: none; - } - - &:last-child { - display: inline-block; - color: $white; - } - } - } - } - } - } - - @media screen and (orientation: landscape) { - .chart-wrapper { - display: none; - } - } -} diff --git a/app/scss/android/_overview_tab.scss b/app/scss/android/_overview_tab.scss new file mode 100644 index 000000000..16ccb95f5 --- /dev/null +++ b/app/scss/android/_overview_tab.scss @@ -0,0 +1,136 @@ +/** + * Overview Tab Component Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +.OverviewTab { + padding-bottom: 35px; + text-align: center; + + .OverviewTab__NavigationLinks { + position: absolute; + + .OverviewTab__NavigationLink { + cursor: pointer; + margin: 0 10px; + } + } + + .OverviewTab__NotScannedContainer { + margin-top: 26px; + } + + .OverviewTab__DonutGraphContainer { + margin: 26px auto; + height: 120px; + width: 120px; + + .DonutGraph__tooltipContainer, + .DonutGraph__textCount span { + display: none; + } + } + + .OverviewTab__PageHostContainer { + margin-top: 20px; + margin-bottom: 10px; + + .OverviewTab__PageHostText { + font-size: 11px; + font-weight: 600; + color: $tundora; + } + } + + .OverviewTab__PageStatsContainer { + margin-bottom: 21px; + + .OverviewTab__PageStat { + line-height: 21px; + font-size: 14px; + font-weight: 600; + } + .OverviewTab__PageStat--red { + color: $red; + } + .OverviewTab__PageStat--blue { + color: $ghosty-blue; + } + } + + .OverviewTab__GhosteryFeaturesContainer { + .GhosteryFeatureButton__text, + .pause-button-text { + font-weight: 600; + } + + .OverviewTab__GhosteryFeature--ExtraMargins { + margin: 12px 28px; + } + + .GhosteryFeatureButton--inactive.clickable { + &:hover { color: $tundora; } + &.trust:hover { background-color: $white; } + &.restrict:hover { background-color: $white; } + } + .GhosteryFeatureButton--active { + &.trust:hover { box-shadow: inset 0px 1px 7px 2px #85b329; } + &.restrict:hover { box-shadow: inset 0px 1px 7px 2px #ce273c; } + } + + .sub-component.pause-button .button { + &.active:hover { + border-color: #0093bd; + background-color: $cliqz-feature--blue; + box-shadow: inset 0px 1px 7px 2px #0093bd; + } + &:not(.active):hover { background-color: $white; } + } + + .tooltip-content { + display: none; + } + } + + .OverviewTab__CliqzFeaturesContainer { + margin: 12px 0; + + .OverviewTab__CliqzFeature { + width: 55px; + display: inline-block; + margin: 0 10px; + } + + .CliqzFeature--active.clickable { + &:hover { + .CliqzFeature__status { color: $cliqz-feature--blue; } + .CliqzFeature__icon--anti-track { background-image: buildIconAntiTracking($cliqz-feature--blue); } + .CliqzFeature__icon--ad-block { background-image: buildIconAdBlocking($cliqz-feature--blue); } + .CliqzFeature__icon--smart-block { background-image: buildIconSmartBlocking($cliqz-feature--blue); } + .CliqzFeature__feature-name { color: $cliqz-feature--blue; } + } + } + + .CliqzFeature--inactive.clickable { + &:hover { + .CliqzFeature__status { color: $cliqz-feature--gray; } + .CliqzFeature__icon--anti-track { background-image: buildIconAntiTracking($cliqz-feature--gray); } + .CliqzFeature__icon--ad-block { background-image: buildIconAdBlocking($cliqz-feature--gray); } + .CliqzFeature__icon--smart-block { background-image: buildIconSmartBlocking($cliqz-feature--gray); } + .CliqzFeature__feature-name { color: $cliqz-feature--gray; } + } + } + + .tooltip-content { + display: none; + } + } +} diff --git a/app/scss/android/_settings.scss b/app/scss/android/_settings.scss new file mode 100644 index 000000000..f13ffe9fb --- /dev/null +++ b/app/scss/android/_settings.scss @@ -0,0 +1,159 @@ +/** + * Settings Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +.Settings#content-settings { + position: relative; + height: 100%; + width: 100%; + + .SettingsHeader { + height: $tab--nav-height; + line-height: $tab--nav-height; + background-color: $ghosty-blue; + text-align: center; + position: relative; + @include prefix('box-shadow', '3px 2px 10px rgba(0,0,0,0.15)'); + + .SettingsHeader__icon { + position: absolute; + left: 0; + height: 14px; + width: 10px; + padding: 8px 10px; + margin: 10px; + line-height: 0; + cursor: pointer; + } + + .SettingsHeader__text { + color: $white; + font-size: 16px; + font-weight: 500; + } + } + + .Settings__link { + margin: 16px 10px; + color: #4A4A4A; + font-size: 16px; + font-weight: 700; + } + + .s-trust-restrict-panel { + left: 0; + width: 100%; + border-left: none; + background-color: #ffffff !important; + + .s-trust-restrict-menu .s-pane-title { + border-left: 1px solid #CCCCCC !important; + + &:last-of-type { + border-left: none !important; + } + + &:not(.s-active-pane) { + background-color: #E5E5E5 !important; + } + } + + .s-site, .s-site:hover { + background-color: $white; + + .s-site-x-container { + @extend %pointer; + background: url('../../app/images/panel/icon-trust-restrict-x.svg') no-repeat center center; + background-size: 12px 12px; + height: 32px; + width: 32px; + } + } + + .s-site-name { + color: #4a4a4a; + font-size: 13px; + line-height: 35px; + } + + .s-site-description span { + font-size: 13px; + } + + .s-sites-input-box input { + height: 35px; + font-size: 13px; + } + + .s-sites-input-icon { + height: 20px; + width: 20px; + + &::before { + height: 20px; + width: 20px; + background-position: center; + background-size: 18px 18px; + background-repeat: no-repeat; + } + } + } + + .s-tabs-panel { + left: 0; + width: 100%; + border-left: none; + + h3 { + font-size: 16px; + padding-top: 10px; + + &.s-special { + max-width: 100%; + } + } + + h5 { + font-size: 14px; + max-width: 100%; + } + + p { + font-size: 14px; + } + + .s-option-group { + margin-right: 0; + + .s-checkbox-label { + margin-left: 24px; + max-width: 100%; + + span { + font-size: 14px; + line-height: 16px; + } + } + + .s-square-checkbox input[type="checkbox"] + label { + margin-left: 24px; + font-size: 14px; + line-height: 16px; + + &::before { + height: 14px; + width: 14px; + } + } + } + } +} diff --git a/app/scss/android/_tabs.scss b/app/scss/android/_tabs.scss new file mode 100644 index 000000000..533f1c3fd --- /dev/null +++ b/app/scss/android/_tabs.scss @@ -0,0 +1,48 @@ +/** + * Tabs Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +.Tabs__component { + .Tabs__navigation { + list-style: none; + flex-wrap: wrap; + padding: 0; + margin: 0; + background-color: $ghosty-blue; + @include prefix('box-shadow', '3px 2px 10px rgba(0,0,0,0.15)'); + } + .Tab__navigation_item { + flex-grow: 1; + flex-basis: 0; + text-align: center; + min-height: $tab--nav-height; + cursor: pointer; + } + .Tab__navigation_item.Tab--active { + border-bottom: 2px solid $white; + @include prefix('animation', 'tab--background-animation 800ms cubic-bezier(0.4, 0, 1, 1) alternate 1'); + } + .Tab__navigation_link { + padding: 0 10px 2px; + color: rgba($white, 0.8); + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + @include prefix('transition', 'color 300ms ease-in'); + } + .Tab__navigation_link.Tab--active { + padding-bottom: 0; + color: $white; + } + + .Tabs__active_content {} +} diff --git a/app/scss/android/_variables.scss b/app/scss/android/_variables.scss new file mode 100644 index 000000000..3663eb73f --- /dev/null +++ b/app/scss/android/_variables.scss @@ -0,0 +1,33 @@ +/** + * Sass Variables + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +// Copied for partials/_settings.scss +$body-font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif; +$mercury: #E5E5E5; +$whisper: #F7F8FB; + +// CSS Properties +$tab--nav-height: 50px; + +// Colors +$ghosty-blue: #00AEF0; +$ghosty-blue--lighter: #28C4FF; +$cliqz-feature--blue: #1dafed; +$cliqz-feature--gray: #c8c7c2; +$white: #FFFFFF; +$tundora: #4A4A4A; +$button-trust: #9FCA4C; +$button-restrict: #BC4A4B; +$button-block: #E44258; +$atlantis: #9ECC42; +$red: #E74055; diff --git a/app/scss/android/content/_accordions.scss b/app/scss/android/content/_accordions.scss deleted file mode 100644 index 467ad2703..000000000 --- a/app/scss/android/content/_accordions.scss +++ /dev/null @@ -1,303 +0,0 @@ -/** - * Accordion 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 - */ - -.accordions { - .accordion { - position: relative; - - .accordionSelect { - position: absolute; - top: 18px; - right: 19px; - border: 1px solid #ccc; - width: 20px; - height: 20px; - background-color: #eee; - background-repeat: no-repeat; - background-position: center; - z-index: 1; - - &.blocked, &.trusted, &.restricted { - border: none; - - &:before { - content: " "; - position: absolute; - top: 1px; - right: 1px; - width: 18px; - height: 18px; - border: 1px solid $white; - } - } - - &.trusted { - background-color: $button-trust; - background-image: buildIconTrust($button-white); - } - - &.restricted { - background-color: $button-restrict; - background-image: buildIconRestrict($button-white); - } - - &.blocked { - background-color: $button-block; - background-image: buildIconBlock($button-white); - } - - &.mixed { - - &:before { - content: " "; - position: absolute; - top: 8px; - right: 1px; - height: 0; - width: 16px; - border: 1px solid #ccc; - } - } - } - - .accordionTitle { - position: relative; - display: block; - cursor: pointer; - background-repeat: no-repeat; - background-size: 35px; - background-position: 12px 16px; - padding: 15px 0 15px 59px; - color: #4A4A4A; - font-weight: 500; - - & > h2 { - font-size: 18px; - } - - & > p { - text-transform: uppercase; - - & > span { - - &.blocked-trackers { - margin-left: 5px; - color: $button-block; - } - } - } - - &:after { - content: " "; - position: absolute; - right: 9px; - bottom: 0; - width: 40px; - height: 40px; - background-image: url(../../app/images/panel-android/down.svg); - background-repeat: no-repeat; - background-position: center; - @include prefix('transition', 'background-image 300ms ease-in'); - } - - &.active { - &:after { - background-image: url(../../app/images/panel-android/up.svg); - } - } - } - - .accordionContent { - overflow: hidden; - @include prefix('transition', 'height 300ms ease-in'); - height: var(--trackers-length, 0px); - - & > p { - margin: 0 10px; - height: 32px; - line-height: 32px; - font-weight: 500; - border-bottom: 1px solid #E0E0E0; - - & > span { - &:last-child { - float: right; - padding-right: 4px; - } - } - } - - .trackers-list { - list-style: none; - - .tracker { - position: relative; - margin-left: 4px; - @include prefix('transition', 'margin 300ms ease-in'); - height: $tracker-height; - - .info { - display: inline-block; - width: 40px; - height: 100%; - float: left; - background-image: url(../../app/images/panel-android/icon-information-blue.svg); - background-repeat: no-repeat; - background-position: center; - background-size: 18px; - } - - .trackerName { - font-style: italic; - - & > span:first-child { - line-height: $tracker-height; - font-size: 14px; - } - } - - .trackerSelect { - position: absolute; - top: 15px; - right: 19px; - border: 1px solid #ccc; - width: 20px; - height: 20px; - background-repeat: no-repeat; - background-position: center; - - &:before { - content: " "; - position: absolute; - top: 1px; - right: 1px; - width: 18px; - height: 18px; - } - } - - .menu { - position: absolute; - top: 0; - right: calc(-3 * #{$tracker-menu-item-width}); - width: calc(3 * #{$tracker-menu-item-width}); - @include prefix('transition', 'right 300ms ease-in'); - height: 100%; - z-index: 1; - overflow: hidden; - - .trackerOption { - padding-top: 20px; - height: 100%; - width: $tracker-menu-item-width; - color: $white; - background-repeat: no-repeat; - background-position: center 10px; - font-size: 11px; - - &.trust { - background-color: $button-trust; - background-image: buildIconTrust($button-white); - } - - &.restrict { - background-color: $button-restrict; - background-image: buildIconRestrict($button-white); - } - - &.block { - background-color: $button-block; - background-image: buildIconBlock($button-white); - - &.disabled { - background-color: #C6C6C6; - pointer-events: none; - } - } - } - - &.global-trackers { - right: -$tracker-menu-item-width; - width: $tracker-menu-item-width; - - .trackerOption { - - &.trust { - display: none; - } - - &.restrict { - display: none; - } - } - } - } - - &.show-menu { - margin-left: -10px; - - .menu { - right: 0; - } - } - - &.blocked, &.trusted, &.restricted { - .trackerSelect:before { - border: 1px solid $white; - } - } - - &.blocked { - - .trackerName { - text-decoration: line-through; - color: #C7C7CD; - } - - .trackerSelect { - border: none; - background-image: buildIconBlock($button-white); - background-color: $button-block; - } - } - - &.trusted { - - .trackerSelect { - border: none; - background-image: buildIconTrust($button-white); - background-color: $button-trust; - } - } - - &.restricted { - - .trackerSelect { - border: none; - background-image: buildIconRestrict($button-white); - background-color: $button-restrict; - } - } - } - - & > li:not(:last-child) .tracker { - border-bottom: 1px solid #eee; - } - } - } - - &:not(:last-child) { - border-bottom: 1px solid #E0E0E0; - } - } -} diff --git a/app/scss/android/content/_dots-menu.scss b/app/scss/android/content/_dots-menu.scss deleted file mode 100644 index aed9cb0bf..000000000 --- a/app/scss/android/content/_dots-menu.scss +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Dots Menu 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 - */ - -.header { - position: relative; - - & > h2 { - font-weight: 500; - padding: 12px 0 25px 10px; - font-size: 24px; - } - - .dots-menu { - position: absolute; - top: 20px; - right: 5px; - z-index: 2; - text-align: right; - - .dots-menu-btn { - background-image: url(../../app/images/panel-android/kebab-menu-black.svg); - background-size: 4px 16px; - background-position: center; - background-repeat: no-repeat; - height: 16px; - width: 34px; - } - - .dots-menu-content { - background-color: $white; - border-radius: 2px; - display: none; - @include prefix('box-shadow', '3px 2px 10px rgba(0,0,0,0.25)'); - - & > ul { - list-style: none; - - & > li { - - .dots-menu-item { - padding: 10px 20px; - width: 100%; - text-align: left; - } - } - } - - &.opening { - display: block; - } - } - } -} diff --git a/app/scss/android/content/_fixed-menu.scss b/app/scss/android/content/_fixed-menu.scss deleted file mode 100644 index 575b97591..000000000 --- a/app/scss/android/content/_fixed-menu.scss +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Fixed Menu 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 - */ - -.fixed-menu { - position: fixed; - bottom: 0; - right: 0; - width: 100%; - top: calc(100% - #{$fixed-menu-header-height}); - @include prefix('transition', 'top 250ms cubic-bezier(0.55, 0.09, 0.68, 0.53)'); - @include prefix('box-shadow', '0px -3px 4px rgba(0,0,0,0.15)'); - z-index: 1; //TODO: @mai check this - - .menuHeader { - height: $fixed-menu-header-height; - background-color: $white; - text-align: center; - color: $cliqz-blue; - - & > p { - position: relative; - padding-top: 12px; - font-size: 16px; - font-weight: 500; - - &:before { - content: " "; - height: 0; - width: 40px; - left: 0; - right: 0; - margin: 0 auto; - border: 2px solid $cliqz-blue; - border-radius: 5px; - position: absolute; - top: 5px; - } - } - } - - .menuContent { - position: relative; - margin: 0; - list-style: none; - height: calc(100% - #{$fixed-menu-header-height}); - background-color: $white; - - .menuItem { - height: calc(100% / 3); - width: 100%; - display: table; - - .menuItemWrapper { - height: 100%; - display: table-cell; - vertical-align: middle; - - .anti_tracking, .ad_block, .smart_block { - background-repeat: no-repeat; - background-position: 20px center; - color: $cliqz-blue; - } - - .anti_tracking { - background-image: buildAntiTrackIcon($button-blue); - } - - .ad_block { - background-image: buildAdBlockIcon($button-blue); - } - - .smart_block { - background-image: buildSmartBlockIcon($button-blue); - } - - .menuItemOverview { - width: calc(100% - 47px); - color: $cliqz-blue; - float: left; - - .anti_tracking, .ad_block, .smart_block { - font-size: 23px; - font-weight: 500; - padding-left: 60px; - padding-right: 10px; - background-size: contain; - } - - .smart_block { - background-position-x: 27px; - background-size: 17px; - } - - .title { - padding-right: 30px; - background-image: url(../../app/images/panel-android/icon-information-grey.svg); - background-repeat: no-repeat; - background-position: right center; - background-size: contain; - font-size: 15px; - font-weight: 500; - } - - .description { - display: none; - } - } - - .switcher { - display: inline-block; - margin-top: 14px; - width: 32px; - height: 16px; - border-radius: 8px; - background-color: #82D6F5; - position: relative; - @include prefix('transition', 'background-color 200ms ease-in'); - - &:after { - content: ' '; - position: absolute; - top: -2px; - left: 50%; - width: 20px; - height: 20px; - border-radius: 50%; - background-color: $cliqz-blue; - @include prefix('transition', 'left 200ms ease-in'); - } - - &:not(.active) { - background-color: #bbb; - - &:after { - background-color: #aaa; - left: 0; - } - } - } - - .menuItemContent { - position: absolute; - top: 0; - left: 100%; - bottom: 0; - width: 100%; - @include prefix('transition', 'left 300ms ease-in'); - z-index: 1; - text-align: center; - background-color: $white; - padding-top: 5px; - - .anti_tracking, .ad_block, .smart_block { - font-size: 40px; - padding-left: 37px; - background-size: 30px; - background-position: 5px center; - } - - .smart_block { - background-position-x: 12px; - background-size: 23px; - } - - .headline { - font-weight: bold; - color: $cliqz-blue; - font-size: 16px; - } - - .description { - padding: 10px 20px 0; - text-align: left; - } - - .close { - position: absolute; - top: 0; - left: 0; - width: 40px; - height: 40px; - background-repeat: no-repeat; - background-image: url(../../app/images/panel-android/back.svg); - background-position: center; - } - - &.opening { - left: 0; - } - } - } - - &:not(:last-child) { - border-bottom: 1px solid #eee; - } - } - } - - &.opened { - top: 55%; - } -} diff --git a/app/scss/android/content/_landscape.scss b/app/scss/android/content/_landscape.scss deleted file mode 100644 index 9cd12f2c7..000000000 --- a/app/scss/android/content/_landscape.scss +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Landscape 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 - */ - -@media screen and (orientation: landscape) { - .tabs-wrapper { - width: 50%; - float: right; - - ul.tabs-nav li.tab-item a.tab-link.custom-link { - font-size: 1.5vw; - } - - .buttons-wrapper { - padding-top: 10px; - } - } - - .fixed-menu { - width: 50%; - - .menuItemOverview { - pointer-events: none; - background-color: $white; - - .title { - background: none; - } - - .description { - display: block; - margin-right: -45px; - padding-left: 20px; - color: #4A4A4A; - } - } - - &.opened { - top: $tab-header-height; - overflow-y: auto; - overflow-x: hidden; - } - } - - & > div > .chart-wrapper { - display: block; - border-right: 1px solid #eee; - padding-top: 0; - - .trackers-chart { - margin: 5px 0; - } - } -} diff --git a/app/scss/android/content/_mixins.scss b/app/scss/android/content/_mixins.scss deleted file mode 100644 index 5e47a0265..000000000 --- a/app/scss/android/content/_mixins.scss +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Mixins - * - * 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 - */ - -@mixin prefix($name, $value) { - @each $vendor in ('-webkit-', '-moz-', '-ms-', '-o-', '') { - #{$vendor}#{$name}: #{$value}; - } -} - -@keyframes dash { - from { - stroke-dashoffset: var(--stroke-length, 0); - } - to { - stroke-dashoffset: 0; - } -} - -@keyframes tab-animation { - from { - background-color: #28C4FF; - } - to { - background-color: #00AEF0; - } -} - -@function buildIconBlock($stroke-color) { - @return url('data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3Csvg%20width%3D%2214px%22%20height%3D%2214px%22%20viewBox%3D%220%200%2020%2020%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Ctitle%3EBlock%3C%2Ftitle%3E%0A%20%20%20%20%3Cdesc%3ECreated%20with%20Sketch.%3C%2Fdesc%3E%0A%20%20%20%20%3Cdefs%3E%3C%2Fdefs%3E%0A%20%20%20%20%3Cg%20id%3D%22Block%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cpolygon%20id%3D%22Shape%22%20fill%3D%22%23#{$stroke-color}%22%20points%3D%2217%204.4%2015.6%203%2010%208.6%204.4%203%203%204.4%208.6%2010%203%2015.6%204.4%2017%2010%2011.4%2015.6%2017%2017%2015.6%2011.4%2010%22%3E%3C%2Fpolygon%3E%0A%20%20%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E'); -} - -@function buildIconTrust($stroke-color) { - @return url('data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3Csvg%20width%3D%2214px%22%20height%3D%2214px%22%20viewBox%3D%220%200%2014%2014%22%20version%3D%221.1%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%3E%0A%20%20%20%20%3C%21--%20Generator%3A%20Sketch%2047.1%20%2845422%29%20-%20http%3A//www.bohemiancoding.com/sketch%20--%3E%0A%20%20%20%20%3Ctitle%3EOval%3C/title%3E%0A%20%20%20%20%3Cdesc%3ECreated%20with%20Sketch.%3C/desc%3E%0A%20%20%20%20%3Cdefs%3E%3C/defs%3E%0A%20%20%20%20%3Cg%20id%3D%22Page-1%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Card-view---list-expanded-MAX---HOVER%22%20transform%3D%22translate%28-687.000000%2C%20-498.000000%29%22%20stroke-width%3D%222%22%20stroke%3D%22%23#{$stroke-color}%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Ccircle%20id%3D%22Oval%22%20cx%3D%22693.875%22%20cy%3D%22504.875%22%20r%3D%225.875%22%3E%3C/circle%3E%0A%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%3C/g%3E%0A%3C/svg%3E'); -} - -@function buildIconRestrict($stroke-color) { - @return url('data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3Csvg%20width%3D%2214px%22%20height%3D%2214px%22%20viewBox%3D%220%200%2014%2014%22%20version%3D%221.1%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%3E%0A%20%20%20%20%3C%21--%20Generator%3A%20Sketch%2047.1%20%2845422%29%20-%20http%3A//www.bohemiancoding.com/sketch%20--%3E%0A%20%20%20%20%3Ctitle%3Eicon-%20restrict%20site%3C/title%3E%0A%20%20%20%20%3Cdesc%3ECreated%20with%20Sketch.%3C/desc%3E%0A%20%20%20%20%3Cdefs%3E%3C/defs%3E%0A%20%20%20%20%3Cg%20id%3D%22Page-1%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Card-view---list-expanded-MAX---HOVER%22%20transform%3D%22translate%28-687.000000%2C%20-536.000000%29%22%20stroke%3D%22%23#{$stroke-color}%22%20stroke-width%3D%222%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20id%3D%22icon--restrict-site%22%20transform%3D%22translate%28688.000000%2C%20537.000000%29%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M1.95833333%2C1.95833333%20L9.79166667%2C9.79166667%22%20id%3D%22Line%22%20stroke-linecap%3D%22square%22%3E%3C/path%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ccircle%20id%3D%22Oval%22%20cx%3D%225.75260417%22%20cy%3D%225.75260417%22%20r%3D%225.75260417%22%3E%3C/circle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%3C/g%3E%0A%3C/svg%3E'); -} - -@function buildIconPause($stroke-color) { - @return url('data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3Csvg%20width%3D%2210px%22%20height%3D%2213px%22%20viewBox%3D%220%200%2010%2013%22%20version%3D%221.1%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%3E%0A%20%20%20%20%3C%21--%20Generator%3A%20Sketch%2047.1%20%2845422%29%20-%20http%3A//www.bohemiancoding.com/sketch%20--%3E%0A%20%20%20%20%3Ctitle%3EShape%3C/title%3E%0A%20%20%20%20%3Cdesc%3ECreated%20with%20Sketch.%3C/desc%3E%0A%20%20%20%20%3Cdefs%3E%3C/defs%3E%0A%20%20%20%20%3Cg%20id%3D%22Page-1%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Card-view---list-expanded-MAX---HOVER%22%20transform%3D%22translate%28-684.000000%2C%20-575.000000%29%22%20fill%3D%22%23#{$stroke-color}%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Group-2%22%20transform%3D%22translate%28684.000000%2C%20575.000000%29%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20id%3D%22pause%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M0%2C12.173913%20L2.60869565%2C12.173913%20L2.60869565%2C0%20L0%2C0%20L0%2C12.173913%20L0%2C12.173913%20Z%20M6.95652174%2C0%20L6.95652174%2C12.173913%20L9.56521739%2C12.173913%20L9.56521739%2C0%20L6.95652174%2C0%20L6.95652174%2C0%20Z%22%20id%3D%22Shape%22%3E%3C/path%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%3C/g%3E%0A%3C/svg%3E'); -} - -@function buildIconResume($stroke-color) { - @return url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20version%3D%221.1%22%20x%3D%220px%22%20y%3D%220px%22%20viewBox%3D%220%200%20100%20100%22%20enable-background%3D%22new%200%200%20100%20100%22%20xml%3Aspace%3D%22preserve%22%20fill%3D%22%23fff%22%3E%3Cpath%20d%3D%22M72%2C48.3l-42-24c-1.313-0.875-3%2C0.295-3%2C1.7v48c0%2C1.533%2C1.767%2C2.523%2C3%2C1.7l42-24C73.589%2C50.639%2C73.047%2C48.824%2C72%2C48.3z%20%20%20M31%2C70.6V29.4L67%2C50L31%2C70.6z%22%3E%3C/path%3E%3C/svg%3E%0A'); -} - -// cliqz drawer buttons -@function buildAntiTrackIcon($stroke-color) { - @return url('data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3Csvg%20width%3D%2222px%22%20height%3D%2222px%22%20viewBox%3D%220%200%2022%2022%22%20version%3D%221.1%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%3E%0A%20%20%20%20%3C%21--%20Generator%3A%20Sketch%2047.1%20%2845422%29%20-%20http%3A//www.bohemiancoding.com/sketch%20--%3E%0A%20%20%20%20%3Ctitle%3EPage%201%20Copy%202%3C/title%3E%0A%20%20%20%20%3Cdesc%3ECreated%20with%20Sketch.%3C/desc%3E%0A%20%20%20%20%3Cdefs%3E%3C/defs%3E%0A%20%20%20%20%3Cg%20id%3D%22Page-1%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Simple-View-Panel---hover---adblocing%22%20transform%3D%22translate%28-1082.000000%2C%20-690.000000%29%22%20stroke%3D%22%23#{$stroke-color}%22%20stroke-width%3D%222%22%20fill%3D%22none%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M1093.2127%2C691.032068%20C1093.08497%2C690.989311%201092.91503%2C690.989311%201092.7873%2C691.032068%20L1083.63836%2C693.457515%20C1083.25545%2C693.542485%201083%2C693.882908%201083%2C694.265816%20C1083.04276%2C700.776614%201086.53196%2C706.81899%201092.53185%2C710.861584%20C1092.65958%2C710.946554%201092.82979%2C710.989311%201093%2C710.989311%20C1093.17021%2C710.989311%201093.34042%2C710.946554%201093.46815%2C710.861584%20C1099.46804%2C706.81899%201102.95724%2C700.776614%201103%2C694.265816%20C1103%2C693.882908%201102.74455%2C693.542485%201102.36164%2C693.457515%20L1093.2127%2C691.032068%20Z%22%20id%3D%22Page-1-Copy-2%22%3E%3C/path%3E%0A%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%3C/g%3E%0A%3C/svg%3E'); -} - -@function buildAdBlockIcon($stroke-color) { - @return url('data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3Csvg%20width%3D%2224px%22%20height%3D%2224px%22%20viewBox%3D%220%200%2024%2024%22%20version%3D%221.1%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%3E%0A%20%20%20%20%3C%21--%20Generator%3A%20Sketch%2047.1%20%2845422%29%20-%20http%3A//www.bohemiancoding.com/sketch%20--%3E%0A%20%20%20%20%3Ctitle%3EFill%201%3C/title%3E%0A%20%20%20%20%3Cdesc%3ECreated%20with%20Sketch.%3C/desc%3E%0A%20%20%20%20%3Cdefs%3E%3C/defs%3E%0A%20%20%20%20%3Cg%20id%3D%22Page-1%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Simple-View-Panel---hover---adblocing%22%20transform%3D%22translate%28-1166.000000%2C%20-735.000000%29%22%20stroke%3D%22%23#{$stroke-color}%22%20stroke-width%3D%220.5%22%20fill%3D%22%23#{$stroke-color}%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M1171.71317%2C754.301569%20C1172.4562%2C755.045891%201173.19537%2C755.790213%201173.94227%2C756.528096%20C1173.99635%2C756.582182%201174.10195%2C756.610513%201174.18565%2C756.610513%20C1176.73025%2C756.614376%201179.27356%2C756.615664%201181.81816%2C756.609225%20C1181.91088%2C756.609225%201182.02806%2C756.56029%201182.09374%2C756.495903%20C1183.89659%2C754.700773%201185.69429%2C752.901779%201187.4907%2C751.098923%20C1187.55638%2C751.03196%201187.60918%2C750.91735%201187.60918%2C750.824631%20C1187.6169%2C748.272302%201187.61561%2C745.72126%201187.61304%2C743.16893%20C1187.61175%2C743.099391%201187.5963%2C743.007961%201187.55252%2C742.961601%20C1186.80562%2C742.203114%201186.05357%2C741.451065%201185.31054%2C740.704168%20C1180.77122%2C745.242215%201176.24734%2C749.766097%201171.71317%2C754.301569%20M1170.68168%2C753.279092%20C1175.21843%2C748.74362%201179.74488%2C744.215875%201184.27648%2C739.684266%20C1183.54761%2C738.955397%201182.80844%2C738.212362%201182.06154%2C737.474479%20C1182.00617%2C737.420393%201181.90186%2C737.389487%201181.82073%2C737.389487%20C1179.27614%2C737.384336%201176.73154%2C737.384336%201174.18823%2C737.390775%20C1174.09422%2C737.390775%201173.97575%2C737.435846%201173.91007%2C737.501522%20C1172.10207%2C739.301803%201170.29664%2C741.104659%201168.49637%2C742.912667%20C1168.43971%2C742.970616%201168.39077%2C743.068485%201168.39077%2C743.147038%20C1168.38562%2C745.714821%201168.38562%2C748.281316%201168.38948%2C750.847811%20C1168.38948%2C750.91735%201168.41524%2C751.003629%201168.46289%2C751.049988%20C1169.20463%2C751.802037%201169.95281%2C752.548935%201170.68168%2C753.279092%20M1188.99608%2C747.014165%20C1188.99608%2C748.386912%201188.99093%2C749.75837%201188.99995%2C751.129829%20C1189.00252%2C751.4273%201188.90981%2C751.655233%201188.6999%2C751.865137%20C1186.75025%2C753.805783%201184.80574%2C755.75158%201182.86381%2C757.699953%20C1182.66163%2C757.903418%201182.44143%2C758%201182.1504%2C758%20C1179.38559%2C757.993561%201176.6195%2C757.993561%201173.85213%2C758%20C1173.56367%2C758%201173.34346%2C757.905994%201173.13871%2C757.702529%20C1171.19292%2C755.749005%201169.24198%2C753.798057%201167.28717%2C751.850972%20C1167.08885%2C751.653945%201167%2C751.437602%201167.00129%2C751.156872%20C1167.00644%2C748.390775%201167.00644%2C745.624678%201167%2C742.857293%20C1167%2C742.568836%201167.08757%2C742.347343%201167.29232%2C742.14259%20C1169.24713%2C740.195505%201171.19807%2C738.245844%201173.14515%2C736.291033%20C1173.34218%2C736.094006%201173.55594%2C736%201173.83667%2C736%20C1176.61049%2C736.005151%201179.38431%2C736.005151%201182.15812%2C736%20C1182.44014%2C736%201182.6552%2C736.090143%201182.85093%2C736.288457%20C1184.80445%2C738.24842%201186.76184%2C740.203231%201188.72051%2C742.156755%20C1188.91109%2C742.347343%201189.00124%2C742.558534%201188.99995%2C742.830251%20C1188.99222%2C744.224889%201188.99608%2C745.619527%201188.99608%2C747.014165%22%20id%3D%22Fill-1%22%3E%3C/path%3E%0A%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%3C/g%3E%0A%3C/svg%3E'); -} - -@function buildSmartBlockIcon($stroke-color) { - @return url('data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%3Csvg%20width%3D%2218px%22%20height%3D%2228px%22%20viewBox%3D%220%200%2018%2028%22%20version%3D%221.1%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%3E%0A%20%20%20%20%3C%21--%20Generator%3A%20Sketch%2047.1%20%2845422%29%20-%20http%3A//www.bohemiancoding.com/sketch%20--%3E%0A%20%20%20%20%3Ctitle%3EGroup%2011%3C/title%3E%0A%20%20%20%20%3Cdesc%3ECreated%20with%20Sketch.%3C/desc%3E%0A%20%20%20%20%3Cdefs%3E%3C/defs%3E%0A%20%20%20%20%3Cg%20id%3D%22Page-1%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Simple-View-Panel---hover---adblocing%22%20transform%3D%22translate%28-1105.000000%2C%20-711.000000%29%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Group-11%22%20transform%3D%22translate%281105.000000%2C%20711.000000%29%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20id%3D%22icon--smart-blocking-light-bulb%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M15.9766215%2C9.23905085%20C15.8801207%2C10.9165559%2015.2802076%2C12.3962344%2014.3232416%2C13.7536786%20C13.8922048%2C14.3648501%2013.456343%2C14.9711966%2013.038173%2C15.5936266%20C12.4414766%2C16.4814337%2011.9637978%2C17.4255329%2011.7804463%2C18.4918664%20C11.7498877%2C18.6671761%2011.6389118%2C18.6543094%2011.5166775%2C18.6543094%20C10.6755125%2C18.652701%209.83434744%2C18.652701%208.99157404%2C18.652701%20C8.16005907%2C18.652701%207.32854409%2C18.647876%206.49542077%2C18.6559177%20C6.31367765%2C18.6575261%206.24934381%2C18.606059%206.21235184%2C18.4178825%20C6.01452526%2C17.3869326%205.55453826%2C16.4637419%204.9707086%2C15.60006%20C4.47372963%2C14.8666541%203.95584216%2C14.146115%203.45725484%2C13.4127092%20C2.5501476%2C12.0826069%202.07568548%2C10.6109701%202.01135163%2C9.00101562%20C1.8987674%2C6.18158477%203.4974635%2C3.72242346%206.19626838%2C2.58049768%20C9.65421266%2C1.118511%2013.7426286%2C2.58532272%2015.3622332%2C5.87439065%20C15.8833374%2C6.93589913%2016.044172%2C8.0633498%2015.9766215%2C9.23905085%20L15.9766215%2C9.23905085%20Z%20M6.35227796%2C21.3000388%20L11.6566037%2C21.3000388%20L11.6566037%2C20.6695671%20L6.35227796%2C20.6695671%20L6.35227796%2C21.3000388%20Z%20M11.1113743%2C23.9811519%20L6.89589897%2C23.9811519%20C6.38766158%2C23.9811519%206.2943775%2C23.8733927%206.36031969%2C23.3281634%20L11.6405202%2C23.3281634%20C11.7145041%2C23.865351%2011.6180033%2C23.9811519%2011.1113743%2C23.9811519%20L11.1113743%2C23.9811519%20Z%20M17.3501491%2C5.40475357%20C15.9010292%2C2.2363116%2013.3839675%2C0.476780878%209.96462348%2C0.0698692952%20C7.05190855%2C-0.277533479%204.51876332%2C0.679432497%202.45364683%2C2.74294064%20C0.148886755%2C5.04930906%20-0.40438433%2C7.86713156%200.26468768%2C10.9841065%20C0.615307147%2C12.6133611%201.41304685%2C14.0303141%202.36840448%2C15.3732832%20C2.80587464%2C15.9908881%203.23691142%2C16.6165348%203.63899796%2C17.2566565%20C4.01052593%2C17.8501363%204.2742947%2C18.4982998%204.30002824%2C19.2027554%20C4.33058682%2C20.0165786%204.30806997%2C20.8304018%204.30806997%2C21.6442249%20L4.34023689%2C21.6442249%20C4.34023689%2C22.3197303%204.33862855%2C22.9952357%204.34184524%2C23.6707411%20C4.34827862%2C24.8753924%205.29398618%2C25.8918672%206.49220408%2C25.9706761%20C6.649822%2C25.9819346%206.70289743%2C26.0269683%206.74149774%2C26.1845862%20C6.99722478%2C27.242878%207.92684887%2C27.9746755%208.99157404%2C27.9762838%20C10.0772077%2C27.9778922%2011.0003984%2C27.2541364%2011.2657755%2C26.1781528%20C11.2995508%2C26.0382267%2011.3381511%2C25.9835429%2011.490944%2C25.9722845%20C12.7325872%2C25.8790004%2013.6622113%2C24.8753924%2013.665428%2C23.6353575%20C13.665428%2C22.3776308%2013.6750781%2C21.1199041%2013.6622113%2C19.860569%20C13.6509529%2C18.9196865%2013.869688%2C18.0447462%2014.3682753%2C17.2502232%20C14.7703619%2C16.6101014%2015.1997903%2C15.9828464%2015.6420855%2C15.3700665%20C16.9046372%2C13.6137525%2017.7972694%2C11.7175123%2017.9645374%2C9.52694482%20C18.0739049%2C8.10355845%2017.9468456%2C6.71073066%2017.3501491%2C5.40475357%20L17.3501491%2C5.40475357%20Z%22%20id%3D%22Page-1%22%20fill%3D%22%23#{$stroke-color}%22%3E%3C/path%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M9.09571339%2C7.21443826%20C9.03823634%2C7.19518725%208.96176366%2C7.19518725%208.90428661%2C7.21443826%20L4.78726273%2C8.3064732%20C4.61495411%2C8.34472999%204.5%2C8.49800235%204.5%2C8.67040311%20C4.51924072%2C11.6018291%206.08938152%2C14.3223522%208.7893325%2C16.1424922%20C8.84680955%2C16.180749%208.92340478%2C16.2%209%2C16.2%20C9.07659522%2C16.2%209.15319045%2C16.180749%209.2106675%2C16.1424922%20C11.9106185%2C14.3223522%2013.4807593%2C11.6018291%2013.5%2C8.67040311%20C13.5%2C8.49800235%2013.3850459%2C8.34472999%2013.2127373%2C8.3064732%20L9.09571339%2C7.21443826%20Z%22%20id%3D%22Page-1-Copy-2%22%20stroke%3D%22%23#{$stroke-color}%22%20stroke-width%3D%221.5%22%3E%3C/path%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%20%20%20%20%3C/g%3E%0A%20%20%20%20%3C/g%3E%0A%3C/svg%3E'); -} diff --git a/app/scss/android/content/_tabs.scss b/app/scss/android/content/_tabs.scss deleted file mode 100644 index 9c36fc993..000000000 --- a/app/scss/android/content/_tabs.scss +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Tabs 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 - */ -#ghostery-header .header-tab-group { - margin-top: 20px; - z-index: 1; -} - -.tabs-wrapper { - ul.tabs-nav { - margin: 0px; - padding: 0px; - list-style: none; - display: flex; - flex-wrap: wrap; - background-color: $cliqz-blue; - @include prefix('box-shadow', '3px 2px 10px rgba(0,0,0,0.15)'); - - li.tab-item { - display: inline-block; - flex-grow: 1; - - a.tab-link.custom-link { - display: block; - padding: 0 10px; - text-align: left; - cursor: pointer; - text-transform: uppercase; - color: #fff; - font-size: 12px; - font-weight: 500; - opacity: 0.8; - height: $tab-header-height; - line-height: $tab-header-height; - @include prefix('transition', 'opacity 300ms ease-in'); - - &.active { - @include prefix('animation', 'tab-animation 800ms cubic-bezier(0.4, 0, 1, 1) alternate 1'); - border-bottom: 2px solid #fff; - opacity: 1; - } - } - } - } - - .tabs-active-content { - - } -} diff --git a/app/scss/android/content/_trackers-chart.scss b/app/scss/android/content/_trackers-chart.scss deleted file mode 100644 index 68a943b78..000000000 --- a/app/scss/android/content/_trackers-chart.scss +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Tracker Chart 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 - */ - -.chart-wrapper { - .trackers-chart { - display: inline-block; - margin-top: 5%; - margin-bottom: 20px; - width: 190px; - max-width: 100%; - position: relative; - - #circle { - & > g { - fill: none; - stroke-width: 25; - @include prefix('transform', 'translate(100px, 100px)'); - - .path { - stroke-dashoffset: 0; - stroke-dasharray: var(--stroke-length, 0); - @include prefix('animation', 'dash calc(var(--stroke-length, 0) / 2 * 1ms) linear alternate 1'); - - &[data-category='advertising'] { - stroke: #cb55cd; - } - - &[data-category='audio_video_player'] { - stroke: #ef671e; - } - - &[data-category='comments'] { - stroke: #43b7c5; - } - - &[data-category='customer_interaction'] { - stroke: #fdc257; - } - - &[data-category='essential'] { - stroke: #fc9734; - } - - &[data-category='pornvertising'] { - stroke: #ecafc2; - } - - &[data-category='site_analytics'] { - stroke: #87d7ef; - } - - &[data-category='social_media'] { - stroke: #388ee8; - } - - &[data-category='default'] { - stroke: #e8e8e8; - } - } - } - } - - .trackers-num { - position: absolute; - top: 48%; - left: 50%; - @include prefix('transform', 'translate(-50%, -50%)'); - text-align: center; - - & > span { - &:first-child { - font-size: 40px; - font-weight: 400; - line-height: 51px; - } - - &:last-child { - font-size: 17px; - line-height: 17px; - display: inline-block; - } - } - } - } - - .trackers-blocked-num { - font-size: 20px; - .number { - color: #e74055; - } - } - - &.paused { - @include prefix('filter', 'grayscale(100%)'); - } -} diff --git a/app/scss/android/content/_variables.scss b/app/scss/android/content/_variables.scss deleted file mode 100644 index 4ff9351b3..000000000 --- a/app/scss/android/content/_variables.scss +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Sass Variables - * - * 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 - */ - -$cliqz-blue: #00AEF0; -$tracker-menu-item-width: 60px; -$tracker-height: 50px; -$fixed-menu-item-height: 50px; -$fixed-menu-header-height: 50px; -$tab-header-height: 50px; - -$white: #FFFFFF; -$tundora: #4A4A4A; - -$button-white: ffffff; -$button-blue: 1DAFED; -$button-dark-grey: 4A4A4A; - -/* Control buttons */ -$button-trust: #9FCA4C; -$button-restrict: #BC4A4B; -$button-block: #E44258; diff --git a/app/scss/hub.scss b/app/scss/hub.scss index 60394aad7..3cd030117 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -31,6 +31,7 @@ html, body, #root { .App__mainContent { margin-left: 54px; } + .android-relative {position: relative;} } // Foundation Overrides diff --git a/app/scss/panel.scss b/app/scss/panel.scss index b0cb28133..88bffcdff 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -7,7 +7,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -32,7 +32,7 @@ html body { // Function helper with color variables @function url-friendly-colour($colour) { - @return '%23' + str-slice('#{$colour}', 2, -1) + @return '%23' + str-slice('#{$colour}', 2, -1); } // Foundation Helpers diff --git a/app/scss/panel_android.scss b/app/scss/panel_android.scss index 316bb1cb8..c302b9665 100644 --- a/app/scss/panel_android.scss +++ b/app/scss/panel_android.scss @@ -4,36 +4,54 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -@import 'android/content/variables'; -@import 'android/content/mixins'; -@import 'android/content/normalize'; -@import 'android/content/tabs'; -@import 'android/content/accordions'; -@import 'android/content/dots-menu'; -@import 'android/content/trackers-chart'; -@import 'android/content/fixed-menu'; +// Android Specific Styles +@import './android/_variables'; +@import './android/_mixins'; +@import './android/_normalize'; // ToDo: Update -#ghostery-content { - @import 'android/overview'; +// Panel Shared Styles +@import './partials/_svgs'; - & > div > .chart-wrapper { - display: none; - position: fixed; - top: 0; - bottom: 0; - left: 0; - width: 50%; - text-align: center; - padding-top: 5%; - } +// Android Specific Component Styles +@import './android/_settings'; +@import './android/_tabs'; +@import './android/_overview_tab'; +@import './android/_blocking_tab'; +@import './android/_dots_menu'; + +// Panel Shared Component Styles +@import './partials/_settings'; +@import './partials/_help'; +@import './partials/_radio_button'; +@import './partials/_not_scanned'; +@import './partials/_donut_graph'; +@import './partials/_ghostery_feature'; +@import './partials/_pause_button'; +@import './partials/_cliqz_feature'; + +// Import shared helpers +@import 'shared_helper_classes'; - /* Landscape mode */ - @import 'android/content/landscape'; +// NeedsReload Header +.NeedsReload { + height: 0; + color: #4a4a4a; + font-weight: 500; + font-style: italic; + background-color: #ffae00; + overflow: none; + opacity: 0; + @include prefix('transition', 'all 300ms ease'); + + &.NeedsReload--show { + height: 25px; + opacity: 1; + } } diff --git a/app/scss/partials/_header.scss b/app/scss/partials/_header.scss index 47684e16e..413cc98ea 100644 --- a/app/scss/partials/_header.scss +++ b/app/scss/partials/_header.scss @@ -4,21 +4,25 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0 */ #ghostery-header { - .header-tab-group { + .header-tab-group-container { position: absolute; height: 36px; - width: 280px; - left: calc(50% - 140px); + width: 580px; + pointer-events: none; + } + .header-tab-group { + height: 36px; } .header-tab { cursor: pointer; + pointer-events: all; height: 20px; width: 140px; text-align: center; diff --git a/app/scss/partials/_pause_button.scss b/app/scss/partials/_pause_button.scss index f2308d5ca..5f546a924 100644 --- a/app/scss/partials/_pause_button.scss +++ b/app/scss/partials/_pause_button.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -21,6 +21,7 @@ transition: background-image 0.25s ease-out, background-color 0.25s ease-out, border-color 0.25s ease-out, + box-shadow 0.25s ease-out, color 0.25s ease-out; } .button.active { diff --git a/app/scss/partials/_settings.scss b/app/scss/partials/_settings.scss index 96981e5aa..c9452812a 100644 --- a/app/scss/partials/_settings.scss +++ b/app/scss/partials/_settings.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -237,6 +237,8 @@ margin-right: 15px; left: -20px; top: 1px; + height: 12px; + width: 12px; } } &:checked { diff --git a/app/scss/partials/_subscribe.scss b/app/scss/partials/_subscribe.scss index c771b102d..59a440ca5 100644 --- a/app/scss/partials/_subscribe.scss +++ b/app/scss/partials/_subscribe.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,9 +12,6 @@ */ .content-subscription { - position: absolute; - height: 479px; - width: 100%; .badge { display: block; margin: 35px auto 35px auto; diff --git a/app/scss/partials/_summary.scss b/app/scss/partials/_summary.scss index 79462279f..b5d87bacf 100644 --- a/app/scss/partials/_summary.scss +++ b/app/scss/partials/_summary.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -60,7 +60,7 @@ .SummaryPageStat.page-load-time-fast .SummaryPageStat__value { color: #9ecc42; } .Summary--simple { - width: 100%; + width: 580px; .Summary__pageHostContainer { margin-bottom: 36px; diff --git a/app/shared-components/ForgotPassword/ForgotPassword.jsx b/app/shared-components/ForgotPassword/ForgotPassword.jsx index 058cef3fb..f13d8c7b1 100644 --- a/app/shared-components/ForgotPassword/ForgotPassword.jsx +++ b/app/shared-components/ForgotPassword/ForgotPassword.jsx @@ -114,51 +114,53 @@ class ForgotPassword extends React.Component { return (
    -
    - {panel && ( -

    - {t('forgot_password_message')} -

    - )} - {hub && ( -

    - {t('forgot_password_message')} -

    - )} -
    - -

    - {t('invalid_email_forgot')} -

    -

    - {t('error_email_forgot')} -

    -
    -
    -
    - {panel && ( - - {t('button_cancel')} - - )} - {hub && ( -
    - {t('button_cancel')} -
    - )} +
    + + {panel && ( +

    + {t('forgot_password_message')} +

    + )} + {hub && ( +

    + {t('forgot_password_message')} +

    + )} +
    + +

    + {t('invalid_email_forgot')} +

    +

    + {t('error_email_forgot')} +

    -
    - +
    +
    + {panel && ( + + {t('button_cancel')} + + )} + {hub && ( +
    + {t('button_cancel')} +
    + )} +
    +
    + +
    -
    - + +
    ); diff --git a/app/shared-components/ForgotPassword/ForgotPassword.scss b/app/shared-components/ForgotPassword/ForgotPassword.scss index 1e272e2d2..0b1d84d89 100644 --- a/app/shared-components/ForgotPassword/ForgotPassword.scss +++ b/app/shared-components/ForgotPassword/ForgotPassword.scss @@ -37,7 +37,8 @@ margin: 40px auto; } .ForgotPasswordForm { - width: 420px; + max-width: 420px; + margin: 0 auto; } #ForgotPasswordMessage { font-size: 1.25rem; @@ -60,6 +61,9 @@ } #send-button { width: 150px; + @include breakpoint(medium down) { + width: 100px; + } } } diff --git a/manifest.json b/manifest.json index 500ff02c3..8a00a9832 100644 --- a/manifest.json +++ b/manifest.json @@ -7,6 +7,8 @@ "version_name": "8.5.2", "default_locale": "en", "description": "__MSG_short_description__", + "debug": true, + "log": true, "icons": { "16": "app/images/icon16.png", "48": "app/images/icon48.png", @@ -14,8 +16,8 @@ }, "browser_action": { "default_icon": { - "19": "app/images/icon19_off.png", - "38": "app/images/icon38_off.png" + "19": "app/images/icon19.png", + "38": "app/images/icon38.png" }, "default_title": "Ghostery" }, diff --git a/package.json b/package.json index d8264c86d..5db26f6ab 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "react-redux": "^7.2.0", "react-router-dom": "^5.2.0", "react-svg": "^11.0.26", + "react-window": "^1.8.5", "redux": "^4.0.5", "redux-object": "^0.5.10", "redux-thunk": "^2.2.0", diff --git a/src/background.js b/src/background.js index 29538812d..f17467dfd 100644 --- a/src/background.js +++ b/src/background.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -17,7 +17,7 @@ import { debounce, every, size } from 'underscore'; import moment from 'moment/min/moment-with-locales.min'; import cliqz, { HUMANWEB_MODULE, HPN_MODULE } from './classes/Cliqz'; -// object class +// object classes import Events from './classes/EventHandlers'; import Policy from './classes/Policy'; // static classes @@ -65,6 +65,7 @@ const { } = globals; const IS_EDGE = (BROWSER_INFO.name === 'edge'); const IS_FIREFOX = (BROWSER_INFO.name === 'firefox'); +const IS_ANDROID = (BROWSER_INFO.os === 'android'); const VERSION_CHECK_URL = `${CDN_BASE_URL}/update/version`; const REAL_ESTATE_ID = 'ghostery'; const onBeforeRequest = events.onBeforeRequest.bind(events); @@ -208,7 +209,7 @@ function reloadTab(data) { * @memberOf Background */ function closeAndroidPanelTabs() { - if (BROWSER_INFO.os !== 'android') { return; } + if (!IS_ANDROID) { return; } chrome.tabs.query({ active: true, url: chrome.extension.getURL('app/templates/panel_android.html*') @@ -746,9 +747,7 @@ function onMessageHandler(request, sender, callback) { } // HANDLE UNIVERSAL EVENTS HERE (NO ORIGIN LISTED ABOVE) - // The 'getPanelData' message is never sent by the panel, which uses ports only since 8.3.2 - // The message is still sent by panel-android and by the setup hub as of 8.4.0 - if (name === 'getPanelData') { + if (name === 'getPanelData') { // Used by panel-android and the intro hub if (!message.tabId) { utils.getActiveTab((activeTab) => { const data = panelData.get(message.view, activeTab); @@ -798,9 +797,17 @@ function onMessageHandler(request, sender, callback) { return false; } if (name === 'getCliqzModuleData') { // panel-android only - utils.getActiveTab((activeTab) => { - sendCliqzModuleCounts(activeTab.id, activeTab.pageHost, callback); - }); + if (!message.tabId) { + utils.getActiveTab((activeTab) => { + const pageHost = (activeTab.url && utils.processUrl(activeTab.url).hostname) || ''; + sendCliqzModuleCounts(activeTab.id, pageHost, callback); + }); + } else { + chrome.tabs.get(+message.tabId, (messageTab) => { + const pageHost = (messageTab.url && utils.processUrl(messageTab.url).hostname) || ''; + sendCliqzModuleCounts(messageTab.id, pageHost, callback); + }); + } return true; } if (name === 'getTrackerDescription') { @@ -970,6 +977,17 @@ function onMessageHandler(request, sender, callback) { closeAndroidPanelTabs(); return false; } + if (name === 'getAndroidSettingsForExport') { + const settings = account.buildUserSettings(); + settings.site_blacklist = conf.site_blacklist; + settings.site_whitelist = conf.site_whitelist; + + const hash = common.hashCode(JSON.stringify({ conf: settings })); + const backup = JSON.stringify({ hash, settings: { conf: settings } }); + const msg = { type: 'Ghostery-Backup', content: backup }; + callback(msg); + return true; + } if (name === 'getSettingsForExport') { utils.getActiveTab((activeTab) => { if (activeTab && activeTab.id && activeTab.url.startsWith('http')) { @@ -982,8 +1000,9 @@ function onMessageHandler(request, sender, callback) { try { const hash = common.hashCode(JSON.stringify({ conf: settings })); const backup = JSON.stringify({ hash, settings: { conf: settings } }); + const msg = { type: 'Ghostery-Backup', content: backup }; utils.injectNotifications(activeTab.id, true).then(() => { - sendMessage(activeTab.id, 'exportFile', backup); + sendMessage(activeTab.id, 'exportFile', msg); }); callback(true); } catch (e) { @@ -1031,6 +1050,15 @@ function onMessageHandler(request, sender, callback) { utils.openNewTab({ url: hubUrl, become_active: true }); return false; } + if (name === 'openAccountAndroid') { + if (confData.account) { + utils.openNewTab({ url: `${globals.ACCOUNT_BASE_URL}/`, become_active: true }); + } else { + const hubUrl = chrome.runtime.getURL('./app/templates/hub.html#log-in'); + utils.openNewTab({ url: hubUrl, become_active: true }); + } + return false; + } if (name === 'promoModals.sawPremiumPromo') { promoModals.recordPremiumPromoSighting(); return false; @@ -1359,7 +1387,7 @@ function getDataForGhosteryTab(callback) { * @memberOf Background */ function initializePopup() { - if (BROWSER_INFO.os === 'android') { + if (IS_ANDROID) { chrome.browserAction.setPopup({ popup: 'app/templates/panel_android.html', }); @@ -1628,9 +1656,9 @@ function initializeGhosteryModules() { conf.enable_ad_block = !adblocker.isDisabled; conf.enable_anti_tracking = !antitracking.isDisabled; conf.enable_human_web = !humanweb.isDisabled; - conf.enable_offers = !offers.isDisabled; + conf.enable_offers = !offers.isDisabled && !IS_ANDROID; - if (IS_FIREFOX) { + if (IS_FIREFOX && BROWSER_INFO.name !== 'ghostery_android') { if (globals.JUST_INSTALLED) { conf.enable_human_web = false; conf.enable_offers = false; @@ -1668,8 +1696,8 @@ function initializeGhosteryModules() { setCliqzModuleEnabled(offers, false); } - // Disable purplebox for Firefox Android users - if (BROWSER_INFO.os === 'android' && IS_FIREFOX) { + // Disable purplebox for Android users + if (IS_ANDROID) { conf.show_alert = false; } @@ -1727,12 +1755,12 @@ function initializeGhosteryModules() { ]).then(() => { // run scheduledTasks on init scheduledTasks().then(() => { - // open the Ghostery Hub on install with justInstalled query parameter set to true - // we need to do this after running scheduledTasks for the first time + // Open the Ghostery Hub on install with justInstalled query parameter set to true. + // We need to do this after running scheduledTasks for the first time // because of an A/B test that determines which promo variant is shown in the Hub on install if (globals.JUST_INSTALLED) { - const route = (conf.hub_promo_variant === 'upgrade' || conf.hub_promo_variant === 'not_yet_set') ? '' : '#home'; - const showPremiumPromoModal = conf.hub_promo_variant === 'midnight'; + const route = ((conf.hub_promo_variant === 'upgrade' || conf.hub_promo_variant === 'not_yet_set') && !IS_ANDROID) ? '' : '#home'; + const showPremiumPromoModal = (conf.hub_promo_variant === 'midnight' && !IS_ANDROID); chrome.tabs.create({ url: chrome.runtime.getURL(`./app/templates/hub.html?$justInstalled=true&pm=${showPremiumPromoModal}${route}`), active: true diff --git a/src/classes/Account.js b/src/classes/Account.js index ccac43193..9f2e55a34 100644 --- a/src/classes/Account.js +++ b/src/classes/Account.js @@ -6,7 +6,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/src/classes/BrowserButton.js b/src/classes/BrowserButton.js index e702fa214..96a290055 100644 --- a/src/classes/BrowserButton.js +++ b/src/classes/BrowserButton.js @@ -21,6 +21,8 @@ import { getTab } from '../utils/utils'; import { log } from '../utils/common'; import globals from './Globals'; +const IS_ANDROID = globals.BROWSER_INFO.os === 'android'; + /** * @class for handling Ghostery button. * @memberof BackgroundClasses @@ -38,6 +40,7 @@ class BrowserButton { * @param {number} tabId tab id */ update(tabId) { + if (IS_ANDROID) { return; } // Update this specific tab if (tabId) { // In ES6 classes, we need to bind context to callback function @@ -73,7 +76,6 @@ class BrowserButton { * @param {boolean} alert is it a special case which requires button to change its background color? */ _setIcon(active, tabId, trackerCount, alert) { - if (globals.BROWSER_INFO.os === 'android') { return; } if (tabId <= 0) { return; } const iconAlt = (!active) ? '_off' : ''; diff --git a/src/classes/Cliqz.js b/src/classes/Cliqz.js index 7ce2f20a9..013889dbc 100644 --- a/src/classes/Cliqz.js +++ b/src/classes/Cliqz.js @@ -18,7 +18,8 @@ import globals from './Globals'; const IS_ANDROID = globals.BROWSER_INFO.os === 'android'; export const HUMANWEB_MODULE = IS_ANDROID ? 'human-web-lite' : 'human-web'; export const HPN_MODULE = IS_ANDROID ? 'hpn-lite' : 'hpnv2'; -// override the default prefs based on the platform + +// Override the default prefs based on the platform CLIQZ.config.default_prefs = { ...CLIQZ.config.default_prefs, // the following are enabled by default on non-android platforms diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 7c49a60b8..c542d5090 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -21,6 +21,7 @@ import { prefsGet } from '../utils/common'; const { IS_CLIQZ, BROWSER_INFO } = globals; const IS_FIREFOX = (BROWSER_INFO.name === 'firefox'); +const IS_ANDROID = (BROWSER_INFO.os === 'android'); /** * Class for handling user configuration properties synchronously. @@ -111,7 +112,7 @@ class ConfData { _initProperty('enable_click2play_social', true); _initProperty('enable_human_web', !IS_CLIQZ && !IS_FIREFOX); _initProperty('enable_metrics', false); - _initProperty('enable_offers', !IS_CLIQZ && !IS_FIREFOX); + _initProperty('enable_offers', !IS_CLIQZ && !IS_FIREFOX && !IS_ANDROID); _initProperty('enable_smart_block', true); _initProperty('expand_all_trackers', true); _initProperty('hide_alert_trusted', false); @@ -134,7 +135,7 @@ class ConfData { _initProperty('rewards_opted_in', false); // Migrated to Cliqz pref myoffrz.opted_in _initProperty('settings_last_imported', 0); _initProperty('settings_last_exported', 0); - _initProperty('show_alert', BROWSER_INFO.os !== 'android' && !IS_FIREFOX); + _initProperty('show_alert', !IS_ANDROID); _initProperty('show_badge', true); _initProperty('show_cmp', true); _initProperty('show_tracker_urls', true); diff --git a/src/classes/Globals.js b/src/classes/Globals.js index 0d8894f93..9aed8fa14 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -193,6 +193,29 @@ class Globals { // Set version property this.BROWSER_INFO.version = version; + + // Check for the Ghostery Android browser + this._checkForGhosteryAndroid(); + } + + /** + * Check for Ghostery Android Browser and update BROWSER_INFO. + * Note: This is asynchronous and not available at runtime. + * @private + * @return boolean + */ + _checkForGhosteryAndroid() { + if (typeof chrome.runtime.getBrowserInfo === 'function') { + chrome.runtime.getBrowserInfo().then((info) => { + if (info.name === 'Ghostery') { + this.BROWSER_INFO.displayName = 'Ghostery Android Browser'; + this.BROWSER_INFO.name = 'ghostery_android'; + this.BROWSER_INFO.token = 'ga'; + this.BROWSER_INFO.os = 'android'; + this.BROWSER_INFO.version = info.version; + } + }); + } } } diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 884ad2ef1..ef103dca6 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -7,7 +7,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -28,7 +28,12 @@ import account from './Account'; import dispatcher from './Dispatcher'; import promoModals from './PromoModals'; import { getCliqzGhosteryBugs, sendCliqzModuleCounts } from '../utils/cliqzModulesData'; -import { getActiveTab, flushChromeMemoryCache, processUrl } from '../utils/utils'; +import { + getTab, + getActiveTab, + flushChromeMemoryCache, + processUrl +} from '../utils/utils'; import { log } from '../utils/common'; const SYNC_SET = new Set(globals.SYNC_ARRAY); @@ -71,7 +76,7 @@ class PanelData { this._panelPort = port; this._mountedComponents.panel = true; - getActiveTab((tab) => { + function tabCallback(tab) { const { url } = tab; this._activeTab = tab; @@ -86,7 +91,21 @@ class PanelData { account.getUserSettings() .then(userSettings => this._postUserSettings(userSettings)) .catch(() => log('Failed getting remote user settings from PanelData#initPort. User not logged in.')); - }); + } + + function tabErrorCallback(err) { + const { message } = err; + log(`Error: ${message}`); + this._postMessage('panel', { error: message }); + } + + const paramTabId = +(new URL(port.sender.url)).searchParams.get('tabId'); + + if (paramTabId) { + getTab(paramTabId, tabCallback.bind(this), tabErrorCallback.bind(this)); + } else { + getActiveTab(tabCallback.bind(this), tabErrorCallback.bind(this)); + } } /** diff --git a/src/utils/cliqzModulesData.js b/src/utils/cliqzModulesData.js index 4a882a34e..ed0cc52cd 100644 --- a/src/utils/cliqzModulesData.js +++ b/src/utils/cliqzModulesData.js @@ -89,11 +89,11 @@ export function getCliqzData(tabId, tabHostUrl, antiTracking) { if (dataPoints || whitelisted) { const type = antiTracking ? 'antiTracking' : 'adBlock'; const { - name, domains, ads, cookies, fingerprints + name, domains, ads, cookies, fingerprints, wtm } = other; unknownTrackers.push({ - name, domains, ads, cookies, fingerprints, whitelisted, type + name, domains, ads, cookies, fingerprints, whitelisted, type, wtm }); } }); diff --git a/webpack.config.js b/webpack.config.js index 6a379a862..95dac4452 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/yarn.lock b/yarn.lock index 73390ca35..ec768faa0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -375,6 +375,13 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" +"@babel/runtime@^7.0.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99" + integrity sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.5.5": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" @@ -5726,6 +5733,11 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +"memoize-one@>=3.1.1 <6": + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -7068,6 +7080,14 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.1: react-is "^16.8.6" scheduler "^0.19.1" +react-window@^1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1" + integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" From a95835f83d109c438d66b7246b95719c753ac7e8 Mon Sep 17 00:00:00 2001 From: Achilles Adams <64440585+hankyje@users.noreply.github.com> Date: Mon, 10 Aug 2020 13:18:32 -0400 Subject: [PATCH 03/35] Added a product_id parameter (#574) Co-authored-by: Christopher Tino --- src/classes/Metrics.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 0578ddfc0..9b43ef305 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -368,11 +368,13 @@ class Metrics { // Theme `&th=${encodeURIComponent(Metrics._getThemeValue().toString())}` + - // New parameter for Ghostery 8.5.2 + // New parameters for Ghostery 8.5.2 // Hub Promo variant `&hp=${encodeURIComponent(Metrics._getHubPromoVariant().toString())}` + // Subscription Interval - `&subscription_interval=${encodeURIComponent(Metrics._getSubscriptionInterval().toString())}`; + `&subscription_interval=${encodeURIComponent(Metrics._getSubscriptionInterval().toString())}` + + // Product ID Parameter + `&pi=${encodeURIComponent('gbe')}`; if (CAMPAIGN_METRICS.includes(type)) { // only send campaign attribution when necessary From e3f9ff8f91f36787fa50fc7adc2b1c7fb9240db4 Mon Sep 17 00:00:00 2001 From: Alberto Franco Date: Mon, 10 Aug 2020 13:44:48 -0400 Subject: [PATCH 04/35] Display error message when locked out (#577) Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- _locales/en/messages.json | 3 +++ app/panel/reducers/panel.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 25a272d39..acd289e62 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2544,5 +2544,8 @@ }, "too_many_password_resets_text": { "message": "Too many password reset requests. Try again in one hour." + }, + "too_many_failed_logins_text": { + "message": "Too many failed logins. Try again in one hour." } } diff --git a/app/panel/reducers/panel.js b/app/panel/reducers/panel.js index f3eff6511..945b0a872 100644 --- a/app/panel/reducers/panel.js +++ b/app/panel/reducers/panel.js @@ -213,6 +213,9 @@ export default (state = initialState, action) => { case '10110': errorText = t('no_such_email_password_combo'); break; + case 'too_many_failed_logins': + errorText = t('too_many_failed_logins_text'); + break; default: errorText = t('server_error_message'); } From c161ecaf9340dcb20a230f909135d0e669597bb8 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Mon, 10 Aug 2020 13:45:18 -0400 Subject: [PATCH 05/35] udpated changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f86cd0f3..a7a547c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +### GHOSTERY 8.5.3 () + ++ Updated Firefox Android extension panel UI and mobile optimizations (#587) ++ Display error message after too many failed login attempts (#577) ++ Added product id parameter to extension pings (#574) + ### GHOSTERY 8.5.2 (July 30, 2020) + Fixes bug where Ghostery icon could be grayed out on restricted sites (#564) From 04c121a39851528e784ff30a1b1137895e2c1e1e Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Tue, 11 Aug 2020 17:08:57 -0400 Subject: [PATCH 06/35] update package dependencies --- .../components/content/OverviewTab.jsx | 2 +- package.json | 36 +- webpack.config.js | 1 + yarn.lock | 1468 ++++++++++------- 4 files changed, 875 insertions(+), 632 deletions(-) diff --git a/app/panel-android/components/content/OverviewTab.jsx b/app/panel-android/components/content/OverviewTab.jsx index 07a67b41f..579d7e78d 100644 --- a/app/panel-android/components/content/OverviewTab.jsx +++ b/app/panel-android/components/content/OverviewTab.jsx @@ -399,7 +399,7 @@ OverviewTab.propTypes = { }).isRequired, }).isRequired, summary: PropTypes.shape({ - categories: PropTypes.array.isRequired, + categories: PropTypes.arrayOf.isRequired, trackerCounts: PropTypes.shape({ allowed: PropTypes.number.isRequired, blocked: PropTypes.number.isRequired, diff --git a/package.json b/package.json index 5db26f6ab..195f06192 100644 --- a/package.json +++ b/package.json @@ -50,16 +50,16 @@ "d3": "^5.16.0", "foundation-sites": "^6.6.2", "history": "^4.10.1", - "json-api-normalizer": "^0.4.16", + "json-api-normalizer": "^1.0.0", "moment": "^2.26.0", "prop-types": "^15.6.2", "query-string": "^6.13.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-markdown": "^4.3.1", - "react-redux": "^7.2.0", + "react-redux": "^7.2.1", "react-router-dom": "^5.2.0", - "react-svg": "^11.0.26", + "react-svg": "^11.0.34", "react-window": "^1.8.5", "redux": "^4.0.5", "redux-object": "^0.5.10", @@ -70,47 +70,47 @@ "underscore": "^1.10.2" }, "devDependencies": { - "@babel/core": "^7.10.2", - "@babel/plugin-proposal-class-properties": "^7.10.1", - "@babel/plugin-proposal-object-rest-spread": "^7.10.1", - "@babel/plugin-transform-modules-commonjs": "^7.10.1", - "@babel/preset-react": "^7.10.1", + "@babel/core": "^7.11.1", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/preset-react": "^7.10.4", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "clean-webpack-plugin": "^3.0.0", "cross-env": "^7.0.2", - "css-loader": "^3.6.0", + "css-loader": "^4.2.1", "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.2", - "eslint": "^7.2.0", + "enzyme-adapter-react-16": "^1.15.3", + "eslint": "^7.6.0", "eslint-config-airbnb": "^18.2.0", "eslint-loader": "^4.0.2", - "eslint-plugin-import": "^2.21.2", + "eslint-plugin-import": "^2.22.0", "eslint-plugin-jsx-a11y": "^6.3.1", - "eslint-plugin-react": "^7.20.0", + "eslint-plugin-react": "^7.20.5", "find-in-files": "^0.5.0", "fs-extra": "^9.0.1", - "jest": "^26.0.1", + "jest": "^26.3.0", "jest-fetch-mock": "^3.0.3", "jest-when": "^2.7.2", - "jsdoc": "^3.6.3", + "jsdoc": "^3.6.5", "jsonfile": "^6.0.1", "license-checker": "^25.0.1", - "mini-css-extract-plugin": "^0.9.0", + "mini-css-extract-plugin": "^0.10.0", "node-sass": "^4.14.1", "oboe": "^2.1.5", "path": "^0.12.7", "react-router": "^5.2.0", "react-test-renderer": "^16.13.1", "redux-mock-store": "^1.5.4", - "sass-loader": "^8.0.2", + "sass-loader": "^9.0.3", "seamless-immutable": "^7.1.3", "sinon-chrome": "^3.0.1", "svg-url-loader": "^6.0.0", "underscore-template-loader": "^1.1.0", "url-loader": "^4.0.0", "vendor-copy": "^2.0.0", - "webpack": "^4.43.0", + "webpack": "^4.44.1", "webpack-cli": "^3.3.12", "webpack-shell-plugin": "^0.5.0" } diff --git a/webpack.config.js b/webpack.config.js index 95dac4452..fb73dc38c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -157,6 +157,7 @@ module.exports = { sassOptions: { includePaths: [ path.resolve(__dirname, 'node_modules/foundation-sites/scss'), + path.resolve(__dirname, 'app/scss'), ] } }, diff --git a/yarn.lock b/yarn.lock index ec768faa0..39b9263f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,14 @@ dependencies: "@babel/highlight" "^7.10.3" -"@babel/core@^7.1.0", "@babel/core@^7.10.2", "@babel/core@^7.7.5": +"@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/core@^7.1.0", "@babel/core@^7.7.5": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.3.tgz#73b0e8ddeec1e3fdd7a2de587a60e17c440ec77e" integrity sha512-5YqWxYE3pyhIi84L84YcwjeEgS+fa7ZjK6IBVGTjDVfm64njkR2lfDhVR5OudLk8x2GK59YoSyVv+L/03k1q9w== @@ -31,6 +38,28 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.11.1": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" + integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.0" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.1" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.0" + "@babel/types" "^7.11.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.10.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.3.tgz#32b9a0d963a71d7a54f5f6c15659c3dbc2a523a5" @@ -41,41 +70,57 @@ lodash "^4.17.13" source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.1": +"@babel/generator@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" + integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== + dependencies: + "@babel/types" "^7.11.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.0.0": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz#f6d08acc6f70bbd59b436262553fb2e259a1a268" integrity sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw== dependencies: "@babel/types" "^7.10.1" -"@babel/helper-builder-react-jsx-experimental@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.1.tgz#9a7d58ad184d3ac3bafb1a452cec2bad7e4a0bc8" - integrity sha512-irQJ8kpQUV3JasXPSFQ+LCCtJSc5ceZrPFVj6TElR6XCHssi3jV8ch3odIrNtjJFRZZVbrOEfJMI79TPU/h1pQ== +"@babel/helper-annotate-as-pure@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" + integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-module-imports" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-builder-react-jsx@^7.10.3": - version "7.10.3" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.3.tgz#62c4b7bb381153a0a5f8d83189b94b9fb5384fc5" - integrity sha512-vkxmuFvmovtqTZknyMGj9+uQAZzz5Z9mrbnkJnPkaYGfKTaSsYcjQdXP0lgrWLVh8wU6bCjOmXOpx+kqUi+S5Q== +"@babel/helper-builder-react-jsx-experimental@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz#f35e956a19955ff08c1258e44a515a6d6248646b" + integrity sha512-Buewnx6M4ttG+NLkKyt7baQn7ScC/Td+e99G914fRU8fGIUivDDgVIQeDHFa5e4CRSJQt58WpNHhsAZgtzVhsg== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/types" "^7.10.3" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-module-imports" "^7.10.4" + "@babel/types" "^7.10.5" -"@babel/helper-create-class-features-plugin@^7.10.1": - version "7.10.3" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.3.tgz#2783daa6866822e3d5ed119163b50f0fc3ae4b35" - integrity sha512-iRT9VwqtdFmv7UheJWthGc/h2s7MqoweBF9RUj77NFZsg9VfISvBTum3k6coAhJ8RWv2tj3yUjA03HxPd0vfpQ== +"@babel/helper-builder-react-jsx@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d" + integrity sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg== dependencies: - "@babel/helper-function-name" "^7.10.3" - "@babel/helper-member-expression-to-functions" "^7.10.3" - "@babel/helper-optimise-call-expression" "^7.10.3" - "@babel/helper-plugin-utils" "^7.10.3" - "@babel/helper-replace-supers" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-create-class-features-plugin@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" "@babel/helper-function-name@^7.10.3": version "7.10.3" @@ -86,20 +131,43 @@ "@babel/template" "^7.10.3" "@babel/types" "^7.10.3" -"@babel/helper-get-function-arity@^7.10.1", "@babel/helper-get-function-arity@^7.10.3": +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-get-function-arity@^7.10.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz#3a28f7b28ccc7719eacd9223b659fdf162e4c45e" integrity sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg== dependencies: "@babel/types" "^7.10.3" -"@babel/helper-member-expression-to-functions@^7.10.1", "@babel/helper-member-expression-to-functions@^7.10.3": +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-member-expression-to-functions@^7.10.1": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.3.tgz#bc3663ac81ac57c39148fef4c69bf48a77ba8dd6" integrity sha512-q7+37c4EPLSjNb2NmWOjNwj0+BOyYlssuQ58kHEWk1Z78K5i8vTUsteq78HMieRPQSl/NtpQyJfdjt3qZ5V2vw== dependencies: "@babel/types" "^7.10.3" +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" + integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== + dependencies: + "@babel/types" "^7.11.0" + "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.1": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz#766fa1d57608e53e5676f23ae498ec7a95e1b11a" @@ -107,6 +175,13 @@ dependencies: "@babel/types" "^7.10.3" +"@babel/helper-module-imports@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" + integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-module-transforms@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz#24e2f08ee6832c60b157bb0936c86bef7210c622" @@ -120,18 +195,43 @@ "@babel/types" "^7.10.1" lodash "^4.17.13" -"@babel/helper-optimise-call-expression@^7.10.1", "@babel/helper-optimise-call-expression@^7.10.3": +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" + integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/template" "^7.10.4" + "@babel/types" "^7.11.0" + lodash "^4.17.19" + +"@babel/helper-optimise-call-expression@^7.10.1": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.3.tgz#f53c4b6783093195b0f69330439908841660c530" integrity sha512-kT2R3VBH/cnSz+yChKpaKRJQJWxdGoc6SjioRId2wkeV3bK0wLLioFpJROrX0U4xr/NmxSSAWT/9Ih5snwIIzg== dependencies: "@babel/types" "^7.10.3" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.1", "@babel/helper-plugin-utils@^7.10.3", "@babel/helper-plugin-utils@^7.8.0": +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.1", "@babel/helper-plugin-utils@^7.8.0": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.3.tgz#aac45cccf8bc1873b99a85f34bceef3beb5d3244" integrity sha512-j/+j8NAWUTxOtx4LKHybpSClxHoq6I91DQ/mKgAXn5oNUPIUiGppjPIX3TDtJWPrdfP9Kfl7e4fgVMiQR9VE/g== +"@babel/helper-plugin-utils@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== + "@babel/helper-replace-supers@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz#ec6859d20c5d8087f6a2dc4e014db7228975f13d" @@ -142,6 +242,16 @@ "@babel/traverse" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-simple-access@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz#08fb7e22ace9eb8326f7e3920a1c2052f13d851e" @@ -150,6 +260,14 @@ "@babel/template" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helper-simple-access@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" + integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== + dependencies: + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-split-export-declaration@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f" @@ -157,11 +275,23 @@ dependencies: "@babel/types" "^7.10.1" +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== + dependencies: + "@babel/types" "^7.11.0" + "@babel/helper-validator-identifier@^7.10.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz#60d9847f98c4cea1b279e005fdb7c28be5412d15" integrity sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw== +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + "@babel/helpers@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.1.tgz#a6827b7cb975c9d9cef5fd61d919f60d8844a973" @@ -171,6 +301,15 @@ "@babel/traverse" "^7.10.1" "@babel/types" "^7.10.1" +"@babel/helpers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" + integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/highlight@^7.10.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.3.tgz#c633bb34adf07c5c13156692f5922c81ec53f28d" @@ -180,27 +319,41 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.7.0", "@babel/parser@^7.9.4": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.3.tgz#7e71d892b0d6e7d04a1af4c3c79d72c1f10f5315" integrity sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA== -"@babel/plugin-proposal-class-properties@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz#046bc7f6550bb08d9bd1d4f060f5f5a4f1087e01" - integrity sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw== +"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1": + version "7.11.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9" + integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA== + +"@babel/plugin-proposal-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" + integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.10.1": - version "7.10.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.3.tgz#b8d0d22f70afa34ad84b7a200ff772f9b9fce474" - integrity sha512-ZZh5leCIlH9lni5bU/wB/UcjtcVLgR8gc+FAgW2OOY+m9h1II3ItTO1/cewNUcsIDZSYcSaz/rYVls+Fb0ExVQ== +"@babel/plugin-proposal-object-rest-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" + integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== dependencies: - "@babel/helper-plugin-utils" "^7.10.3" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.10.1" + "@babel/plugin-transform-parameters" "^7.10.4" "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -237,12 +390,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.1.tgz#0ae371134a42b91d5418feb3c8c8d43e1565d2da" - integrity sha512-+OxyOArpVFXQeXKLO9o+r2I4dIoVoy6+Uu0vKELrlweDM3QJADZj+Z+5ERansZqIZBcLj42vHnDI8Rz9BnRIuQ== +"@babel/plugin-syntax-jsx@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c" + integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.1" @@ -286,88 +439,88 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-transform-modules-commonjs@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz#d5ff4b4413ed97ffded99961056e1fb980fb9301" - integrity sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg== +"@babel/plugin-transform-modules-commonjs@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" + integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== dependencies: - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-simple-access" "^7.10.1" + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-parameters@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz#b25938a3c5fae0354144a720b07b32766f683ddd" - integrity sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg== +"@babel/plugin-transform-parameters@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" + integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== dependencies: - "@babel/helper-get-function-arity" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-react-display-name@^7.10.1": - version "7.10.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.3.tgz#e3c246e1b4f3e52cc7633e237ad9194c0ec482e7" - integrity sha512-dOV44bnSW5KZ6kYF6xSHBth7TFiHHZReYXH/JH3XnFNV+soEL1F5d8JT7AJ3ZBncd19Qul7SN4YpBnyWOnQ8KA== +"@babel/plugin-transform-react-display-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.4.tgz#b5795f4e3e3140419c3611b7a2a3832b9aef328d" + integrity sha512-Zd4X54Mu9SBfPGnEcaGcOrVAYOtjT2on8QZkLKEq1S/tHexG39d9XXGZv19VfRrDjPJzFmPfTAqOQS1pfFOujw== dependencies: - "@babel/helper-plugin-utils" "^7.10.3" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-react-jsx-development@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.1.tgz#1ac6300d8b28ef381ee48e6fec430cc38047b7f3" - integrity sha512-XwDy/FFoCfw9wGFtdn5Z+dHh6HXKHkC6DwKNWpN74VWinUagZfDcEJc3Y8Dn5B3WMVnAllX8Kviaw7MtC5Epwg== +"@babel/plugin-transform-react-jsx-development@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.4.tgz#6ec90f244394604623880e15ebc3c34c356258ba" + integrity sha512-RM3ZAd1sU1iQ7rI2dhrZRZGv0aqzNQMbkIUCS1txYpi9wHQ2ZHNjo5TwX+UD6pvFW4AbWqLVYvKy5qJSAyRGjQ== dependencies: - "@babel/helper-builder-react-jsx-experimental" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-jsx" "^7.10.1" + "@babel/helper-builder-react-jsx-experimental" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx-self@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.1.tgz#22143e14388d72eb88649606bb9e46f421bc3821" - integrity sha512-4p+RBw9d1qV4S749J42ZooeQaBomFPrSxa9JONLHJ1TxCBo3TzJ79vtmG2S2erUT8PDDrPdw4ZbXGr2/1+dILA== +"@babel/plugin-transform-react-jsx-self@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369" + integrity sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-jsx" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx-source@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.1.tgz#30db3d4ee3cdebbb26a82a9703673714777a4273" - integrity sha512-neAbaKkoiL+LXYbGDvh6PjPG+YeA67OsZlE78u50xbWh2L1/C81uHiNP5d1fw+uqUIoiNdCC8ZB+G4Zh3hShJA== +"@babel/plugin-transform-react-jsx-source@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz#34f1779117520a779c054f2cdd9680435b9222b4" + integrity sha512-wTeqHVkN1lfPLubRiZH3o73f4rfon42HpgxUSs86Nc+8QIcm/B9s8NNVXu/gwGcOyd7yDib9ikxoDLxJP0UiDA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-jsx" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx@^7.10.1": - version "7.10.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.3.tgz#c07ad86b7c159287c89b643f201f59536231048e" - integrity sha512-Y21E3rZmWICRJnvbGVmDLDZ8HfNDIwjGF3DXYHx1le0v0mIHCs0Gv5SavyW5Z/jgAHLaAoJPiwt+Dr7/zZKcOQ== +"@babel/plugin-transform-react-jsx@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz#673c9f913948764a4421683b2bef2936968fddf2" + integrity sha512-L+MfRhWjX0eI7Js093MM6MacKU4M6dnCRa/QPDwYMxjljzSCzzlzKzj9Pk4P3OtrPcxr2N3znR419nr3Xw+65A== dependencies: - "@babel/helper-builder-react-jsx" "^7.10.3" - "@babel/helper-builder-react-jsx-experimental" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.3" - "@babel/plugin-syntax-jsx" "^7.10.1" + "@babel/helper-builder-react-jsx" "^7.10.4" + "@babel/helper-builder-react-jsx-experimental" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-pure-annotations@^7.10.1": - version "7.10.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.3.tgz#97840981673fcb0df2cc33fb25b56cc421f7deef" - integrity sha512-n/fWYGqvTl7OLZs/QcWaKMFdADPvC3V6jYuEOpPyvz97onsW9TXn196fHnHW1ZgkO20/rxLOgKnEtN1q9jkgqA== +"@babel/plugin-transform-react-pure-annotations@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz#3eefbb73db94afbc075f097523e445354a1c6501" + integrity sha512-+njZkqcOuS8RaPakrnR9KvxjoG1ASJWpoIv/doyWngId88JoFlPlISenGXjrVacZUIALGUr6eodRs1vmPnF23A== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.3" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/preset-react@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.1.tgz#e2ab8ae9a363ec307b936589f07ed753192de041" - integrity sha512-Rw0SxQ7VKhObmFjD/cUcKhPTtzpeviEFX1E6PgP+cYOhQ98icNqtINNFANlsdbQHrmeWnqdxA4Tmnl1jy5tp3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-transform-react-display-name" "^7.10.1" - "@babel/plugin-transform-react-jsx" "^7.10.1" - "@babel/plugin-transform-react-jsx-development" "^7.10.1" - "@babel/plugin-transform-react-jsx-self" "^7.10.1" - "@babel/plugin-transform-react-jsx-source" "^7.10.1" - "@babel/plugin-transform-react-pure-annotations" "^7.10.1" - -"@babel/runtime-corejs3@^7.10.2", "@babel/runtime-corejs3@^7.8.3": +"@babel/preset-react@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.4.tgz#92e8a66d816f9911d11d4cc935be67adfc82dbcf" + integrity sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-react-display-name" "^7.10.4" + "@babel/plugin-transform-react-jsx" "^7.10.4" + "@babel/plugin-transform-react-jsx-development" "^7.10.4" + "@babel/plugin-transform-react-jsx-self" "^7.10.4" + "@babel/plugin-transform-react-jsx-source" "^7.10.4" + "@babel/plugin-transform-react-pure-annotations" "^7.10.4" + +"@babel/runtime-corejs3@^7.10.2": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a" integrity sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw== @@ -389,6 +542,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.11.0": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.1", "@babel/template@^7.10.3", "@babel/template@^7.3.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.3.tgz#4d13bc8e30bf95b0ce9d175d30306f42a2c9a7b8" @@ -398,6 +558,15 @@ "@babel/parser" "^7.10.3" "@babel/types" "^7.10.3" +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/traverse@^7.1.0", "@babel/traverse@^7.10.1", "@babel/traverse@^7.10.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.3.tgz#0b01731794aa7b77b214bcd96661f18281155d7e" @@ -413,6 +582,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" + integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.0" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.11.0" + "@babel/types" "^7.11.0" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/types@^7.0.0", "@babel/types@^7.10.1", "@babel/types@^7.10.3", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.7.0": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.3.tgz#6535e3b79fea86a6b09e012ea8528f935099de8e" @@ -422,6 +606,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" + integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -540,89 +733,93 @@ chalk "^2.0.1" slash "^2.0.0" -"@jest/console@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.1.0.tgz#f67c89e4f4d04dbcf7b052aed5ab9c74f915b954" - integrity sha512-+0lpTHMd/8pJp+Nd4lyip+/Iyf2dZJvcCqrlkeZQoQid+JlThA4M9vxHtheyrQ99jJTMQam+es4BcvZ5W5cC3A== +"@jest/console@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.3.0.tgz#ed04063efb280c88ba87388b6f16427c0a85c856" + integrity sha512-/5Pn6sJev0nPUcAdpJHMVIsA8sKizL2ZkcKPE5+dJrCccks7tcM7c9wbgHudBJbxXLoTbqsHkG1Dofoem4F09w== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" + "@types/node" "*" chalk "^4.0.0" - jest-message-util "^26.1.0" - jest-util "^26.1.0" + jest-message-util "^26.3.0" + jest-util "^26.3.0" slash "^3.0.0" -"@jest/core@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.1.0.tgz#4580555b522de412a7998b3938c851e4f9da1c18" - integrity sha512-zyizYmDJOOVke4OO/De//aiv8b07OwZzL2cfsvWF3q9YssfpcKfcnZAwDY8f+A76xXSMMYe8i/f/LPocLlByfw== +"@jest/core@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.3.0.tgz#da496913ce7385b5e597b527078bf4ca12d2b627" + integrity sha512-WAAqGMpc+U+GS0oSr/ikI1JdRyPQyTZSVOr1xjnVcfvfUTZCK+wGoN0Cb7dm7HVdpbMQr/NvtM6vBVChctmzHA== dependencies: - "@jest/console" "^26.1.0" - "@jest/reporters" "^26.1.0" - "@jest/test-result" "^26.1.0" - "@jest/transform" "^26.1.0" - "@jest/types" "^26.1.0" + "@jest/console" "^26.3.0" + "@jest/reporters" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/transform" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" - jest-changed-files "^26.1.0" - jest-config "^26.1.0" - jest-haste-map "^26.1.0" - jest-message-util "^26.1.0" + jest-changed-files "^26.3.0" + jest-config "^26.3.0" + jest-haste-map "^26.3.0" + jest-message-util "^26.3.0" jest-regex-util "^26.0.0" - jest-resolve "^26.1.0" - jest-resolve-dependencies "^26.1.0" - jest-runner "^26.1.0" - jest-runtime "^26.1.0" - jest-snapshot "^26.1.0" - jest-util "^26.1.0" - jest-validate "^26.1.0" - jest-watcher "^26.1.0" + jest-resolve "^26.3.0" + jest-resolve-dependencies "^26.3.0" + jest-runner "^26.3.0" + jest-runtime "^26.3.0" + jest-snapshot "^26.3.0" + jest-util "^26.3.0" + jest-validate "^26.3.0" + jest-watcher "^26.3.0" micromatch "^4.0.2" p-each-series "^2.1.0" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.1.0.tgz#378853bcdd1c2443b4555ab908cfbabb851e96da" - integrity sha512-86+DNcGongbX7ai/KE/S3/NcUVZfrwvFzOOWX/W+OOTvTds7j07LtC+MgGydH5c8Ri3uIrvdmVgd1xFD5zt/xA== +"@jest/environment@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.3.0.tgz#e6953ab711ae3e44754a025f838bde1a7fd236a0" + integrity sha512-EW+MFEo0DGHahf83RAaiqQx688qpXgl99wdb8Fy67ybyzHwR1a58LHcO376xQJHfmoXTu89M09dH3J509cx2AA== dependencies: - "@jest/fake-timers" "^26.1.0" - "@jest/types" "^26.1.0" - jest-mock "^26.1.0" + "@jest/fake-timers" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" + jest-mock "^26.3.0" -"@jest/fake-timers@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.1.0.tgz#9a76b7a94c351cdbc0ad53e5a748789f819a65fe" - integrity sha512-Y5F3kBVWxhau3TJ825iuWy++BAuQzK/xEa+wD9vDH3RytW9f2DbMVodfUQC54rZDX3POqdxCgcKdgcOL0rYUpA== +"@jest/fake-timers@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.3.0.tgz#f515d4667a6770f60ae06ae050f4e001126c666a" + integrity sha512-ZL9ytUiRwVP8ujfRepffokBvD2KbxbqMhrXSBhSdAhISCw3gOkuntisiSFv+A6HN0n0fF4cxzICEKZENLmW+1A== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" "@sinonjs/fake-timers" "^6.0.1" - jest-message-util "^26.1.0" - jest-mock "^26.1.0" - jest-util "^26.1.0" + "@types/node" "*" + jest-message-util "^26.3.0" + jest-mock "^26.3.0" + jest-util "^26.3.0" -"@jest/globals@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.1.0.tgz#6cc5d7cbb79b76b120f2403d7d755693cf063ab1" - integrity sha512-MKiHPNaT+ZoG85oMaYUmGHEqu98y3WO2yeIDJrs2sJqHhYOy3Z6F7F/luzFomRQ8SQ1wEkmahFAz2291Iv8EAw== +"@jest/globals@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.3.0.tgz#41a931c5bce4572b437dffab7146850044c7d359" + integrity sha512-oPe30VG9zor2U3Ev7khCM2LkjO3D+mgAv6s5D3Ed0sxfELxoRZwR8d1VgYWVQljcpumMwe9tDrKNuzgVjbEt7g== dependencies: - "@jest/environment" "^26.1.0" - "@jest/types" "^26.1.0" - expect "^26.1.0" + "@jest/environment" "^26.3.0" + "@jest/types" "^26.3.0" + expect "^26.3.0" -"@jest/reporters@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.1.0.tgz#08952e90c90282e14ff49e927bdf1873617dae78" - integrity sha512-SVAysur9FOIojJbF4wLP0TybmqwDkdnFxHSPzHMMIYyBtldCW9gG+Q5xWjpMFyErDiwlRuPyMSJSU64A67Pazg== +"@jest/reporters@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.3.0.tgz#12112cc0a073a92e7205d7ceee4de7cfac232105" + integrity sha512-MfLJOUPxhGb3sRT/wFjHXd6gyVQ1Fb1XxbEwY+gqdDBpg3pq5qAB5eiBUvcTheFRHmhu3gOv3UZ/gtxmqGBA+Q== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.1.0" - "@jest/test-result" "^26.1.0" - "@jest/transform" "^26.1.0" - "@jest/types" "^26.1.0" + "@jest/console" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/transform" "^26.3.0" + "@jest/types" "^26.3.0" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" @@ -633,15 +830,15 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.0.2" - jest-haste-map "^26.1.0" - jest-resolve "^26.1.0" - jest-util "^26.1.0" - jest-worker "^26.1.0" + jest-haste-map "^26.3.0" + jest-resolve "^26.3.0" + jest-util "^26.3.0" + jest-worker "^26.3.0" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^4.1.3" + v8-to-istanbul "^5.0.1" optionalDependencies: node-notifier "^7.0.0" @@ -654,10 +851,10 @@ graceful-fs "^4.1.15" source-map "^0.6.0" -"@jest/source-map@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.1.0.tgz#a6a020d00e7d9478f4b690167c5e8b77e63adb26" - integrity sha512-XYRPYx4eEVX15cMT9mstnO7hkHP3krNtKfxUYd8L7gbtia8JvZZ6bMzSwa6IQJENbudTwKMw5R1BePRD+bkEmA== +"@jest/source-map@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.3.0.tgz#0e646e519883c14c551f7b5ae4ff5f1bfe4fc3d9" + integrity sha512-hWX5IHmMDWe1kyrKl7IhFwqOuAreIwHhbe44+XH2ZRHjrKIh0LO5eLQ/vxHFeAfRwJapmxuqlGAEYLadDq6ZGQ== dependencies: callsites "^3.0.0" graceful-fs "^4.2.4" @@ -672,42 +869,42 @@ "@jest/types" "^24.9.0" "@types/istanbul-lib-coverage" "^2.0.0" -"@jest/test-result@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.1.0.tgz#a93fa15b21ad3c7ceb21c2b4c35be2e407d8e971" - integrity sha512-Xz44mhXph93EYMA8aYDz+75mFbarTV/d/x0yMdI3tfSRs/vh4CqSxgzVmCps1fPkHDCtn0tU8IH9iCKgGeGpfw== +"@jest/test-result@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.3.0.tgz#46cde01fa10c0aaeb7431bf71e4a20d885bc7fdb" + integrity sha512-a8rbLqzW/q7HWheFVMtghXV79Xk+GWwOK1FrtimpI5n1la2SY0qHri3/b0/1F0Ve0/yJmV8pEhxDfVwiUBGtgg== dependencies: - "@jest/console" "^26.1.0" - "@jest/types" "^26.1.0" + "@jest/console" "^26.3.0" + "@jest/types" "^26.3.0" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.1.0.tgz#41a6fc8b850c3f33f48288ea9ea517c047e7f14e" - integrity sha512-Z/hcK+rTq56E6sBwMoQhSRDVjqrGtj1y14e2bIgcowARaIE1SgOanwx6gvY4Q9gTKMoZQXbXvptji+q5GYxa6Q== +"@jest/test-sequencer@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.3.0.tgz#f22b4927f8eef391ebaba6205d6aba328af9fda9" + integrity sha512-G7TA0Z85uj5l1m9UKZ/nXbArn0y+MeLKbojNLDHgjb1PpNNFDAOO6FJhk9We34m/hadcciMcJFnxV94dV2TX+w== dependencies: - "@jest/test-result" "^26.1.0" + "@jest/test-result" "^26.3.0" graceful-fs "^4.2.4" - jest-haste-map "^26.1.0" - jest-runner "^26.1.0" - jest-runtime "^26.1.0" + jest-haste-map "^26.3.0" + jest-runner "^26.3.0" + jest-runtime "^26.3.0" -"@jest/transform@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.1.0.tgz#697f48898c2a2787c9b4cb71d09d7e617464e509" - integrity sha512-ICPm6sUXmZJieq45ix28k0s+d/z2E8CHDsq+WwtWI6kW8m7I8kPqarSEcUN86entHQ570ZBRci5OWaKL0wlAWw== +"@jest/transform@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.3.0.tgz#c393e0e01459da8a8bfc6d2a7c2ece1a13e8ba55" + integrity sha512-Isj6NB68QorGoFWvcOjlUhpkT56PqNIsXKR7XfvoDlCANn/IANlh8DrKAA2l2JKC3yWSMH5wS0GwuQM20w3b2A== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" babel-plugin-istanbul "^6.0.0" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.4" - jest-haste-map "^26.1.0" + jest-haste-map "^26.3.0" jest-regex-util "^26.0.0" - jest-util "^26.1.0" + jest-util "^26.3.0" micromatch "^4.0.2" pirates "^4.0.1" slash "^3.0.0" @@ -723,13 +920,14 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@jest/types@^26.1.0": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.1.0.tgz#f8afaaaeeb23b5cad49dd1f7779689941dcb6057" - integrity sha512-GXigDDsp6ZlNMhXQDeuy/iYCDsRIHJabWtDzvnn36+aqFfG14JmFV0e/iXxY4SP9vbXSiPNOWdehU5MeqrYHBQ== +"@jest/types@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.3.0.tgz#97627bf4bdb72c55346eef98e3b3f7ddc4941f71" + integrity sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^1.1.1" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" "@types/yargs" "^15.0.0" chalk "^4.0.0" @@ -804,12 +1002,12 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@tanem/svg-injector@^8.0.54": - version "8.0.55" - resolved "https://registry.yarnpkg.com/@tanem/svg-injector/-/svg-injector-8.0.55.tgz#7bd6ab60b700725f85838766abaeea25b43dd3cf" - integrity sha512-+HjH9tZNGaJOllGiBjoQ5H8LVw+RrHZhUteFetedqGfEN/+1ML+UwKbJHWUXDWCXVcAHsBUD3m3/fOxsptgS5w== +"@tanem/svg-injector@^8.0.61": + version "8.0.62" + resolved "https://registry.yarnpkg.com/@tanem/svg-injector/-/svg-injector-8.0.62.tgz#c8e6829b98554942a9d798f9fd4b80df9a209942" + integrity sha512-Utp89g9kDNb0Y6uD6RjNJa+n3mgO0moMaJgY0VIv4/qUiJCSErthpivI8MWiFiN0YBN8PY9RyGRlY4uO6WKeaA== dependencies: - "@babel/runtime" "^7.10.2" + "@babel/runtime" "^7.11.0" "@types/anymatch@*": version "1.3.1" @@ -919,6 +1117,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/json-schema@^7.0.4": version "7.0.5" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" @@ -1204,26 +1409,30 @@ acorn@^6.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.1.1, acorn@^7.2.0: +acorn@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== -airbnb-prop-types@^2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef" - integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA== +acorn@^7.3.1: + version "7.4.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" + integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + +airbnb-prop-types@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" + integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== dependencies: - array.prototype.find "^2.1.0" - function.prototype.name "^1.1.1" - has "^1.0.3" - is-regex "^1.0.4" - object-is "^1.0.1" + array.prototype.find "^2.1.1" + function.prototype.name "^1.1.2" + is-regex "^1.1.0" + object-is "^1.1.2" object.assign "^4.1.0" - object.entries "^1.1.0" + object.entries "^1.1.2" prop-types "^15.7.2" prop-types-exact "^1.2.0" - react-is "^16.9.0" + react-is "^16.13.1" ajv-errors@^1.0.0: version "1.0.1" @@ -1408,7 +1617,7 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.find@^2.1.0: +array.prototype.find@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c" integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA== @@ -1424,6 +1633,15 @@ array.prototype.flat@^1.2.3: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" +array.prototype.flatmap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443" + integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + asap@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -1530,16 +1748,16 @@ babel-eslint@^10.1.0: eslint-visitor-keys "^1.0.0" resolve "^1.12.0" -babel-jest@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.1.0.tgz#b20751185fc7569a0f135730584044d1cb934328" - integrity sha512-Nkqgtfe7j6PxLO6TnCQQlkMm8wdTdnIF8xrdpooHCuD5hXRzVEPbPneTJKknH5Dsv3L8ip9unHDAp48YQ54Dkg== +babel-jest@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.3.0.tgz#10d0ca4b529ca3e7d1417855ef7d7bd6fc0c3463" + integrity sha512-sxPnQGEyHAOPF8NcUsD0g7hDCnvLL2XyblRBcgrzTWBB/mAIpWow3n1bEL+VghnnZfreLhFSBsFluRoK2tRK4g== dependencies: - "@jest/transform" "^26.1.0" - "@jest/types" "^26.1.0" + "@jest/transform" "^26.3.0" + "@jest/types" "^26.3.0" "@types/babel__core" "^7.1.7" babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^26.1.0" + babel-preset-jest "^26.3.0" chalk "^4.0.0" graceful-fs "^4.2.4" slash "^3.0.0" @@ -1573,10 +1791,10 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.1.0.tgz#c6a774da08247a28285620a64dfadbd05dd5233a" - integrity sha512-qhqLVkkSlqmC83bdMhM8WW4Z9tB+JkjqAqlbbohS9sJLT5Ha2vfzuKqg5yenXrAjOPG2YC0WiXdH3a9PvB+YYw== +babel-plugin-jest-hoist@^26.2.0: + version "26.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.2.0.tgz#bdd0011df0d3d513e5e95f76bd53b51147aca2dd" + integrity sha512-B/hVMRv8Nh1sQ1a3EY8I0n4Y1Wty3NrR5ebOyVT302op+DOAau+xNEImGMsUWOC3++ZlMooCytKz+NgN8aKGbA== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -1598,7 +1816,7 @@ babel-plugin-syntax-jsx@^6.18.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY= -babel-preset-current-node-syntax@^0.1.2: +babel-preset-current-node-syntax@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz#b4b547acddbf963cba555ba9f9cbbb70bfd044da" integrity sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ== @@ -1615,13 +1833,13 @@ babel-preset-current-node-syntax@^0.1.2: "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -babel-preset-jest@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.1.0.tgz#612f714e5b457394acfd863793c564cbcdb7d1c1" - integrity sha512-na9qCqFksknlEj5iSdw1ehMVR06LCCTkZLGKeEtxDDdhg8xpUF09m29Kvh1pRbZ07h7AQ5ttLYUwpXL4tO6w7w== +babel-preset-jest@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.3.0.tgz#ed6344506225c065fd8a0b53e191986f74890776" + integrity sha512-5WPdf7nyYi2/eRxCbVrE1kKCWxgWY4RsPEbdJWFm7QsesFGqjdkyLeu1zRkwM1cxK6EPIlNd6d2AxLk7J+t4pw== dependencies: - babel-plugin-jest-hoist "^26.1.0" - babel-preset-current-node-syntax "^0.1.2" + babel-plugin-jest-hoist "^26.2.0" + babel-preset-current-node-syntax "^0.1.3" bail@^1.0.0: version "1.0.5" @@ -2063,10 +2281,10 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" - integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== +chokidar@^3.4.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -2144,15 +2362,6 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -2396,24 +2605,24 @@ css-color-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= -css-loader@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" - integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ== +css-loader@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.1.tgz#9f48fd7eae1219d629a3f085ba9a9102ca1141a7" + integrity sha512-MoqmF1if7Z0pZIEXA4ZF9PgtCXxWbfzfJM+3p+OYfhcrwcqhaCRb74DSnfzRl7e024xEiCRn5hCvfUbTf2sgFA== dependencies: - camelcase "^5.3.1" + camelcase "^6.0.0" cssesc "^3.0.0" icss-utils "^4.1.1" - loader-utils "^1.2.3" + loader-utils "^2.0.0" normalize-path "^3.0.0" postcss "^7.0.32" postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.2" + postcss-modules-local-by-default "^3.0.3" postcss-modules-scope "^2.2.0" postcss-modules-values "^3.0.0" postcss-value-parser "^4.1.0" schema-utils "^2.7.0" - semver "^6.3.0" + semver "^7.3.2" css-select@~1.2.0: version "1.2.0" @@ -2876,10 +3085,10 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== -diff-sequences@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.0.0.tgz#0760059a5c287637b842bd7085311db7060e88a6" - integrity sha512-JC/eHYEC3aSS0vZGjuoc4vHA0yAQTzhQQldXMeMF+JlxLGJlCO38Gma82NV9gk1jGFz8mDzUMeaKXvjRRdJ2dg== +diff-sequences@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.3.0.tgz#62a59b1b29ab7fd27cef2a33ae52abe73042d0a2" + integrity sha512-5j5vdRcw3CNctePNYN0Wy2e/JbWT6cAYnXv5OuqPhDpyCGc0uLu2TK0zOCJWNB9kOIfYMSpIulRaDgIi4HJ6Ig== diff@^3.5.0: version "3.5.0" @@ -3037,6 +3246,11 @@ elliptic@^6.0.0, elliptic@^6.5.2: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +emittery@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.1.tgz#c02375a927a40948c0345cc903072597f5270451" + integrity sha512-d34LN4L6h18Bzz9xpoku2nPwKxCPlPMr3EEKTkoEBi+1/+b0lcRkRJ1UVyyZaKNeqGR3swcGl6s390DNO4YVgQ== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -3069,7 +3283,7 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1: +enhanced-resolve@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz#5d43bda4a0fd447cb0ebbe71bef8deff8805ad0d" integrity sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ== @@ -3078,6 +3292,15 @@ enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1: memory-fs "^0.5.0" tapable "^1.0.0" +enhanced-resolve@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + enquirer@^2.3.5: version "2.3.5" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" @@ -3095,27 +3318,27 @@ entities@^2.0.0, entities@~2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== -enzyme-adapter-react-16@^1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz#b16db2f0ea424d58a808f9df86ab6212895a4501" - integrity sha512-SkvDrb8xU3lSxID8Qic9rB8pvevDbLybxPK6D/vW7PrT0s2Cl/zJYuXvsd1EBTz0q4o3iqG3FJhpYz3nUNpM2Q== +enzyme-adapter-react-16@^1.15.3: + version "1.15.3" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.3.tgz#90154055be3318d70a51df61ac89cfa22e3d5f60" + integrity sha512-98rqNI4n9HZslWIPuuwy4hK1bxRuMy+XX0CU1dS8iUqcgisTxeBaap6oPp2r4MWC8OphCbbqAT8EU/xHz3zIaQ== dependencies: - enzyme-adapter-utils "^1.13.0" - enzyme-shallow-equal "^1.0.1" + enzyme-adapter-utils "^1.13.1" + enzyme-shallow-equal "^1.0.4" has "^1.0.3" object.assign "^4.1.0" object.values "^1.1.1" prop-types "^15.7.2" - react-is "^16.12.0" + react-is "^16.13.1" react-test-renderer "^16.0.0-0" semver "^5.7.0" -enzyme-adapter-utils@^1.13.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.0.tgz#01c885dde2114b4690bf741f8dc94cee3060eb78" - integrity sha512-YuEtfQp76Lj5TG1NvtP2eGJnFKogk/zT70fyYHXK2j3v6CtuHqc8YmgH/vaiBfL8K1SgVVbQXtTcgQZFwzTVyQ== +enzyme-adapter-utils@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.1.tgz#59c1b734b0927543e3d8dc477299ec957feb312d" + integrity sha512-5A9MXXgmh/Tkvee3bL/9RCAAgleHqFnsurTYCbymecO4ohvtNO5zqIhHxV370t7nJAwaCfkgtffarKpC0GPt0g== dependencies: - airbnb-prop-types "^2.15.0" + airbnb-prop-types "^2.16.0" function.prototype.name "^1.1.2" object.assign "^4.1.0" object.fromentries "^2.0.2" @@ -3130,6 +3353,14 @@ enzyme-shallow-equal@^1.0.1: has "^1.0.3" object-is "^1.0.2" +enzyme-shallow-equal@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" + integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== + dependencies: + has "^1.0.3" + object-is "^1.1.2" + enzyme@^3.11.0: version "3.11.0" resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" @@ -3265,10 +3496,10 @@ eslint-module-utils@^2.6.0: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-import@^2.21.2: - version "2.21.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.21.2.tgz#8fef77475cc5510801bedc95f84b932f7f334a7c" - integrity sha512-FEmxeGI6yaz+SnEB6YgNHlQK1Bs2DKLM+YF+vuTk5H8J9CLbJLtlPvRFgZZ2+sXiKAlN5dpdlrWOjK8ZoZJpQA== +eslint-plugin-import@^2.22.0: + version "2.22.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" + integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg== dependencies: array-includes "^3.1.1" array.prototype.flat "^1.2.3" @@ -3301,22 +3532,22 @@ eslint-plugin-jsx-a11y@^6.3.1: jsx-ast-utils "^2.4.1" language-tags "^1.0.5" -eslint-plugin-react@^7.20.0: - version "7.20.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.0.tgz#f98712f0a5e57dfd3e5542ef0604b8739cd47be3" - integrity sha512-rqe1abd0vxMjmbPngo4NaYxTcR3Y4Hrmc/jg4T+sYz63yqlmJRknpEQfmWY+eDWPuMmix6iUIK+mv0zExjeLgA== +eslint-plugin-react@^7.20.5: + version "7.20.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.5.tgz#29480f3071f64a04b2c3d99d9b460ce0f76fb857" + integrity sha512-ajbJfHuFnpVNJjhyrfq+pH1C0gLc2y94OiCbAXT5O0J0YCKaFEHDV8+3+mDOr+w8WguRX+vSs1bM2BDG0VLvCw== dependencies: array-includes "^3.1.1" + array.prototype.flatmap "^1.2.3" doctrine "^2.1.0" has "^1.0.3" - jsx-ast-utils "^2.2.3" - object.entries "^1.1.1" + jsx-ast-utils "^2.4.1" + object.entries "^1.1.2" object.fromentries "^2.0.2" object.values "^1.1.1" prop-types "^15.7.2" - resolve "^1.15.1" + resolve "^1.17.0" string.prototype.matchall "^4.0.2" - xregexp "^4.3.0" eslint-scope@^4.0.3: version "4.0.3" @@ -3334,22 +3565,22 @@ eslint-scope@^5.1.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^2.0.0: +eslint-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== dependencies: eslint-visitor-keys "^1.1.0" -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.2.0: +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -eslint@^7.2.0: - version "7.3.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.3.1.tgz#76392bd7e44468d046149ba128d1566c59acbe19" - integrity sha512-cQC/xj9bhWUcyi/RuMbRtC3I0eW8MH0jhRELSvpKYkWep3C6YZ2OkvcvJVUeO6gcunABmzptbXBuDoXsjHmfTA== +eslint@^7.6.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.6.0.tgz#522d67cfaea09724d96949c70e7a0550614d64d6" + integrity sha512-QlAManNtqr7sozWm5TF4wIH9gmUm2hE3vNRUvyoYAa4y1l5/jxD/PQStEjBMQtCqZmSep8UxrcecI60hOpe61w== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" @@ -3359,9 +3590,9 @@ eslint@^7.2.0: doctrine "^3.0.0" enquirer "^2.3.5" eslint-scope "^5.1.0" - eslint-utils "^2.0.0" - eslint-visitor-keys "^1.2.0" - espree "^7.1.0" + eslint-utils "^2.1.0" + eslint-visitor-keys "^1.3.0" + espree "^7.2.0" esquery "^1.2.0" esutils "^2.0.2" file-entry-cache "^5.0.1" @@ -3375,7 +3606,7 @@ eslint@^7.2.0: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.14" + lodash "^4.17.19" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -3388,14 +3619,14 @@ eslint@^7.2.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.1.0.tgz#a9c7f18a752056735bf1ba14cb1b70adc3a5ce1c" - integrity sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw== +espree@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.2.0.tgz#1c263d5b513dbad0ac30c4991b93ac354e948d69" + integrity sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g== dependencies: - acorn "^7.2.0" + acorn "^7.3.1" acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.2.0" + eslint-visitor-keys "^1.3.0" esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" @@ -3519,16 +3750,16 @@ expect@^24.8.0: jest-message-util "^24.9.0" jest-regex-util "^24.9.0" -expect@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-26.1.0.tgz#8c62e31d0f8d5a8ebb186ee81473d15dd2fbf7c8" - integrity sha512-QbH4LZXDsno9AACrN9eM0zfnby9G+OsdNgZUohjg/P0mLy1O+/bzTAJGT6VSIjVCe8yKM6SzEl/ckEOFBT7Vnw== +expect@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.3.0.tgz#6145b4999a2c9bd64a644360d0c781c44d369c54" + integrity sha512-3tC0dpPgkTGkycM9H+mMjzIhm8I3ZAOV+y1Cj3xmF9iKxDeHBCAB64hf1OY//bMzQ/AftfodNy2pQWMKpTIV8Q== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" ansi-styles "^4.0.0" - jest-get-type "^26.0.0" - jest-matcher-utils "^26.1.0" - jest-message-util "^26.1.0" + jest-get-type "^26.3.0" + jest-matcher-utils "^26.3.0" + jest-message-util "^26.3.0" jest-regex-util "^26.0.0" extend-shallow@^2.0.1: @@ -3840,7 +4071,7 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.1.1, function.prototype.name@^1.1.2: +function.prototype.name@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45" integrity sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg== @@ -4627,7 +4858,7 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= -is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0: +is-regex@^1.0.5, is-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== @@ -4771,57 +5002,57 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.1.0.tgz#de66b0f30453bca2aff98e9400f75905da495305" - integrity sha512-HS5MIJp3B8t0NRKGMCZkcDUZo36mVRvrDETl81aqljT1S9tqiHRSpyoOvWg9ZilzZG9TDisDNaN1IXm54fLRZw== +jest-changed-files@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.3.0.tgz#68fb2a7eb125f50839dab1f5a17db3607fe195b1" + integrity sha512-1C4R4nijgPltX6fugKxM4oQ18zimS7LqQ+zTTY8lMCMFPrxqBFb7KJH0Z2fRQJvw2Slbaipsqq7s1mgX5Iot+g== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" execa "^4.0.0" throat "^5.0.0" -jest-cli@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.1.0.tgz#eb9ec8a18cf3b6aa556d9deaa9e24be12b43ad87" - integrity sha512-Imumvjgi3rU7stq6SJ1JUEMaV5aAgJYXIs0jPqdUnF47N/Tk83EXfmtvNKQ+SnFVI6t6mDOvfM3aA9Sg6kQPSw== +jest-cli@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.3.0.tgz#046164f0b8194234aaa76bb58e867f5d6e3fcf53" + integrity sha512-vrlDluEjnNTJNpmw+lJ1Dvjhc+2o7QG0dG8n+iDu3NaoQ9OzqNeZsZZ0a9KP7SdtD5BXgvGSpCWTlLH5SqtxcA== dependencies: - "@jest/core" "^26.1.0" - "@jest/test-result" "^26.1.0" - "@jest/types" "^26.1.0" + "@jest/core" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/types" "^26.3.0" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" import-local "^3.0.2" is-ci "^2.0.0" - jest-config "^26.1.0" - jest-util "^26.1.0" - jest-validate "^26.1.0" + jest-config "^26.3.0" + jest-util "^26.3.0" + jest-validate "^26.3.0" prompts "^2.0.1" yargs "^15.3.1" -jest-config@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.1.0.tgz#9074f7539acc185e0113ad6d22ed589c16a37a73" - integrity sha512-ONTGeoMbAwGCdq4WuKkMcdMoyfs5CLzHEkzFOlVvcDXufZSaIWh/OXMLa2fwKXiOaFcqEw8qFr4VOKJQfn4CVw== +jest-config@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.3.0.tgz#adb776fa88fc45ea719287cc09e4f0f5d5b3ce00" + integrity sha512-xzvmhKYOXOc/JjGabUUXoi7Nxu6QpY5zJxND85wdqFrdP7raJT5wqlrVJbp6Bv4Sj1e83Z8bkxjsZCpwPASaPw== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.1.0" - "@jest/types" "^26.1.0" - babel-jest "^26.1.0" + "@jest/test-sequencer" "^26.3.0" + "@jest/types" "^26.3.0" + babel-jest "^26.3.0" chalk "^4.0.0" deepmerge "^4.2.2" glob "^7.1.1" graceful-fs "^4.2.4" - jest-environment-jsdom "^26.1.0" - jest-environment-node "^26.1.0" - jest-get-type "^26.0.0" - jest-jasmine2 "^26.1.0" + jest-environment-jsdom "^26.3.0" + jest-environment-node "^26.3.0" + jest-get-type "^26.3.0" + jest-jasmine2 "^26.3.0" jest-regex-util "^26.0.0" - jest-resolve "^26.1.0" - jest-util "^26.1.0" - jest-validate "^26.1.0" + jest-resolve "^26.3.0" + jest-util "^26.3.0" + jest-validate "^26.3.0" micromatch "^4.0.2" - pretty-format "^26.1.0" + pretty-format "^26.3.0" jest-diff@^24.9.0: version "24.9.0" @@ -4833,15 +5064,15 @@ jest-diff@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" -jest-diff@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.1.0.tgz#00a549bdc936c9691eb4dc25d1fbd78bf456abb2" - integrity sha512-GZpIcom339y0OXznsEKjtkfKxNdg7bVbEofK8Q6MnevTIiR1jNhDWKhRX6X0SDXJlwn3dy59nZ1z55fLkAqPWg== +jest-diff@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.3.0.tgz#485eea87b7003d34628c960c6c625ffe4de8ab04" + integrity sha512-q5OZAtnr5CbHzrhjANzc3wvROk7+rcjCUI5uqM4cjOjtscNKfbJKBs3YhsWWhsdsIZzI3gc6wOpm49r6S61beg== dependencies: chalk "^4.0.0" - diff-sequences "^26.0.0" - jest-get-type "^26.0.0" - pretty-format "^26.1.0" + diff-sequences "^26.3.0" + jest-get-type "^26.3.0" + pretty-format "^26.3.0" jest-docblock@^26.0.0: version "26.0.0" @@ -4850,39 +5081,41 @@ jest-docblock@^26.0.0: dependencies: detect-newline "^3.0.0" -jest-each@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.1.0.tgz#e35449875009a22d74d1bda183b306db20f286f7" - integrity sha512-lYiSo4Igr81q6QRsVQq9LIkJW0hZcKxkIkHzNeTMPENYYDw/W/Raq28iJ0sLlNFYz2qxxeLnc5K2gQoFYlu2bA== +jest-each@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.3.0.tgz#f70192d59f6a8d72b4ccfe8e9a39ddf667b1263e" + integrity sha512-OSAnLv0Eo/sDVhV0ifT2u6Q4aYUBoZ97R4k9cQshUFLTco0iRDbViJiW3Y6ySZjW95Tb83/xMYCppBih/7sW/A== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" chalk "^4.0.0" - jest-get-type "^26.0.0" - jest-util "^26.1.0" - pretty-format "^26.1.0" - -jest-environment-jsdom@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.1.0.tgz#9dc7313ffe1b59761dad1fedb76e2503e5d37c5b" - integrity sha512-dWfiJ+spunVAwzXbdVqPH1LbuJW/kDL+FyqgA5YzquisHqTi0g9hquKif9xKm7c1bKBj6wbmJuDkeMCnxZEpUw== - dependencies: - "@jest/environment" "^26.1.0" - "@jest/fake-timers" "^26.1.0" - "@jest/types" "^26.1.0" - jest-mock "^26.1.0" - jest-util "^26.1.0" + jest-get-type "^26.3.0" + jest-util "^26.3.0" + pretty-format "^26.3.0" + +jest-environment-jsdom@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.3.0.tgz#3b749ba0f3a78e92ba2c9ce519e16e5dd515220c" + integrity sha512-zra8He2btIMJkAzvLaiZ9QwEPGEetbxqmjEBQwhH3CA+Hhhu0jSiEJxnJMbX28TGUvPLxBt/zyaTLrOPF4yMJA== + dependencies: + "@jest/environment" "^26.3.0" + "@jest/fake-timers" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" + jest-mock "^26.3.0" + jest-util "^26.3.0" jsdom "^16.2.2" -jest-environment-node@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.1.0.tgz#8bb387b3eefb132eab7826f9a808e4e05618960b" - integrity sha512-DNm5x1aQH0iRAe9UYAkZenuzuJ69VKzDCAYISFHQ5i9e+2Tbeu2ONGY7YStubCLH8a1wdKBgqScYw85+ySxqxg== +jest-environment-node@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.3.0.tgz#56c6cfb506d1597f94ee8d717072bda7228df849" + integrity sha512-c9BvYoo+FGcMj5FunbBgtBnbR5qk3uky8PKyRVpSfe2/8+LrNQMiXX53z6q2kY+j15SkjQCOSL/6LHnCPLVHNw== dependencies: - "@jest/environment" "^26.1.0" - "@jest/fake-timers" "^26.1.0" - "@jest/types" "^26.1.0" - jest-mock "^26.1.0" - jest-util "^26.1.0" + "@jest/environment" "^26.3.0" + "@jest/fake-timers" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" + jest-mock "^26.3.0" + jest-util "^26.3.0" jest-fetch-mock@^3.0.3: version "3.0.3" @@ -4897,61 +5130,63 @@ jest-get-type@^24.9.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== -jest-get-type@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.0.0.tgz#381e986a718998dbfafcd5ec05934be538db4039" - integrity sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg== +jest-get-type@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" + integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== -jest-haste-map@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.1.0.tgz#ef31209be73f09b0d9445e7d213e1b53d0d1476a" - integrity sha512-WeBS54xCIz9twzkEdm6+vJBXgRBQfdbbXD0dk8lJh7gLihopABlJmIQFdWSDDtuDe4PRiObsjZSUjbJ1uhWEpA== +jest-haste-map@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.3.0.tgz#c51a3b40100d53ab777bfdad382d2e7a00e5c726" + integrity sha512-DHWBpTJgJhLLGwE5Z1ZaqLTYqeODQIZpby0zMBsCU9iRFHYyhklYqP4EiG73j5dkbaAdSZhgB938mL51Q5LeZA== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" "@types/graceful-fs" "^4.1.2" + "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.4" - jest-serializer "^26.1.0" - jest-util "^26.1.0" - jest-worker "^26.1.0" + jest-regex-util "^26.0.0" + jest-serializer "^26.3.0" + jest-util "^26.3.0" + jest-worker "^26.3.0" micromatch "^4.0.2" sane "^4.0.3" walker "^1.0.7" - which "^2.0.2" optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.1.0.tgz#4dfe349b2b2d3c6b3a27c024fd4cb57ac0ed4b6f" - integrity sha512-1IPtoDKOAG+MeBrKvvuxxGPJb35MTTRSDglNdWWCndCB3TIVzbLThRBkwH9P081vXLgiJHZY8Bz3yzFS803xqQ== +jest-jasmine2@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.3.0.tgz#5c9d365d3032342801cfd15abd2cdcccc7fb01ff" + integrity sha512-ZPkkA2XfH/fcLOp0SjeR4uDrMoNFilcwxLHORpjfMrcU0BFHNNRaF3DnslCdmewzqaERqtmHpYo8jj34RT+m2g== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.1.0" - "@jest/source-map" "^26.1.0" - "@jest/test-result" "^26.1.0" - "@jest/types" "^26.1.0" + "@jest/environment" "^26.3.0" + "@jest/source-map" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^26.1.0" + expect "^26.3.0" is-generator-fn "^2.0.0" - jest-each "^26.1.0" - jest-matcher-utils "^26.1.0" - jest-message-util "^26.1.0" - jest-runtime "^26.1.0" - jest-snapshot "^26.1.0" - jest-util "^26.1.0" - pretty-format "^26.1.0" + jest-each "^26.3.0" + jest-matcher-utils "^26.3.0" + jest-message-util "^26.3.0" + jest-runtime "^26.3.0" + jest-snapshot "^26.3.0" + jest-util "^26.3.0" + pretty-format "^26.3.0" throat "^5.0.0" -jest-leak-detector@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.1.0.tgz#039c3a07ebcd8adfa984b6ac015752c35792e0a6" - integrity sha512-dsMnKF+4BVOZwvQDlgn3MG+Ns4JuLv8jNvXH56bgqrrboyCbI1rQg6EI5rs+8IYagVcfVP2yZFKfWNZy0rK0Hw== +jest-leak-detector@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.3.0.tgz#74c077a243585cc1d2cfd50d231d373100dd6e6f" + integrity sha512-8C2Bur0S6n2xgW5kx22bDbe+Jjz9sM7/abr7DRQ48ww6q4w7vVzEpDEZiY7KatjTHtUloLTAqwTXEXg+tuETTg== dependencies: - jest-get-type "^26.0.0" - pretty-format "^26.1.0" + jest-get-type "^26.3.0" + pretty-format "^26.3.0" jest-matcher-utils@^24.9.0: version "24.9.0" @@ -4963,15 +5198,15 @@ jest-matcher-utils@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" -jest-matcher-utils@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.1.0.tgz#cf75a41bd413dda784f022de5a65a2a5c73a5c92" - integrity sha512-PW9JtItbYvES/xLn5mYxjMd+Rk+/kIt88EfH3N7w9KeOrHWaHrdYPnVHndGbsFGRJ2d5gKtwggCvkqbFDoouQA== +jest-matcher-utils@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.3.0.tgz#41dfecac8e7a38e38330c159789711a50edffaed" + integrity sha512-M5ZRSp6qpyzZyrLwXD2Sop7xaxm6qu/mKvqWU+BOSPTa4Y0ZEoKUYBzus/emg6kaVt7Ov9xMDLLZR1SrC8FxCw== dependencies: chalk "^4.0.0" - jest-diff "^26.1.0" - jest-get-type "^26.0.0" - pretty-format "^26.1.0" + jest-diff "^26.3.0" + jest-get-type "^26.3.0" + pretty-format "^26.3.0" jest-message-util@^24.9.0: version "24.9.0" @@ -4987,13 +5222,13 @@ jest-message-util@^24.9.0: slash "^2.0.0" stack-utils "^1.0.1" -jest-message-util@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.1.0.tgz#52573fbb8f5cea443c4d1747804d7a238a3e233c" - integrity sha512-dY0+UlldiAJwNDJ08SF0HdF32g9PkbF2NRK/+2iMPU40O6q+iSn1lgog/u0UH8ksWoPv0+gNq8cjhYO2MFtT0g== +jest-message-util@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.3.0.tgz#3bdb538af27bb417f2d4d16557606fd082d5841a" + integrity sha512-xIavRYqr4/otGOiLxLZGj3ieMmjcNE73Ui+LdSW/Y790j5acqCsAdDiLIbzHCZMpN07JOENRWX5DcU+OQ+TjTA== dependencies: "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" "@types/stack-utils" "^1.0.1" chalk "^4.0.0" graceful-fs "^4.2.4" @@ -5001,17 +5236,18 @@ jest-message-util@^26.1.0: slash "^3.0.0" stack-utils "^2.0.2" -jest-mock@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.1.0.tgz#80d8286da1f05a345fbad1bfd6fa49a899465d3d" - integrity sha512-1Rm8EIJ3ZFA8yCIie92UbxZWj9SuVmUGcyhLHyAhY6WI3NIct38nVcfOPWhJteqSn8V8e3xOMha9Ojfazfpovw== +jest-mock@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.3.0.tgz#ee62207c3c5ebe5f35b760e1267fee19a1cfdeba" + integrity sha512-PeaRrg8Dc6mnS35gOo/CbZovoDPKAeB1FICZiuagAgGvbWdNNyjQjkOaGUa/3N3JtpQ/Mh9P4A2D4Fv51NnP8Q== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" + "@types/node" "*" -jest-pnp-resolver@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" - integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== jest-regex-util@^24.9.0: version "24.9.0" @@ -5023,147 +5259,151 @@ jest-regex-util@^26.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-resolve-dependencies@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.1.0.tgz#1ce36472f864a5dadf7dc82fa158e1c77955691b" - integrity sha512-fQVEPHHQ1JjHRDxzlLU/buuQ9om+hqW6Vo928aa4b4yvq4ZHBtRSDsLdKQLuCqn5CkTVpYZ7ARh2fbA8WkRE6g== +jest-resolve-dependencies@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.3.0.tgz#98e4a2d17ffa352e6be72a3d180f2260d9d4f473" + integrity sha512-j5rZ2BUh8vVjJZ7bpgCre0t6mbFLm5BWfVhYb1H35A3nbPN3kepzMqkMnKXPhwyLIVwn25uYkv6LHc2/Xa1sGw== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" jest-regex-util "^26.0.0" - jest-snapshot "^26.1.0" + jest-snapshot "^26.3.0" -jest-resolve@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.1.0.tgz#a530eaa302b1f6fa0479079d1561dd69abc00e68" - integrity sha512-KsY1JV9FeVgEmwIISbZZN83RNGJ1CC+XUCikf/ZWJBX/tO4a4NvA21YixokhdR9UnmPKKAC4LafVixJBrwlmfg== +jest-resolve@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.3.0.tgz#c497cded13714b9ec98848837525323184fb4c95" + integrity sha512-+oKVWDkXjdZ4Xciuxv+M5e5v/Z3RLjrKIzen9tq3IO6HpzsLf9Mk3rET5du1uU8iVUCvz4/1PmjzNF50Uc7l2A== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" chalk "^4.0.0" graceful-fs "^4.2.4" - jest-pnp-resolver "^1.2.1" - jest-util "^26.1.0" + jest-pnp-resolver "^1.2.2" + jest-util "^26.3.0" read-pkg-up "^7.0.1" resolve "^1.17.0" slash "^3.0.0" -jest-runner@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.1.0.tgz#457f7fc522afe46ca6db1dccf19f87f500b3288d" - integrity sha512-elvP7y0fVDREnfqit0zAxiXkDRSw6dgCkzPCf1XvIMnSDZ8yogmSKJf192dpOgnUVykmQXwYYJnCx641uLTgcw== +jest-runner@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.3.0.tgz#30093549b31659e64e987728a6ef601f464916b6" + integrity sha512-eiPKgbhTM4q6A7RBh4qzKf6hwFDJMfqoFJubFvWSrHdZUsvSiBWYDqQI+FUXDFxDAOn/AfZjKURACAH3fUDjwA== dependencies: - "@jest/console" "^26.1.0" - "@jest/environment" "^26.1.0" - "@jest/test-result" "^26.1.0" - "@jest/types" "^26.1.0" + "@jest/console" "^26.3.0" + "@jest/environment" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" chalk "^4.0.0" + emittery "^0.7.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-config "^26.1.0" + jest-config "^26.3.0" jest-docblock "^26.0.0" - jest-haste-map "^26.1.0" - jest-jasmine2 "^26.1.0" - jest-leak-detector "^26.1.0" - jest-message-util "^26.1.0" - jest-resolve "^26.1.0" - jest-runtime "^26.1.0" - jest-util "^26.1.0" - jest-worker "^26.1.0" + jest-haste-map "^26.3.0" + jest-leak-detector "^26.3.0" + jest-message-util "^26.3.0" + jest-resolve "^26.3.0" + jest-runtime "^26.3.0" + jest-util "^26.3.0" + jest-worker "^26.3.0" source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.1.0.tgz#45a37af42115f123ed5c51f126c05502da2469cb" - integrity sha512-1qiYN+EZLmG1QV2wdEBRf+Ci8i3VSfIYLF02U18PiUDrMbhfpN/EAMMkJtT02jgJUoaEOpHAIXG6zS3QRMzRmA== - dependencies: - "@jest/console" "^26.1.0" - "@jest/environment" "^26.1.0" - "@jest/fake-timers" "^26.1.0" - "@jest/globals" "^26.1.0" - "@jest/source-map" "^26.1.0" - "@jest/test-result" "^26.1.0" - "@jest/transform" "^26.1.0" - "@jest/types" "^26.1.0" +jest-runtime@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.3.0.tgz#2f2d030b8a3d6c7653cb9c40544d687a1a5c09af" + integrity sha512-cqCz+S76qwZcPnddkLCjuNw9O8/lB+i1odjz2hpvpDogXLp0qSMs+Slh1gBjB5V4feUyBHav/550Mr3FeTdmnA== + dependencies: + "@jest/console" "^26.3.0" + "@jest/environment" "^26.3.0" + "@jest/fake-timers" "^26.3.0" + "@jest/globals" "^26.3.0" + "@jest/source-map" "^26.3.0" + "@jest/test-result" "^26.3.0" + "@jest/transform" "^26.3.0" + "@jest/types" "^26.3.0" "@types/yargs" "^15.0.0" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.4" - jest-config "^26.1.0" - jest-haste-map "^26.1.0" - jest-message-util "^26.1.0" - jest-mock "^26.1.0" + jest-config "^26.3.0" + jest-haste-map "^26.3.0" + jest-message-util "^26.3.0" + jest-mock "^26.3.0" jest-regex-util "^26.0.0" - jest-resolve "^26.1.0" - jest-snapshot "^26.1.0" - jest-util "^26.1.0" - jest-validate "^26.1.0" + jest-resolve "^26.3.0" + jest-snapshot "^26.3.0" + jest-util "^26.3.0" + jest-validate "^26.3.0" slash "^3.0.0" strip-bom "^4.0.0" yargs "^15.3.1" -jest-serializer@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.1.0.tgz#72a394531fc9b08e173dc7d297440ac610d95022" - integrity sha512-eqZOQG/0+MHmr25b2Z86g7+Kzd5dG9dhCiUoyUNJPgiqi38DqbDEOlHcNijyfZoj74soGBohKBZuJFS18YTJ5w== +jest-serializer@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.3.0.tgz#1c9d5e1b74d6e5f7e7f9627080fa205d976c33ef" + integrity sha512-IDRBQBLPlKa4flg77fqg0n/pH87tcRKwe8zxOVTWISxGpPHYkRZ1dXKyh04JOja7gppc60+soKVZ791mruVdow== dependencies: + "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.1.0.tgz#c36ed1e0334bd7bd2fe5ad07e93a364ead7e1349" - integrity sha512-YhSbU7eMTVQO/iRbNs8j0mKRxGp4plo7sJ3GzOQ0IYjvsBiwg0T1o0zGQAYepza7lYHuPTrG5J2yDd0CE2YxSw== +jest-snapshot@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.3.0.tgz#8bec08bda1133ad0a7fa0184b1c385f801e3b1df" + integrity sha512-tHVUIeOTN/0SZN2ZjBZHzPG5txs/6uEQx2mwjxIT7QRE7pddPLd8jktXthyIz6bV+3GKetWXSV4YAoPUQwrfMA== dependencies: "@babel/types" "^7.0.0" - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" "@types/prettier" "^2.0.0" chalk "^4.0.0" - expect "^26.1.0" + expect "^26.3.0" graceful-fs "^4.2.4" - jest-diff "^26.1.0" - jest-get-type "^26.0.0" - jest-haste-map "^26.1.0" - jest-matcher-utils "^26.1.0" - jest-message-util "^26.1.0" - jest-resolve "^26.1.0" + jest-diff "^26.3.0" + jest-get-type "^26.3.0" + jest-haste-map "^26.3.0" + jest-matcher-utils "^26.3.0" + jest-message-util "^26.3.0" + jest-resolve "^26.3.0" natural-compare "^1.4.0" - pretty-format "^26.1.0" + pretty-format "^26.3.0" semver "^7.3.2" -jest-util@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.1.0.tgz#80e85d4ba820decacf41a691c2042d5276e5d8d8" - integrity sha512-rNMOwFQevljfNGvbzNQAxdmXQ+NawW/J72dmddsK0E8vgxXCMtwQ/EH0BiWEIxh0hhMcTsxwAxINt7Lh46Uzbg== +jest-util@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.3.0.tgz#a8974b191df30e2bf523ebbfdbaeb8efca535b3e" + integrity sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" + "@types/node" "*" chalk "^4.0.0" graceful-fs "^4.2.4" is-ci "^2.0.0" micromatch "^4.0.2" -jest-validate@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.1.0.tgz#942c85ad3d60f78250c488a7f85d8f11a29788e7" - integrity sha512-WPApOOnXsiwhZtmkDsxnpye+XLb/tUISP+H6cHjfUIXvlG+eKwP+isnivsxlHCPaO9Q5wvbhloIBkdF3qUn+Nw== +jest-validate@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.3.0.tgz#751c3f8e20a15b9d7ada8d1a361d0975ba793249" + integrity sha512-oIJWqkIdgh1Q1O7ku4kDGkQoFKUOtZyDMbfYs4DsBi6r+FDY37xKTyZ30nM8F6yGZfB72qc7XB+3qKRgokwoXg== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" camelcase "^6.0.0" chalk "^4.0.0" - jest-get-type "^26.0.0" + jest-get-type "^26.3.0" leven "^3.1.0" - pretty-format "^26.1.0" + pretty-format "^26.3.0" -jest-watcher@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.1.0.tgz#99812a0cd931f0cb3d153180426135ab83e4d8f2" - integrity sha512-ffEOhJl2EvAIki613oPsSG11usqnGUzIiK7MMX6hE4422aXOcVEG3ySCTDFLn1+LZNXGPE8tuJxhp8OBJ1pgzQ== +jest-watcher@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.3.0.tgz#f8ef3068ddb8af160ef868400318dc4a898eed08" + integrity sha512-XnLdKmyCGJ3VoF6G/p5ohbJ04q/vv5aH9ENI+i6BL0uu9WWB6Z7Z2lhQQk0d2AVZcRGp1yW+/TsoToMhBFPRdQ== dependencies: - "@jest/test-result" "^26.1.0" - "@jest/types" "^26.1.0" + "@jest/test-result" "^26.3.0" + "@jest/types" "^26.3.0" + "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^26.1.0" + jest-util "^26.3.0" string-length "^4.0.1" jest-when@^2.7.2: @@ -5174,22 +5414,23 @@ jest-when@^2.7.2: bunyan "^1.8.12" expect "^24.8.0" -jest-worker@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.1.0.tgz#65d5641af74e08ccd561c240e7db61284f82f33d" - integrity sha512-Z9P5pZ6UC+kakMbNJn+tA2RdVdNX5WH1x+5UCBZ9MxIK24pjYtFt96fK+UwBTrjLYm232g1xz0L3eTh51OW+yQ== +jest-worker@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" + integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== dependencies: + "@types/node" "*" merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^26.0.1: - version "26.1.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-26.1.0.tgz#2f3aa7bcffb9bfd025473f83bbbf46a3af026263" - integrity sha512-LIti8jppw5BcQvmNJe4w2g1N/3V68HUfAv9zDVm7v+VAtQulGhH0LnmmiVkbNE4M4I43Bj2fXPiBGKt26k9tHw== +jest@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.3.0.tgz#366e25827831e65743a324bc476de54f41f2e07b" + integrity sha512-LFCry7NS6bTa4BUGUHC+NvZ3B9WG7Jv8F+Lb96dAJFM23LMwSsL5RiJcw9S+nejsh8lS1VxHq+RSH4Xa9tujpA== dependencies: - "@jest/core" "^26.1.0" + "@jest/core" "^26.3.0" import-local "^3.0.2" - jest-cli "^26.1.0" + jest-cli "^26.3.0" jquery@^3.5.0: version "3.5.1" @@ -5226,10 +5467,10 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsdoc@^3.6.3: - version "3.6.4" - resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.6.4.tgz#246b2832a0ea8b37a441b61745509cfe29e174b6" - integrity sha512-3G9d37VHv7MFdheviDCjUfQoIjdv4TC5zTTf5G9VODLtOnVS6La1eoYBDlbWfsRT3/Xo+j2MIqki2EV12BZfwA== +jsdoc@^3.6.5: + version "3.6.5" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.6.5.tgz#e004372ca6f2dcdf19b3d2ebcd7c725528485502" + integrity sha512-SbY+i9ONuxSK35cgVHaI8O9senTE4CDYAmGSDJ5l3+sfe62Ff4gy96osy6OW84t4K4A8iGnMrlRrsSItSNp3RQ== dependencies: "@babel/parser" "^7.9.4" bluebird "^3.7.2" @@ -5288,10 +5529,10 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-api-normalizer@^0.4.16: - version "0.4.16" - resolved "https://registry.yarnpkg.com/json-api-normalizer/-/json-api-normalizer-0.4.16.tgz#43fd741e346ec2d934cd00f0a9611e651a20533f" - integrity sha512-ZWhN23gmauEjLXdV2Gdm7PhoGy67zCyO7hIhrhrRMEqzfCYm1U4fexNZHXqyRYDeU/ougwhW12ImEz3s907cuA== +json-api-normalizer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-api-normalizer/-/json-api-normalizer-1.0.0.tgz#183c53017aa8f8cfd96dbed45227bb4790bbfd04" + integrity sha512-usS3CbKJliWLVhARZF4sUlalLVetITDeGZghV9lpBbP25Gm8xx0GUmmMXM0aimCGb9ntva+1gvTbQ/MekcMucA== dependencies: lodash "^4.17.15" @@ -5370,7 +5611,7 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsx-ast-utils@^2.2.3, jsx-ast-utils@^2.4.1: +jsx-ast-utils@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e" integrity sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w== @@ -5419,6 +5660,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +klona@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/klona/-/klona-1.1.2.tgz#a79e292518a5a5412ec8d097964bff1571a64db0" + integrity sha512-xf88rTeHiXk+XE2Vhi6yj8Wm3gMZrygGdKjJqN8HkV+PwF/t50/LdAKHoHpPcxFAlmQszTZ1CugrK25S7qDRLA== + language-subtag-registry@~0.3.2: version "0.3.20" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz#a00a37121894f224f763268e431c55556b0c0755" @@ -5592,6 +5838,11 @@ lodash@^4.0.0, lodash@^4.15.0, lodash@^4.16.3, lodash@^4.17.11, lodash@^4.17.13, resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.17.19: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + lolex@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" @@ -5835,10 +6086,10 @@ mini-create-react-context@^0.4.0: "@babel/runtime" "^7.5.5" tiny-warning "^1.0.3" -mini-css-extract-plugin@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" - integrity sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A== +mini-css-extract-plugin@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.10.0.tgz#a0e6bfcad22a9c73f6c882a3c7557a98e2d3d27d" + integrity sha512-QgKgJBjaJhxVPwrLNqqwNS0AGkuQQ31Hp4xGXEK/P7wehEg6qmNtReHKai3zRXqY60wGVWLYcOMJK2b98aGc3A== dependencies: loader-utils "^1.1.0" normalize-url "1.9.1" @@ -6009,6 +6260,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -6254,7 +6510,7 @@ object-inspect@^1.7.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== -object-is@^1.0.1, object-is@^1.0.2: +object-is@^1.0.2, object-is@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== @@ -6284,7 +6540,7 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2: +object.entries@^1.1.1, object.entries@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== @@ -6698,15 +6954,15 @@ postcss-modules-extract-imports@^2.0.0: dependencies: postcss "^7.0.5" -postcss-modules-local-by-default@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915" - integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ== +postcss-modules-local-by-default@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== dependencies: icss-utils "^4.1.1" - postcss "^7.0.16" + postcss "^7.0.32" postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.0.0" + postcss-value-parser "^4.1.0" postcss-modules-scope@^2.2.0: version "2.2.0" @@ -6733,12 +6989,12 @@ postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: +postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: version "7.0.32" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -6772,12 +7028,12 @@ pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" -pretty-format@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.1.0.tgz#272b9cd1f1a924ab5d443dc224899d7a65cb96ec" - integrity sha512-GmeO1PEYdM+non4BKCj+XsPJjFOJIPnsLewqhDVoqY1xo0yNmDas7tC2XwpMrRAHR3MaE2hPo37deX5OisJ2Wg== +pretty-format@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.3.0.tgz#d9a7b4bb2948cabc646e6a7729b12f686f3fed36" + integrity sha512-24kRw4C2Ok8+SHquydTZZCZPF2fvANI7rChGs8sNu784+1Jkq5jVFMvNAJSLuLy6XUcP3Fnw+SscLIQag/CG8Q== dependencies: - "@jest/types" "^26.1.0" + "@jest/types" "^26.3.0" ansi-regex "^5.0.0" ansi-styles "^4.0.0" react-is "^16.12.0" @@ -6987,7 +7243,7 @@ react-dom@^16.13.1: prop-types "^15.6.2" scheduler "^0.19.1" -react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -7021,10 +7277,10 @@ react-modal@3.11.2: react-lifecycles-compat "^3.0.0" warning "^4.0.3" -react-redux@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" - integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== +react-redux@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985" + integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg== dependencies: "@babel/runtime" "^7.5.5" hoist-non-react-statics "^3.3.0" @@ -7061,13 +7317,13 @@ react-router@5.2.0, react-router@^5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-svg@^11.0.26: - version "11.0.27" - resolved "https://registry.yarnpkg.com/react-svg/-/react-svg-11.0.27.tgz#c594546969135b4b6d7014e6594deb8138d31971" - integrity sha512-rrNTyHmQexOw62I+cxS7NU09L9ardllnFMYQOIdnP3LZl9k/ZXCsHjHaoGGrUEb0SutMztlPWeozc5MaN0L44Q== +react-svg@^11.0.34: + version "11.0.34" + resolved "https://registry.yarnpkg.com/react-svg/-/react-svg-11.0.34.tgz#c122bd5dba673b9518fab3864ae2312a4e63212d" + integrity sha512-Cn2DcxQCmVPxrk71QW9B1rShYu6/oqDKhaj8qTPbvk1e5y0hsw8xcWbuCwv2OG4Of6nWOGI5fP6rcSx91KN/FQ== dependencies: - "@babel/runtime" "^7.10.2" - "@tanem/svg-injector" "^8.0.54" + "@babel/runtime" "^7.11.0" + "@tanem/svg-injector" "^8.0.61" prop-types "^15.7.2" react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.1: @@ -7442,7 +7698,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.17.0, resolve@^1.3.2: +resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -7586,16 +7842,16 @@ sass-graph@2.2.5: scss-tokenizer "^0.2.3" yargs "^13.3.2" -sass-loader@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" - integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== +sass-loader@^9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-9.0.3.tgz#086adcf0bfdcc9d920413e2cdc3ba3321373d547" + integrity sha512-fOwsP98ac1VMme+V3+o0HaaMHp8Q/C9P+MUazLFVi3Jl7ORGHQXL1XeRZt3zLSGZQQPC8xE42Y2WptItvGjDQg== dependencies: - clone-deep "^4.0.1" - loader-utils "^1.2.3" - neo-async "^2.6.1" - schema-utils "^2.6.1" - semver "^6.3.0" + klona "^1.1.2" + loader-utils "^2.0.0" + neo-async "^2.6.2" + schema-utils "^2.7.0" + semver "^7.3.2" sax@^1.1.3, sax@^1.2.4: version "1.2.4" @@ -7626,7 +7882,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.7.0: +schema-utils@^2.6.5, schema-utils@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== @@ -7703,13 +7959,6 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - shallowequal@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" @@ -8848,10 +9097,10 @@ v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== -v8-to-istanbul@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz#b97936f21c0e2d9996d4985e5c5156e9d4e49cd6" - integrity sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ== +v8-to-istanbul@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-5.0.1.tgz#0608f5b49a481458625edb058488607f25498ba5" + integrity sha512-mbDNjuDajqYe3TXFk5qxcQy8L1msXNE37WTlLoqqpBfRsimbNcrlhQlDPntmECEcUvdC+AQ8CyMMf6EUx1r74Q== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" @@ -8949,15 +9198,15 @@ watchpack-chokidar2@^2.0.0: dependencies: chokidar "^2.1.8" -watchpack@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa" - integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g== +watchpack@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" + integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== dependencies: graceful-fs "^4.1.2" neo-async "^2.5.0" optionalDependencies: - chokidar "^3.4.0" + chokidar "^3.4.1" watchpack-chokidar2 "^2.0.0" webextension-polyfill-ts@^0.17.0: @@ -9012,10 +9261,10 @@ webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.43.0: - version "4.43.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" - integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== +webpack@^4.44.1: + version "4.44.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" + integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" @@ -9025,7 +9274,7 @@ webpack@^4.43.0: ajv "^6.10.2" ajv-keywords "^3.4.1" chrome-trace-event "^1.0.2" - enhanced-resolve "^4.1.0" + enhanced-resolve "^4.3.0" eslint-scope "^4.0.3" json-parse-better-errors "^1.0.2" loader-runner "^2.4.0" @@ -9038,7 +9287,7 @@ webpack@^4.43.0: schema-utils "^1.0.0" tapable "^1.1.3" terser-webpack-plugin "^1.4.3" - watchpack "^1.6.1" + watchpack "^1.7.4" webpack-sources "^1.4.1" whatwg-encoding@^1.0.5: @@ -9170,13 +9419,6 @@ xmlcreate@^2.0.3: resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.3.tgz#df9ecd518fd3890ab3548e1b811d040614993497" integrity sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ== -xregexp@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" - integrity sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g== - dependencies: - "@babel/runtime-corejs3" "^7.8.3" - xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From 7e0a3c27bc8a29e7cd46809e279afb463cfb6867 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Fri, 14 Aug 2020 12:36:41 -0400 Subject: [PATCH 07/35] GH-2086, GH-2084, GH-2040: Update intro hub Get Plus, Midnight & Insights Modal CTA link (#595) * Update Intro Hub Get Plus to link to plus description page, and Ghostery Midnight Modal CTA to link to landing page * Use ghostery root domain from globals * Use checkout base URL * Add GHOSTERY_WEBSITE_BASE_URL to globals and use in PromoModal.jsx * Add / * Use GHOSTERY_WEBSITE_BASE_URL in insights CTA * Remove and replace GHOSTERY_WEBSITE_BASE_URL with GHOSTERY_BASE_URL --- app/shared-components/PromoModal/PromoModal.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/shared-components/PromoModal/PromoModal.jsx b/app/shared-components/PromoModal/PromoModal.jsx index 45ac1bbc5..eb98a4d51 100644 --- a/app/shared-components/PromoModal/PromoModal.jsx +++ b/app/shared-components/PromoModal/PromoModal.jsx @@ -23,7 +23,7 @@ import { sendMessage } from '../../panel/utils/msg'; import globals from '../../../src/classes/Globals'; import ModalExitButton from '../../panel/components/BuildingBlocks/ModalExitButton'; -const DOMAIN = globals.DEBUG ? 'ghosterystage' : 'ghostery'; +const { GHOSTERY_BASE_URL } = globals; const INSIGHTS = 'insights'; const PLUS = 'plus'; const PREMIUM = 'premium'; @@ -69,14 +69,14 @@ class PromoModal extends React.Component { let url; switch (product) { case PLUS: - url = `https://checkout.${DOMAIN}.com/plus?utm_source=gbe&utm_campaign=${utm_campaign}`; + url = `${GHOSTERY_BASE_URL}/products/plus?utm_source=gbe&utm_campaign=${utm_campaign}`; break; case PREMIUM: - url = `https://ghostery.com/thanks-for-downloading-midnight?utm_source=gbe&utm_campaign=${utm_campaign}`; + url = `${GHOSTERY_BASE_URL}/midnight?utm_source=gbe&utm_campaign=${utm_campaign}`; break; case INSIGHTS: sendMessage('ping', 'promo_modals_insights_upgrade_cta'); - url = `https://checkout.${DOMAIN}.com/insights?utm_source=gbe&utm_campaign=${utm_campaign}`; + url = `${GHOSTERY_BASE_URL}/insights/?utm_source=gbe&utm_campaign=${utm_campaign}`; break; default: } From 124d633ff242811b284a9df896717b28f792627a Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Mon, 17 Aug 2020 10:53:57 -0400 Subject: [PATCH 08/35] GH-2035: Update In-App Plus/Premium pricing (#596) * Update in app plus/premium pricing * Revert to 7-day free trial string * Fix linter * Update snapshot Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- _locales/en/messages.json | 6 +++--- app/hub/Views/PlusView/PlusView.jsx | 4 +--- .../__snapshots__/PlusView.test.jsx.snap | 18 ++++++++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index acd289e62..6447ba6c6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1729,7 +1729,7 @@ "message": "Resolve issues fast with our Priority help desk service - and more perks to come" }, "hub_supporter_manifesto": { - "message": "We strive to deliver the best privacy protection services to our users free of cost. While we do not charge for our browser extension, you may choose to support us through a small monthly subscription. Join us in our mission by upgrading to Ghostery Plus - and unlock cool perks along the way!" + "message": "We strive to deliver the best privacy protection services to our users free of cost. While we do not charge for our browser extension, you may choose to support us through a monthly subscription of $4.99. Join us in our mission by upgrading to Ghostery Plus - and unlock cool perks along the way!" }, "hub_supporter_feature_theme_description": { "message": "Customize the Ghostery colors for a new visual experience! Introduced through popular request. Check out our special Dark Blue theme, and more to come." @@ -1944,7 +1944,7 @@ "message": "Themes" }, "subscribe_pitch": { - "message": "While Ghostery is free, you can choose to support us through a small monthly subscription in exchange for special perks like color themes, personal tracking statistics, and more. Join our mission and subscribe!" + "message": "While Ghostery is free, you can choose to support us through a monthly subscription of $4.99 in exchange for special perks, like color themes, personal tracking statistics, and more. Join our mission and subscribe!" }, "subscribe_pitch_spring": { "message": "Like what we do? Support us and unlock new spring themes, personal tracking insights, and other special perks by upgrading to Ghostery Plus!" @@ -2513,7 +2513,7 @@ "message": "Try Ghostery Midnight" }, "seven_day_free_trial": { - "message": "7 Day Free Trial" + "message": "7-day free trial" }, "spring_is_here": { "message": "Spring is here!" diff --git a/app/hub/Views/PlusView/PlusView.jsx b/app/hub/Views/PlusView/PlusView.jsx index d44dbab01..7026a643a 100644 --- a/app/hub/Views/PlusView/PlusView.jsx +++ b/app/hub/Views/PlusView/PlusView.jsx @@ -97,9 +97,7 @@ class PlusView extends Component {
    -
    - {t('hub_supporter_manifesto')} -
    +
    diff --git a/app/hub/Views/PlusView/__tests__/__snapshots__/PlusView.test.jsx.snap b/app/hub/Views/PlusView/__tests__/__snapshots__/PlusView.test.jsx.snap index 2b10d624e..4bce4e092 100644 --- a/app/hub/Views/PlusView/__tests__/__snapshots__/PlusView.test.jsx.snap +++ b/app/hub/Views/PlusView/__tests__/__snapshots__/PlusView.test.jsx.snap @@ -97,9 +97,12 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere >
    - hub_supporter_manifesto -
    + dangerouslySetInnerHTML={ + Object { + "__html": "hub_supporter_manifesto", + } + } + />
    - hub_supporter_manifesto -
    + dangerouslySetInnerHTML={ + Object { + "__html": "hub_supporter_manifesto", + } + } + />
    Date: Mon, 17 Aug 2020 17:19:06 +0200 Subject: [PATCH 09/35] Remove obsolete @cliqz/adblocker-circumvention dependency (#598) Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- app/content-scripts/content_script_bundle.js | 3 --- package.json | 1 - yarn.lock | 9 +-------- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/app/content-scripts/content_script_bundle.js b/app/content-scripts/content_script_bundle.js index 3e6f98767..256377de6 100644 --- a/app/content-scripts/content_script_bundle.js +++ b/app/content-scripts/content_script_bundle.js @@ -19,6 +19,3 @@ */ import 'browser-core/build/core/content-script'; -import injectCircumvention from '@cliqz/adblocker-circumvention'; - -injectCircumvention(window); diff --git a/package.json b/package.json index 195f06192..ddb0466f1 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ }, "homepage": "https://github.com/ghostery/ghostery-extension#readme", "dependencies": { - "@cliqz/adblocker-circumvention": "^1.12.2", "@cliqz/url-parser": "^1.1.3", "browser-core": "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.2/browser-core-7.47.2.tgz", "classnames": "^2.2.5", diff --git a/yarn.lock b/yarn.lock index 39b9263f6..c32f32e44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -625,14 +625,7 @@ resolved "https://registry.yarnpkg.com/@cliqz-oss/dexie/-/dexie-2.0.4.tgz#0e710504e2b9198baa9b046abd3a82731b94d56e" integrity sha512-HxMbBQfdy0CehThTFierXbRPI+PHDEucUUriCCzViAKbCWWQIlL6uZcyDaaPRMPWy45v78lezPB4457kfjS72g== -"@cliqz/adblocker-circumvention@^1.12.2": - version "1.12.2" - resolved "https://registry.yarnpkg.com/@cliqz/adblocker-circumvention/-/adblocker-circumvention-1.12.2.tgz#9e5bc986d6c8e5919901faa0ae241e9df9019d5e" - integrity sha512-O/CIQ2XDzYuJx0ld+H5qqJNIz37VloyPfzSDifSF+xPUoMQ4trofUUxy7UL0co6VOARLoDAv8jk+DTvFKSsVWw== - dependencies: - "@cliqz/adblocker-content" "^1.12.2" - -"@cliqz/adblocker-content@^1.12.2", "@cliqz/adblocker-content@^1.16.0": +"@cliqz/adblocker-content@^1.16.0": version "1.16.0" resolved "https://registry.yarnpkg.com/@cliqz/adblocker-content/-/adblocker-content-1.16.0.tgz#a20ab15af6495354b8c83b69e6914b4e8ec0db7d" integrity sha512-UnT3xxWCqNpyhQsKsf7q+qZtXP468i+H00avTbsgKrDJcjaupS9rSEesP/2pDXmHn12vcEQtlA/T5GA3ZD9Sgw== From efccf8ca8bbe0ca6994edcbdd00cf04cebb59279 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Tue, 25 Aug 2020 10:09:02 -0400 Subject: [PATCH 10/35] Fix bug where content overflows past the specified panel height in russian and simplified chinese (#599) --- app/scss/partials/_subscribe.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scss/partials/_subscribe.scss b/app/scss/partials/_subscribe.scss index 59a440ca5..0c26983df 100644 --- a/app/scss/partials/_subscribe.scss +++ b/app/scss/partials/_subscribe.scss @@ -14,7 +14,7 @@ .content-subscription { .badge { display: block; - margin: 35px auto 35px auto; + margin: 15px auto; width: 137px; height: 97px; background-image: url('/app/images/panel/subscribe-badge.svg'); From fde426ce894ca5e2afaba9c4eacbb931712824bf Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Tue, 25 Aug 2020 18:18:27 -0400 Subject: [PATCH 11/35] Shorten uninstall url by shorterning subscription_interval param name to si (#600) Co-authored-by: Frank Chiarulli Jr --- src/classes/Metrics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 9b43ef305..2ebf6d2e6 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -372,7 +372,7 @@ class Metrics { // Hub Promo variant `&hp=${encodeURIComponent(Metrics._getHubPromoVariant().toString())}` + // Subscription Interval - `&subscription_interval=${encodeURIComponent(Metrics._getSubscriptionInterval().toString())}` + + `&si=${encodeURIComponent(Metrics._getSubscriptionInterval().toString())}` + // Product ID Parameter `&pi=${encodeURIComponent('gbe')}`; From 1719bc7a8e32b7da19127eaa5a28ec16b6336a38 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Tue, 8 Sep 2020 13:24:22 -0400 Subject: [PATCH 12/35] bump browser core --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ddb0466f1..0d326aa35 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "homepage": "https://github.com/ghostery/ghostery-extension#readme", "dependencies": { "@cliqz/url-parser": "^1.1.3", - "browser-core": "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.2/browser-core-7.47.2.tgz", + "browser-core": "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.3/browser-core-7.47.3.tgz", "classnames": "^2.2.5", "d3": "^5.16.0", "foundation-sites": "^6.6.2", diff --git a/yarn.lock b/yarn.lock index c32f32e44..63b9ee4c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1959,9 +1959,9 @@ brorand@^1.0.1: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= -"browser-core@https://github.com/cliqz-oss/browser-core/releases/download/v7.47.2/browser-core-7.47.2.tgz": - version "7.47.2" - resolved "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.2/browser-core-7.47.2.tgz#cdd67904a05d06e0fec314dc1f758602ad56085e" +"browser-core@https://github.com/cliqz-oss/browser-core/releases/download/v7.47.3/browser-core-7.47.3.tgz": + version "7.47.3" + resolved "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.3/browser-core-7.47.3.tgz#83577a371c777bf4418f591f3f1ce868e546f7a5" dependencies: "@cliqz-oss/dexie" "^2.0.4" "@cliqz/adblocker-webextension" "^1.14.2" From 8c3c62b32a289d511a98b6565fba295461fd0e6d Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Fri, 11 Sep 2020 12:56:02 -0400 Subject: [PATCH 13/35] update CODEOWNERS --- CODEOWNERS | 22 +++++++++++----------- manifest.json | 7 +++---- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 63a84492d..0ec6fd468 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,23 +3,23 @@ # the last matching pattern has the most precedence. # Core Ghostery team -* @ghostery/ghostery +* @ghostery/extension # CMP / Metrics / WebRequest -/src/classes/ABTest.js @jsignanini -/src/classes/CMP.js @jsignanini -/src/classes/EventHandlers.js @jsignanini -/src/classes/Metrics.js @jsignanini -/src/classes/PolicySmartBlock.js @jsignanini -/src/classes/Conf.js @zarembsky -/src/classes/ConfData.js @zarembsky +/src/classes/ABTest.js @wlycdgr +/src/classes/CMP.js @christophertino +/src/classes/EventHandlers.js @christophertino +/src/classes/Metrics.js @wlycdgr +/src/classes/PolicySmartBlock.js @christophertino +/src/classes/Conf.js @christophertino +/src/classes/ConfData.js @christophertino /src/classes/PanelData.js @wlycdgr # Background -/src/background.js @zarembsky +/src/background.js @christophertino # The Ghostery Hub -/app/hub/ @Eden12345 +/app/hub/ @benstrumeyer # Shared Components /app/shared-components @wlycdgr @@ -30,4 +30,4 @@ babel.config.js @christophertino webpack.config.js @christophertino # Unit Tests -/test/ @Eden12345 +/test/ @christophertino diff --git a/manifest.json b/manifest.json index 8a00a9832..0cc3b9ee0 100644 --- a/manifest.json +++ b/manifest.json @@ -3,11 +3,10 @@ "author": "Ghostery", "name": "__MSG_name__", "short_name": "Ghostery", - "version": "8.5.2", - "version_name": "8.5.2", + "version": "8.5.3", + "version_name": "8.5.3", "default_locale": "en", "description": "__MSG_short_description__", - "debug": true, "log": true, "icons": { "16": "app/images/icon16.png", @@ -116,4 +115,4 @@ "cliqz/offers-templates/checkout.html", "cliqz/offers-templates/control-center.html" ] -} \ No newline at end of file +} From 0769c492fa82521f9cd29eb4f0cc9ff39f6b18c1 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Sun, 13 Sep 2020 11:39:56 -0400 Subject: [PATCH 14/35] bump browser core --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 0d326aa35..cf1b3077d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "homepage": "https://github.com/ghostery/ghostery-extension#readme", "dependencies": { "@cliqz/url-parser": "^1.1.3", - "browser-core": "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.3/browser-core-7.47.3.tgz", + "browser-core": "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.4/browser-core-7.47.4.tgz", "classnames": "^2.2.5", "d3": "^5.16.0", "foundation-sites": "^6.6.2", diff --git a/yarn.lock b/yarn.lock index 63b9ee4c4..df78e298e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1959,9 +1959,9 @@ brorand@^1.0.1: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= -"browser-core@https://github.com/cliqz-oss/browser-core/releases/download/v7.47.3/browser-core-7.47.3.tgz": - version "7.47.3" - resolved "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.3/browser-core-7.47.3.tgz#83577a371c777bf4418f591f3f1ce868e546f7a5" +"browser-core@https://github.com/cliqz-oss/browser-core/releases/download/v7.47.4/browser-core-7.47.4.tgz": + version "7.47.4" + resolved "https://github.com/cliqz-oss/browser-core/releases/download/v7.47.4/browser-core-7.47.4.tgz#e47d9c8ab20112361d5a65b4dcff2ebfb88fd77f" dependencies: "@cliqz-oss/dexie" "^2.0.4" "@cliqz/adblocker-webextension" "^1.14.2" From 387f037bdf8da4928cff055cb63ae7dfaff7b208 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Mon, 21 Sep 2020 10:11:43 -0400 Subject: [PATCH 15/35] Return early from setupHubPromoABTest if A/B tests have not yet been fetched to avoid misinterpreting absense of 'hub_plain' or 'hub_midnight' (#601) Co-authored-by: Frank Chiarulli Jr --- src/background.js | 7 ++++++- src/classes/ABTest.js | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/background.js b/src/background.js index f17467dfd..4df749520 100644 --- a/src/background.js +++ b/src/background.js @@ -1118,7 +1118,12 @@ function getAntitrackingTestConfig() { * @return {Object} Hub promotion configuration parameters */ function setupHubPromoABTest() { - if (conf.hub_promo_variant !== 'not_yet_set') return; + if ( + !abtest.hasBeenFetched + || conf.hub_promo_variant !== 'not_yet_set' + ) { + return; + } if (abtest.hasTest('hub_plain')) { conf.hub_promo_variant = 'plain'; diff --git a/src/classes/ABTest.js b/src/classes/ABTest.js index df7a235fb..1236310ce 100644 --- a/src/classes/ABTest.js +++ b/src/classes/ABTest.js @@ -27,6 +27,7 @@ const { BROWSER_INFO, CMP_BASE_URL, EXTENSION_VERSION } = globals; class ABTest { constructor() { this.tests = {}; + this.hasBeenFetched = false; } /** @@ -63,6 +64,7 @@ class ABTest { (tests, test) => Object.assign(tests, { [test.name]: test.data }), {} ); + this.hasBeenFetched = true; } else { log('A/B Tests: no tests found.'); } From bdf6206be4bcdf42392ac684c09a512f17b4a1f4 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Mon, 21 Sep 2020 16:22:59 +0200 Subject: [PATCH 16/35] Detect Ghostery Desktop browser (#602) Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- src/classes/Globals.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/classes/Globals.js b/src/classes/Globals.js index 9aed8fa14..bbbbe3477 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -178,6 +178,11 @@ class Globals { this.BROWSER_INFO.displayName = 'Yandex'; this.BROWSER_INFO.name = 'yandex'; this.BROWSER_INFO.token = 'yx'; + } else if (navigator.userAgent.includes('Ghostery')) { + // ua-parser library doesn't parse the desktop browser UA properly + this.BROWSER_INFO.displayName = 'Ghostery Desktop Browser'; + this.BROWSER_INFO.name = 'ghostery_desktop'; + this.BROWSER_INFO.token = 'gd'; } // Set OS property @@ -195,7 +200,9 @@ class Globals { this.BROWSER_INFO.version = version; // Check for the Ghostery Android browser - this._checkForGhosteryAndroid(); + if (platform.includes('android')) { + this._checkForGhosteryAndroid(); + } } /** From 30a0d04828401e2ba67734226cd556052e2d04aa Mon Sep 17 00:00:00 2001 From: Frank Chiarulli Jr Date: Mon, 21 Sep 2020 10:46:29 -0400 Subject: [PATCH 17/35] GH-2171: Add Missing Page Title to Upgrade Plan Hub Page (#604) * add title to hub * add title to upgrade pane * only set title once Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- _locales/en/messages.json | 3 +++ app/hub/Views/UpgradePlanView/UpgradePlanView.jsx | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6447ba6c6..673b8fc22 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1351,6 +1351,9 @@ "hub_home_plus_full_protection": { "message": "You are fully protected!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - Upgrade Plan" + }, "hub_upgrade_your": { "message": "Upgrade your" }, diff --git a/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx b/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx index d3515c418..3a3608965 100644 --- a/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx +++ b/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx @@ -11,7 +11,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import React, { useRef, Fragment } from 'react'; +import React, { Fragment, useRef, useEffect } from 'react'; import ClassNames from 'classnames'; import PropTypes from 'prop-types'; import { NavLink } from 'react-router-dom'; @@ -119,6 +119,11 @@ const premiumAlreadyProtectedButton = () => ( * @memberof HubComponents */ const UpgradePlanView = (props) => { + useEffect(() => { + const title = t('hub_upgrade_page_title'); + window.document.title = title; + }, []); + const { protection_level, show_yearly_prices, From 34c6223b986efd070a96c9759e1f98d396b53134 Mon Sep 17 00:00:00 2001 From: Leury Rodriguez Date: Mon, 21 Sep 2020 10:55:03 -0400 Subject: [PATCH 18/35] GH-2168: Change hub logo link (#605) * Link to Ghostery site from logo in Hub * Update component snapshot Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- .../SideNavigationView/SideNavigationView.jsx | 11 ++++++- .../SideNavigationView.test.jsx.snap | 30 +++++++++---------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/hub/Views/SideNavigationView/SideNavigationView.jsx b/app/hub/Views/SideNavigationView/SideNavigationView.jsx index 525fb7af2..b0aa74d8d 100644 --- a/app/hub/Views/SideNavigationView/SideNavigationView.jsx +++ b/app/hub/Views/SideNavigationView/SideNavigationView.jsx @@ -15,6 +15,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; import { NavLink } from 'react-router-dom'; +import globals from '../../../../src/classes/Globals'; + +const { GHOSTERY_BASE_URL } = globals; /** * Helper render function for rendering a list item for the Navigation Main section @@ -96,7 +99,13 @@ const SideNavigationView = (props) => { return (
    - +
    {menuItems.map(item => _renderMenuItem(item, disableNav))}
    diff --git a/app/hub/Views/SideNavigationView/__tests__/__snapshots__/SideNavigationView.test.jsx.snap b/app/hub/Views/SideNavigationView/__tests__/__snapshots__/SideNavigationView.test.jsx.snap index 09a5d9f7f..a53d7c338 100644 --- a/app/hub/Views/SideNavigationView/__tests__/__snapshots__/SideNavigationView.test.jsx.snap +++ b/app/hub/Views/SideNavigationView/__tests__/__snapshots__/SideNavigationView.test.jsx.snap @@ -5,11 +5,11 @@ exports[`app/hub/Views/SideNavigationView component More Snapshot tests with rea className="SideNavigation flex-container flex-dir-column" >
    Date: Mon, 21 Sep 2020 11:10:57 -0400 Subject: [PATCH 19/35] update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1dbf74764..8791845eb 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,8 @@ See [CONTRIBUTING](CONTRIBUTING.md) and [CODE OF CONDUCT](CODE-OF-CONDUCT.md) ## Additional Open Source Ghostery Projects + [Ghostery Lite for Safari](https://github.com/ghostery/GhosterySafari) + [Ghostery iOS Browser](https://github.com/ghostery/user-agent-ios) -+ [Ghostery Android Browser](https://github.com/ghostery/browser-android) ++ [Ghostery Android Browser](https://github.com/ghostery/user-agent-android) ++ [Ghostery Desktop Browser](https://github.com/ghostery/user-agent-desktop) ## Ghostery Team Ghostery relies on [contributions](https://github.com/ghostery/ghostery-extension/graphs/contributors) from lots of talented people. From 4e0560bd9faa39e558437f08f6ad62f10fc2d6a4 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Mon, 21 Sep 2020 11:14:11 -0400 Subject: [PATCH 20/35] prune dead links from readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8791845eb..2658849e5 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ $ node tools/transifex.js ``` ## Cliqz Source Code -Ghostery implements the following open-source products from [Cliqz](https://cliqz.com/en/) +Ghostery implements the following open-source products from Cliqz: [**Human Web**](https://cliqz.com/en/whycliqz/human-web) + [How it works](https://cliqz.com/en/magazine/techblog-human-web-reliably-removes-uids) From f1c89417e2a75e198af9f2a14e56d1edc5b4a202 Mon Sep 17 00:00:00 2001 From: Leury Rodriguez Date: Tue, 22 Sep 2020 10:46:01 -0400 Subject: [PATCH 21/35] GH-2100, GH-2097: Onboarding test 2 (#603) * Implement A/B test for hub with modified tabs * Remove promo variant test, repurpose ping param * Make comments and ternaries more clear * Change param for A/B test Co-authored-by: wlycdgr --- .../SideNavigationView/SideNavigationView.jsx | 8 +++++- .../SideNavigationViewContainer.jsx | 10 ++++++- app/hub/index.jsx | 6 +++- manifest.json | 3 +- src/background.js | 28 ++++++++----------- src/classes/ConfData.js | 2 +- src/classes/Metrics.js | 20 ++++++------- 7 files changed, 44 insertions(+), 33 deletions(-) diff --git a/app/hub/Views/SideNavigationView/SideNavigationView.jsx b/app/hub/Views/SideNavigationView/SideNavigationView.jsx index b0aa74d8d..1e26d995c 100644 --- a/app/hub/Views/SideNavigationView/SideNavigationView.jsx +++ b/app/hub/Views/SideNavigationView/SideNavigationView.jsx @@ -15,8 +15,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; import { NavLink } from 'react-router-dom'; +import QueryString from 'query-string'; import globals from '../../../../src/classes/Globals'; +// Flag to display alternate hub view (used for A/B testing ticket GH-2097) +const ah = (QueryString.parse(window.location.search).ah === 'true') || false; + const { GHOSTERY_BASE_URL } = globals; /** @@ -97,6 +101,8 @@ const SideNavigationView = (props) => { disabled: disableNav, }); + const menuClassNames = ClassNames(`SideNavigation__menu ${ah ? '' : 'flex-child-grow'} flex-container flex-dir-column`); + return (
    { target="_blank" className={topClassNames} /> -
    +
    {menuItems.map(item => _renderMenuItem(item, disableNav))}
    diff --git a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx index 644f773b1..6ba4f00fa 100644 --- a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx +++ b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx @@ -13,12 +13,16 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import QueryString from 'query-string'; import SideNavigationView from './SideNavigationView'; import globals from '../../../../src/classes/Globals'; const { IS_CLIQZ, BROWSER_INFO } = globals; const IS_ANDROID = (BROWSER_INFO.os === 'android'); +// Flag to display alternate hub view (used for A/B testing ticket GH-2097) +const ah = (QueryString.parse(window.location.search).ah === 'true') || false; + /** * @class Implement the Side Navigation View for the Ghostery Hub * @extends Component @@ -50,7 +54,10 @@ class SideNavigationViewContainer extends Component { const { user, location } = this.props; const disableRegEx = /^(\/setup(?!\/4$))|(\/tutorial(?!\/6$))/; - const menuItems = [ + const menuItems = ah ? [ + { href: '/home', icon: 'home', text: t('hub_side_navigation_home') }, + { href: '/setup', icon: 'setup', text: t('customize_setup') }, + ] : [ { href: '/home', icon: 'home', text: t('hub_side_navigation_home') }, { href: '/', icon: 'shield', text: t('hub_side_navigation_upgrade_plan') }, { href: '/setup', icon: 'setup', text: t('customize_setup') }, @@ -59,6 +66,7 @@ class SideNavigationViewContainer extends Component { ...((IS_CLIQZ || IS_ANDROID) ? [] : [{ href: '/rewards', icon: 'rewards', text: t('hub_side_navigation_rewards') }]), ...((IS_ANDROID) ? [] : [{ href: '/products', icon: 'products', text: t('hub_side_navigation_products') }]) ]; + const bottomItems = user ? [ { id: 'email', href: `${globals.ACCOUNT_BASE_URL}/`, text: user.email }, { id: 'logout', text: t('sign_out'), clickHandler: this._handleLogoutClick }, diff --git a/app/hub/index.jsx b/app/hub/index.jsx index 6e8f215ab..aa9e73e1d 100644 --- a/app/hub/index.jsx +++ b/app/hub/index.jsx @@ -17,6 +17,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { HashRouter as Router, Route } from 'react-router-dom'; import { Provider } from 'react-redux'; +import QueryString from 'query-string'; import createStore from './createStore'; // Containers @@ -34,6 +35,9 @@ import UpgradePlanView from './Views/UpgradePlanView'; const store = createStore(); +// Flag to display alternate hub view (used for A/B testing ticket GH-2097) +const ah = (QueryString.parse(window.location.search).ah === 'true') || false; + /** * Top-Level Component for the Ghostery Hub * @memberof HubComponents @@ -41,7 +45,7 @@ const store = createStore(); const Hub = () => ( - + diff --git a/manifest.json b/manifest.json index 0cc3b9ee0..5d66a00a0 100644 --- a/manifest.json +++ b/manifest.json @@ -114,5 +114,6 @@ "cliqz/offers-templates/reminder.html", "cliqz/offers-templates/checkout.html", "cliqz/offers-templates/control-center.html" - ] + ], + "debug": true } diff --git a/src/background.js b/src/background.js index 4df749520..acc30028c 100644 --- a/src/background.js +++ b/src/background.js @@ -1111,26 +1111,20 @@ function getAntitrackingTestConfig() { } /** - * Set option for Hub promo A/B/C test based + * Set option for Hub Layout A/B test based * on the results returned from the abtest endpoint. * @memberOf Background - * - * @return {Object} Hub promotion configuration parameters */ -function setupHubPromoABTest() { +function setupHubLayoutABTest() { if ( !abtest.hasBeenFetched - || conf.hub_promo_variant !== 'not_yet_set' - ) { - return; - } + || conf.hub_layout !== 'not_yet_set' + ) { return; } - if (abtest.hasTest('hub_plain')) { - conf.hub_promo_variant = 'plain'; - } else if (abtest.hasTest('hub_midnight')) { - conf.hub_promo_variant = 'midnight'; + if (abtest.hasTest('hub_alternate')) { + conf.hub_layout = 'alternate'; } else { - conf.hub_promo_variant = 'upgrade'; + conf.hub_layout = 'default'; } } @@ -1155,7 +1149,7 @@ function setupABTest() { cliqz.prefs.set('attrackBloomFilter', false); } - setupHubPromoABTest(); + setupHubLayoutABTest(); } /** @@ -1764,10 +1758,10 @@ function initializeGhosteryModules() { // We need to do this after running scheduledTasks for the first time // because of an A/B test that determines which promo variant is shown in the Hub on install if (globals.JUST_INSTALLED) { - const route = ((conf.hub_promo_variant === 'upgrade' || conf.hub_promo_variant === 'not_yet_set') && !IS_ANDROID) ? '' : '#home'; - const showPremiumPromoModal = (conf.hub_promo_variant === 'midnight' && !IS_ANDROID); + const showAlternateHub = conf.hub_layout === 'alternate'; + const route = showAlternateHub ? '#home' : ''; chrome.tabs.create({ - url: chrome.runtime.getURL(`./app/templates/hub.html?$justInstalled=true&pm=${showPremiumPromoModal}${route}`), + url: chrome.runtime.getURL(`./app/templates/hub.html?$justInstalled=true&ah=${showAlternateHub}${route}`), active: true }); } diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index c542d5090..f50f64145 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -116,7 +116,7 @@ class ConfData { _initProperty('enable_smart_block', true); _initProperty('expand_all_trackers', true); _initProperty('hide_alert_trusted', false); - _initProperty('hub_promo_variant', 'not_yet_set'); + _initProperty('hub_layout', 'not_yet_set'); _initProperty('ignore_first_party', true); _initProperty('import_callout_dismissed', true); _initProperty('insights_promo_modal_last_seen', 0); diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 2ebf6d2e6..2efe7b1a8 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -369,8 +369,8 @@ class Metrics { `&th=${encodeURIComponent(Metrics._getThemeValue().toString())}` + // New parameters for Ghostery 8.5.2 - // Hub Promo variant - `&hp=${encodeURIComponent(Metrics._getHubPromoVariant().toString())}` + + // Hub Layout View + `&t2=${encodeURIComponent(Metrics._getHubLayoutView().toString())}` + // Subscription Interval `&si=${encodeURIComponent(Metrics._getSubscriptionInterval().toString())}` + // Product ID Parameter @@ -534,20 +534,18 @@ class Metrics { } /** - * Get the Int associated with the Hub promo variant shown on install + * Get the Int associated with the Hub layout view shown on install * @private - * @return {number} Int associated with the Hub promo variant + * @return {number} Int associated with the Hub layout view */ - static _getHubPromoVariant() { - const { hub_promo_variant } = conf; + static _getHubLayoutView() { + const { hub_layout } = conf; - switch (hub_promo_variant) { - case 'upgrade': + switch (hub_layout) { + case 'default': return 1; - case 'plain': + case 'alternate': return 2; - case 'midnight': - return 3; case 'not_yet_set': default: return 0; From 07813532772b3b3b9ecd5e3930ae98c5c819f676 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Thu, 24 Sep 2020 15:28:48 -0400 Subject: [PATCH 22/35] Feature/console debugging (#568) * add linting for no-param-reassign and fix resulting linting errors * add linting for prefer-object-spread and fix resulting linting errors * add linting for no-restricted-syntax and fix 1/2 of resulting errors * add linting for no-prototype-builtins and fix resulting linting errors * add linting for class-methods-use-this and fix most resulting errors. Add /tools and /tests to linter. Update height in UpgradeBanner * finish linting for class-methods-use-this * add linting for no-mixed-operators and fix resulting linting errors * add linting for import/prefer-default-export and fix resulting linting errors * add linting for react/no-access-state-in-setstate and fix resulting linting errors * add linting for react/jsx-props-no-spreading and fix resulting linting errors * finish linting errors for no-restricted-syntax. 1 remains: couldn't resolve removing iterator loops for urlSearchParams * Fix linting errors resulting from the merge with develop * Refactor UNSAFE_componentWillMount into either constructor or componentDidMount, leave notes for how decision was made. * Refactor UNSAFE_componentWillReceiveProps to componentDidUpdate or getDerivedStateFromProps * re-enable lint exception for no-prototype-builtins and revert calls back to hasOwnProperty * add single line exception for no-restricted-syntax linting rule * add linting for react/destructuring-assignment and fix errors. ToDo: test code and check for errors * Fix minor bugs * Fix General Settings last updated text * rework linting rule no-param-reassign to have more exceptions and param object destructuring * Remove file and line linting exceptions. * re-add linting rule react/sort-comp and fix resulting errors * remove added linting exception consistent-return and fix resulting errors * remove added linting expression no-use-before-define and fix resulting errors. fix BugDB.js bug. * Fix linting error * fix minor bugs * Code cleanup: fix PromoModal imports * remove unnecessary hasOwnProperty calls after refactored for...in loops * Fix missing strings bug * Fix last remaining string bug * Begin work on a global object for debugging. * Add accountEvents array to Ghostery Debug for logging login/out events * Add way to output window.GHOSTERY for the user * prevent error when not signed in * Promises don't reject, just give important data to user * Download DebugInfo as a JSON file. * Fix linting error * Fix bug with race condition on Debug Information page * Add translation string for opening debug information page * address some issues on PR * Address remaining PR issues * Update Help component test snapshot * Remove front-end parts of the debugging interface. Debugger will be console only * Remove log key reference from README * Remove LOG global and refactor log utility to gate on debug value instead of log value * Delete unneeded debug_information message handler from background * Implement logging toggle in console debugger * Implement globals getter with support for string and regex literal arguments for console debugger * Add support for hitting AB server with arbitrary ir from debug console * Reorganize the tab info objects * Rename getActiveTabInfo to accurately reflect function. Add support for logging that overrides global logging setting * Add help and status method stubs to GhosteryDebug. Add support for getting all conf props, a single one, or a subset using regexp * Improve pretty printing for GhosteryDebug help and status methods * Only expose global GHOSTERY debug option if consoleDebug is set in manifest * Factor out getting an object slice to getObjectSlice utility function * Remove the consoleDebug manifest key and related code * Add support for forcing promo modal display to console debugger * Rename GHOSTERY console debug object to ghostery because toggling Caps Lock is ANNoyINg * Fleshing out help function for console debugger * Prettify ghostery console debugger help function output. Continue fleshing out help contents * Continue fleshing out debugger help. Add first handling of a case where a function name is passed in to the help method * Exploring console API to format debugger output better * Continue iterating on debugger output formatting * Add ads to console debugger * Iterate on debugger help output * Flesh out implementation of ghosteryDebug#getConfData output. Flesh out debugger help output for this function * Flesh out implementation of ghosteryDebug#getGlobals output and its help output * Factor pickRandomArrEl function out to utils, flesh it out, and document it. Revise help output for getABTests to conform to standard format for function help output. Remove addition of linebreaks from typeset method to help fix and improve output formatting. Adjust output styles accordingly and define sub header styles. * Give debugger's typeset method the responsibility for adding new lines before and after debugger help output. Add CSS style markers for a standard way to specify which styles the printToConsole method should apply * Fix bug in debugger output formatting * Add debugger help output for ghostery.hitABServerWithIr() * Begin to factor out help strings from GhosteryDebug#help to a static field on GhosteryDebug * Factor help strings out to static class fields on GhosteryDebug * Organize GhosteryDebug by grouping formatting and output code and grouping help CLI & strings. Document GhosteryDebug#printToConsole and GhosteryDebug#typeset. Add ToC. * Rearrange static class fields in help strings section of GhosteryDebug to account for the apparent fact that static class fields must be defined before being referenced by other class fields. Fix small typos. * Move toggleLogging to settings GhosteryDebug instance field. Move status to settings.show. Move isLog and objectDisplayStyle into settings * Add help output for GhosteryDebug#settings.toggleLogging * Add help output for GhosteryDebug#settings.show(). Standardize show() and toggleLogging() output to use typeset() and printToConsole(). Add Babel support for optional chaining syntax * Flesh out help output for GhosteryDebug#settings.show * Begin GhosteryDebug#settings.setOutputStyle implementation. Factor common printToConsole code out to a helper * Finish implementing GhosteryDebug#settings.toggleOutputStyle. Factor out common code in the settings togglers to helper methods * Flesh out GhosteryDebugger#forceOnePromoModalDisplay to produce formatted output and handle bad arguments. Rework PromoModals so that modals can be added, removed, and priority-shifted around without necessitating any changes to the GhosteryDebug help output or other code. * Factor string array assembly out from GhosteryDebugger#help to a helper that can be called independently by other methods. Update showPromoModal output to use the standard formatting provided by the typeset & printToConsole chain. Improve docs for PromoModals#showPromoModal. Make the PromoModals#showPromoModal argument handling case-insensitive. Add PromoModals.getActiveModalTypes. Improve getObjectSlice util function docs. Improve pickRandomArrEl util function docs and error messaging. Add capitalize utility function. Solve global warming. * Remove some unused or redundant code from GhosteryDebug * Prep printToConsole and typeset for handling objects, so that printToConsole and its helpers can take sole responsibility for console calls * Add error handling to getObjectSlice util function and return whether a match was found, so we can provide better messaging about the return values in GhosteryDebug methods that use the utility * Adjust error check order in getObjectSlice util function * Remove extra line * Pipe getConfData and getGlobals output through the standard typeset / printToConsole chain * Move GhosteryDebug#_outputObjectSlice out of the printing section since it now delegates printing to printToConsole * Convert GhosteryDebug#getABTests to use standard typeset and printToConsole output pipeline. Factor pushing an object to output array as object or string based on debugger setting out to helper * Update ABTest to support hitting the A/B server without automatically logging out the return values * Implement standardized output formatting and error checking for GhosteryDebug#hitABServerWithIr * rename hitABServerWithIr to hitABServerWithIrAsync to make it clear that it is beautiful * Rename hitABServerWithIrAsync to fetchABTestsWithIr cos it is shorter and will match fetchCMPCampaigns * Replace some magic strings. Clean up fetchABTestsWithIr output formatting * Rework debugger log toggling to avoid creating a circular dependency * Replace '__SUBHEADER__' magic string with string constant * Replace '__MAINHEADER__' magic string with string constant * Replace '__HIGHLIGHT__' CSS marker magic string with string constant in GhosteryDebugger * Allow changing values of CSS marker string constants without having to modify GhosteryDebug#printToConsole or printFormatted * Variety of minor org and doc tweaks to GhosteryDebug * Minor reorg in GhosteryDebug * Rework CMP to accept fetch requests from debugger * Continue implementing GhosteryDebug#fetchCMPCampaigns. Improve documentation for several utility functions. * Fleshing out documentation for GhosteryDebug fields * Further document fields in GhosteryDebug * Flesh out fetchCMPCampaigns implementation to conform to standard output formatting. Add docs * add openPanel function to debugger for automation testing * rename debugger class * rename ghostery debugger vars * open intro hub and android panel from debugger * Cut Debugger#getDebugInfo * Remove babel optional chaining plugin and the couple Debugger lines that used it * Update naming and itdy up export * rework getActiveTabInfo and add to help menu * Refactor Debugger#getUserData to print to console better and to handle errors better * Convert Debugger#getUserData to an arrow func. JSDoc it. * Add help output for Debugger#getUserData. Clean up main help menu a little. * handle undefined returns in console * Print account events at the end of Debugger#getUserData output * Cap Debugger accountEvents array at 1000 most recent events * Update help output for Debugger#getUserData * Fix typo * Conceal Debugger _toggleSettingsHelper from console * Rename Debugger accountEvents array to signal that it's private * Add JSDocs for Debugger getABTests and getConfData. Tidy up existing JSDocs * Add Debugger getGlobals JSDoc. Tidy up openPanel and openIntroHub JSDocs * Add JSDoc for Debugger showPromoModal. Remove @async tag in other ones as it is only intended for use with functions defined with the async keyword * Add Debugger addAccountEvent JSDoc * Add JSDoc for Debugger _printObjectSlice * Add JSDoc for Debugger _push * JSDoc Debugger settings object and settings.show. Convert settings to a closure to force JSDoc to document inner properties * JSDoc the Debugger's toggle setting methods * Fix accountEvents printing in Debugger getUserData * Fix bug that was preventing subscription data from showing in Debugger getUserData output * Make it so timestamps are not double quoted in ghostery._accountEvents output Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> Co-authored-by: wlycdgr Co-authored-by: Christopher Tino --- README.md | 1 - manifest.json | 2 +- src/background.js | 8 +- src/classes/ABTest.js | 71 ++- src/classes/Account.js | 15 +- src/classes/CMP.js | 84 ++- src/classes/Debugger.js | 1141 ++++++++++++++++++++++++++++++++++++ src/classes/Globals.js | 1 - src/classes/PromoModals.js | 46 +- src/utils/common.js | 53 +- src/utils/utils.js | 182 +++++- 11 files changed, 1537 insertions(+), 67 deletions(-) create mode 100644 src/classes/Debugger.js diff --git a/README.md b/README.md index 2658849e5..a7fc29020 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,6 @@ $ yarn build.watch ```javascript // In manifest.json set "debug": true, -"log": true, ``` ## Testing and Linting diff --git a/manifest.json b/manifest.json index 5d66a00a0..e96cf7961 100644 --- a/manifest.json +++ b/manifest.json @@ -1,4 +1,5 @@ { + "debug": true, "manifest_version": 2, "author": "Ghostery", "name": "__MSG_name__", @@ -7,7 +8,6 @@ "version_name": "8.5.3", "default_locale": "en", "description": "__MSG_short_description__", - "log": true, "icons": { "16": "app/images/icon16.png", "48": "app/images/icon48.png", diff --git a/src/background.js b/src/background.js index acc30028c..e8d463035 100644 --- a/src/background.js +++ b/src/background.js @@ -17,6 +17,7 @@ import { debounce, every, size } from 'underscore'; import moment from 'moment/min/moment-with-locales.min'; import cliqz, { HUMANWEB_MODULE, HPN_MODULE } from './classes/Cliqz'; +import ghosteryDebugger from './classes/Debugger'; // object classes import Events from './classes/EventHandlers'; import Policy from './classes/Policy'; @@ -53,6 +54,9 @@ import { sendCliqzModuleCounts } from './utils/cliqzModulesData'; // module from Developer Tools Console. window.CLIQZ = cliqz; +// For debug purposes, provide access to Ghostery's internal data. +window.ghostery = ghosteryDebugger; + // class instantiation const events = new Events(); // function shortcuts @@ -1233,7 +1237,7 @@ function initializeDispatcher() { } /** - * WebRequest pipeline initialisation: find which Cliqz modules are enabled, + * WebRequest pipeline initialization: find which Cliqz modules are enabled, * add their handlers, then put Ghostery event handlers before them all. * If Cliqz modules are subsequently enabled, their event handlers will always * be added after Ghostery's. @@ -1784,6 +1788,7 @@ function init() { account.migrate() .then(() => { if (conf.account !== null) { + ghosteryDebugger.addAccountEvent('app started', 'signed in', conf.account); return account.getUser() .then(account.getUserSettings) .then(() => { @@ -1793,6 +1798,7 @@ function init() { return false; }); } + ghosteryDebugger.addAccountEvent('app started', 'not signed in'); if (globals.JUST_INSTALLED) { setGhosteryDefaultBlocking(); } diff --git a/src/classes/ABTest.js b/src/classes/ABTest.js index 1236310ce..da4d122b3 100644 --- a/src/classes/ABTest.js +++ b/src/classes/ABTest.js @@ -38,44 +38,71 @@ class ABTest { return this.tests.hasOwnProperty(name); } + /** + * Return the tests object + * @return {Object} + */ + getTests() { + return this.tests; + } + /** * Send parameters to A/B Test server and receive tests data. - * @return {Promise} dictionary with all tests to be executed + * @param {Number} irDebugOverride optional. supports hitting AB server with custom ir from debug console + * @return {Promise} dictionary with all tests to be executed */ - fetch() { + fetch(irDebugOverride) { log('A/B Tests: fetching...'); - const URL = `${CMP_BASE_URL}/abtestcheck - ?os=${encodeURIComponent(BROWSER_INFO.os)} - &install_date=${encodeURIComponent(conf.install_date)} - &ir=${encodeURIComponent(conf.install_random_number)} - &gv=${encodeURIComponent(EXTENSION_VERSION)} - &si=${conf.account ? '1' : '0'} - &ua=${encodeURIComponent(BROWSER_INFO.name)} - &v=${encodeURIComponent(conf.cmp_version)} - &l=${encodeURIComponent(conf.language)}`; + const URL = ABTest._buildURL(irDebugOverride); return getJson(URL).then((data) => { if (data && Array.isArray(data)) { log('A/B Tests: fetched', JSON.stringify(data)); - // merge all tests into this.tests object - // this will overwrite all previous tests - this.tests = data.reduce( - (tests, test) => Object.assign(tests, { [test.name]: test.data }), - {} - ); - this.hasBeenFetched = true; + this._updateTests(data); + log('A/B Tests: tests updated to', this.getTests()); } else { log('A/B Tests: no tests found.'); } - - // update conf - globals.SESSION.abtests = this.tests; - log('A/B Tests: tests updated to', JSON.stringify(this.tests)); }).catch(() => { log('A/B Tests: error fetching.'); }); } + + silentFetch(ir) { + const URL = ABTest._buildURL(ir); + + return getJson(URL).then((data) => { + if (data && Array.isArray(data)) { + this._updateTests(data); + } + return 'resolved'; + }).catch(() => 'rejected'); + } + + static _buildURL(ir) { + return (`${CMP_BASE_URL}/abtestcheck + ?os=${encodeURIComponent(BROWSER_INFO.os)} + &install_date=${encodeURIComponent(conf.install_date)} + &ir=${encodeURIComponent((typeof ir === 'number') ? ir : conf.install_random_number)} + &gv=${encodeURIComponent(EXTENSION_VERSION)} + &si=${conf.account ? '1' : '0'} + &ua=${encodeURIComponent(BROWSER_INFO.name)} + &v=${encodeURIComponent(conf.cmp_version)} + &l=${encodeURIComponent(conf.language)}` + ); + } + + _updateTests(data) { + // merge all tests into this.tests object + // this will overwrite all previous tests + this.tests = data.reduce( + (tests, test) => Object.assign(tests, { [test.name]: test.data }), + {} + ); + // update conf + globals.SESSION.abtests = this.tests; + } } // Return the class as a singleton diff --git a/src/classes/Account.js b/src/classes/Account.js index 9f2e55a34..e7b58afad 100644 --- a/src/classes/Account.js +++ b/src/classes/Account.js @@ -22,6 +22,7 @@ import conf from './Conf'; import dispatcher from './Dispatcher'; import { log } from '../utils/common'; import Api from '../utils/api'; +import ghosteryDebugger from './Debugger'; const api = new Api(); const { @@ -83,6 +84,7 @@ class Account { if (res.status >= 400) { return res.json(); } + ghosteryDebugger.addAccountEvent('login', 'cookie set by fetch POST'); this._getUserIDFromCookie().then((userID) => { this._setAccountInfo(userID); this.getUserSubscriptionData(); @@ -103,6 +105,7 @@ class Account { credentials: 'include', }).then((res) => { if (res.status >= 400) { + ghosteryDebugger.addAccountEvent('register', 'cookie set by fetch POST'); return res.json(); } this._getUserIDFromCookie().then((userID) => { @@ -124,7 +127,10 @@ class Account { credentials: 'include', headers: { 'X-CSRF-Token': cookie.value }, }).then((res) => { - if (res.status < 400) { return resolve(); } + if (res.status < 400) { + ghosteryDebugger.addAccountEvent('logout', 'cookie set by fetch POST'); + return resolve(); + } return res.json().then(json => reject(json)); }).catch(err => reject(err)); }); @@ -254,15 +260,18 @@ class Account { migrate = () => ( new Promise((resolve) => { + ghosteryDebugger.addAccountEvent('migrate', 'migrate start'); const legacyLoginInfoKey = 'login_info'; chrome.storage.local.get(legacyLoginInfoKey, (items) => { if (chrome.runtime.lastError) { + ghosteryDebugger.addAccountEvent('migrate', 'runtime error'); resolve(new Error(chrome.runtime.lastError)); return; } const { login_info } = items; if (!items || !login_info) { + ghosteryDebugger.addAccountEvent('migrate', 'no items found'); resolve(); return; } @@ -270,6 +279,7 @@ class Account { // ensure we have all the necessary info const { decoded_user_token, user_token } = login_info; if (!decoded_user_token || !user_token) { + ghosteryDebugger.addAccountEvent('migrate', 'found items, not enough info I'); chrome.storage.local.remove(legacyLoginInfoKey, () => resolve()); return; } @@ -277,6 +287,7 @@ class Account { UserId, csrf_token, RefreshToken, exp } = decoded_user_token; if (!UserId || !csrf_token || !RefreshToken || !exp) { + ghosteryDebugger.addAccountEvent('migrate', 'found items, not enough info II'); chrome.storage.local.remove(legacyLoginInfoKey, () => resolve()); return; } @@ -311,8 +322,10 @@ class Account { // login this._setAccountInfo(UserId); this.getUserSubscriptionData(); + ghosteryDebugger.addAccountEvent('migrate', 'remove legacy items'); chrome.storage.local.remove(legacyLoginInfoKey, () => resolve()); }).catch((err) => { + ghosteryDebugger.addAccountEvent('migrate', 'cookies set error'); resolve(err); }); }); diff --git a/src/classes/CMP.js b/src/classes/CMP.js index f63d3e8f4..c4e3b0bc5 100644 --- a/src/classes/CMP.js +++ b/src/classes/CMP.js @@ -36,36 +36,11 @@ class CMP { return Promise.resolve(false); } - const URL = `${CMP_BASE_URL}/check - ?os=${encodeURIComponent(BROWSER_INFO.os)} - &offers=${encodeURIComponent(conf.enable_offers ? '1' : '0')} - &hw=${encodeURIComponent(conf.enable_human_web ? '1' : '0')} - &install_date=${encodeURIComponent(conf.install_date)} - &ir=${encodeURIComponent(conf.install_random_number)} - &gv=${encodeURIComponent(EXTENSION_VERSION)} - &si=${encodeURIComponent(conf.account ? '1' : '0')} - &ua=${encodeURIComponent(BROWSER_INFO.name)} - &lc=${encodeURIComponent(conf.last_cmp_date)} - &v=${encodeURIComponent(conf.cmp_version)} - &l=${encodeURIComponent(conf.language)}`; + const URL = CMP._buildUrl(); return getJson(URL).then((data) => { - if (data && (!conf.cmp_version || data.Version > conf.cmp_version)) { - // set default dismiss - data.Campaigns.forEach((dataEntry) => { - if (dataEntry.Dismiss === 0) { - dataEntry.Dismiss = 10; - } - - // set last campaign (dataEntry) run timestamp to avoid running campaigns more than once - if (!conf.last_cmp_date || conf.last_cmp_date < dataEntry.Timestamp) { - conf.last_cmp_date = dataEntry.Timestamp; - } - }); - // update Conf and local CMP_DATA - conf.cmp_version = data.Version; - globals.SESSION.cmp_data = data.Campaigns; - this.CMP_DATA = data.Campaigns; + if (CMP._isNewData(data)) { + this._updateCampaigns(data); return this.CMP_DATA; } // getJson() returned a 204, meaning no new campaigns available @@ -77,6 +52,59 @@ class CMP { return false; }); } + + debugFetch() { + const URL = CMP._buildUrl(); + + return getJson(URL) + .then((data) => { + if (CMP._isNewData(data)) { + this._updateCampaigns(data); + return ({ ok: true, testsUpdated: true }); + } + globals.SESSION.cmp_data = []; + return ({ ok: true, testsUpdated: false }); + }) + .catch(() => ({ ok: false, testsUpdated: false })); + } + + _updateCampaigns(data) { + // set default dismiss + data.Campaigns.forEach((dataEntry) => { + if (dataEntry.Dismiss === 0) { + dataEntry.Dismiss = 10; + } + + // set last campaign (dataEntry) run timestamp to avoid running campaigns more than once + if (!conf.last_cmp_date || conf.last_cmp_date < dataEntry.Timestamp) { + conf.last_cmp_date = dataEntry.Timestamp; + } + }); + // update Conf and local CMP_DATA + conf.cmp_version = data.Version; + globals.SESSION.cmp_data = data.Campaigns; + this.CMP_DATA = data.Campaigns; + } + + static _buildUrl() { + return (`${CMP_BASE_URL}/check + ?os=${encodeURIComponent(BROWSER_INFO.os)} + &offers=${encodeURIComponent(conf.enable_offers ? '1' : '0')} + &hw=${encodeURIComponent(conf.enable_human_web ? '1' : '0')} + &install_date=${encodeURIComponent(conf.install_date)} + &ir=${encodeURIComponent(conf.install_random_number)} + &gv=${encodeURIComponent(EXTENSION_VERSION)} + &si=${encodeURIComponent(conf.account ? '1' : '0')} + &ua=${encodeURIComponent(BROWSER_INFO.name)} + &lc=${encodeURIComponent(conf.last_cmp_date)} + &v=${encodeURIComponent(conf.cmp_version)} + &l=${encodeURIComponent(conf.language)}` + ); + } + + static _isNewData(data) { + return (data && (!conf.cmp_version || data.Version > conf.cmp_version)); + } } // return the class as a singleton diff --git a/src/classes/Debugger.js b/src/classes/Debugger.js new file mode 100644 index 000000000..3c55d95de --- /dev/null +++ b/src/classes/Debugger.js @@ -0,0 +1,1141 @@ +/** + * Ghostery Debug Class + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import abtest from './ABTest'; +import account from './Account'; +import cmp from './CMP'; +import confData from './ConfData'; +import globals from './Globals'; +import tabInfo from './TabInfo'; +import foundBugs from './FoundBugs'; +import PromoModals from './PromoModals'; +import { isLog, activateLog } from '../utils/common'; +import { capitalize, getObjectSlice, pickRandomArrEl } from '../utils/utils'; + +/** + * @class for debugging Ghostery via the background.js console. + * @memberof BackgroundClasses + */ + +const OBJECT_OUTPUT_STYLE = true; +const STRING_OUTPUT_STYLE = false; + +const THANKS = 'Thanks for using Ghostery'; +const UP_REMINDER = 'Remember you can press up to avoid having to retype your previous command'; +const CSS_SUBHEADER = 'css_subheader__'; +const CSS_MAINHEADER = 'css_mainheader__'; +const CSS_HIGHLIGHT = 'css_highlight__'; +const OUTPUT_COLUMN_WIDTH = 40; +const ACCOUNT_EVENTS_CAP = 1000; + +/** + * Class that implements an interactive console debugger. + * + * @since 8.5.3 + * + * @memberOf BackgroundClasses + */ +class Debugger { + // ToC + // Search for these strings to quickly jump to their sections + // [[Output styling, formatting, and printing]] + // [[Help CLI & strings]] + // [[Main Actions]] + // [[Settings Actions]] + + constructor() { + // Public settings methods are defined in a public instance field in the [[Settings Actions]] section + // These are the private settings methods and properties + this.settings._isLog = isLog(); + this.settings._objectOutputStyle = OBJECT_OUTPUT_STYLE; + + this._accountEvents = []; + + const _cookieChangeEvent = (changeInfo) => { + const { removed, cookie, cause } = changeInfo; + const { domain, name } = cookie; + if (domain.includes(globals.GHOSTERY_ROOT_DOMAIN)) { + const type = `Cookie ${name} ${removed ? 'Removed' : 'Added'}`; + this.addAccountEvent(type, cause, cookie); + } + }; + + // Chrome Documentation: https://developer.chrome.com/extensions/cookies#event-onChanged + // Mozilla Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/onChanged + chrome.cookies.onChanged.addListener(_cookieChangeEvent); + } + + // START [[Output styling, formatting, and printing]] SECTION + /** + * @access private + * @since 8.5.3 + * + * Styles used to format debugger output. + */ + static _outputStyles = { + [CSS_HIGHLIGHT]: 'font-weight: bold; padding: 2px 0px;', + [CSS_MAINHEADER]: 'font-size: 16px; font-weight: bold; padding: 4px 0px', + [CSS_SUBHEADER]: 'font-weight: bold; padding: 2px 0px;', + }; + + /** + * @private + * @since 8.5.3 + * + * `Debugger._printToConsole` helper. Applies the provided styles + * and prints the text argument. + * + * @param {String} text The string to output. + * @param {String} style The style to apply. + * + * @return {undefined} No explicit return. + */ + static _printFormatted(text, style) { + // eslint-disable-next-line no-console + console.log( + `%c${text.replace(style, '')}`, + Debugger._outputStyles[style] + ); + } + + /** + * @private + * @since 8.5.3 + * + * Output an array of strings and/or objects to the browser developer console. + * Scans strings for CSS markers, applies the specified styles when they are found, + * and removes the markers from the final output. + * + * @param {Array} lines An array of strings and/or objects to be logged. Strings may start with a CSS marker. + * @return {undefined} No explicit return. + */ + static _printToConsole(lines) { + // Individual log statements for each line allow for + // more legible and appealing output spacing and formatting + lines.forEach((line) => { + // eslint-disable-next-line no-console + if (typeof line === 'object') console.dir(line); + else if (line.startsWith(CSS_MAINHEADER)) Debugger._printFormatted(line, CSS_MAINHEADER); + else if (line.startsWith(CSS_SUBHEADER)) Debugger._printFormatted(line, CSS_SUBHEADER); + else if (line.startsWith(CSS_HIGHLIGHT)) Debugger._printFormatted(line, CSS_HIGHLIGHT); + // eslint-disable-next-line no-console + else console.log(line); + }); + } + + /** + * @private + * @since 8.5.3 + * + * Takes an array whose elements are a combination of strings, two element string arrays, and objects + * and processes it into an array ready for printing to the console by `Debugger#_printToConsole`. + * String and object input array elements are passed through unaltered. + * String array input array elements have their elements concatenated with padding to create columns. + * Newlines are added at the beginning and the end. + * + * @param {Array} rawTexts An array of string, two element string arrays, and/or objects. + * @return {Array} An array of strings and/or objects tidied and padded for printing. + */ + static _typeset(rawTexts) { + const formattedLines = []; + + formattedLines.push('\n'); + + rawTexts.forEach((rawText) => { + if (typeof rawText === 'string') { + formattedLines.push(rawText); + return; + } + + if ((!Array.isArray(rawText)) && (typeof rawText === 'object')) { + formattedLines.push(rawText); + } + + if (Array.isArray(rawText)) { + const leftSide = rawText[0]; + const rightSide = rawText[1]; + const cssStyleMarkerLength = + (leftSide.startsWith(CSS_MAINHEADER) && CSS_MAINHEADER.length) + || (leftSide.startsWith(CSS_SUBHEADER) && CSS_SUBHEADER.length) + || (leftSide.startsWith(CSS_HIGHLIGHT) && CSS_HIGHLIGHT.length) + || 0; + formattedLines.push( + leftSide.padEnd(OUTPUT_COLUMN_WIDTH + cssStyleMarkerLength, ' ').concat(rightSide) + ); + } + }); + + formattedLines.push('\n'); + + return formattedLines; + } + // END [[Output styling, formatting, and printing]] SECTION + + // START [[Help CLI & strings]] SECTION + // The order of definition matters in this section: + // it appears that static class fields must be defined + // before they can be referenced by other static class fields + + /** + * @access private + * @since 8.5.3 + * + * Function name strings used in help output. + */ + static _helpFunctionNames = { + fetchABTestsWithIr: 'ghostery.fetchABTestsWithIr()', + getABTests: 'ghostery.getABTests()', + getActiveTabInfo: 'ghostery.getActiveTabInfo()', + getConfData: 'ghostery.getConfData()', + getGlobals: 'ghostery.getGlobals()', + getUserData: 'ghostery.getUserData()', + openIntroHub: 'ghostery.openIntroHub()', + openPanel: 'ghostery.openPanel()', + showPromoModal: 'ghostery.showPromoModal()', + settingsShow: 'ghostery.settings.show()', + settingsToggleLogging: 'ghostery.settings.toggleLogging()', + settingsToggleOutputStyle: 'ghostery.settings.toggleOutputStyle()', + }; + + /** + * @access private + * @since 8.5.3 + * + * Header string for the help main menu. + */ + static _helpHeader = [ + `${CSS_MAINHEADER}Ghostery Extension Debugger (GED) Help`, + '', + `${CSS_SUBHEADER}Usage:`, + ['ghostery.help()', 'Show this message'], + ["ghostery.help('functionName')", 'Show function usage details like supported argument types/values'], + ['', "Example: ghostery.help('getABTests')"], + ]; + + // In static fields, 'this' refers to the class instance, so this works, + // as long as the referenced class property has already been defined + /** + * @access private + * @since 8.5.3 + * + * Brief descriptions of the debugger API main methods. Displayed in the main help menu. + */ + static _helpAvailableFunctions = [ + [`${this._helpFunctionNames.fetchABTestsWithIr}`, 'Hit the A/B server endpoint with the supplied install random number'], + [`${this._helpFunctionNames.getABTests}`, 'Display what A/B tests have been fetched from the A/B test server'], + [`${this._helpFunctionNames.getActiveTabInfo}`, 'Shows TabInfo and FoundBugs data for any active tabs'], + [`${this._helpFunctionNames.getConfData}`, 'Show the current value of a config property or properties'], + [`${this._helpFunctionNames.getGlobals}`, 'Show the current value of a global property or properties'], + [`${this._helpFunctionNames.getUserData}`, 'Show account data for the logged in user and account event history'], + [`${this._helpFunctionNames.openIntroHub}`, 'Open the Ghostery Intro Hub in a new tab for automation testing'], + [`${this._helpFunctionNames.openPanel}`, 'Open the Ghostery panel window in a new tab for automation testing'], + [`${this._helpFunctionNames.showPromoModal}`, 'Show specified promo modal at the next opportunity'], + ] + + /** + * @access private + * @since 8.5.3 + * + * Brief descriptions of the debugger API settings methods. Displayed in the main help menu. + */ + static _helpAvailableSettingsFunctions = [ + [`${this._helpFunctionNames.settingsShow}`, 'Show the current debugger settings'], + [`${this._helpFunctionNames.settingsToggleLogging}`, 'Toggle all other debug logging on/off'], + [`${this._helpFunctionNames.settingsToggleOutputStyle}`, 'Change debugger method return value formatting'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The entire help main menu text + */ + static _helpMainMenu = [ + ...this._helpHeader, + '', + `${CSS_SUBHEADER}Available functions:`, + ...this._helpAvailableFunctions, + '', + `${CSS_SUBHEADER}Available settings functions:`, + ...this._helpAvailableSettingsFunctions, + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `fetchABTestsWithIr()` method. + * Displayed after calling ghostery.help('fetchABTestsWithIr'). + */ + static helpFetchABTestsWithIr = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.fetchABTestsWithIr}`, + 'A random number between 1 and 100 is generated and saved to local storage', + 'when the extension is first installed. This number is included in requests', + 'to the A/B test server as the value of the ir query parameter, and it determines', + 'which test buckets the user is placed in.', + 'This function lets you hit the A/B server with any valid ir number', + 'to make it easier to check whether different A/B tests are returned as expected', + 'and check the functionality of the different test scenarios', + '', + [`${CSS_SUBHEADER}When called with...`, 'Returns...'], + ['A number between 1 and 100', 'The tests returned by the A/B server for that ir value'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `getABTests()` method. + * Displayed after calling ghostery.help('getABTests'). + */ + static helpGetABTests = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.getABTests}`, + 'Display what A/B tests have been fetched from the A/B test server', + 'Fetches happen on browser startup and then at regularly scheduled intervals', + '', + [`${CSS_SUBHEADER}When called with...`, 'Returns...'], + ['No argument or any arguments', 'The A/B test strings currently in memory'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `getActiveTabInfo()` method. + * Displayed after calling ghostery.help('getActiveTabInfo'). + */ + static helpGetActiveTabInfo = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.getActiveTabInfo}`, + 'Display the current value(s) of an active tab property or properties', + '', + [`${CSS_SUBHEADER}When called with...`, 'Returns...'], + ['No argument', 'The whole ActiveTabInfo object'], + ['A property key string', 'An object with just that property'], + ['', "Example: ghostery.getActiveTabInfo('activeTabIds | foundBugs | tabInfo')"], + ['Anything else', 'The whole ActiveTabInfo object. Also returned if there are no matching results'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `getConfData()` method. + * Displayed after calling ghostery.help('getConfData'). + */ + static helpGetConfData = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.getConfData}`, + 'Display the current value(s) of a config property or properties', + '', + [`${CSS_SUBHEADER}When called with...`, 'Returns...'], + ['No argument', 'The whole config object'], + ['A property key string', 'An object with just that property'], + ['', "Example: ghostery.getConfData('enable_smart_block')"], + ['A property key regex', 'An object with all matching properties'], + ['', 'Example: ghostery.getConfData(/setup_/)'], + ['Anything else', 'The whole config object. Also returned if there are no matching results'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `getGlobals()` method. + * Displayed after calling ghostery.help('getGlobals'). + */ + static helpGetGlobals = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.getGlobals}`, + 'Display the current value(s) of a global property or properties', + '', + [`${CSS_SUBHEADER}When called with...`, 'Returns...'], + ['No argument', 'The whole globals object'], + ['A property key string', 'An object with just that property'], + ['', "Example: ghostery.getGlobals('BROWSER_INFO')"], + ['A property key regex', 'An object with all matching properties'], + ['', 'Example: ghostery.getGlobals(/ACCOUNT_/)'], + ['Anything else', 'The whole globals object. Also returned if there are no matching results'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `getUserData()` method. + * Displayed after calling ghostery.help('getUserData'). + */ + static helpGetUserData = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.getUserData}`, + 'Display account details for the logged-in user, or an error message if no user is logged in.', + `Also display up to ${ACCOUNT_EVENTS_CAP} of the most recent account events`, + '', + [`${CSS_SUBHEADER}When called with...`, 'Returns...'], + ['No/any argument(s)', "Account event history and the user's account details,"], + ['', 'subscription details, synced settings, and cookies'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `openIntroHub()` method. + * Displayed after calling ghostery.help('openIntroHub'). + */ + static helpOpenIntroHub = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.openIntroHub}`, + 'Open the Ghostery Intro Hub in a new tab for automation testing.', + '', + [`${CSS_SUBHEADER}When called with...`, 'Opens...'], + ['No argument', 'The hub on the default route'], + ['modal', 'The hub with any promo modals displayed'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `openPanel()` method. + * Displayed after calling ghostery.help('openPanel'). + */ + static helpOpenPanel = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.openPanel}`, + 'Open the Ghostery panel window in a new tab for automation testing.', + 'Uses the current active tabID to populate panel data.', + '', + [`${CSS_SUBHEADER}When called with...`, 'Opens...'], + ['No argument', 'The standard panel for desktop'], + ['mobile', 'The mobile panel for Android'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `showPromoModal()` method. + * Displayed after calling ghostery.help('showPromoModal'). + */ + static helpShowPromoModal = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.showPromoModal}`, + 'Force the specified promo modal to display at the next opportunity.', + 'That may be, for example, the next time you open the extension panel.', + 'Resets after one display. If you need to see the modal again, call this function again', + '', + [`${CSS_SUBHEADER}When called with...`, 'Does...'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `settings.show()` method. + * Displayed after calling ghostery.help('show'). + */ + static helpSettingsShow = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.settingsShow}`, + 'Show the current debugger settings.', + 'Settings persist until you end the browser session', + '', + [`${CSS_SUBHEADER}Setting`, 'Explanation'], + ['Logging', 'Turn extension debug output on/off'], + ['Object Output Style', 'Set return value display style to object or string'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `settings.toggleLogging()` method. + * Displayed after calling ghostery.help('toggleLogging'). + */ + static helpSettingsToggleLogging = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.settingsToggleLogging}`, + 'Toggle regular debug output on/off.', + 'This overrides the debug property in the manifest', + 'and allows you to turn on logging in production builds', + "and any other builds that don't have debug set in the manifest", + '', + [`${CSS_SUBHEADER}When called with...`, 'Does...'], + ["'ON'", 'Turns logging on'], + ["'OFF'", 'Turns logging off'], + ['Any other argument or no argument', 'Turns logging on if it was off and vice versa'], + ] + + /** + * @access private + * @since 8.5.3 + * + * The help text for the public `settings.toggleOutputStyle()` method. + * Displayed after calling ghostery.help('toggleOutputStyle'). + */ + static helpSettingsToggleOutputStyle = [ + `${CSS_MAINHEADER}${this._helpFunctionNames.settingsToggleOutputStyle}`, + 'Change the output style for debugger method return values.', + 'Strings are easy to copy and easier to grok at a glance.', + 'Object style output looks nicer and shows the structure better', + '', + [`${CSS_SUBHEADER}When called with...`, 'Does...'], + ["'OBJECT'", 'Debugger method return values will now be output as objects'], + ["'STRING'", 'Debugger method return values will now be output as strings'], + ['Any other argument or no argument', 'Changes the output style from the current setting to the other one'], + ]; + + /** + * @access private + * @since 8.5.3 + * + * Short ads and thank you messages used as the return value for `help` calls + * so they are printed to the console instead of `undefined`. + */ + static _helpPromoMessages = [ + THANKS, + 'Try our desktop tracker blocker & VPN Midnight for free', + 'Try our tracker research & analytics extension Insights for free', + 'Visit ghostery.com to learn more about our values and products', + ]; + + /** + * @private + * @since 8.5.3 + * + * Prepares and returns the help strings array for the requested function. + * Exists as a separate function so that public methods can concatenate some custom messages + * with standard help output before forwarding all the string to `_typeset` and `_printToConsole`. + * + * @param {String} fnName The name of the function for which help output was requested. + * @return {Array} Returns the help strings array for the requested function, or an error strings array if the argument is missing or not supported. + */ + static _assembleHelpStringArr(fnName) { + const { + _helpMainMenu, + _helpAvailableFunctions, + helpFetchABTestsWithIr, + helpGetABTests, + helpGetActiveTabInfo, + helpGetConfData, + helpGetGlobals, + helpGetUserData, + helpOpenIntroHub, + helpOpenPanel, + helpShowPromoModal, + helpSettingsShow, + helpSettingsToggleLogging, + helpSettingsToggleOutputStyle, + } = Debugger; + + const invalidArgumentError = [ + `${CSS_MAINHEADER}'${fnName}' is not a GED function. Here are the valid ones:`, + '', + ..._helpAvailableFunctions, + ]; + + const helpStringArr = []; + const eeFnName = (fnName && typeof fnName === 'string' && fnName.toLowerCase()) || undefined; + if (fnName === undefined) helpStringArr.push(..._helpMainMenu); + else if (eeFnName === 'fetchabtestswithir') helpStringArr.push(...helpFetchABTestsWithIr); + else if (eeFnName === 'getabtests') helpStringArr.push(...helpGetABTests); + else if (eeFnName === 'getactivetabinfo') helpStringArr.push(...helpGetActiveTabInfo); + else if (eeFnName === 'getconfdata') helpStringArr.push(...helpGetConfData); + else if (eeFnName === 'getglobals') helpStringArr.push(...helpGetGlobals); + else if (eeFnName === 'getuserdata') helpStringArr.push(...helpGetUserData); + else if (eeFnName === 'openintrohub') helpStringArr.push(...helpOpenIntroHub); + else if (eeFnName === 'openpanel') helpStringArr.push(...helpOpenPanel); + else if (eeFnName === 'showpromomodal') { + helpStringArr.push(...helpShowPromoModal); + const activeModalTypes = PromoModals.getActiveModalTypes(); + activeModalTypes.forEach((amt) => { + const { val: cappedAmt } = capitalize(amt.toLowerCase()); + helpStringArr.push([ + `'${amt}'`, `Open the ${cappedAmt} modal at the next opportunity`, + ]); + }); + } else if (eeFnName === 'show') helpStringArr.push(...helpSettingsShow); + else if (eeFnName === 'togglelogging') helpStringArr.push(...helpSettingsToggleLogging); + else if (eeFnName === 'toggleoutputstyle') helpStringArr.push(...helpSettingsToggleOutputStyle); + else helpStringArr.push(...invalidArgumentError); + + return helpStringArr; + } + + /** + * @since 8.5.3 + * + * Prints general and function-specific help output. Part of the public CLI. + * + * @param {String} [fnName] The name of the function for which help output was requested, if any. + * @return {String} An ad or thank you message (printed to the console as the last line of output). + */ + help = (fnName) => { + const { + _assembleHelpStringArr, + _helpPromoMessages, + _printToConsole, + _typeset + } = Debugger; + + _printToConsole( + _typeset( + _assembleHelpStringArr(fnName) + ) + ); + + // Display a little ad or thank you note instead of "undefined" + return (pickRandomArrEl(_helpPromoMessages).val); + } + // END [[Help CLI & strings]] SECTION + + // START [[Main Actions]] SECTION + // [[Main Actions]] public API + /** + * @since 8.5.3 + * + * Hits the A/B server with a user-supplied `ir` value and prints the return value to the console, + * or an error if the call did not work, or no argument was supplied, or an invalid argument was supplied. + * Part of the public CLI. + * + * @param {Number} ir The ir value to use. Should be an integer between 1 and 100 inclusive. + * @return {Promise|String} Returns a tip string if the argument was missing or invalid. Otherwise, returns the Promise for the call to the A/B server. This Promise, once it resolves or rejects, returns a thank you message. + */ + fetchABTestsWithIr = (ir) => { + if (ir === undefined) { + Debugger._printToConsole(Debugger._typeset([ + `${CSS_SUBHEADER}Oops: required argument missing`, + 'You must provide an integer number argument between 1 and 100 inclusive', + ])); + return UP_REMINDER; + } + + if (typeof ir !== 'number') { + Debugger._printToConsole(Debugger._typeset([ + `${CSS_SUBHEADER}Oops: invalid argument type`, + 'The argument must be an integer between 1 and 100 inclusive', + ])); + return UP_REMINDER; + } + + if ((ir < 1) || (ir > 100)) { + Debugger._printToConsole(Debugger._typeset([ + `${CSS_SUBHEADER}Oops: invalid argument value`, + 'The argument must be an integer >between 1 and 100 inclusive<', + ])); + return UP_REMINDER; + } + + if (Math.floor(ir) !== ir) { + Debugger._printToConsole(Debugger._typeset([ + `${CSS_SUBHEADER}Oops: invalid argument value`, + 'The argument must be an >integer< between 1 and 100 inclusive', + ])); + return UP_REMINDER; + } + + Debugger._printToConsole(Debugger._typeset([ + 'We are about to make an async call to the A/B server. Results should appear below shortly:' + ])); + + return (abtest.silentFetch(ir) + .then((result) => { + const output = []; + if (result === 'resolved') { + output.push(`${CSS_HIGHLIGHT}The call to the A/B server with ir=${ir} succeeded`); + output.push('These are the tests that are now in memory:'); + this._push(abtest.getTests(), output); + } else { + output.push(`${CSS_HIGHLIGHT}Something went wrong with the call to the A/B server`); + output.push('If this keeps happening, we would greatly appreciate hearing about it at support@ghostery.com'); + output.push('The tests in memory were not updated, but here they are anyway just in case:'); + this._push(abtest.getTests(), output); + } + Debugger._printToConsole(Debugger._typeset(output)); + + return THANKS; + }) + .catch(() => { + const output = []; + output.push(`${CSS_HIGHLIGHT}Something went wrong with the call to the A/B server`); + output.push('If this keeps happening, we would greatly appreciate hearing about it at support@ghostery.com'); + output.push('The tests in memory were not updated, but here they are anyway just in case:'); + this._push(abtest.getTests(), output); + Debugger._printToConsole(Debugger._typeset(output)); + + return THANKS; + })); + } + + /** + * @since 8.5.3 + * + * Make a request to the CMP server for the most up-to-date campaigns info and print the result to the console, + * or an error if something went wrong with the request. Part of the public API. + * + * @return {Promise|String} The Promise for the call to the CMP server. Once the Promise resolves or rejects, it returns a thank you message. + */ + fetchCMPCampaigns = () => { + Debugger._printToConsole(Debugger._typeset([ + 'We are about to make an async call to the CMP server. Results should appear below shortly:' + ])); + + return (cmp.debugFetch() + .then((result) => { + const output = []; + + if (result.ok) { + output.push(`${CSS_HIGHLIGHT}The call to the CMP server succeeded`); + if (result.testsUpdated) { + output.push('New campaigns were found. The updated campaigns are:'); + } else { + output.push('No new campaigns were found. Here are the (unupdated) campaigns now in memory:'); + } + this._push((getObjectSlice(globals, 'SESSION').val.CMP_DATA), output); + } else { + output.push(`${CSS_HIGHLIGHT}Something went wrong with the call to the CMP server`); + output.push('If this keeps happening, we would greatly appreciate hearing about it at support@ghostery.com'); + output.push('The campaigns in memory were not updated, but here they are anyway just in case:'); + this._push((getObjectSlice(globals, 'SESSION').val.CMP_DATA), output); + } + + Debugger._printToConsole(Debugger._typeset(output)); + + return THANKS; + }) + .catch(() => { + const output = []; + output.push(`${CSS_HIGHLIGHT}Something went wrong with the call to the CMP server`); + output.push('If this keeps happening, we would greatly appreciate hearing about it at support@ghostery.com'); + output.push('The campaigns in memory were not updated, but here they are anyway just in case:'); + this._push((getObjectSlice(globals, 'SESSION').val.CMP_DATA), output); + + Debugger._printToConsole(Debugger._typeset(output)); + + return THANKS; + }) + ); + } + + /** + * @since 8.5.3 + * + * Print the AB tests currently in memory. + * + * @return {String} A thank you message. + */ + getABTests = () => { + const output = []; + const tests = abtest.getTests(); + + output.push(`${CSS_SUBHEADER}These are all the A/B tests currently in memory:`); + this._push(tests, output); + Debugger._printToConsole(Debugger._typeset(output)); + + return (THANKS); + } + + /** + * @since 8.5.3 + * + * Print the requested conf value or values (from ConfData). + * + * @param {String|RegExp} [slice] A string property key or a regexp literal intended to match a subset of properties. + * @return {String} A thank you message. + */ + getConfData = slice => this._printObjectSlice(confData, slice, 'config'); + + /** + * @since 8.5.3 + * + * Print the requested global value or values (from Globals). + * + * @param {String|RegExp} [slice] A string property key or a regexp literal intended to match a subset of properties. + * @return {String} A thank you message. + */ + getGlobals = slice => this._printObjectSlice(globals, slice, 'globals'); + + /** + * @since 8.5.3 + * + * Open the Ghostery panel window in a new tab for automation testing. Uses + * the current active tabID to populate panel data. + * + * @param {String} [mobile] Open the Android panel if value is 'mobile'. + * @return {String} A thank you message. + */ + openPanel = (mobile) => { + chrome.tabs.query({ + active: true + }, (tabs) => { + if (chrome.runtime.lastError) { + Debugger._printToConsole(Debugger._typeset([ + `${CSS_SUBHEADER}Error fetching active tab:`, + `${chrome.runtime.lastError.message}`, + ])); + } else if (tabs.length === 0) { + Debugger._printToConsole(Debugger._typeset([ + `${CSS_SUBHEADER}Error fetching active tab:`, + 'Active tab not found', + ])); + } else { + const android = (mobile.toLowerCase() === 'mobile') ? '_android' : ''; + chrome.tabs.create({ + url: chrome.runtime.getURL(`app/templates/panel${android}.html?tabId=${tabs[0].id}`), + active: true + }); + } + }); + return THANKS; + } + + /** + * @since 8.5.3 + * + * Open the Ghostery Intro Hub in a new tab for automation testing. + * + * @param {String} [modal=''] Trigger upgrade modal(s) in addition to opening the hub if the value is 'modal'. + * @return {String} A thank you message. + */ + openIntroHub = (modal = '') => { + const showModal = modal.toLowerCase() === 'modal'; + chrome.tabs.create({ + url: chrome.runtime.getURL(`./app/templates/hub.html?$justInstalled=true&pm=${showModal}`), + active: true + }); + return THANKS; + } + + /** + * @since 8.5.3 + * + * Get all info for the active tab(s), including TabInfo, FoundBugs and active tab IDs. + * + * @param {String|RegExp} [slice] Limit the debugger output to a particular slice of the activeTabInfo object. + * @return {String} A thank you message. + */ + getActiveTabInfo = (slice) => { + chrome.tabs.query({ + active: true, + }, (tabs) => { + if (chrome.runtime.lastError) { + Debugger._printToConsole(Debugger._typeset([ + `${CSS_SUBHEADER}Error fetching active tab:`, + `${chrome.runtime.lastError.message}`, + ])); + } else if (tabs.length === 0) { + Debugger._printToConsole(Debugger._typeset([ + `${CSS_SUBHEADER}Error fetching active tab:`, + 'Active tab not found', + ])); + } else { + const tabIds = tabs.map(tab => tab.id); + this._printObjectSlice({ + activeTabIds: tabIds, + tabInfo: { ...tabInfo._tabInfo }, + foundBugs: { + foundApps: { ...foundBugs._foundApps }, + foundBugs: { ...foundBugs._foundBugs }, + }, + }, slice, 'ActiveTabInfo'); + } + }); + return THANKS; + } + + /** + * @since 8.5.3 + * + * Print the logged in user's account information, settings, and subscription data to the console + * (or an error message if no user is logged in). Also print a log of account events. + * + * @return {Promise} The Promise for the calls to the account server. When the Promise fulfills, it returns a thank you message. + */ + getUserData = () => { + function _getUserCookies() { + return new Promise((resolve) => { + chrome.cookies.getAll({ + url: globals.COOKIE_URL, + }, resolve); + }); + } + + const _getUserSettings = () => new Promise(r => account.getUserSettings().catch(r).then(r)); + + const _getUserSubscriptionData = () => new Promise(r => account.getUserSubscriptionData().catch(r).then(r)); + + const _printError = (error) => { + const output = []; + + output.push(`${CSS_HIGHLIGHT}There was an error getting the user data:`); + this._push(error, output); + + Debugger._printToConsole(Debugger._typeset(output)); + + return THANKS; + }; + + const _printUserData = ([userCookies, userData, syncedUserSettings, userSubscriptionData]) => { + this._printObjectSlice({ + userCookies, + userData, + syncedUserSettings, + userSubscriptionData, + }, undefined, 'UserData'); + + return THANKS; + }; + + const _printAccountEvents = () => { + const output = []; + const accountEvents = this._accountEvents.map(([timestamp, details]) => [JSON.stringify(timestamp), details]); + + output.push("Here are the account events we've recorded during this session:"); + this._push(Object.fromEntries(accountEvents), output); + + Debugger._printToConsole(Debugger._typeset(output)); + + return THANKS; + }; + + return Promise.all([ + _getUserCookies(), + account.getUser(), + _getUserSettings(), + _getUserSubscriptionData(), + ]) + .then(data => _printUserData(data)) + .catch(error => _printError(error)) + .finally(() => _printAccountEvents()); + } + + /** + * @since 8.5.3 + * + * Force the specified promo modal to trigger once, at the next opportunity. + * + * @param {String} modalType The modal to trigger. The help output for this function shows currently valid values, which may vary over time as modals are added and removed. Valid values are also printed if an invalid one is supplied. + * @return {String} A thank you message. + */ + showPromoModal = (modalType) => { + const result = PromoModals.showOnce(modalType); + + if (result === 'success') { + const { val: cappedModalType } = capitalize(modalType.toLowerCase()); + Debugger._printToConsole( + Debugger._typeset([ + `${CSS_SUBHEADER}Success!`, + `The ${cappedModalType} modal will trigger at the next opportunity`, + ]) + ); + + return (THANKS); + } + + if (result === 'failure') { + const noDice = [ + `${CSS_SUBHEADER}No dice`, + 'That was not a valid argument. Here are the valid ones:', + '', + ...Debugger._assembleHelpStringArr('showPromoModal') + ]; + Debugger._printToConsole(Debugger._typeset(noDice)); + + return (THANKS); + } + + Debugger._printToConsole(Debugger._typeset([ + 'The function neither succeeded nor failed. If you have a minute to spare, we would greatly appreciate hearing about this likely bug at support@ghostery.com.', + ])); + + return ('Welcome to the Twilight Zone'); + } + + // [[Main Actions]] public wrt to other background code, but intentionally not fully exposed to the console + /** + * @since 8.5.3 + * + * Adds an entry to the account event log maintained by the Debugger instance, + * adding a timestamp to the provided event details. + * Not intended for use from the command line. + * + * @param {String} type The event type. For example, 'migrate'. User defined - any value is valid. + * @param {String} event What happened. For example, 'migrate start'. User defined. + * @param {*} details Additional details. For example, a cookie object associated with a cookie change event. + */ + addAccountEvent(type, event, details) { + const timestamp = new Date(); + const pushObj = { type, event }; + if (details) { + pushObj.details = details; + } + + this._accountEvents.push([timestamp, pushObj]); + + if (this._accountEvents.length > ACCOUNT_EVENTS_CAP) { + // Performance should be ok even for a somewhat large array: + // https://medium.com/@erictongs/the-best-way-to-remove-the-first-element-of-an-array-in-javascript-shift-vs-splice-694378a7b416 + this._accountEvents.shift(); + } + } + + // [[Main Actions]] private helpers + /** + * @private + * @since 8.5.3 + * + * Gets and prints the requested slice of the supplied object. + * + * @param {Object} obj The object to print a slice from. + * @param {String|RegExp} slice The specific property (if string) or property subset (if regex) slice to print. + * @param {String} objStr The name to use for the object in the output. + * @return {String} A thank you message. + */ + _printObjectSlice(obj, slice, objStr) { + const objSlice = getObjectSlice(obj, slice); + const output = []; + + if (slice === undefined) { + output.push(`${CSS_SUBHEADER}You didn't provide an argument, so here's the whole ${objStr} object:`); + } else if (typeof slice === 'string') { + if (objSlice.foundMatch) { + output.push(`${CSS_SUBHEADER}We found the property you asked for:`); + } else { + output.push(`${CSS_SUBHEADER}We did not find '${slice}' on the ${objStr} object, so here is the whole thing instead:`); + } + } else if (slice instanceof RegExp) { + if (objSlice.foundMatch) { + output.push(`${CSS_SUBHEADER}Here are the matches we found for that regex:`); + } else { + output.push(`${CSS_SUBHEADER}That regex produced no matches, so here is the whole ${objStr} object instead:`); + } + } + + this._push(objSlice.val, output); + + Debugger._printToConsole(Debugger._typeset(output)); + + return (THANKS); + } + + /** + * @private + * @since 8.5.3 + * + * Pushes the supplied object to the supplied array unaltered or JSON stringified depending on + * the Debugger instance's current object output style setting. Part of the output formatting pipeline. + * + * @param {Object} obj The object to push to the array. + * @param {Array} arr The array to push the object to. + */ + _push(obj, arr) { + if (this.settings._objectOutputStyle === OBJECT_OUTPUT_STYLE) { + arr.push(obj); + } else if (this.settings._objectOutputStyle === STRING_OUTPUT_STYLE) { + arr.push(JSON.stringify(obj)); + } + } + // END [[Main Actions]] SECTION + + // START [[Settings Actions]] SECTION + /** + * @since 8.5.3 + * + * Public CLI methods relating to debugger settings. + * + * @namespace + * @property {Function} show - Print the current debugger settings. + * @property {Function} toggleLogging - Toggle logging on and off. Overrides manifest debug setting. + * @property {Function} toggleOutputStyle - Toggle object output style between 'object' and 'string'. + * + * @type {{toggleLogging: function(*=): string, show: function(*): string, toggleOutputStyle: function(*=): string}} + */ + // The closure tricks JSDoc into documenting the inner functions as expected. + // eslint-disable-next-line arrow-body-style + settings = (() => { + return ({ + /** + * @since 8.5.3 + * + * Prints the current debugger settings. + * + * @param {String} [_updated] A setting to highlight because it was just updated. Used internally by the methods that update settings and not intended for use in the public CLI. + * @return {String} A thank you message. + */ + show: (_updated) => { + const _updatedOrCurrent = (_updated === 'logging' || _updated === 'outputStyle') ? 'Updated' : 'Current'; + const potentialLoggingHighlight = (_updated === 'logging') ? CSS_HIGHLIGHT : ''; + const potentialOutputStyleHighlight = (_updated === 'outputStyle') ? CSS_HIGHLIGHT : ''; + + const currentSettings = [ + `${CSS_MAINHEADER}${_updatedOrCurrent} Settings`, + [ + `${potentialLoggingHighlight}Logging`, + `${this.settings._isLog ? 'On' : 'Off'}` + ], + [ + `${potentialOutputStyleHighlight}Object Output Style`, + `${this.settings._objectOutputStyle === OBJECT_OUTPUT_STYLE ? 'Object' : 'String'}` + ], + ]; + + Debugger._printToConsole(Debugger._typeset(currentSettings)); + + return (THANKS); + }, + + /** + * @since 8.5.3 + * + * Toggle the logging setting, or set to optional specific value. + * Prints all settings with the updated logging setting highlighted. + * + * @param {String} [newValue] Explicitly turns logging 'on' or 'off', if provided. + * @return {String} A thank you message. + */ + toggleLogging: (newValue) => { + this._toggleSetting('on', 'off', '_isLog', newValue); + activateLog(this.settings._isLog); + return (this.settings.show('logging')); + }, + + /** + * @since 8.5.3 + * + * Toggle the output style setting, or set to optional specific value. + * Prints all settings with the updated output style setting highlighted. + * + * @param {String} [newValue] Explicitly sets output style to 'object' or 'string', if provided. + * @return {String} A thank you message. + */ + toggleOutputStyle: (newValue) => { + this._toggleSetting('object', 'string', '_objectOutputStyle', newValue); + return (this.settings.show('outputStyle')); + }, + }); + })() + + /** + * @private + * @since 8.5.3 + * + * Toggle the requested debugger setting, or set it to the optional specific requested value. + * Helper abstraction used by the public CLI's settings toggle methods. + * + * @param {String} optOn The 'activate option' string. + * @param {String} optOff The 'deactivate option' string. + * @param {String} setting The debugger setting to adjust. + * @param {String} [requested] A specific value to set the option to. Used if it matches either the optOn or the optOff value. + */ + _toggleSetting(optOn, optOff, setting, requested) { + if (typeof requested !== 'string') this.settings[setting] = !this.settings[setting]; + else if (requested.toLowerCase() === optOn) this.settings[setting] = true; + else if (requested.toLowerCase() === optOff) this.settings[setting] = false; + else this.settings[setting] = !this.settings[setting]; + } + // END [[Settings Actions]] SECTION +} + +export default new Debugger(); diff --git a/src/classes/Globals.js b/src/classes/Globals.js index bbbbe3477..99fedbe9c 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -26,7 +26,6 @@ class Globals { constructor() { // environment variables this.DEBUG = manifest.debug || false; - this.LOG = this.DEBUG && manifest.log; this.EXTENSION_NAME = manifest.name || 'Ghostery'; this.EXTENSION_VERSION = manifest.version_name || manifest.version; // Firefox does not support "version_name" this.BROWSER_INFO = { diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index cc48730f9..326c8ffcd 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -29,22 +29,60 @@ const INSIGHTS = 'insights'; const PLUS = 'plus'; const PROMO_MODAL_LAST_SEEN = 'promo_modal_last_seen'; +const PRIORITY_ORDERED_ACTIVE_MODALS = [INSIGHTS, PREMIUM]; + /** * Static 'namespace' class for handling the business logic for the display of promo modals (Premium, Insights, etc...) * @memberOf BackgroundClasses */ class PromoModals { + /** + * Tracks which promo modal, if any, should be FORCED to trigger + * at moments when a promo modal MIGHT trigger. + * Originally intended to facilitate QA of modal UI + * @type {string} + */ + static forcedModalType = ''; + + /** + * Specify a modal type that should be forced to trigger at the next opportunity + * Originally added to facilitate modal UI QA + * @param {String} modalType The modal type to trigger + * @return {String} Either 'success' or 'failure' + */ + static showOnce(modalType) { + if ( + modalType + && typeof modalType === 'string' + && PRIORITY_ORDERED_ACTIVE_MODALS.includes(modalType.toLowerCase()) + ) { + PromoModals.forcedModalType = modalType; + return 'success'; + } + + return 'failure'; + } + + static getActiveModalTypes() { + return PRIORITY_ORDERED_ACTIVE_MODALS; + } + /** * Determine if a modal should be shown. Called from PanelData * when the panel is opened. * - * @return {string} Type of promo to show + * @return {String|null} Type of promo to show, or null if we should not show a promo */ static whichPromoModalShouldWeDisplay() { + if (PRIORITY_ORDERED_ACTIVE_MODALS.includes(PromoModals.forcedModalType)) { + const type = PromoModals.forcedModalType; + PromoModals.forcedModalType = ''; + return type; + } + // The order is important - // Insights takes priority over Premium - if (this._isTimeForAPromo(INSIGHTS)) return INSIGHTS; - if (this._isTimeForAPromo(PREMIUM)) return PREMIUM; + const promoType = PRIORITY_ORDERED_ACTIVE_MODALS.find(poam => this._isTimeForAPromo(poam)); + if (promoType !== undefined) return promoType; return null; } diff --git a/src/utils/common.js b/src/utils/common.js index d947d25a3..d2f1e78e6 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -16,20 +16,41 @@ // DO NOT IMPORT MODULES TO THIS FILE -const LOG = chrome.runtime.getManifest().log || false; +// Private variable that controls whether calls to log() produce the requested console output or do nothing +let _shouldLog = chrome.runtime.getManifest().debug || false; /** - * Custom Debug Logger. + * Report whether logging is active + * @memberOf BackgroundUtils + * + * @return {Boolean} True if logging is active and false otherwise + */ +export function isLog() { + return _shouldLog; +} + +/** + * Activate / deactivate logging + * Allows modules like the console debugger to override the manifest debug setting + * @memberOf BackgroundUtils + * + * @param {Boolean} shouldActivate Whether logging should be activated or deactivated. Optional; defaults to true + * + * @return {undefined} No explicit return value + */ +export function activateLog(shouldActivate = true) { + _shouldLog = shouldActivate; +} + +/** + * Log to console regardless of log settings * @memberOf BackgroundUtils * * @param {array} args ES6 Rest parameter * - * @return {boolean} false if disabled, otherwise true + * @return {boolean} Always true */ -export function log(...args) { - if (!LOG) { - return false; - } +export function alwaysLog(...args) { // check for error messages const hasErrors = args.toString().toLowerCase().includes('error'); // add timestamp to first position @@ -44,6 +65,24 @@ export function log(...args) { return true; } +/** + * Custom Debug Logger. + * Unliked alwaysLog, only logs if logging is turned on + * through the manifest and/or GhosteryDebugger + * @memberOf BackgroundUtils + * + * @param {array} args ES6 Rest parameter + * + * @return {boolean} false if disabled, otherwise true + */ +export function log(...args) { + if (!_shouldLog) { + return false; + } + + return alwaysLog(...args); +} + /** * Get multiple prefs. * @memberOf BackgroundUtils diff --git a/src/utils/utils.js b/src/utils/utils.js index d4e4e3c81..866641db3 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -241,7 +241,7 @@ export function getActiveTab(callback, error) { } } else if (tabs.length === 0) { if (error && typeof error === 'function') { - error(); + error({ message: 'Active tab not found' }); } } else if (callback && typeof callback === 'function') { callback(tabs[0]); @@ -302,6 +302,8 @@ function _openNewTab(data) { active: data.become_active || false }); } + }, (err) => { + log(`_openNewTab Error: ${err}`); }); } /** @@ -541,6 +543,184 @@ export function fetchLocalJSONResource(url) { })); } +/** + * Like Array.prototype.slice(), but for objects. + * Get a property on an object (if props is a property key string), + * OR Get a subset of properties (if props is a regex), + * OR Get the whole supplied object back (if props is missing, invalid, or produces no matches). + * If the property is not defined on the object, + * returns the whole object as the val prop of the return object. + * + * @memberOf BackgroundUtils + * + * @param {Object} obj The object to extract a property or properties from + * @param {string|RegExp} props String name of the property, or regex to match against all properties. Optional. + * @return {Object} { val: undefined|Object, foundMatch: Boolean, err: Boolean, errMsg: undefined|String } + */ +export function getObjectSlice(obj, props) { + if (obj === undefined || typeof obj !== 'object') { + return ({ + val: undefined, + foundMatch: false, + err: true, + errMsg: 'You must provide an object as the first argument', + }); + } + + if (props === undefined) { + return ({ + val: obj, + foundMatch: false, + err: false, + errMsg: undefined, + }); + } + + if ((typeof props !== 'string') && !(props instanceof RegExp)) { + return ({ + val: obj, + foundMatch: false, + err: true, + errMsg: 'The second argument must be either a property name string, or a regex. Returning whole object.' + }); + } + + if (typeof props === 'string') { + if (obj[props] === undefined) { + return ({ + val: obj, + foundMatch: false, + err: false, + errMsg: undefined, + }); + } + + return ({ + val: { [props]: obj[props] }, + foundMatch: true, + err: false, + errMsg: undefined, + }); + } + + // A regex literal has been passed in + if (props instanceof RegExp) { + const matches = {}; + Object.keys(obj).forEach((key) => { + if (props.test(key)) { + matches[key] = obj[key]; + } + }); + if (Object.keys(matches).length > 0) { + return ({ + val: matches, + foundMatch: true, + err: false, + errMsg: undefined, + }); + } + } + + return ({ + val: obj, + foundMatch: false, + err: false, + errMsg: undefined, + }); +} + +/** + * Pick a random element from the array argument. + * If no argument is provided, if the argument is not an array, or if the argument array is empty, + * the err property on the return object is set to true, errMsg has details, and val is left undefined. + * Otherwise, err is false, errMsg is undefined, and val contains the randomly picked element. + * + * @memberOf BackgroundUtils + * + * @param {Array} arr The array to pick a value from. Elements can be of any type(s) + * @return {Object} { val: *, err: Boolean, errMsg: undefined|String } + */ +export function pickRandomArrEl(arr) { + if (arr === undefined) { + return ({ + err: true, + errMsg: 'Undefined or no argument provided', + val: undefined, + }); + } + + if (!Array.isArray(arr)) { + return ({ + err: true, + errMsg: 'The argument must be an array', + val: undefined, + }); + } + + const len = arr.length; + + if (len === 0) { + return ({ + err: true, + errMsg: 'It is beyond the power of pickRandomArrEl to pick a random element from an empty array', + val: undefined, + }); + } + + return ({ + err: false, + errMsg: undefined, + val: arr[Math.floor((Math.random() * len))], + }); +} + +/** + * Uppercase the first character of each word in the phrase argument. + * Assumes that words are space-separated by default, + * but accepts an optional custom string separator argument. + * + * @memberOf BackgroundUtils + * + * @param {String} phrase The string to capitalize. + * @param {String|undefined} separator Separator string. Optional; defaults to a space. + * @return {Object} { val: String|undefined, err: Boolean, errMsg: undefined|String } + */ +export function capitalize(phrase, separator = ' ') { + if (phrase === undefined) { + return ({ + err: true, + errMsg: 'Undefined or no argument provided', + val: undefined, + }); + } + + if (typeof phrase !== 'string') { + return ({ + err: true, + errMsg: 'The first argument must be a string', + val: undefined, + }); + } + + if (typeof separator !== 'string') { + return ({ + err: true, + errMsg: 'The second argument is optional, but must be a string if provided', + val: undefined, + }); + } + + const words = phrase.split(separator); + const trimmedWords = words.map(word => word.trim()); + const capitalizedWords = trimmedWords.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`); + + return ({ + err: false, + errMsg: undefined, + val: capitalizedWords.join(separator), + }); +} + /** * Inject content scripts and CSS into a given tabID (top-level frame only). * Note: Chrome 61 blocks content scripts on the new tab page (_/chrome/newtab). Be From e4b0a0b9c653455a91c8e4268ac28a382f1948fe Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Thu, 24 Sep 2020 15:51:10 -0400 Subject: [PATCH 23/35] ignore purplebox creation and updates on Android --- src/classes/EventHandlers.js | 4 ++-- src/classes/PurpleBox.js | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/classes/EventHandlers.js b/src/classes/EventHandlers.js index 1f950675d..77d9b1ed0 100644 --- a/src/classes/EventHandlers.js +++ b/src/classes/EventHandlers.js @@ -495,7 +495,7 @@ class EventHandlers { } const appWithLatencyId = latency.logLatency(details); - if (appWithLatencyId) { + if (appWithLatencyId && conf.show_alert) { this.purplebox.updateBox(details.tabId, appWithLatencyId); } } @@ -515,7 +515,7 @@ class EventHandlers { if (details.type !== 'main_frame') { const appWithLatencyId = latency.logLatency(details); - if (appWithLatencyId) { + if (appWithLatencyId && conf.show_alert) { this.purplebox.updateBox(details.tabId, appWithLatencyId); } } diff --git a/src/classes/PurpleBox.js b/src/classes/PurpleBox.js index 685c3b72b..8d0c76a54 100644 --- a/src/classes/PurpleBox.js +++ b/src/classes/PurpleBox.js @@ -46,7 +46,8 @@ class PurpleBox { if (!conf.show_alert || globals.SESSION.paused_blocking || (conf.hide_alert_trusted && !!Policy.checkSiteWhitelist(tab.url)) || - !tab || tab.purplebox || tab.path.includes('_/chrome/newtab') || tab.protocol === 'about' || globals.EXCLUDES.includes(tab.host)) { + !tab || tab.purplebox || tab.path.includes('_/chrome/newtab') || tab.protocol === 'about' || globals.EXCLUDES.includes(tab.host) || + globals.BROWSER_INFO.os === 'android') { return Promise.resolve(false); } @@ -135,7 +136,7 @@ class PurpleBox { } /** - * Update the purple box with new bugs. Called from 'processBug' + * Update the purple box with new bugs. Called from EventHandlers * @param {number} tab_id tab id * @param {number} app_id tracker id */ @@ -143,7 +144,7 @@ class PurpleBox { const tab = tabInfo.getTabInfo(tab_id); const apps = foundBugs.getApps(tab_id, true, tab.url, app_id); // prefetching and purplebox are already checked in background.js - if (!apps || apps.length === 0 || globals.EXCLUDES.includes(tab.host)) { + if (!apps || apps.length === 0 || globals.EXCLUDES.includes(tab.host) || globals.BROWSER_INFO.os === 'android') { return false; } From 443ca8ae72e8c71dcc94d259e963f5a14b90db7b Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Thu, 24 Sep 2020 16:10:13 -0400 Subject: [PATCH 24/35] fixing bug in debugger openPanel --- src/classes/Debugger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/Debugger.js b/src/classes/Debugger.js index 3c55d95de..64f204d07 100644 --- a/src/classes/Debugger.js +++ b/src/classes/Debugger.js @@ -781,7 +781,7 @@ class Debugger { 'Active tab not found', ])); } else { - const android = (mobile.toLowerCase() === 'mobile') ? '_android' : ''; + const android = (mobile && mobile.toLowerCase() === 'mobile') ? '_android' : ''; chrome.tabs.create({ url: chrome.runtime.getURL(`app/templates/panel${android}.html?tabId=${tabs[0].id}`), active: true From 7f975d19363ecc70447fc9e4022344daaff24448 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Thu, 24 Sep 2020 16:44:00 -0400 Subject: [PATCH 25/35] GH-2159: Update frequency reduction, AB test opt-out, remove Cliqz AB test (#608) * Reduce scheduledTasks and autoUpdatedBugDb frequencies from once an hour to once a day * Refactor autoUpdateBugDb so it always does the same thing when called with the same argument * Update autoUpdateBugDB documentation and refactor a little more to make sure it always does the same thing when called with the same args * Clean up autoUpdateBugDB a bit more: Clarify docs, flatten, and move log statement to make sure that it is triggered every time the function is called, since the message just says that the function was called * Minor comment in autoUpdateBugDb * Renamed autoUpdateBugDb to clarify connection to checkLibraryVersion. Tweak docs. * Edit db update function names and docs to improve clarity * Remove Cliqz anti-tracking A/B test config code from background. Tidy up setupAbTest * Split up and reorganize setupABTest so outside code can only call those parts of the original it actually needs. * Clean up OptIn a bit in preparation for adding A/B test checkbox * Finish OptIn tidy-up * Stub out new A/B test opt-in setting. Add draft strings for it. * Implement the enable_abtests conf setting and plug it into the checkbox stub in OptIn * Do not call setupABTests if enable_abtests is false * Skip the call to the AB server altogether if ab tests are disabled * Update AB test setting tooltip copy. remove enable_abtests from uninstall URL. Add ts metrics query string param for whether tests are enabled. Disabled sending other AB test query string params unless tests are enabled. * Remove unused import from Metrics * Update test snapshot * Fix typo * Fix comment errors * Remove bloom filter Cliqz test * remove comment Co-authored-by: Christopher Tino Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- _locales/en/messages.json | 6 + app/panel/components/Settings/OptIn.jsx | 117 +++++++++------ .../__tests__/__snapshots__/OptIn.jsx.snap | 64 ++++++++ src/background.js | 141 ++++++++---------- src/classes/ConfData.js | 1 + src/classes/Globals.js | 1 + src/classes/Metrics.js | 13 +- src/classes/PanelData.js | 3 +- 8 files changed, 220 insertions(+), 126 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 673b8fc22..ceb9eafaf 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -947,6 +947,9 @@ "settings_allow_offers": { "message": "Participating in Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "Participating in A/B Tests" + }, "settings_signin_create_header": { "message": "Sign In / Create Account" }, @@ -1037,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards is a private-by-design feature that delivers you discounts and special offers from our partner companies as you browse." }, + "settings_abtests_tooltip": { + "message": "Participating in randomized A/B tests helps Ghostery understand which version of a new layout or feature users like you prefer." + }, "settings_opt_in": { "message": "Opt In / Out" }, diff --git a/app/panel/components/Settings/OptIn.jsx b/app/panel/components/Settings/OptIn.jsx index f74262c0b..e84546329 100644 --- a/app/panel/components/Settings/OptIn.jsx +++ b/app/panel/components/Settings/OptIn.jsx @@ -18,62 +18,84 @@ import globals from '../../../../src/classes/Globals'; const { IS_CLIQZ, BROWSER_INFO } = globals; const IS_ANDROID = (BROWSER_INFO.os === 'android'); +const TOOLTIP_SVG_FILEPATH = '../../app/images/panel/icon-information-tooltip.svg'; + /** * @class Implement Opt In subview as a React component. * The view opens from the left-side menu of the main Settings view. * It invites user to opt in for telemetry options, human web and offers * @memberOf SettingsComponents */ -const OptIn = ({ settingsData, toggleCheckbox }) => ( -
    -
    -
    -

    { t('settings_support_ghostery') }

    -
    - { t('settings_support_ghostery_by') } - : -
    -
    -
    - - -
    - -
    -
    +const OptIn = ({ settingsData, toggleCheckbox }) => { + const checkbox = (opt, name) => ( + + ); + + const labelFor = (opt, text) => ( + + ); + + const tooltipSVG = (text, dir) => ( +
    + +
    + ); + + const option = (cbox, label, tooltip, id = '') => ( +
    +
    + {cbox} + {label} + {tooltip} +
    +
    + ); + + return ( +
    +
    +
    +

    {t('settings_support_ghostery')}

    +
    + {t('settings_support_ghostery_by')} + : +
    + {option( + checkbox('share-usage', 'enable_metrics'), + labelFor('share-usage', t('settings_share_usage')), + tooltipSVG(t('settings_share_usage_tooltip'), 'down') + )} + {!IS_CLIQZ && option( + checkbox('share-human-web', 'enable_human_web'), + labelFor('share-human-web', t('settings_share_human_web')), + tooltipSVG(t('settings_human_web_tooltip'), 'up'), + 'human-web-section' + )} + {!IS_CLIQZ && !IS_ANDROID && option( + checkbox('allow-offers', 'enable_offers'), + labelFor('allow-offers', t('settings_allow_offers')), + tooltipSVG(t('settings_offers_tooltip'), 'up'), + 'offers-section' + )} + {option( + checkbox('allow-abtests', 'enable_abtests'), + labelFor('allow-abtests', t('settings_allow_abtests')), + tooltipSVG(t('settings_abtests_tooltip'), 'up'), + 'abtests-section' + )}
    - {!IS_CLIQZ && ( -
    -
    - - -
    - -
    -
    -
    - )} - {!IS_CLIQZ && !IS_ANDROID && ( -
    -
    - - -
    - -
    -
    -
    - )}
    -
    -); + ); +}; OptIn.propTypes = { toggleCheckbox: PropTypes.func.isRequired, @@ -81,6 +103,7 @@ OptIn.propTypes = { enable_metrics: PropTypes.bool.isRequired, enable_human_web: PropTypes.bool.isRequired, enable_offers: PropTypes.bool.isRequired, + enable_abtests: PropTypes.bool.isRequired, }).isRequired, }; diff --git a/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap index 3953f909d..c30d04ec8 100644 --- a/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap +++ b/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap @@ -19,6 +19,7 @@ exports[`app/panel/Settings/OptIn.jsx Snapshot tests with react-test-renderer Op
    +
    +
    + + +
    + +
    +
    +
    @@ -136,6 +168,7 @@ exports[`app/panel/Settings/OptIn.jsx Snapshot tests with react-test-renderer Op
    +
    +
    + + +
    + +
    +
    +
    diff --git a/src/background.js b/src/background.js index e8d463035..80e3e63a3 100644 --- a/src/background.js +++ b/src/background.js @@ -72,6 +72,8 @@ const IS_FIREFOX = (BROWSER_INFO.name === 'firefox'); const IS_ANDROID = (BROWSER_INFO.os === 'android'); const VERSION_CHECK_URL = `${CDN_BASE_URL}/update/version`; const REAL_ESTATE_ID = 'ghostery'; +const ONE_DAY_MSEC = 86400000; +const ONE_HOUR_MSEC = 3600000; const onBeforeRequest = events.onBeforeRequest.bind(events); const { onHeadersReceived } = Events; @@ -107,12 +109,12 @@ function setCliqzModuleEnabled(module, enabled) { /** * Pulls down latest version.json and triggers - * updates of all db files. + * updates of all db files. FKA checkLibraryVersion. * @memberOf Background * * @return {Promise} database updated data */ -function checkLibraryVersion() { +function updateDBs() { return new Promise(((resolve, reject) => { const failed = { success: false, updated: false }; utils.getJson(VERSION_CHECK_URL).then((data) => { @@ -139,25 +141,33 @@ function checkLibraryVersion() { }); }); }).catch((err) => { - log('Error in checkLibraryVersion', err); + log('Error in updateDBs', err); reject(failed); }); })); } /** - * Check and fetch a new tracker library every hour as needed + * Call updateDBs if auto updating is enabled and enough time has passed since the last check. + * Debug log that the function was called and when. Called at browser startup and at regular intervals thereafter. + * * @memberOf Background + * + * @param {Boolean} isAutoUpdateEnabled Whether bug db auto updating is enabled. + * @param {Number} bugsLastCheckedMsec The Unix msec timestamp to check against to make sure it is not too soon to call updateDBs again. */ -function autoUpdateBugDb() { - if (conf.enable_autoupdate) { - const result = conf.bugs_last_checked; - const nowTime = Number((new Date()).getTime()); - // offset by 15min so that we don't double fetch - if (!result || nowTime > (Number(result) + 900000)) { - log('autoUpdateBugDb called', new Date()); - checkLibraryVersion(); - } +function autoUpdateDBs(isAutoUpdateEnabled, bugsLastCheckedMsec) { + const date = new Date(); + + log('autoUpdateDBs called', date); + + if (!isAutoUpdateEnabled) return; + + if ( + !bugsLastCheckedMsec // the value is 0, signifying that we have never checked yet + || date.getTime() > (Number(bugsLastCheckedMsec) + ONE_HOUR_MSEC) // guard against double fetching + ) { + updateDBs(); } } @@ -961,7 +971,7 @@ function onMessageHandler(request, sender, callback) { return true; } if (name === 'update_database') { - checkLibraryVersion().then((result) => { + updateDBs().then((result) => { callback(result); }); return true; @@ -1082,38 +1092,6 @@ function onMessageHandler(request, sender, callback) { return false; } -/** - * Determine Antitracking configuration parameters based - * on the results returned from the abtest endpoint. - * @memberOf Background - * - * @return {Object} Antitracking configuration parameters - */ -function getAntitrackingTestConfig() { - if (abtest.hasTest('antitracking_full')) { - return { - qsEnabled: true, - telemetryMode: 2, - }; - } - if (abtest.hasTest('antitracking_half')) { - return { - qsEnabled: true, - telemetryMode: 1, - }; - } - if (abtest.hasTest('antitracking_collect')) { - return { - qsEnabled: false, - telemetryMode: 1, - }; - } - return { - qsEnabled: true, - telemetryMode: 1, - }; -} - /** * Set option for Hub Layout A/B test based * on the results returned from the abtest endpoint. @@ -1133,29 +1111,35 @@ function setupHubLayoutABTest() { } /** - * Adjust antitracking parameters based on the current state - * of ABTest and availability of Human Web. + * Configure A/B tests based on data fetched from the A/B server + * @memberOf Background */ -function setupABTest() { - const antitrackingConfig = getAntitrackingTestConfig(); - if (antitrackingConfig && conf.enable_anti_tracking) { - if (!conf.enable_human_web) { - // force disable anti-tracking telemetry on humanweb opt-out - antitrackingConfig.telemetryMode = 0; - } - Object.keys(antitrackingConfig).forEach((opt) => { - const val = antitrackingConfig[opt]; - log('antitracking', 'set config option', opt, val); - antitracking.action('setConfigOption', opt, val); - }); - } - if (abtest.hasTest('antitracking_whitelist2')) { - cliqz.prefs.set('attrackBloomFilter', false); - } - +function setupABTests() { setupHubLayoutABTest(); } +/** + * @since 8.5.3 + * + * Update config options for the Cliqz antitracking module to match the current human web setting. + * Log out the updates. Returns without doing anything if antitracking is disabled. + * + * @param {Boolean} isAntitrackingEnabled Whether antitracking is currently enabled. + */ +function setCliqzAntitrackingConfig(isAntitrackingEnabled) { + if (!isAntitrackingEnabled) return; + + const antitrackingConfig = { + qsEnabled: true, + telemetryMode: conf.enable_human_web ? 1 : 0, + }; + + Object.entries(antitrackingConfig).forEach(([opt, val]) => { + log('antitracking', 'set config option', opt, val); + antitracking.action('setConfigOption', opt, val); + }); +} + /** * Initialize Dispatcher Events. * All Conf properties trigger a dispatcher pub event @@ -1182,7 +1166,7 @@ function initializeDispatcher() { dispatcher.on('conf.save.enable_human_web', (enableHumanWeb) => { if (!IS_CLIQZ) { setCliqzModuleEnabled(humanweb, enableHumanWeb).then(() => { - setupABTest(); + setCliqzAntitrackingConfig(conf.enable_anti_tracking); }); } else { setCliqzModuleEnabled(humanweb, false); @@ -1208,7 +1192,11 @@ function initializeDispatcher() { }); dispatcher.on('conf.save.enable_anti_tracking', (enableAntitracking) => { if (!IS_CLIQZ) { - setCliqzModuleEnabled(antitracking, enableAntitracking); + setCliqzModuleEnabled(antitracking, enableAntitracking).then(() => { + // enable_human_web could have been toggled while antitracking was off, + // so we want to make sure to update the antitracking telemetry option + setCliqzAntitrackingConfig(conf.enable_anti_tracking); + }); } else { setCliqzModuleEnabled(antitracking, false); } @@ -1710,11 +1698,10 @@ function initializeGhosteryModules() { // auto-fetch from CMP cmp.fetchCMPData(); - if (!IS_CLIQZ) { - // auto-fetch human web offer + if (!IS_CLIQZ && conf.enable_abtests) { abtest.fetch() .then(() => { - setupABTest(); + setupABTests(); }) .catch(() => { log('Unable to reach abtest server'); @@ -1726,13 +1713,17 @@ function initializeGhosteryModules() { }); } - // Check CMP and ABTest every hour. - setInterval(scheduledTasks, 3600000); + // Check CMP and ABTest every day. + setInterval(scheduledTasks, ONE_DAY_MSEC); // Update db right away. - autoUpdateBugDb(); - // Schedule it to run every hour. - setInterval(autoUpdateBugDb, 3600000); + autoUpdateDBs(conf.enable_autoupdate, conf.bugs_last_checked); + + // Schedule it to run every day. + setInterval( + () => autoUpdateDBs(conf.enable_autoupdate, conf.bugs_last_checked), + ONE_DAY_MSEC + ); // listen for changes to specific conf properties initializeDispatcher(); diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index f50f64145..990db8cd4 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -113,6 +113,7 @@ class ConfData { _initProperty('enable_human_web', !IS_CLIQZ && !IS_FIREFOX); _initProperty('enable_metrics', false); _initProperty('enable_offers', !IS_CLIQZ && !IS_FIREFOX && !IS_ANDROID); + _initProperty('enable_abtests', true); _initProperty('enable_smart_block', true); _initProperty('expand_all_trackers', true); _initProperty('hide_alert_trusted', false); diff --git a/src/classes/Globals.js b/src/classes/Globals.js index 99fedbe9c..8040ed8f5 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -110,6 +110,7 @@ class Globals { 'enable_human_web', 'enable_metrics', 'enable_offers', + 'enable_abtests', 'enable_smart_block', 'expand_all_trackers', 'hide_alert_trusted', diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 2efe7b1a8..9907ad27c 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -369,12 +369,19 @@ class Metrics { `&th=${encodeURIComponent(Metrics._getThemeValue().toString())}` + // New parameters for Ghostery 8.5.2 - // Hub Layout View - `&t2=${encodeURIComponent(Metrics._getHubLayoutView().toString())}` + // Subscription Interval `&si=${encodeURIComponent(Metrics._getSubscriptionInterval().toString())}` + // Product ID Parameter - `&pi=${encodeURIComponent('gbe')}`; + `&pi=${encodeURIComponent('gbe')}` + + + // New parameter for Ghostery 8.5.3 + // AB tests enabled? + `&ts=${encodeURIComponent(conf.enable_abtests ? '1' : '0')}`; + + if (conf.enable_abtests) { + // Hub Layout A/B test. Added in 8.5.3. GH-2097, GH-2100 + metrics_url += `&t2=${encodeURIComponent(Metrics._getHubLayoutView().toString())}`; + } if (CAMPAIGN_METRICS.includes(type)) { // only send campaign attribution when necessary diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index ef103dca6..eb2734ff3 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -501,7 +501,7 @@ class PanelData { const { alert_bubble_pos, alert_bubble_timeout, block_by_default, cliqz_adb_mode, enable_autoupdate, enable_click2play, enable_click2play_social, enable_human_web, enable_offers, - enable_metrics, hide_alert_trusted, ignore_first_party, notify_library_updates, + enable_metrics, enable_abtests, hide_alert_trusted, ignore_first_party, notify_library_updates, notify_promotions, notify_upgrade_updates, selected_app_ids, show_alert, show_badge, show_cmp, show_tracker_urls, toggle_individual_trackers } = userSettingsSource; @@ -517,6 +517,7 @@ class PanelData { enable_human_web, enable_offers, enable_metrics, + enable_abtests, hide_alert_trusted, ignore_first_party, notify_library_updates, From 653b7d4fc31242bbb3932032fd4e42bb62bf3585 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Thu, 24 Sep 2020 21:59:28 -0400 Subject: [PATCH 26/35] GH-2105: Sync button hover states in the hub (#607) * Add and use mixin for button hover backgrounds in the hub * Remove mixin...Use sass * Use color variable in hub Co-authored-by: wlycdgr --- app/hub/Views/HomeView/HomeView.scss | 9 --------- app/hub/Views/UpgradePlanView/UpgradePlanView.jsx | 6 +++--- app/hub/Views/UpgradePlanView/UpgradePlanView.scss | 5 +---- app/scss/hub.scss | 12 ++++++++++++ app/scss/partials/_colors.scss | 1 + 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index fd556f407..1e0d4a1fc 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -16,15 +16,6 @@ padding-top: 45px; padding-bottom: 25px; color: $tundora; - .button { - &:not(.hollow):hover { - background-color: #0078CA; - } - &.hollow:hover { - color: #0078CA; - border-color: #0078CA; - } - } } .HomeView--bolded { font-weight: 700; diff --git a/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx b/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx index 3a3608965..0a226c0c9 100644 --- a/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx +++ b/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx @@ -89,7 +89,7 @@ const basicCard = () => (

    {t('hub_upgrade_plan_free')}

    - + {t('hub_upgrade_already_protected')}

    {t('hub_upgrade_basic_protection')}

    @@ -405,7 +405,7 @@ const UpgradePlanView = (props) => { - + {t('hub_upgrade_already_protected')} @@ -487,7 +487,7 @@ const UpgradePlanView = (props) => {
    - + {t('hub_upgrade_already_protected')} diff --git a/app/hub/Views/UpgradePlanView/UpgradePlanView.scss b/app/hub/Views/UpgradePlanView/UpgradePlanView.scss index 58d6fda86..0d75ecd9e 100644 --- a/app/hub/Views/UpgradePlanView/UpgradePlanView.scss +++ b/app/hub/Views/UpgradePlanView/UpgradePlanView.scss @@ -550,11 +550,8 @@ section.home-template .section.section-pricing { text-transform: none; white-space: nowrap; font-family: 'Open Sans'; - background-color: $price-blue; font-weight: 600; - &:hover { - background-color: $price-blue-hover; - } + background-color: $price-blue; } .button-gold { diff --git a/app/scss/hub.scss b/app/scss/hub.scss index 3cd030117..b25b44a08 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -11,6 +11,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ +// Import Global Partials +@import './partials/colors'; + html, body, #root { height: 100%; width: 100%; @@ -41,6 +44,15 @@ html, body, #root { line-height: 1.3; text-transform: uppercase; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12); + &.primary { + &:not(.hollow):hover { + background-color: $dark-ghosty-blue; + } + &.hollow:hover, &.hollow:focus { + border-color: $dark-ghosty-blue; + color: $dark-ghosty-blue; + } + } } // Helper Classes diff --git a/app/scss/partials/_colors.scss b/app/scss/partials/_colors.scss index c9152e37d..cd15e484b 100644 --- a/app/scss/partials/_colors.scss +++ b/app/scss/partials/_colors.scss @@ -41,6 +41,7 @@ $link-blue: #2092BF; //primary-color $button-primary: #3AA2CF; $dark-cyan-blue: #325e97; //insights modal border $baby-blue: #DAF4FF; //plus-upgrade icon +$dark-ghosty-blue: #0078CA; //button primary hover color in the hub /* GREENS */ $spring-green: #6aa103; From 52f1a50a2064b1f9c0181b7e8e365f1498b57331 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Mon, 28 Sep 2020 17:39:25 -0400 Subject: [PATCH 27/35] GH-2158: Subscription_type ping parameter not sending on login (#606) * Move sign_in_success ping to Account#getUserSubscriptionData to fix race condition * Fix linter Co-authored-by: wlycdgr Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- src/background.js | 3 --- src/classes/Account.js | 8 ++++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/background.js b/src/background.js index 80e3e63a3..88a71298e 100644 --- a/src/background.js +++ b/src/background.js @@ -836,9 +836,6 @@ function onMessageHandler(request, sender, callback) { const { email, password } = message; account.login(email, password) .then((response) => { - if (!response.hasOwnProperty('errors')) { - metrics.ping('sign_in_success'); - } callback(response); }) .catch((err) => { diff --git a/src/classes/Account.js b/src/classes/Account.js index e7b58afad..28ac75ab8 100644 --- a/src/classes/Account.js +++ b/src/classes/Account.js @@ -22,6 +22,7 @@ import conf from './Conf'; import dispatcher from './Dispatcher'; import { log } from '../utils/common'; import Api from '../utils/api'; +import metrics from './Metrics'; import ghosteryDebugger from './Debugger'; const api = new Api(); @@ -87,7 +88,7 @@ class Account { ghosteryDebugger.addAccountEvent('login', 'cookie set by fetch POST'); this._getUserIDFromCookie().then((userID) => { this._setAccountInfo(userID); - this.getUserSubscriptionData(); + this.getUserSubscriptionData({ calledFrom: 'login' }); }); return {}; }); @@ -168,7 +169,7 @@ class Account { /** * @return {array} All subscriptions the user has, empty if none */ - getUserSubscriptionData = () => ( + getUserSubscriptionData = options => ( this._getUserID() .then(userID => api.get('stripe/customers', userID, 'cards,subscriptions')) .then((res) => { @@ -195,6 +196,9 @@ class Account { this._setSubscriptionData(plusSubscription); } } + if (options && options.calledFrom === 'login') { + metrics.ping('sign_in_success'); + } return subscriptions; }) From 73318a0ec88f79f21bc1d9be4cc21761daf1c05a Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Wed, 30 Sep 2020 12:05:56 -0400 Subject: [PATCH 28/35] GH-2177: Remove broken page pings (#609) * Remove broken page functionality implemented in GH-1763 * Remove unused imports Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- app/panel/reducers/blocking.js | 1 - app/panel/reducers/summary.js | 1 - app/panel/utils/blocking.js | 5 +- src/background.js | 2 - src/classes/EventHandlers.js | 8 +-- src/classes/Globals.js | 7 --- src/classes/Metrics.js | 91 +-------------------------------- src/classes/PanelData.js | 9 ---- src/classes/PolicySmartBlock.js | 2 - 9 files changed, 3 insertions(+), 123 deletions(-) diff --git a/app/panel/reducers/blocking.js b/app/panel/reducers/blocking.js index 597645a80..cbbc6243b 100644 --- a/app/panel/reducers/blocking.js +++ b/app/panel/reducers/blocking.js @@ -108,7 +108,6 @@ const _updateTrackerTrustRestrict = (state, action) => { sendMessage('setPanelData', { site_specific_unblocks: updated_site_specific_unblocks, site_specific_blocks: updated_site_specific_blocks, - brokenPageMetricsTrackerTrustOrUnblock: msg.trust || (!msg.trust && !msg.restrict), }); return { diff --git a/app/panel/reducers/summary.js b/app/panel/reducers/summary.js index b459ca57c..514cc8e4c 100644 --- a/app/panel/reducers/summary.js +++ b/app/panel/reducers/summary.js @@ -118,7 +118,6 @@ const _updateSitePolicy = (state, action) => { sendMessage('setPanelData', { site_whitelist: updated_whitelist, site_blacklist: updated_blacklist, - brokenPageMetricsWhitelistSite: updated_site_policy === 2, }); return { diff --git a/app/panel/utils/blocking.js b/app/panel/utils/blocking.js index ab2d7b0c9..d3166355d 100644 --- a/app/panel/utils/blocking.js +++ b/app/panel/utils/blocking.js @@ -214,10 +214,7 @@ export function updateTrackerBlocked(state, action) { }); // persist to background - sendMessage('setPanelData', { - selected_app_ids: updated_app_ids, - brokenPageMetricsTrackerTrustOrUnblock: !blocked - }); + sendMessage('setPanelData', { selected_app_ids: updated_app_ids }); return { categories: updated_categories, diff --git a/src/background.js b/src/background.js index 88a71298e..1644488a0 100644 --- a/src/background.js +++ b/src/background.js @@ -1214,8 +1214,6 @@ function initializeDispatcher() { log('Conf value changed for a watched user setting:', key); }, 200)); dispatcher.on('globals.save.paused_blocking', () => { - // if user has paused Ghostery, suspect broken page - if (globals.SESSION.paused_blocking) { metrics.handleBrokenPageTrigger(globals.BROKEN_PAGE_PAUSE); } // update content script state when blocking is paused/unpaused cliqz.modules.core.action('refreshAppState'); }); diff --git a/src/classes/EventHandlers.js b/src/classes/EventHandlers.js index 77d9b1ed0..ccba49597 100644 --- a/src/classes/EventHandlers.js +++ b/src/classes/EventHandlers.js @@ -25,7 +25,6 @@ import conf from './Conf'; import foundBugs from './FoundBugs'; import globals from './Globals'; import latency from './Latency'; -import metrics from './Metrics'; import panelData from './PanelData'; import Policy, { BLOCK_REASON_SS_UNBLOCKED, BLOCK_REASON_C2P_ALLOWED_THROUGH } from './Policy'; import PolicySmartBlock from './PolicySmartBlock'; @@ -109,7 +108,6 @@ class EventHandlers { if (frameId === 0) { // update reload info before creating/clearing tab info if (transitionType === 'reload' && !transitionQualifiers.includes('forward_back')) { - metrics.handleBrokenPageTrigger(globals.BROKEN_PAGE_REFRESH); tabInfo.setTabInfo(tabId, 'numOfReloads', tabInfo.getTabInfo(tabId, 'numOfReloads') + 1); } else if (transitionType !== 'auto_subframe' && transitionType !== 'manual_subframe') { tabInfo.setTabInfo(tabId, 'reloaded', false); @@ -539,11 +537,7 @@ class EventHandlers { * * @param {Object} tab Details of the tab that was created */ - static onTabCreated(tab) { - const { url } = tab; - - metrics.handleBrokenPageTrigger(globals.BROKEN_PAGE_NEW_TAB, url); - } + static onTabCreated() {} /** * Handler for tabs.onActivated event. diff --git a/src/classes/Globals.js b/src/classes/Globals.js index 8040ed8f5..c98ed82be 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -73,13 +73,6 @@ class Globals { this.BLACKLISTED = 1; this.WHITELISTED = 2; - // Broken page metrics named constants - this.BROKEN_PAGE_REFRESH = 1; - this.BROKEN_PAGE_WHITELIST = 2; - this.BROKEN_PAGE_PAUSE = 3; - this.BROKEN_PAGE_TRACKER_TRUST_OR_UNBLOCK = 4; - this.BROKEN_PAGE_NEW_TAB = 5; - // data stores this.REDIRECT_MAP = new Map(); this.BLOCKED_REDIRECT_DATA = {}; diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 9907ad27c..84c9c23c8 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -14,7 +14,7 @@ import globals from './Globals'; import conf from './Conf'; import { log, prefsSet, prefsGet } from '../utils/common'; -import { getActiveTab, processUrlQuery } from '../utils/utils'; +import { processUrlQuery } from '../utils/utils'; // CONSTANTS const FREQUENCIES = { // in milliseconds @@ -28,11 +28,6 @@ const CAMPAIGN_METRICS = ['install', 'active', 'uninstall']; const { METRICS_BASE_URL, EXTENSION_VERSION, BROWSER_INFO } = globals; const MAX_DELAYED_PINGS = 100; -// Note that this threshold is intentionally different from the 30 second threshold in PolicySmartBlock, -// which is used to set the reloaded property on tab objects and to activate smart unblock behavior -// see GH-1797 for more details -const BROKEN_PAGE_METRICS_THRESHOLD = 60000; // 60 seconds - /** * Class for handling telemetry pings. * @memberOf BackgroundClasses @@ -42,13 +37,6 @@ class Metrics { this.utm_source = ''; this.utm_campaign = ''; this.ping_set = new Set(); - this._brokenPageWatcher = { - on: false, - triggerId: '', - triggerTime: '', - timeoutId: null, - url: '', - }; } /** @@ -94,75 +82,6 @@ class Metrics { }); } - /** - * Responds to individual user actions and sequences of user actions that may indicate a broken page, - * sending broken_page pings as needed - * For example, sends a broken_page ping when the user whitelists a site, - * then refreshes the page less than a minute later - * @param {number} triggerId 'what specifically triggered this broken_page ping?' identifier sent along to the metrics server - * @param {string} newTabUrl for checking whether user has opened the same url in a new tab, which confirms a suspicion raised by certain triggers - */ - handleBrokenPageTrigger(triggerId, newTabUrl = null) { - if (this._brokenPageWatcher.on && triggerId === globals.BROKEN_PAGE_REFRESH) { - this.ping('broken_page'); - this._unplugBrokenPageWatcher(); - return; - } - - if (this._brokenPageWatcher.on && triggerId === globals.BROKEN_PAGE_NEW_TAB && this._brokenPageWatcher.url === newTabUrl) { - this.ping('broken_page'); - this._unplugBrokenPageWatcher(); - return; - } - - if (triggerId === globals.BROKEN_PAGE_NEW_TAB) { return; } - - this._resetBrokenPageWatcher(triggerId); - } - - /** - * handleBrokenPageTrigger helper - * starts the temporary watch for a second suspicious user action in response to a first - * @param {number} triggerId 'what specifically triggered this broken_page ping?' identifier sent along to the metrics server - * @private - */ - _resetBrokenPageWatcher(triggerId) { - this._clearBrokenPageWatcherTimeout(); - - getActiveTab((tab) => { - const tabUrl = tab && tab.url ? tab.url : ''; - - this._brokenPageWatcher = { - on: true, - triggerId, - triggerTime: Date.now(), - timeoutId: setTimeout(this._clearBrokenPageWatcherTimeout.bind(this), BROKEN_PAGE_METRICS_THRESHOLD), - url: tabUrl - }; - }); - } - - /** - * handleBrokenPageTrigger helper - * @private - */ - _unplugBrokenPageWatcher() { - this._clearBrokenPageWatcherTimeout(); - - this._brokenPageWatcher = { - on: false, - triggerId: '', - triggerTime: '', - timeoutId: null, - url: '' - }; - } - - _clearBrokenPageWatcherTimeout() { - const { timeoutId } = this._brokenPageWatcher; - if (timeoutId) { clearTimeout(timeoutId); } - } - /** * Prepare data and send telemetry pings. * @param {string} type type of the telemetry ping @@ -240,7 +159,6 @@ class Metrics { case 'priority_support_submit': case 'theme_change': case 'manage_subscription': - case 'broken_page': this._sendReq(type, ['all']); break; @@ -390,14 +308,7 @@ class Metrics { `&us=${encodeURIComponent(this.utm_source)}` + // Marketing campaign (Former utm_campaign) `&uc=${encodeURIComponent(this.utm_campaign)}`; - } else if (type === 'broken_page' && this._brokenPageWatcher.on) { - metrics_url += - // What triggered the broken page ping? - `&setup_path=${encodeURIComponent(this._brokenPageWatcher.triggerId.toString())}` + - // How much time passed between the trigger and the page refresh / open in new tab? - `&setup_block=${encodeURIComponent((Date.now() - this._brokenPageWatcher.triggerTime).toString())}`; } - return metrics_url; } diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index eb2734ff3..c47f542d1 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -20,7 +20,6 @@ import conf from './Conf'; import foundBugs from './FoundBugs'; import bugDb from './BugDb'; import globals from './Globals'; -import metrics from './Metrics'; import Policy from './Policy'; import tabInfo from './TabInfo'; import Rewards from './Rewards'; @@ -636,14 +635,6 @@ class PanelData { tabInfo.setTabInfo(this._activeTab.id, 'needsReload', data.needsReload); } - if (data.brokenPageMetricsTrackerTrustOrUnblock) { - metrics.handleBrokenPageTrigger(globals.BROKEN_PAGE_TRACKER_TRUST_OR_UNBLOCK); - } - - if (data.brokenPageMetricsWhitelistSite) { - metrics.handleBrokenPageTrigger(globals.BROKEN_PAGE_WHITELIST); - } - if (syncSetDataChanged) { // Push conf changes to the server account.saveUserSettings().catch(err => log('PanelData saveUserSettings', err)); diff --git a/src/classes/PolicySmartBlock.js b/src/classes/PolicySmartBlock.js index efac2a000..76d1f8538 100644 --- a/src/classes/PolicySmartBlock.js +++ b/src/classes/PolicySmartBlock.js @@ -224,8 +224,6 @@ class PolicySmartBlock { static checkReloadThreshold(tabId) { if (!PolicySmartBlock.shouldCheck(tabId)) { return false; } - // Note that this threshold is different from the broken page ping threshold in Metrics, which is 60 seconds - // see GH-1797 for more details const SMART_BLOCK_BEHAVIOR_THRESHOLD = 30000; // 30 seconds return ( From 98895ab1f7ab32341e8d4303527751bafb3bcdc8 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Fri, 2 Oct 2020 11:05:57 -0400 Subject: [PATCH 29/35] GH-2158: Subscription_type ping parameter not sending on login (#610) * Ensure sign_in_success ping fires for free accounts as well as plus and premium * Pacify linter Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- src/classes/Account.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/classes/Account.js b/src/classes/Account.js index 28ac75ab8..d30c6d32d 100644 --- a/src/classes/Account.js +++ b/src/classes/Account.js @@ -196,11 +196,13 @@ class Account { this._setSubscriptionData(plusSubscription); } } + + return subscriptions; + }) + .finally(() => { if (options && options.calledFrom === 'login') { metrics.ping('sign_in_success'); } - - return subscriptions; }) ) From 5b86f6c7358dbd3178d0e4daac76ccc96e3eb061 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Fri, 2 Oct 2020 11:07:12 -0400 Subject: [PATCH 30/35] GH-2097/t2-to-utm-content-fix (#612) * Remove t2 code from Metrics * Add utm_content * Pacify linter * Modify ah comment in UpgradePlan Co-authored-by: Christopher Tino <4699516+christophertino@users.noreply.github.com> --- .../Views/UpgradePlanView/UpgradePlanView.jsx | 12 +++++++--- src/classes/Metrics.js | 24 ------------------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx b/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx index 0a226c0c9..f360977a6 100644 --- a/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx +++ b/app/hub/Views/UpgradePlanView/UpgradePlanView.jsx @@ -15,6 +15,7 @@ import React, { Fragment, useRef, useEffect } from 'react'; import ClassNames from 'classnames'; import PropTypes from 'prop-types'; import { NavLink } from 'react-router-dom'; +import QueryString from 'query-string'; import { BASIC, PLUS, PREMIUM } from './UpgradePlanViewConstants'; import globals from '../../../../src/classes/Globals'; @@ -113,8 +114,11 @@ const premiumAlreadyProtectedButton = () => ( ); +// Whether we are displaying this Upgrade Plan view in the alternate or the default Hub layout (as per the A/B test in ticket GH-2097) +const ah = (QueryString.parse(window.location.search).ah === 'true') || false; + /** - * A React class component for rendering the Upgrade Plan View + * A React function component for rendering the Upgrade Plan View * @return {JSX} JSX for rendering the Upgrade Plan View of the Hub app * @memberof HubComponents */ @@ -178,7 +182,8 @@ const UpgradePlanView = (props) => { const plusCTAButton = (position) => { const utm_campaign = (position === 'top' ? 'c_1' : 'c_2'); - const plusCheckoutLink = `${globals.CHECKOUT_BASE_URL}/plus?${params}&utm_campaign=intro_hub_${utm_campaign}`; + const utm_content = (ah ? '2' : '1'); + const plusCheckoutLink = `${globals.CHECKOUT_BASE_URL}/plus?${params}&utm_campaign=intro_hub_${utm_campaign}&utm_content=${utm_content}`; return ( @@ -189,7 +194,8 @@ const UpgradePlanView = (props) => { const premiumCTAButton = (position) => { const utm_campaign = (position === 'top' ? 'c_3' : 'c_4'); - const premiumCheckoutLink = `${globals.CHECKOUT_BASE_URL}/premium?${params}&utm_campaign=intro_hub_${utm_campaign}`; + const utm_content = (ah ? '2' : '1'); + const premiumCheckoutLink = `${globals.CHECKOUT_BASE_URL}/premium?${params}&utm_campaign=intro_hub_${utm_campaign}&utm_content=${utm_content}`; return ( diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 84c9c23c8..e9c9cd31b 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -296,11 +296,6 @@ class Metrics { // AB tests enabled? `&ts=${encodeURIComponent(conf.enable_abtests ? '1' : '0')}`; - if (conf.enable_abtests) { - // Hub Layout A/B test. Added in 8.5.3. GH-2097, GH-2100 - metrics_url += `&t2=${encodeURIComponent(Metrics._getHubLayoutView().toString())}`; - } - if (CAMPAIGN_METRICS.includes(type)) { // only send campaign attribution when necessary metrics_url += @@ -451,25 +446,6 @@ class Metrics { } } - /** - * Get the Int associated with the Hub layout view shown on install - * @private - * @return {number} Int associated with the Hub layout view - */ - static _getHubLayoutView() { - const { hub_layout } = conf; - - switch (hub_layout) { - case 'default': - return 1; - case 'alternate': - return 2; - case 'not_yet_set': - default: - return 0; - } - } - /** * Get the Int associated with the users subscription interval * @private From eafc2093ac219c651cd9ebcaae0d7b6b0b19a863 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Fri, 2 Oct 2020 11:08:58 -0400 Subject: [PATCH 31/35] update translations --- _locales/de/messages.json | 61 ++++++++++++++++++++++++++++++++-- _locales/es/messages.json | 61 ++++++++++++++++++++++++++++++++-- _locales/fr/messages.json | 63 ++++++++++++++++++++++++++++++++++-- _locales/hu/messages.json | 61 ++++++++++++++++++++++++++++++++-- _locales/it/messages.json | 63 ++++++++++++++++++++++++++++++++++-- _locales/ja/messages.json | 63 ++++++++++++++++++++++++++++++++++-- _locales/ko/messages.json | 63 ++++++++++++++++++++++++++++++++++-- _locales/nl/messages.json | 61 ++++++++++++++++++++++++++++++++-- _locales/pl/messages.json | 63 ++++++++++++++++++++++++++++++++++-- _locales/pt_BR/messages.json | 61 ++++++++++++++++++++++++++++++++-- _locales/ru/messages.json | 63 ++++++++++++++++++++++++++++++++++-- _locales/zh_CN/messages.json | 61 ++++++++++++++++++++++++++++++++-- _locales/zh_TW/messages.json | 61 ++++++++++++++++++++++++++++++++-- 13 files changed, 773 insertions(+), 32 deletions(-) diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 488bf6c71..a5361c941 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Konto" }, + "settings_import_export": { + "message": "Einstellungen importieren und exportieren" + }, "settings_trackers": { "message": "Tracker" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Teilnahme an Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "Teilnahme an A/B-Tests" + }, "settings_signin_create_header": { "message": "Anmelden/Konto erstellen" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards ist eine Private-by-Design-Funktion, die Ihnen beim Browsen Rabatte und Sonderangebote unserer Partnerunternehmen liefert." }, + "settings_abtests_tooltip": { + "message": "Die Teilnahme an randomisierten A/B-Tests hilft Ghostery zu verstehen, welche Version eines neuen Layouts oder einer neuen Funktion Benutzer wie Sie bevorzugen." + }, "settings_opt_in": { "message": "Opt-in/-out" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "Übersicht" + }, + "android_tab_site_blocking": { + "message": "Blockieren von Websites" + }, + "android_tab_global_blocking": { + "message": "Global blockieren" + }, + "android_site_blocking_header": { + "message": "Tracker auf dieser Website" + }, + "android_global_blocking_header": { + "message": "Globales Tracking" + }, + "android_blocking_reset": { + "message": "Einstellungen zurücksetzen" + }, + "android_block": { + "message": "Blockieren" + }, + "android_unblock": { + "message": "Nicht Blockieren" + }, + "android_restrict": { + "message": "Einschränken" + }, + "android_unrestrict": { + "message": "Rückgängig machen" + }, + "android_trust": { + "message": "Vertrauen" + }, + "android_untrust": { + "message": "Rückgängig machen" + }, + "android_anonymize": { + "message": "Anonymisieren" + }, + "android_anonymized": { + "message": "Anonymisiert" + }, "hub_side_navigation_home": { "message": "Startseite" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "Sie sind umfassend geschützt!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - Upgrade-Plan" + }, "hub_upgrade_your": { "message": "Upgrade für Ihren" }, @@ -1684,7 +1738,7 @@ "message": "Lösen Sie Probleme schneller mit unserem Priority Helpdesk-Service – und weitere Vorteile" }, "hub_supporter_manifesto": { - "message": "Wir versuchen, unseren Nutzern kostenlos die besten Dienstleistungen zum Schutz der Privatsphäre zu bieten. Wir verlangen keine Gebühr für unsere Browser-Erweiterung, aber Sie können uns durch ein kleines monatliches Abonnement unterstützen. Stufen Sie Ihre Version auf Ghostery Plus hoch - es erwarten Sie tolle Vorteile." + "message": "Wir sind bestrebt, unseren Nutzern kostenlos die besten Dienstleistungen zum Schutz der Privatsphäre zur Verfügung zu stellen. Wir verlangen keine Gebühr für unsere Browser-Erweiterung, aber Sie können uns mit einem monatlichen Abonnement in Höhe von 4,99 Dollar unterstützen. Stufen Sie Ihre Version auf Ghostery Plus hoch - es erwarten Sie tolle Vorteile." }, "hub_supporter_feature_theme_description": { "message": "Passen Sie die Ghostery-Farben für eine neue visuelle Erfahrung an! Eingeführt auf vielfachen Wunsch. Sehen Sie sich unser spezielles dunkelblaues Thema an. Weitere Themen folgen noch." @@ -1899,7 +1953,7 @@ "message": "Themes" }, "subscribe_pitch": { - "message": "Ghostery ist kostenlos, aber Sie können uns durch ein kleines monatliches Abonnement unterstützen. Als Gegenleistung erhalten Sie besondere Vorteile wie bunte Themes, persönliche Tracking-Statistiken und mehr. Kaufen Sie jetzt ein Abonnement." + "message": "Wir verlangen keine Gebühr für unsere Browser-Erweiterung, aber Sie können uns mit einem monatlichen Abonnement in Höhe von 4,99 Dollar unterstützen. Dafür erhalten Sie besondere Vorteile wie bunte Themes, persönliche Tracking-Statistiken und mehr. Kaufen Sie jetzt ein Abonnement." }, "subscribe_pitch_spring": { "message": "Gefällt Ihnen, was wir tun? Unterstützen Sie uns und schalten Sie durch ein Upgrade auf Ghostery Plus neue Frühlingsthemen, persönliche Tracking-Informationen und weitere besondere Vergünstigungen frei!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "Zu viele Anforderungen zur Passwortzurücksetzung gesendet. Versuchen Sie es in einer Stunde erneut." + }, + "too_many_failed_logins_text": { + "message": "Zu viele fehlgeschlagene Anmeldeversuche. Versuchen Sie es in einer Stunde erneut." } } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 8e67180dd..4fe8dc2d6 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Cuenta" }, + "settings_import_export": { + "message": "Importar y exportar ajustes" + }, "settings_trackers": { "message": "Rastreadores" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Participando en Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "Participar en Pruebas A/B" + }, "settings_signin_create_header": { "message": "Iniciar sesión / Crear cuenta" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards es una función de diseño privado que ofrece descuentos y ofertas especiales de nuestras empresas asociadas mientras se navega." }, + "settings_abtests_tooltip": { + "message": "Participar en Pruebas A/B ayuda a Ghostery a entender qué versión de un nuevo diseño o función prefieren los usuarios como tú." + }, "settings_opt_in": { "message": "Suscripción sí/no" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "Resumen" + }, + "android_tab_site_blocking": { + "message": "Bloqueo de sitios web" + }, + "android_tab_global_blocking": { + "message": "Bloqueo global" + }, + "android_site_blocking_header": { + "message": "rastreadores en este sitio" + }, + "android_global_blocking_header": { + "message": "Rastreo global" + }, + "android_blocking_reset": { + "message": "Restablecer ajustes" + }, + "android_block": { + "message": "Bloquear" + }, + "android_unblock": { + "message": "Desbloquear" + }, + "android_restrict": { + "message": "Restringir" + }, + "android_unrestrict": { + "message": "Deshacer" + }, + "android_trust": { + "message": "Confiar" + }, + "android_untrust": { + "message": "Deshacer" + }, + "android_anonymize": { + "message": "Anonimizar" + }, + "android_anonymized": { + "message": "Anonimizado" + }, "hub_side_navigation_home": { "message": "Inicio" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "¡Estás totalmente protegido!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - Mejorar plan" + }, "hub_upgrade_your": { "message": "Mejora tu" }, @@ -1684,7 +1738,7 @@ "message": "Resuelve problemas rápidamente con nuestro servicio de ayuda prioritario (próximamente más ventajas)" }, "hub_supporter_manifesto": { - "message": "Nos esforzamos por ofrecer los mejores servicios de protección de la privacidad a nuestros usuarios de forma gratuita. A pesar de que no cobramos por nuestra extensión de navegador, puedes darnos tu apoyo a través de una pequeña suscripción mensual. ¡Únete a nosotros en nuestra misión pasando a Ghostery Plus y desbloquea a la vez estupendas ventajas!" + "message": "Nos esforzamos por ofrecer los mejores servicios de protección de la privacidad a nuestros usuarios de forma gratuita. A pesar de que no cobramos por nuestra extensión de navegador, puedes darnos tu apoyo a través de una suscripción mensual de 4,99 $. ¡Únete a nosotros en nuestra misión pasando a Ghostery Plus y desbloquea a la vez estupendas ventajas!" }, "hub_supporter_feature_theme_description": { "message": "¡Personaliza los colores de Ghostery para una nueva experiencia visual! Introducido por petición popular. Echa un vistazo a nuestro tema especial Dark Blue. Hay muchos más por venir." @@ -1899,7 +1953,7 @@ "message": "Temas" }, "subscribe_pitch": { - "message": "Aunque Ghostery sea gratuito, puedes optar por apoyarnos con una pequeña suscripción mensual a cambio de interesantes ventajas como temas de color, estadísticas de seguimiento personalizadas y mucho más. ¡Únete a nuestra misión y suscríbete!" + "message": "Aunque Ghostery sea gratuito, puedes optar por apoyarnos con una suscripción mensual de 4,99 $ a cambio de interesantes ventajas como temas de color, estadísticas de seguimiento personalizadas y mucho más. ¡Únete a nuestra misión y suscríbete!" }, "subscribe_pitch_spring": { "message": "¿Te gusta lo que hacemos? Apóyanos y desbloquea nuevos temas primaverales, información de rastreo personal y otras funciones especiales al mejorar a Ghostery Plus." @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "Demasiadas solicitudes de restablecimiento de contraseña. Vuelve a intentarlo dentro de una hora." + }, + "too_many_failed_logins_text": { + "message": "Demasiados inicios de sesión fallidos. Vuelve a intentarlo dentro de una hora." } } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 6416b2cb4..07706b3f6 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Compte" }, + "settings_import_export": { + "message": "Paramètres Import & Export" + }, "settings_trackers": { "message": "mouchards" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Participation aux Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "Participation aux tests A/B" + }, "settings_signin_create_header": { "message": "Connexion / Créer un compte" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards est une fonction respectueuse de la vie privée, dès la conception, qui vous donne droit à des réductions comme des offres spéciales de sociétés partenaires lors de votre navigation." }, + "settings_abtests_tooltip": { + "message": "La participation à des tests A/B au hasard permet à Ghostery de comprendre quelle version d'une nouvelle mise en page ou fonction les utilisateurs comme vous préfèrent." + }, "settings_opt_in": { "message": "Accepter / Refuser" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "Aperçu" + }, + "android_tab_site_blocking": { + "message": "Blocage de site" + }, + "android_tab_global_blocking": { + "message": "Blocage global" + }, + "android_site_blocking_header": { + "message": "Mouchards sur ce site" + }, + "android_global_blocking_header": { + "message": "Pistage global" + }, + "android_blocking_reset": { + "message": "Réinitialiser les paramètres" + }, + "android_block": { + "message": "Bloquer" + }, + "android_unblock": { + "message": "Ne pas bloquer" + }, + "android_restrict": { + "message": "Restreindre" + }, + "android_unrestrict": { + "message": "Annuler" + }, + "android_trust": { + "message": "Se fier" + }, + "android_untrust": { + "message": "Annuler" + }, + "android_anonymize": { + "message": "Anonymiser" + }, + "android_anonymized": { + "message": "Anonymisé" + }, "hub_side_navigation_home": { "message": "Page d'accueil" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "Vous êtes entièrement protégé !" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - Surclasser votre plan" + }, "hub_upgrade_your": { "message": "Surclassez votre" }, @@ -1684,7 +1738,7 @@ "message": "Résolvez rapidement vos problèmes avec notre service d'assistance prioritaire et ses avantages" }, "hub_supporter_manifesto": { - "message": "Nous nous efforçons d'offrir à nos utilisateurs les meilleurs services de protection de la vie privée, en toute gratuité. Bien que nous notre extension de navigateur ne soit pas payante, vous pouvez décider de nous soutenir en souscrivant un abonnement mensuel. Soutenez notre mission en passant à Ghostery Plus — et découvrez de super avantages lors de votre navigation !" + "message": "Nous nous efforçons d'offrir à nos utilisateurs les meilleurs services de protection de la vie privée, en toute gratuité. Bien que notre extension de navigateur ne soit pas payante, vous pouvez décider de nous soutenir en souscrivant un abonnement mensuel au prix de $4.99. Soutenez notre mission en passant à Ghostery Plus et découvrez de super avantages lors de votre navigation !" }, "hub_supporter_feature_theme_description": { "message": "Personnalisez les couleurs de Ghostery pour une nouvelle expérience visuelle ! Lancé à la demande générale. Découvrez notre thème spécial Dark Blue, et bien d'autres à venir." @@ -1899,7 +1953,7 @@ "message": "Thèmes" }, "subscribe_pitch": { - "message": "Ghostery est entièrement gratuit ; vous pouvez dès lors décider de nous soutenir en souscrivant un abonnement mensuel. Vous bénéficiez en retour de super avantages tels que des thèmes de couleur, un service d'assistance prioritaire, parmi tant d'autres. Soutenez notre mission en vous abonnant !" + "message": "Ghostery est entièrement gratuit ; vous pouvez dès lors décider de nous soutenir en souscrivant un abonnement mensuel de $4.99. Vous bénéficiez en retour de super avantages tels que des thèmes de couleur, des statistiques de pistage personnelles parmi tant d'autres. Soutenez notre mission en vous abonnant !" }, "subscribe_pitch_spring": { "message": "Vous appréciez notre travail ? Soutenez-nous et débloquez de nouveaux thèmes de printemps, des informations de suivi personnalisées et d'autres avantages spéciaux en passant à Ghostery Plus !" @@ -2468,7 +2522,7 @@ "message": "Essayez Ghostery Midnight" }, "seven_day_free_trial": { - "message": "Essai gratuit de 7 jours" + "message": "Essai gratuit de 7 jours" }, "spring_is_here": { "message": "Le printemps est arrivé !" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "Vous avez dépassé le nombre de demandes de réinitialisation de mot de passe autorisé. Veuillez réessayer dans une heure." + }, + "too_many_failed_logins_text": { + "message": "Vous avez dépassé le nombre de tentatives de connexion autorisé. Veuillez réessayer dans une heure." } } diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index cfe306a69..51b7534ad 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Fiók" }, + "settings_import_export": { + "message": "Beállítások importálása és exportálása" + }, "settings_trackers": { "message": "Trackerek" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Részvétel a Ghostery Rewards programban" }, + "settings_allow_abtests": { + "message": "Részvétel A/B-tesztekben" + }, "settings_signin_create_header": { "message": "Bejelentkezés / Fiók létrehozása" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "A Ghostery Rewards olyan személyesre tervezett funkció, amely böngészés közben árengedményeket és akciókat szolgáltat önnek partnervállalainktól." }, + "settings_abtests_tooltip": { + "message": "A véletlenszerű A/B-tesztekben való részvétellel segít a Ghostery vállalatnak, hogy megtudja, az új elrendezés vagy funkció melyik verziója tetszik Önnek." + }, "settings_opt_in": { "message": "Feliratkozás / Leiratkozás" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "Áttekintés" + }, + "android_tab_site_blocking": { + "message": "Webhely blokkolása" + }, + "android_tab_global_blocking": { + "message": "Globális blokkolás" + }, + "android_site_blocking_header": { + "message": "Trackerek ezen az oldalon" + }, + "android_global_blocking_header": { + "message": "Globális követés" + }, + "android_blocking_reset": { + "message": "Beállítások visszaállítása" + }, + "android_block": { + "message": "Blokkolás" + }, + "android_unblock": { + "message": "Feloldás" + }, + "android_restrict": { + "message": "Korlátozás" + }, + "android_unrestrict": { + "message": "Visszavonás" + }, + "android_trust": { + "message": "Megbízhatónak jelölés" + }, + "android_untrust": { + "message": "Visszavonás" + }, + "android_anonymize": { + "message": "Anonimizálás" + }, + "android_anonymized": { + "message": "Anonimizált" + }, "hub_side_navigation_home": { "message": "Kezdőlap" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "Teljes védelmet élvez" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub – Csomag frissítése" + }, "hub_upgrade_your": { "message": "Frissítse" }, @@ -1684,7 +1738,7 @@ "message": "Hárítsa el a hibákat gyorsan az Elsőbbségi támogató szolgáltatásunkkal - és további szolgáltatások lesznek elérhetőek" }, "hub_supporter_manifesto": { - "message": "Arra törekszünk, hogy a legjobb adatvédelmi szolgáltatást nyújtsuk felhasználóinknak költségek nélkül. Annak ellenére, hogy adatvédelmi szolgáltatásunk ingyenes, ön dönthet úgy, hogy támogatja munkánkat egy alacsony összegű havi feliratkozással. Csatlakozzon ön is a küldetésünkhöz azzal, hogy kibővíti szolgáltatásunkat a Ghostery Plus verzióra - és egyúttal nagyszerű extra funkciókhoz férhet hozzá!" + "message": "Arra törekszünk, hogy a legjobb adatvédelmi szolgáltatást nyújtsuk felhasználóinknak költségek nélkül. Bár böngészőbővítményünk ingyenes, Ön dönthet úgy, hogy támogatja munkánkat egy alacsony, $4,99 összegű havi előfizetési díjjal. Csatlakozzon Ön is a küldetésünkhöz, és válassza a Ghostery Plus verzióját, így nagyszerű extra funkciókhoz férhet hozzá!" }, "hub_supporter_feature_theme_description": { "message": "Állítsa be egyénileg a Ghostery színeit egy új vizuális élményért! Népszerű kérés alapján bevezetve. Próbálja ki a különleges Dark Blue sablonunkat, amelyet még sok fog követni." @@ -1899,7 +1953,7 @@ "message": "Sablonok" }, "subscribe_pitch": { - "message": "Annak ellenére, hogy a Ghostery ingyenes, ön dönthet úgy, hogy támogatja munkánkat egy alacsony, és cserébe nagyszerű extra funkciókhoz férhet hozzá, mint például a színsablonok, elsőbbségi támogató szolgáltatás és még sok minden más. Csatlakozzon a küldetésünkhöz és iratkozzon fel!" + "message": "Bár a Ghostery ingyenes, Ön dönthet úgy, hogy támogatja munkánkat egy alacsony, $4,99 összegű havi előfizetési díjjal, így cserébe nagyszerű extra funkciókhoz férhet hozzá, mint például a színsablonok, személyes követési statisztikák, és még sok minden más. Csatlakozzon küldetésünkhöz, és fizessen elő!" }, "subscribe_pitch_spring": { "message": "Tetszik a termékünk? Támogassunk bennünket, és fedezze fel az új tavaszi sablonokat, személyes nyomonkövetési lehetőségeket, és számos különleges előnyt a Ghostery Plus történő frissítéssel!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "Túl sok a jelszó-visszaállítási kérés." + }, + "too_many_failed_logins_text": { + "message": "Túl sok sikertelen bejelentkezés. Próbálja újra egy óra múlva." } } diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 9c3035962..28ac61039 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Account" }, + "settings_import_export": { + "message": "Importa ed esporta le impostazioni" + }, "settings_trackers": { "message": "Tracker" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Partecipare a Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "Partecipare a test A/B" + }, "settings_signin_create_header": { "message": "Accedi / Crea un Accont" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards è una funzione progettata pensando alla privacy, tramite la quale le aziende nostre partner ti propongono sconti e offerte speciali mentre navighi." }, + "settings_abtests_tooltip": { + "message": "La partecipazione a test A/B casuali aiuta Ghostery a capire quale versione di un nuovo layout o funzione preferiscono gli utenti come te." + }, "settings_opt_in": { "message": "Opt In / Out" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "Panoramica " + }, + "android_tab_site_blocking": { + "message": "Blocco del sito" + }, + "android_tab_global_blocking": { + "message": "Blocco Globale" + }, + "android_site_blocking_header": { + "message": "Tracker su questo sito" + }, + "android_global_blocking_header": { + "message": "Tracciamento globale" + }, + "android_blocking_reset": { + "message": "Ripristina impostazioni" + }, + "android_block": { + "message": "Blocca" + }, + "android_unblock": { + "message": "Sblocca" + }, + "android_restrict": { + "message": "Limita" + }, + "android_unrestrict": { + "message": "Annulla" + }, + "android_trust": { + "message": "Considera Affidabile" + }, + "android_untrust": { + "message": "Annulla" + }, + "android_anonymize": { + "message": "Rendi anonimo" + }, + "android_anonymized": { + "message": "Reso anonimo" + }, "hub_side_navigation_home": { "message": "Home" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "Sei completamente protetto!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - Esegui l'upgrade del piano" + }, "hub_upgrade_your": { "message": "Effettua l'upgrade del tuo" }, @@ -1684,7 +1738,7 @@ "message": "Risolvi rapidamente i problemi con il nostro servizio di assistenza prioritario e molti altri vantaggi futuri" }, "hub_supporter_manifesto": { - "message": "Ci impegniamo a offrire gratuitamente ai nostri utenti i migliori servizi di tutela della privacy. Anche se la nostra estensione per browser è gratuita, puoi decidere di sostenerci con un piccolo abbonamento mensile. Unisciti a noi nella nostra missione effettuando l'upgrade a Ghostery Plus, e sblocca fantastici extra lungo la strada!" + "message": "Ci impegniamo a offrire gratuitamente ai nostri utenti i migliori servizi di tutela della privacy. Anche se la nostra estensione per browser è gratuita, puoi decidere di sostenerci con un abbonamento mensile di $4.99. Unisciti a noi nella nostra missione effettuando l'upgrade a Ghostery Plus e sblocca fantastici extra!" }, "hub_supporter_feature_theme_description": { "message": "Personalizza i colori di Ghostery per una nuova esperienza visiva! Introdotto a grande richiesta. Dai un'occhiata al nostro tema speciale Dark Blue e agli altri in arrivo." @@ -1899,7 +1953,7 @@ "message": "Temi" }, "subscribe_pitch": { - "message": "Anche se Ghostery è gratuito, puoi decidere di sostenerci con un piccolo abbonamento mensile in cambio di fantastici extra, come temi colorati, statistiche sul tracciamento personale e altro ancora. Unisciti alla nostra missione e abbonati!" + "message": "Anche se Ghostery è gratuito, puoi decidere di sostenerci con un abbonamento mensile di $4.99 in cambio di fantastici extra, come temi colorati, statistiche sul tracciamento personale e altro ancora. Unisciti alla nostra missione e abbonati!" }, "subscribe_pitch_spring": { "message": "Ti piace quello che facciamo? Sostienici e passa a Ghostery Plus per sbloccare nuovi temi primaverili, approfondimenti sul tracciamento personale e altri extra speciali." @@ -2468,7 +2522,7 @@ "message": "Prova Ghostery Midnight" }, "seven_day_free_trial": { - "message": "Prova gratuita di 7 giorni" + "message": "7 giorni di prova gratuiti" }, "spring_is_here": { "message": "La primavera è arrivata!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "Troppe richieste di ripristino della password. Riprova tra un'ora." + }, + "too_many_failed_logins_text": { + "message": "Troppi login non riusciti. Riprova tra un'ora." } } diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index bc9f1e621..93e118f67 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "アカウント" }, + "settings_import_export": { + "message": "インポートおよびエクスポート設定" + }, "settings_trackers": { "message": "トラッカー" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Ghostery Rewardsに参加する" }, + "settings_allow_abtests": { + "message": "A/Bテストに参加する" + }, "settings_signin_create_header": { "message": "サインイン/アカウント作成" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewardsは、ブラウジングの際にGhosteryのパートナー企業から割引やスペシャルオファーを受けられる、プライバシーバイデザインの機能です。" }, + "settings_abtests_tooltip": { + "message": "無作為のA/Bテストに参加していただくことで、Ghosteryはユーザーの好みの新しいレイアウトや機能がどのバージョンであるかを把握することができます。" + }, "settings_opt_in": { "message": "オプトイン/オプトアウト" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "概要" + }, + "android_tab_site_blocking": { + "message": "サイト・ブロッキング" + }, + "android_tab_global_blocking": { + "message": "グローバル・ブロッキング" + }, + "android_site_blocking_header": { + "message": "このサイトのトラッカー" + }, + "android_global_blocking_header": { + "message": "グローバル・トラッキング" + }, + "android_blocking_reset": { + "message": "設定をリセット" + }, + "android_block": { + "message": "ブロック" + }, + "android_unblock": { + "message": "ブロックを解除" + }, + "android_restrict": { + "message": "制限" + }, + "android_unrestrict": { + "message": "元に戻す" + }, + "android_trust": { + "message": "信頼" + }, + "android_untrust": { + "message": "元に戻す" + }, + "android_anonymize": { + "message": "匿名化" + }, + "android_anonymized": { + "message": "匿名化済み" + }, "hub_side_navigation_home": { "message": "ホーム" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "あなたは完全に保護されています。" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - アップグレード・プラン" + }, "hub_upgrade_your": { "message": "アップグレード" }, @@ -1684,7 +1738,7 @@ "message": "プライオリティ・ヘルプデスクが問題をすぐに解決。充実した特典が満載です" }, "hub_supporter_manifesto": { - "message": "Ghosteryは、最高のプライバシー保護サービスをユーザーに無料で提供できるように努めています。ブラウザ拡張機能は無料ですが、一部のお客様からは毎月少額のサブスクリプション料をいただくことで機能改善にご協力いただいております。「Ghostery Plus」にアップグレードして当社のミッションにご参加ください。そして、さらなるサービス特典をご利用ください!" + "message": "Ghosteryは、最高のプライバシー保護サービスをユーザーに無料で提供できるように努めています。ブラウザ拡張機能は無料ですが、一部のお客様からは毎月$4.99のサブスクリプション料をいただくことで機能改善にご協力いただいております。「Ghostery Plus」にアップグレードして当社のミッションにご参加ください。そして、さらなるサービス特典をご利用ください!" }, "hub_supporter_feature_theme_description": { "message": "Ghosteryのカラーをカスタマイズして新しいビジュアル体験を!たくさんのご要望にお応えし、ついに実現。Dark Blueやその他の新着テーマをチェックしよう。" @@ -1899,7 +1953,7 @@ "message": "テーマ" }, "subscribe_pitch": { - "message": "Ghosteryは無料でもご利用いただけますが、毎月少額のサブスクリプション料を通じてご協力いただくと、カラーテーマ、個人追跡履歴統計などの特典をご利用いただけます。ご登録いただき、当社のミッションにご参加ください!" + "message": "Ghosteryは無料でもご利用いただけますが、毎月$4.99のサブスクリプション料を通じてご協力いただくと、カラーテーマ、個人追跡履歴統計などの特典をご利用いただけます。ご登録いただき、当社のミッションにご参加ください!" }, "subscribe_pitch_spring": { "message": "気に入っていただけましたか?Ghostery Plusにアップグレードして、新しい春のテーマ、個人追跡情報など、特別機能をご利用ください!" @@ -2468,7 +2522,7 @@ "message": "Ghostery Midnightを試す" }, "seven_day_free_trial": { - "message": "7日間無料トライアル" + "message": "7日間の無料試用版" }, "spring_is_here": { "message": "春がやって来ました!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": " パスワードのリセットリクエストが多すぎます。1 時間後にもう一度お試しください。" + }, + "too_many_failed_logins_text": { + "message": "ログインの失敗回数が多すぎます。1時間後にもう一度お試しください。" } } diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index 78a97d24e..2682b8e3d 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "계정 " }, + "settings_import_export": { + "message": "설정 가져오기 & 내보내기" + }, "settings_trackers": { "message": "트래커 " }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Ghostery Rewards 기능 이용" }, + "settings_allow_abtests": { + "message": "A/B 테스트 기능 이용" + }, "settings_signin_create_header": { "message": "로그인 / 계정 만들기 " }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards는 사용자에게 당사 파트너 기업의 할인과 특별 혜택 정보를 제공하는 이용자 맞춤 기능입니다." }, + "settings_abtests_tooltip": { + "message": "무작위 A/B 테스트에 참여하면 Ghostery가 사용자의 선호 새 레이아웃 또는 기능 버전을 이해하는 데 도움이 됩니다." + }, "settings_opt_in": { "message": "참여/미참여" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "개요" + }, + "android_tab_site_blocking": { + "message": "사이트 차단" + }, + "android_tab_global_blocking": { + "message": "전역 차단 " + }, + "android_site_blocking_header": { + "message": "이 사이트에 대한 트래커 " + }, + "android_global_blocking_header": { + "message": "전역 트래킹" + }, + "android_blocking_reset": { + "message": "설정 초기화" + }, + "android_block": { + "message": "차단" + }, + "android_unblock": { + "message": "차단 해제 " + }, + "android_restrict": { + "message": "제한 " + }, + "android_unrestrict": { + "message": "실행 취소 " + }, + "android_trust": { + "message": "신뢰" + }, + "android_untrust": { + "message": "실행 취소 " + }, + "android_anonymize": { + "message": "익명화" + }, + "android_anonymized": { + "message": "익명화됨" + }, "hub_side_navigation_home": { "message": "홈" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "완벽하게 보호됩니다." }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - 플랜 업그레이드" + }, "hub_upgrade_your": { "message": "보호 서비스" }, @@ -1684,7 +1738,7 @@ "message": "우선 지원 데스크 서비스를 통해 빠르게 문제를 해결하고, 향후 더 많은 특전을 받으세요" }, "hub_supporter_manifesto": { - "message": "당사는 최고의 개인 정보 보호 서비스를 사용자에게 무료로 제공하기 위해 노력하고 있습니다. 브라우저 확장에 대한 비용은 청구하지 않지만, 소정의 월별 구독 요금을 지불하여 당사를 후원하실 수 있습니다. Ghostery Plus로 업그레이드하여 당사의 활동을 후원하고 계속해서 멋진 혜택을 받아보세요!" + "message": "당사는 최고의 개인 정보 보호 서비스를 사용자에게 무료로 제공하기 위해 노력하고 있습니다. 브라우저 확장에 대한 비용은 청구하지 않지만, $4.99의 월별 구독 요금을 지불하여 당사를 후원하실 수 있습니다. Ghostery Plus로 업그레이드하여 당사의 활동을 후원하고 계속해서 멋진 혜택을 받아보세요!" }, "hub_supporter_feature_theme_description": { "message": "Ghostery 컬러를 사용자 지정하여 새로운 비주얼을 경험해보세요! 폭넓은 요청에 따라 선보입니다. 특별한 Dark Blue 테마를 확인하고 앞으로 소개될 다양한 테마도 기대해주세요." @@ -1899,7 +1953,7 @@ "message": "테마" }, "subscribe_pitch": { - "message": "Ghostery는 무료로 이용이 가능하나 매달 소정의 구독 요금으로 저희를 후원해 주시면 컬러 테마, 개인 추적 통계 등 특별 혜택이 제공됩니다. 저희를 후원하고 서비스에 가입해 주세요!" + "message": "Ghostery는 무료로 이용이 가능하나 매달 $4.99의 구독 요금으로 저희를 후원해 주시면 컬러 테마, 개인 추적 통계 등 특별 혜택이 제공됩니다. 저희를 후원하고 서비스에 가입해 주세요!" }, "subscribe_pitch_spring": { "message": "현재 서비스가 마음에 드시나요? Ghostery Plus로 업그레이드하여 당사를 후원하고 새로운 봄 테마, 개인 추적 서비스, 기타 혜택을 받아보세요." @@ -2468,7 +2522,7 @@ "message": "Ghostery Midnight를 사용해 보세요" }, "seven_day_free_trial": { - "message": "7일 무료 평가판" + "message": "7일 무료 체험판" }, "spring_is_here": { "message": "봄이 왔습니다!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "암호 재설정을 너무 많이 요청했습니다. 한 시간 후에 다시 시도하세요." + }, + "too_many_failed_logins_text": { + "message": "로그인 실패 횟수가 너무 많습니다. 1시간 후에 다시 시도하십시오." } } diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index 4204206d9..0ace297c7 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Account" }, + "settings_import_export": { + "message": "Instellingen importeren en exporteren" + }, "settings_trackers": { "message": "Trackers" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Deel te nemen in Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "Deelnemen aan A/B-tests" + }, "settings_signin_create_header": { "message": "Inloggen / Account Aanmaken" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards is een private by-design functie waarmee je kortingen en speciale aanbiedingen van onze partners krijgt tijdens het browsen." }, + "settings_abtests_tooltip": { + "message": "Door deel te nemen aan willekeurige A/B-tests help je Ghostery te begrijpen welke versie van een nieuwe lay-out of functie gebruikers zoals jij liever zien." + }, "settings_opt_in": { "message": "Wel/niet meedoen" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "Overzicht" + }, + "android_tab_site_blocking": { + "message": "Websites blokkeren" + }, + "android_tab_global_blocking": { + "message": "Algemeen Blokkeren" + }, + "android_site_blocking_header": { + "message": "Tracker op deze website." + }, + "android_global_blocking_header": { + "message": "Algemeen tracken" + }, + "android_blocking_reset": { + "message": "Instellingen opnieuw instellen" + }, + "android_block": { + "message": "Blokkeer" + }, + "android_unblock": { + "message": "Deblokkeer" + }, + "android_restrict": { + "message": "Beperk" + }, + "android_unrestrict": { + "message": "Ongedaan Maken" + }, + "android_trust": { + "message": "Vertrouw" + }, + "android_untrust": { + "message": "Ongedaan Maken" + }, + "android_anonymize": { + "message": "Anoniem maken" + }, + "android_anonymized": { + "message": "Anoniem gemaakt" + }, "hub_side_navigation_home": { "message": "Start" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "Je wordt volledig beschermd!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - Abonnement upgraden" + }, "hub_upgrade_your": { "message": "Upgrade je" }, @@ -1684,7 +1738,7 @@ "message": "Los problemen snel op met onze Priority Ondersteuning - en meer nog te komen extra's" }, "hub_supporter_manifesto": { - "message": "We willen gratis de beste privacybeschermingsdiensten leveren aan onze klanten. Hoewel wij geen kosten rekenen voor onze browserextensie, kun je ons ondersteunen via een goedkoop maandelijks abonnement. Sluit je aan bij onze missie. Upgrade naar Ghostery Plus en ontgrendel coole extras!" + "message": "We willen gratis de beste privacybeschermingsdiensten leveren aan onze klanten. Hoewel wij geen kosten rekenen voor onze browserextensie, kun je ons ondersteunen via een maandelijks abonnement op $4.99. Sluit je aan bij onze missie. Upgrade naar Ghostery Plus en ontgrendel coole extras!" }, "hub_supporter_feature_theme_description": { "message": "Pas de Ghostery-kleuren aan voor een nieuwe, visuele beleving! Ingevoerd na vele verzoeken. Test ons speciale Dark Blue-thema. Binnenkort volgen er meer." @@ -1899,7 +1953,7 @@ "message": "Thema's" }, "subscribe_pitch": { - "message": "Ghostery is gratis, maar je kunt ervoor kiezen ons te steunen door een goedkoop maandabonnement te nemen in ruil voor coole extra's zoals kleurthema's, persoonlijke trackingstatistieken en meer. Sluit je aan bij onze missie en word lid!" + "message": "Ghostery is gratis, maar je kunt ervoor kiezen ons te steunen door een maandabonnement te nemen op $4.99 in ruil voor coole extra's zoals kleurthema's, persoonlijke trackingstatistieken en meer. Sluit je aan bij onze missie en word lid!" }, "subscribe_pitch_spring": { "message": "Vind je het leuk wat wij doen? Steun ons en ontgrendel nieuwe thema's voor de lente, persoonlijke trackinginzichten en andere speciale extra's door naar Ghostery Plus te upgraden!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "Te veel verzoeken voor wachtwoord opnieuw instellen. Probeer het over een uur nog eens." + }, + "too_many_failed_logins_text": { + "message": "Te veel mislukte inlogpogingen. Probeer het over één uur opnieuw." } } diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 36d822911..d603cb745 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Konto" }, + "settings_import_export": { + "message": "Importuj i eksportuj ustawienia" + }, "settings_trackers": { "message": "Tropiciele" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Udział w Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "Udział w testach A/B" + }, "settings_signin_create_header": { "message": "Zaloguj się / Utwórz konto" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards to funkcja o ściśle prywatnym charakterze umożliwiająca otrzymywanie od współpracujących z nami firm zniżek i ofert specjalnych podczas przeglądania treści." }, + "settings_abtests_tooltip": { + "message": "Udział w losowych testach A/B pomaga zespołowi Ghostery zrozumieć, która wersja nowego układu lub funkcji bardziej Ci się podoba." + }, "settings_opt_in": { "message": "Przyłącz się / Zrezygnuj" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "Podsumowanie" + }, + "android_tab_site_blocking": { + "message": "Blokowanie witryn" + }, + "android_tab_global_blocking": { + "message": "Blokowanie globalne" + }, + "android_site_blocking_header": { + "message": "Tropiciele na tej stronie" + }, + "android_global_blocking_header": { + "message": "Globalne śledzenie" + }, + "android_blocking_reset": { + "message": "Resetuj ustawienia" + }, + "android_block": { + "message": "Blokuj" + }, + "android_unblock": { + "message": "Odblokuj" + }, + "android_restrict": { + "message": "Zastrzeż" + }, + "android_unrestrict": { + "message": "Cofnij" + }, + "android_trust": { + "message": "Ufaj" + }, + "android_untrust": { + "message": "Cofnij" + }, + "android_anonymize": { + "message": "Anonimizuj" + }, + "android_anonymized": { + "message": "Zanonimizowane" + }, "hub_side_navigation_home": { "message": "Strona główna" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "Jesteś w pełni chroniony!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub – Uaktualnij plan" + }, "hub_upgrade_your": { "message": "Uaktualnij swój" }, @@ -1684,7 +1738,7 @@ "message": "Rozwiązuj szybko problemy przy pomocy naszego działu wsparcia priorytetowego - i otrzymaj dodatkowe korzyści" }, "hub_supporter_manifesto": { - "message": "Staramy się dostarczać naszym użytkownikom najwyższej jakości bezpłatne usługi z zakresu ochrony prywatności. Chociaż nie pobieramy opłat za naszą wtyczkę do przeglądarki, możesz wesprzeć nas, decydując się na niewielki miesięczny abonament. Przyłącz się do naszej misji, przechodząc do Ghostery Plus — i tym samym odblokuj super dodatki!" + "message": "Staramy się dostarczać naszym użytkownikom najwyższej jakości bezpłatne usługi z zakresu ochrony prywatności. Chociaż nie pobieramy opłat za naszą wtyczkę do przeglądarki, możesz wesprzeć nas, decydując się na miesięczny abonament w cenie 4,99 USD. Przyłącz się do naszej misji, przechodząc do Ghostery Plus — i tym samym odblokuj super dodatki!" }, "hub_supporter_feature_theme_description": { "message": "Dostosuj kolorystykę Ghostery, zyskując nowe wrażenia wizualne! Funkcja wprowadzona na prośbę użytkowników. Wypróbuj specjalny temat Dark Blue, a w przyszłości także i inne." @@ -1899,7 +1953,7 @@ "message": "Tematy" }, "subscribe_pitch": { - "message": "Chociaż usługi Ghostery są bezpłatne, możesz wesprzeć nas, decydując się na niewielki miesięczny abonament w zamian za super dodatki, takie jak kolorowe tematy, osobiste statystyki śledzenia i wiele innych. Przyłącz się do naszej misji i subskrybuj!" + "message": "Chociaż usługi Ghostery są bezpłatne, możesz wesprzeć nas, decydując się na miesięczny abonament w cenie 4,99 USD w zamian za super dodatki, takie jak kolorowe motywy, osobiste statystyki śledzenia i wiele innych. Przyłącz się do naszej misji i subskrybuj!" }, "subscribe_pitch_spring": { "message": "Podoba Ci się to, co robimy? Wesprzyj nas i odblokuj nowe wiosenne motywy, osobiste analizy dotyczące śledzenia i inne specjalne dodatki, przechodząc do wersji Ghostery Plus!" @@ -2468,7 +2522,7 @@ "message": "Wypróbuj Ghostery Midnight" }, "seven_day_free_trial": { - "message": "7-dniowy bezpłatny okres próbny" + "message": "7-dniowy darmowy okres próbny" }, "spring_is_here": { "message": "Nadeszła wiosna!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "Zbyt wiele żądań zresetowania hasła. Spróbuj ponownie za godzinę." + }, + "too_many_failed_logins_text": { + "message": "Zbyt wiele nieudanych prób logowania. Spróbuj ponownie za godzinę." } } diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index 10d3074a0..a0d5e5378 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Conta" }, + "settings_import_export": { + "message": "Configurações de importação e exportação" + }, "settings_trackers": { "message": "Rastreadores" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Participando das Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "Participando de tests A/B" + }, "settings_signin_create_header": { "message": "Entrar / criar conta" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "O Ghostery Rewards é um recurso privado por design que oferece descontos e ofertas especiais de empresas parceiras enquanto você navega." }, + "settings_abtests_tooltip": { + "message": "Participar de testes A/B aleatórios ajuda o Ghostery a entender qual versão de um novo layout ou recurso usuários como você preferem." + }, "settings_opt_in": { "message": "Optar por inclusão/exclusão" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "Visão geral" + }, + "android_tab_site_blocking": { + "message": "Bloqueio de site" + }, + "android_tab_global_blocking": { + "message": "Bloqueio global" + }, + "android_site_blocking_header": { + "message": "Rastreadores neste site" + }, + "android_global_blocking_header": { + "message": "Rastreamento global" + }, + "android_blocking_reset": { + "message": "Redefinir configurações" + }, + "android_block": { + "message": "Bloquear" + }, + "android_unblock": { + "message": "Desbloquear" + }, + "android_restrict": { + "message": "Restringir" + }, + "android_unrestrict": { + "message": "Desfazer" + }, + "android_trust": { + "message": "Confiar" + }, + "android_untrust": { + "message": "Desfazer" + }, + "android_anonymize": { + "message": "Anonimizar" + }, + "android_anonymized": { + "message": "Anonimizado" + }, "hub_side_navigation_home": { "message": "Início" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "Você está totalmente protegido!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - Plano de atualização" + }, "hub_upgrade_your": { "message": "Atualize o seu" }, @@ -1684,7 +1738,7 @@ "message": "Resolva os problemas rapidamente com o nosso serviço de help desk prioritário - e muitas outras vantagens" }, "hub_supporter_manifesto": { - "message": "Nós nos esforçamos para oferecer os melhores serviços de proteção de privacidade para nossos usuários sem custos. Apesar de não cobrarmos pelo nosso extensão de navegador, você pode optar por nos apoiar através de uma pequena assinatura mensal. Junte-se a nós em nossa missão atualizando para Ghostery Plus - e desbloqueie regalias legais pelo caminho!" + "message": "Nós nos esforçamos para oferecer os melhores serviços de proteção de privacidade para nossos usuários sem custos. Apesar de não cobrarmos pelo nosso extensão de navegador, você pode optar por nos apoiar através de uma pequena assinatura mensal de $4,99 . Junte-se a nós em nossa missão atualizando para Ghostery Plus - e desbloqueie regalias legais pelo caminho!" }, "hub_supporter_feature_theme_description": { "message": "Personalize as cores do Ghostery para uma nova experiência visual! Introduzido a pedido popular. Confira nosso tema especial Dark Blue e muito mais por vir." @@ -1899,7 +1953,7 @@ "message": "Temas" }, "subscribe_pitch": { - "message": "Embora o Ghostery seja gratuito, você pode optar por nos apoiar por meio de uma pequena assinatura em troca de vantagens especiais, como temas de cores, estatísticas pessoais de rastreamento e muito mais. Participe de nossa missão e assine!" + "message": "Embora o Ghostery seja gratuito, você pode optar por nos apoiar por meio de uma pequena assinatura mensal de $4,99 em troca de vantagens especiais, como temas de cores, estatísticas pessoais de rastreamento e muito mais. Participe de nossa missão e assine!" }, "subscribe_pitch_spring": { "message": "Gosta do que fazemos? Nos apoie e desbloqueie novos temas de primavera, insights pessoais de acompanhamento e outras vantagens especiais atualizando para o Ghostery Plus!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "Muitas solicitações para redefinição de senha. Tente novamente em uma hora." + }, + "too_many_failed_logins_text": { + "message": "Muitas falhas ao efetuar login. Tente novamente em uma hora." } } diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 849fb2f07..f806d94b7 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Учетная запись" }, + "settings_import_export": { + "message": "Настройки импорта и экспорта" + }, "settings_trackers": { "message": "Трекеры" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "Участие в Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "Участие в тестировании А/B" + }, "settings_signin_create_header": { "message": "Войти / Создать учетную запись" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards - это конфиденциальная функция, которая предоставляет вам скидки и специальные предложения от наших компаний-партнеров." }, + "settings_abtests_tooltip": { + "message": "Участие в случайных тестированиях A/B помогает Ghostery определить, какую версию новой схемы или функции предпочитают подобные вам пользователи." + }, "settings_opt_in": { "message": "Принять участие / отказаться" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "Обзор" + }, + "android_tab_site_blocking": { + "message": "Блокировка сайта" + }, + "android_tab_global_blocking": { + "message": "Глобальная блокировка" + }, + "android_site_blocking_header": { + "message": "Трекеры на этом сайте" + }, + "android_global_blocking_header": { + "message": "Глобальный трекинг" + }, + "android_blocking_reset": { + "message": "Сбросить настройки" + }, + "android_block": { + "message": "Заблокировать" + }, + "android_unblock": { + "message": "Разблокировать" + }, + "android_restrict": { + "message": "Запретить" + }, + "android_unrestrict": { + "message": "Вернуть" + }, + "android_trust": { + "message": "Доверять" + }, + "android_untrust": { + "message": "Вернуть" + }, + "android_anonymize": { + "message": "Сделать анонимным" + }, + "android_anonymized": { + "message": "Сделано анонимным" + }, "hub_side_navigation_home": { "message": "Домашняя страница" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "Вы полностью защищены!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - План обновления" + }, "hub_upgrade_your": { "message": "Обновите ваш" }, @@ -1684,7 +1738,7 @@ "message": "Быстро разрешайте проблемы с помощью нашей приоритетной службы поддержки - доп. функции ожидаются в ближайшее время" }, "hub_supporter_manifesto": { - "message": "Мы стремимся обеспечивать конфиденциальность для наших пользователей бесплатно. Хотя мы не взимаем плату за наше расширение браузера, вы можете поддержать нас посредством недорогой ежемесячной подписки. Поддержите нас в нашей миссии, перейдя на Ghostery Plus, и получите отличные бонусы!" + "message": "Мы стремимся обеспечивать конфиденциальность для наших пользователей бесплатно. Хотя мы не взимаем плату за наше расширение браузера, вы можете поддержать нас, оформив ежемесячную подписку в размере $4,99. Поддержите нас в нашей миссии, перейдя на Ghostery Plus, и получите отличные бонусы!" }, "hub_supporter_feature_theme_description": { "message": "Настройте цвета Ghostery и получите новые впечатления! Добавлено в связи с популярным запросом. Попробуйте нашу специальную тему \"Dark Blue\" и многое другое." @@ -1899,7 +1953,7 @@ "message": "Темы" }, "subscribe_pitch": { - "message": "Несмотря на то, что использование Ghostery бесплатно, вы можете поддержать нас, оформив недорогую ежемесячную подписку, и получить специальные привилегии, такие как цветовые темы, личная статистика отслеживания и многое другое. Поддержите нас в нашей мисии и подпишитесь!" + "message": "Несмотря на то, что использование Ghostery бесплатно, вы можете поддержать нас, оформив ежемесячную подписку в размере $4.99. После этого вам будут доступны специальные привилегии, такие как цветовые темы, личная статистика отслеживания и многое другое. Поддержите нас в нашей мисии и подпишитесь!" }, "subscribe_pitch_spring": { "message": "Вам нравитя то, что мы делаем? Поддержите нас и откройте для себя новые весенние темы и личные сведения о трекинге, а также получите другие специальные привелегии, перейдя на Ghostery Plus!" @@ -2468,7 +2522,7 @@ "message": "Попробовать Ghostery Midnight" }, "seven_day_free_trial": { - "message": "7-дневная бесплатная бета-версия" + "message": "7-дневная бесплатная версия" }, "spring_is_here": { "message": "Весна уже наступила!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "Слишком много запросов на сброс пароля. Повторите попытку через один час." + }, + "too_many_failed_logins_text": { + "message": "Слишком много неудачных попыток входа. Повторите попытку через час." } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 7bae1b65d..c9e4b037c 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "帐户" }, + "settings_import_export": { + "message": "导入和导出设置" + }, "settings_trackers": { "message": "跟踪器" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "已参与Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "参与 A/B 测试" + }, "settings_signin_create_header": { "message": "登录/创建帐户" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards是一个按私密原则设计的功能,能在您上网浏览时为您提供来自我们合作公司的折扣和特惠。" }, + "settings_abtests_tooltip": { + "message": "参与随机 A/B 测试有助于 Ghostery 了解您这类用户喜欢哪种版本的新布局或功能。 " + }, "settings_opt_in": { "message": "加入/退出" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "概览" + }, + "android_tab_site_blocking": { + "message": "站点拦截" + }, + "android_tab_global_blocking": { + "message": "全局拦截" + }, + "android_site_blocking_header": { + "message": "此网站上的追踪器" + }, + "android_global_blocking_header": { + "message": "全局追踪" + }, + "android_blocking_reset": { + "message": "重置设置" + }, + "android_block": { + "message": "拦截" + }, + "android_unblock": { + "message": "取消拦截" + }, + "android_restrict": { + "message": "限制" + }, + "android_unrestrict": { + "message": "撤消" + }, + "android_trust": { + "message": "信任" + }, + "android_untrust": { + "message": "撤消" + }, + "android_anonymize": { + "message": "匿名" + }, + "android_anonymized": { + "message": "保持匿名" + }, "hub_side_navigation_home": { "message": "主页" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "您已获得全面保护!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - 更新套餐" + }, "hub_upgrade_your": { "message": "升级您的" }, @@ -1684,7 +1738,7 @@ "message": "使用我们的优先帮助台服务快速解决问题,更多特权即将推出" }, "hub_supporter_manifesto": { - "message": "我们致力于为用户免费提供最出色的隐私保护服务。尽管我们的浏览器扩展程序不收费,但是您可以选择通过低价按月订阅来支持我们。升级为 Ghostery Plus,加入我们的使命——并解锁各种酷炫福利!" + "message": "我们致力于为用户免费提供最出色的隐私保护服务。尽管我们的浏览器扩展程序不收费,但是您可以选择通过价格为 $4.99 的按月订阅来支持我们。升级为 Ghostery Plus,加入我们的使命——并解锁各种酷炫福利!" }, "hub_supporter_feature_theme_description": { "message": "定制 Ghostery 的颜色,获得全新的视觉体验!通过热门请求引入。快看看我们的特别版 Dark Blue 主题,更多主题敬请期待。" @@ -1899,7 +1953,7 @@ "message": "主题" }, "subscribe_pitch": { - "message": "尽管 Ghostery 是免费的,但是您可以选择通过低价按月订阅来支持我们,同时还能获得专属福利,例如彩色主题、个人追踪统计数据等等。加入我们的使命并订阅!" + "message": "尽管 Ghostery 是免费的,但是您可以选择通过价格为 $4.99 的按月订阅来支持我们,同时还能获得专属福利,例如彩色主题、个人追踪统计数据等等。加入我们的使命并订阅!" }, "subscribe_pitch_spring": { "message": "喜欢我们的创意?升级至 Ghostery Plus,解锁全新的春季主题、个人追踪洞察和其他特别福利!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "密码重置请求次数过多。请一小时后重试。" + }, + "too_many_failed_logins_text": { + "message": "登录失败次数过多。请一小时后重试。" } } diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index bd1b68e33..4a5c84891 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "帳戶" }, + "settings_import_export": { + "message": "匯入與匯出設定" + }, "settings_trackers": { "message": "網頁跟蹤器" }, @@ -944,6 +947,9 @@ "settings_allow_offers": { "message": "參與Ghostery Rewards" }, + "settings_allow_abtests": { + "message": "參加 A/B 測試" + }, "settings_signin_create_header": { "message": "登錄/創建帳戶" }, @@ -1034,6 +1040,9 @@ "settings_offers_tooltip": { "message": "Ghostery Rewards是一項特別設計的私人特色,旨在您瀏覽時為您提供來自我們夥伴公司的折扣和特別優惠。" }, + "settings_abtests_tooltip": { + "message": "參加隨機的 A/B 測試,協助 Ghostery 了解使用者偏好哪一版本的新版面佈局或功能。" + }, "settings_opt_in": { "message": "選擇加入/退出" }, @@ -1234,6 +1243,48 @@ } } }, + "android_tab_overview": { + "message": "概覽" + }, + "android_tab_site_blocking": { + "message": "網站封鎖" + }, + "android_tab_global_blocking": { + "message": "全球封鎖" + }, + "android_site_blocking_header": { + "message": "此網站上的網頁跟蹤器" + }, + "android_global_blocking_header": { + "message": "全球追蹤" + }, + "android_blocking_reset": { + "message": "重置設定" + }, + "android_block": { + "message": "阻止" + }, + "android_unblock": { + "message": "解除阻止" + }, + "android_restrict": { + "message": "限制" + }, + "android_unrestrict": { + "message": "還原" + }, + "android_trust": { + "message": "信任" + }, + "android_untrust": { + "message": "還原" + }, + "android_anonymize": { + "message": "匿名" + }, + "android_anonymized": { + "message": "已匿名" + }, "hub_side_navigation_home": { "message": "首頁" }, @@ -1306,6 +1357,9 @@ "hub_home_plus_full_protection": { "message": "您已受到全面防護!" }, + "hub_upgrade_page_title": { + "message": "Ghostery Hub - 升級計劃" + }, "hub_upgrade_your": { "message": "升級您的" }, @@ -1684,7 +1738,7 @@ "message": "用我們的優先幫助服務快速解決問題——還有更多福利" }, "hub_supporter_manifesto": { - "message": "我們努力免費為我們的客戶提供最佳隱私保護服務。我們的瀏覽器擴充功能不收費,您可以選擇透過每月小額訂閱支持我們。升級至 Ghostery Plus 加入我們,支持我們的使命——一路解鎖附帶福利!" + "message": "我們努力免費為我們的客戶提供最佳隱私保護服務。我們的瀏覽器擴充功能不收費,您可以選擇透過每月訂閱 $4.99 支持我們。升級至 Ghostery Plus 加入我們,支持我們的使命——一路解鎖附帶福利!" }, "hub_supporter_feature_theme_description": { "message": "自訂 Ghostery 色彩,獲得全新視覺體驗!使用者千呼萬喚的功能登場了。查看我們獨特的深藍色主題,更多內容即將推出。" @@ -1899,7 +1953,7 @@ "message": "主題" }, "subscribe_pitch": { - "message": "Ghostery 是免費的,您可以選擇透過每月小額訂閱支持我們,訂閱後可獲得特別福利,如顏色主題、個人追蹤統計數據以及更多。加入我們,支持我們的使命並訂閱!" + "message": "Ghostery 是免費的,您可以選擇透過每月訂閱 $4.99 支持我們,訂閱後可獲得特別福利,如顏色主題、個人追蹤統計數據以及更多。加入我們,支持我們的使命並訂閱!" }, "subscribe_pitch_spring": { "message": "喜歡我們提供的服務嗎?您可以升級至 Ghostery Plus 來支援我們,並解鎖全新春日主題、個人追蹤資訊及其他特別的好處!" @@ -2499,5 +2553,8 @@ }, "too_many_password_resets_text": { "message": "密碼重設要求次數過多。請一小時後再重試。" + }, + "too_many_failed_logins_text": { + "message": "登入失敗次數過多,請一小時後重試。" } } From 4f9b05bd2640320c971fb275a1341a4ff1d3e7bb Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Fri, 2 Oct 2020 12:19:11 -0400 Subject: [PATCH 32/35] udpate travis.yml --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8db76b7a1..7c22839f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,9 @@ language: node_js +cache: yarn node_js: - "lts/dubnium" sudo: false -cache: - yarn: true - install: - yarn install --frozen-lockfile From cfad92297033da4a1e2be1e008b2c2148cf1097d Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Fri, 2 Oct 2020 12:36:35 -0400 Subject: [PATCH 33/35] update Changelog --- .travis.yml | 3 +-- CHANGELOG.md | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7c22839f6..eb4d47f79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: node_js -cache: yarn node_js: - "lts/dubnium" -sudo: false +cache: yarn install: - yarn install --frozen-lockfile diff --git a/CHANGELOG.md b/CHANGELOG.md index a7a547c8e..60a92c7b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ ### GHOSTERY 8.5.3 () + Updated Firefox Android extension panel UI and mobile optimizations (#587) ++ New console debugging interface for user troubleshooting (#568) + Display error message after too many failed login attempts (#577) ++ Add opt-out for AB Tests (#608) + Added product id parameter to extension pings (#574) ++ Detect Ghostery Desktop Browser (#602) ++ Remove broken page pings (#609) ++ On-boarding AB Tests (#603) ++ Updated translations + +See the complete GitHub [milestone](https://github.com/ghostery/ghostery-extension/milestone/14?closed=1) ### GHOSTERY 8.5.2 (July 30, 2020) From 3c1a081b2ff1323f9247353b0c6bdad3028c2131 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Fri, 2 Oct 2020 12:41:26 -0400 Subject: [PATCH 34/35] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7fc29020..87edbff6a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Ghostery](https://www.ghostery.com/wp-content/themes/ghostery/images/ghostery_logo_black.svg)](https://www.ghostery.com) --- -[![Build Status](https://travis-ci.org/ghostery/ghostery-extension.svg?branch=master)](https://travis-ci.org/ghostery/ghostery-extension)   ![GitHub manifest version](https://img.shields.io/github/manifest-json/v/ghostery/ghostery-extension.svg?style=flat-square)   [![Chat on Gitter](https://img.shields.io/gitter/room/ghostery/ghostery-expenstion.svg?style=flat-square)](https://gitter.im/ghostery/ghostery-extension)   [![Twitter Follow](https://img.shields.io/twitter/follow/ghostery.svg?style=social&maxAge=3600)](https://twitter.com/ghostery) +[![Build Status](https://travis-ci.com/ghostery/ghostery-extension.svg?branch=master)](https://travis-ci.com/ghostery/ghostery-extension)   ![GitHub manifest version](https://img.shields.io/github/manifest-json/v/ghostery/ghostery-extension.svg?style=flat-square)   [![Twitter Follow](https://img.shields.io/twitter/follow/ghostery.svg?style=social&maxAge=3600)](https://twitter.com/ghostery) Ghostery helps you browse smarter by giving you control over ads and tracking technologies to speed up page loads, eliminate clutter, and protect your data. This is the unified code repository for the Ghostery browser extensions in Chrome, Firefox, Opera and Edge. From 45832c2f5e47e269a342db2b8b0195683fc3e9ad Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 7 Oct 2020 12:24:13 -0400 Subject: [PATCH 35/35] Fix bug that always made ABTest clients think tests had not been fetched yet --- src/classes/ABTest.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/classes/ABTest.js b/src/classes/ABTest.js index da4d122b3..76b380256 100644 --- a/src/classes/ABTest.js +++ b/src/classes/ABTest.js @@ -102,6 +102,8 @@ class ABTest { ); // update conf globals.SESSION.abtests = this.tests; + // let clients know that if a test is absent it is not because tests have not yet been fetched + this.hasBeenFetched = true; } }