diff --git a/.travis.yml b/.travis.yml index 8db76b7a1..eb4d47f79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,7 @@ language: node_js node_js: - "lts/dubnium" -sudo: false - -cache: - yarn: true +cache: yarn install: - yarn install --frozen-lockfile diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f86cd0f3..60a92c7b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +### 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) + Fixes bug where Ghostery icon could be grayed out on restricted sites (#564) 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/README.md b/README.md index 1dbf74764..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. @@ -51,7 +51,6 @@ $ yarn build.watch ```javascript // In manifest.json set "debug": true, -"log": true, ``` ## Testing and Linting @@ -115,7 +114,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) @@ -161,7 +160,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. 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/en/messages.json b/_locales/en/messages.json index b3f1807f1..ceb9eafaf 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" }, @@ -944,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" }, @@ -1034,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" }, @@ -1234,6 +1243,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" }, @@ -1306,6 +1357,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" }, @@ -1684,7 +1738,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." @@ -1899,7 +1953,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!" @@ -2468,7 +2522,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!" @@ -2499,5 +2553,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/_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": "登入失敗次數過多,請一小時後重試。" } } 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/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/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/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", + } + } + />
{ const { extraRoutes, sendMountActions, steps } = props; return ( -
+
{steps.map(step => (
{ disabled: disableNav, }); + const menuClassNames = ClassNames(`SideNavigation__menu ${ah ? '' : 'flex-child-grow'} flex-container flex-dir-column`); + return (
- -
+ +
{menuItems.map(item => _renderMenuItem(item, disableNav))}
diff --git a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx index 0fa5028e5..6ba4f00fa 100644 --- a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx +++ b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx @@ -13,10 +13,15 @@ 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 } = 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 @@ -49,15 +54,19 @@ 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') }, { href: '/tutorial', icon: 'tutorial', text: t('hub_side_navigation_tutorial') }, { href: '/plus', icon: 'plus', text: t('get_ghostery_plus') }, - ...((IS_CLIQZ) ? [] : [{ href: '/rewards', icon: 'rewards', text: t('hub_side_navigation_rewards') }]), - { href: '/products', icon: 'products', text: t('hub_side_navigation_products') } + ...((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/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" >
{ - 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
(

{t('hub_upgrade_plan_free')}

- + {t('hub_upgrade_already_protected')}

{t('hub_upgrade_basic_protection')}

@@ -113,12 +114,20 @@ 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 */ const UpgradePlanView = (props) => { + useEffect(() => { + const title = t('hub_upgrade_page_title'); + window.document.title = title; + }, []); + const { protection_level, show_yearly_prices, @@ -173,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 ( @@ -184,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 ( @@ -400,7 +411,7 @@ const UpgradePlanView = (props) => { - + {t('hub_upgrade_already_protected')} @@ -482,7 +493,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 e6e73f46d..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 { @@ -749,6 +746,7 @@ section.home-template .section.section-pricing { align-items: center; } @include breakpoint($medium-large-breakpoint down) { + max-width: 100%; padding-left: rem-calc(20); padding-right: rem-calc(20); } 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/app/images/hub/tutorial/antisuite-simple-android.png b/app/images/hub/tutorial/antisuite-simple-android.png new file mode 100644 index 000000000..b72a4f983 Binary files /dev/null and b/app/images/hub/tutorial/antisuite-simple-android.png differ 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 000000000..c4b03659a Binary files /dev/null and b/app/images/hub/tutorial/blocking-detailed-android.png differ diff --git a/app/images/hub/tutorial/layout-detailed-android.png b/app/images/hub/tutorial/layout-detailed-android.png new file mode 100644 index 000000000..6c295ec42 Binary files /dev/null and b/app/images/hub/tutorial/layout-detailed-android.png differ diff --git a/app/images/hub/tutorial/layout-simple-android.png b/app/images/hub/tutorial/layout-simple-android.png new file mode 100644 index 000000000..d55900873 Binary files /dev/null and b/app/images/hub/tutorial/layout-simple-android.png differ diff --git a/app/images/hub/tutorial/tracker-list-android.png b/app/images/hub/tutorial/tracker-list-android.png new file mode 100644 index 000000000..ccc656131 Binary files /dev/null and b/app/images/hub/tutorial/tracker-list-android.png differ diff --git a/app/images/hub/tutorial/trustrestrict-simple-android.png b/app/images/hub/tutorial/trustrestrict-simple-android.png new file mode 100644 index 000000000..418af0e6f Binary files /dev/null and b/app/images/hub/tutorial/trustrestrict-simple-android.png differ diff --git a/app/images/panel-android/categories/unknown.svg b/app/images/panel-android/categories/unknown.svg new file mode 100644 index 000000000..1408249f7 --- /dev/null +++ b/app/images/panel-android/categories/unknown.svg @@ -0,0 +1,7 @@ + + + + + + + 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 ( -
-
-

Global Trackers

- -
- -
- ); - } -} - -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..579d7e78d --- /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.arrayOf.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..e84546329 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,13 @@ */ 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'); + +const TOOLTIP_SVG_FILEPATH = '../../app/images/panel/icon-information-tooltip.svg'; /** * @class Implement Opt In subview as a React component. @@ -22,55 +26,85 @@ const { IS_CLIQZ } = globals; * 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 && ( -
    -
    - - -
    - -
    -
    -
    - )}
    -
    -); + ); +}; + +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, + enable_abtests: 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..c30d04ec8 --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap @@ -0,0 +1,299 @@ +// 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/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/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'); } 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/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..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%; @@ -31,6 +34,7 @@ html, body, #root { .App__mainContent { margin-left: 54px; } + .android-relative {position: relative;} } // Foundation Overrides @@ -40,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/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/_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; 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..0c26983df 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,12 +12,9 @@ */ .content-subscription { - position: absolute; - height: 479px; - width: 100%; .badge { display: block; - margin: 35px auto 35px auto; + margin: 15px auto; width: 137px; height: 97px; background-image: url('/app/images/panel/subscribe-badge.svg'); 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/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: } diff --git a/manifest.json b/manifest.json index 500ff02c3..e96cf7961 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,11 @@ { + "debug": true, "manifest_version": 2, "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__", "icons": { @@ -14,8 +15,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" }, @@ -113,5 +114,6 @@ "cliqz/offers-templates/reminder.html", "cliqz/offers-templates/checkout.html", "cliqz/offers-templates/control-center.html" - ] -} \ No newline at end of file + ], + "debug": true +} diff --git a/package.json b/package.json index 56eb33e5d..cf1b3077d 100644 --- a/package.json +++ b/package.json @@ -43,23 +43,23 @@ }, "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.1/browser-core-7.47.1.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", "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", "redux-thunk": "^2.2.0", @@ -69,47 +69,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/src/background.js b/src/background.js index 29538812d..1644488a0 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,8 @@ 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 +import ghosteryDebugger from './classes/Debugger'; +// object classes import Events from './classes/EventHandlers'; import Policy from './classes/Policy'; // static classes @@ -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 @@ -65,8 +69,11 @@ 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 ONE_DAY_MSEC = 86400000; +const ONE_HOUR_MSEC = 3600000; const onBeforeRequest = events.onBeforeRequest.bind(events); const { onHeadersReceived } = Events; @@ -102,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) => { @@ -134,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(); } } @@ -208,7 +223,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 +761,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 +811,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') { @@ -815,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) => { @@ -950,7 +968,7 @@ function onMessageHandler(request, sender, callback) { return true; } if (name === 'update_database') { - checkLibraryVersion().then((result) => { + updateDBs().then((result) => { callback(result); }); return true; @@ -970,6 +988,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 +1011,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 +1061,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; @@ -1051,78 +1090,51 @@ function onMessageHandler(request, sender, callback) { } /** - * Determine Antitracking configuration parameters based + * Set option for Hub Layout A/B test 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, - }; +function setupHubLayoutABTest() { + if ( + !abtest.hasBeenFetched + || conf.hub_layout !== 'not_yet_set' + ) { return; } + + if (abtest.hasTest('hub_alternate')) { + conf.hub_layout = 'alternate'; + } else { + conf.hub_layout = 'default'; + } } /** - * Set option for Hub promo A/B/C test based - * on the results returned from the abtest endpoint. + * Configure A/B tests based on data fetched from the A/B server * @memberOf Background - * - * @return {Object} Hub promotion configuration parameters */ -function setupHubPromoABTest() { - if (conf.hub_promo_variant !== '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'; - } else { - conf.hub_promo_variant = 'upgrade'; - } +function setupABTests() { + setupHubLayoutABTest(); } /** - * Adjust antitracking parameters based on the current state - * of ABTest and availability of Human Web. + * @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 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 setCliqzAntitrackingConfig(isAntitrackingEnabled) { + if (!isAntitrackingEnabled) return; + + const antitrackingConfig = { + qsEnabled: true, + telemetryMode: conf.enable_human_web ? 1 : 0, + }; - setupHubPromoABTest(); + Object.entries(antitrackingConfig).forEach(([opt, val]) => { + log('antitracking', 'set config option', opt, val); + antitracking.action('setConfigOption', opt, val); + }); } /** @@ -1151,7 +1163,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); @@ -1177,7 +1189,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); } @@ -1198,15 +1214,13 @@ 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'); }); } /** - * 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. @@ -1359,7 +1373,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 +1642,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 +1682,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; } @@ -1679,11 +1693,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'); @@ -1695,13 +1708,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(); @@ -1727,14 +1744,14 @@ 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 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 }); } @@ -1757,6 +1774,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(() => { @@ -1766,6 +1784,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 df7a235fb..76b380256 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; } /** @@ -37,43 +38,73 @@ 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._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; + // let clients know that if a test is absent it is not because tests have not yet been fetched + this.hasBeenFetched = true; + } } // Return the class as a singleton diff --git a/src/classes/Account.js b/src/classes/Account.js index ccac43193..d30c6d32d 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 @@ -22,6 +22,8 @@ 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(); const { @@ -83,9 +85,10 @@ 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(); + this.getUserSubscriptionData({ calledFrom: 'login' }); }); return {}; }); @@ -103,6 +106,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 +128,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)); }); @@ -162,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) => { @@ -192,6 +199,11 @@ class Account { return subscriptions; }) + .finally(() => { + if (options && options.calledFrom === 'login') { + metrics.ping('sign_in_success'); + } + }) ) saveUserSettings = () => ( @@ -254,15 +266,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 +285,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 +293,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 +328,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/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/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/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..990db8cd4 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,11 +112,12 @@ 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_abtests', true); _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); @@ -134,7 +136,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/Debugger.js b/src/classes/Debugger.js new file mode 100644 index 000000000..64f204d07 --- /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 && 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/EventHandlers.js b/src/classes/EventHandlers.js index 1f950675d..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); @@ -495,7 +493,7 @@ class EventHandlers { } const appWithLatencyId = latency.logLatency(details); - if (appWithLatencyId) { + if (appWithLatencyId && conf.show_alert) { this.purplebox.updateBox(details.tabId, appWithLatencyId); } } @@ -515,7 +513,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); } } @@ -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 0d8894f93..c98ed82be 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 = { @@ -74,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 = {}; @@ -111,6 +103,7 @@ class Globals { 'enable_human_web', 'enable_metrics', 'enable_offers', + 'enable_abtests', 'enable_smart_block', 'expand_all_trackers', 'hide_alert_trusted', @@ -178,6 +171,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 @@ -193,6 +191,31 @@ class Globals { // Set version property this.BROWSER_INFO.version = version; + + // Check for the Ghostery Android browser + if (platform.includes('android')) { + 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/Metrics.js b/src/classes/Metrics.js index 0578ddfc0..e9c9cd31b 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; @@ -368,11 +286,15 @@ class Metrics { // Theme `&th=${encodeURIComponent(Metrics._getThemeValue().toString())}` + - // New parameter for Ghostery 8.5.2 - // Hub Promo variant - `&hp=${encodeURIComponent(Metrics._getHubPromoVariant().toString())}` + + // New parameters for Ghostery 8.5.2 // Subscription Interval - `&subscription_interval=${encodeURIComponent(Metrics._getSubscriptionInterval().toString())}`; + `&si=${encodeURIComponent(Metrics._getSubscriptionInterval().toString())}` + + // Product ID Parameter + `&pi=${encodeURIComponent('gbe')}` + + + // New parameter for Ghostery 8.5.3 + // AB tests enabled? + `&ts=${encodeURIComponent(conf.enable_abtests ? '1' : '0')}`; if (CAMPAIGN_METRICS.includes(type)) { // only send campaign attribution when necessary @@ -381,14 +303,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; } @@ -531,27 +446,6 @@ class Metrics { } } - /** - * Get the Int associated with the Hub promo variant shown on install - * @private - * @return {number} Int associated with the Hub promo variant - */ - static _getHubPromoVariant() { - const { hub_promo_variant } = conf; - - switch (hub_promo_variant) { - case 'upgrade': - return 1; - case 'plain': - return 2; - case 'midnight': - return 3; - case 'not_yet_set': - default: - return 0; - } - } - /** * Get the Int associated with the users subscription interval * @private diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 884ad2ef1..c47f542d1 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 @@ -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'; @@ -28,7 +27,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 +75,7 @@ class PanelData { this._panelPort = port; this._mountedComponents.panel = true; - getActiveTab((tab) => { + function tabCallback(tab) { const { url } = tab; this._activeTab = tab; @@ -86,7 +90,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)); + } } /** @@ -482,7 +500,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; @@ -498,6 +516,7 @@ class PanelData { enable_human_web, enable_offers, enable_metrics, + enable_abtests, hide_alert_trusted, ignore_first_party, notify_library_updates, @@ -616,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 ( 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/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; } 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/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 diff --git a/webpack.config.js b/webpack.config.js index 6a379a862..fb73dc38c 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 @@ -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 fb45e82a7..df78e298e 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== - dependencies: - "@babel/helper-get-function-arity" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - -"@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== - dependencies: - "@babel/helper-plugin-utils" "^7.10.3" - -"@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== - 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/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== - dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-jsx" "^7.10.1" - -"@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== - dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-jsx" "^7.10.1" - -"@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== - 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/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== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.3" - -"@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/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.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@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.4" + +"@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.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.10.4" + +"@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.4" + "@babel/plugin-syntax-jsx" "^7.10.4" + +"@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.4" + "@babel/plugin-syntax-jsx" "^7.10.4" + +"@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.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.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.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@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== @@ -375,6 +528,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" @@ -382,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" @@ -391,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" @@ -406,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" @@ -415,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" @@ -425,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== @@ -533,89 +726,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" @@ -626,15 +823,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" @@ -647,10 +844,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" @@ -665,42 +862,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" @@ -716,13 +913,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" @@ -797,12 +995,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" @@ -912,6 +1110,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" @@ -1197,26 +1402,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" @@ -1401,7 +1610,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== @@ -1417,6 +1626,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" @@ -1523,16 +1741,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" @@ -1566,10 +1784,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" @@ -1591,7 +1809,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== @@ -1608,13 +1826,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" @@ -1741,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.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.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" @@ -2056,10 +2274,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" @@ -2137,15 +2355,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" @@ -2389,24 +2598,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" @@ -2869,10 +3078,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" @@ -3030,6 +3239,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" @@ -3062,7 +3276,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== @@ -3071,6 +3285,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" @@ -3088,27 +3311,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" @@ -3123,6 +3346,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" @@ -3258,10 +3489,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" @@ -3294,22 +3525,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" @@ -3327,22 +3558,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" @@ -3352,9 +3583,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" @@ -3368,7 +3599,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" @@ -3381,14 +3612,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" @@ -3512,16 +3743,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: @@ -3833,7 +4064,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== @@ -4620,7 +4851,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== @@ -4764,57 +4995,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" @@ -4826,15 +5057,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" @@ -4843,39 +5074,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" @@ -4890,61 +5123,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" @@ -4956,15 +5191,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" @@ -4980,13 +5215,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" @@ -4994,17 +5229,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" @@ -5016,147 +5252,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: @@ -5167,22 +5407,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" @@ -5219,10 +5460,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" @@ -5281,10 +5522,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" @@ -5363,7 +5604,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== @@ -5412,6 +5653,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" @@ -5585,6 +5831,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" @@ -5726,6 +5977,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" @@ -5823,10 +6079,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" @@ -5997,6 +6253,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" @@ -6242,7 +6503,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== @@ -6272,7 +6533,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== @@ -6686,15 +6947,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" @@ -6721,12 +6982,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== @@ -6760,12 +7021,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" @@ -6975,7 +7236,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== @@ -7009,10 +7270,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" @@ -7049,13 +7310,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: @@ -7068,6 +7329,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" @@ -7422,7 +7691,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== @@ -7566,16 +7835,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" @@ -7606,7 +7875,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== @@ -7683,13 +7952,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" @@ -8828,10 +9090,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" @@ -8929,15 +9191,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: @@ -8992,10 +9254,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" @@ -9005,7 +9267,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" @@ -9018,7 +9280,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: @@ -9150,13 +9412,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"