diff --git a/locales/cy/exposure-card.ftl b/locales/cy/exposure-card.ftl index e2c0f0c369b..c5dafb7df0f 100644 --- a/locales/cy/exposure-card.ftl +++ b/locales/cy/exposure-card.ftl @@ -28,10 +28,10 @@ exposure-card-other = Arall exposure-card-description-data-breach-action-needed = Cafodd eich manylion eu datgelu yn y tor-data { $data_breach_company } ar { $data_breach_date }. Byddwn yn eich arwain drwy'r camau angenrheidiol i'w drwsio. exposure-card-description-data-breach-fixed = Rydych wedi cymryd y camau angenrheidiol i drwsio'r tor-data hwn. Byddwn yn monitro tor-data yn barhaus ac yn eich rhybuddio am unrhyw ddatgeliadau newydd. exposure-card-your-exposed-info = Eich manylion sydd wedu eu datgelu: -exposure-card-found-the-following-data = Daeth { -brand-monitor } o hyd i'r data datguddio canlynol: +exposure-card-found-the-following-data = Daeth { -brand-monitor } o hyd i'r data sydd yn yr amlwg canlynol: exposure-card-exposure-type-data-broker = Manylion ar werth exposure-card-exposure-type-data-breach = Tor-data -exposure-card-resolve-exposures-cta = Datrys datguddiadau +exposure-card-resolve-exposures-cta = Datrys materion yn yr amlwg exposure-card-label-company-logo = Logo cwmni exposure-card-label-company = Cwmni # Status of the exposure card, could be In Progress, Fixed or Action Needed @@ -40,4 +40,4 @@ exposure-card-label-status = Statws # $category_label is the data breach exposure type that was leaked. Eg. Email, IP Address. # $count is the number of times that the data type was leaked. exposure-card-label-and-count = { $category_label } : { $count } -exposure-card-manual-resolution-praise = Swydd wych! Rydych wedi datrys y datguddiad hwn. +exposure-card-manual-resolution-praise = Da iawn! Rydych wedi datrys yr amlygu hwn. diff --git a/locales/cy/settings.ftl b/locales/cy/settings.ftl index 18bef5821e6..ae603732529 100644 --- a/locales/cy/settings.ftl +++ b/locales/cy/settings.ftl @@ -72,10 +72,10 @@ settings-alert-preferences-allow-monthly-monitor-report-subtitle = Diweddariad m ## Settings page redesign -settings-tab-label-edit-info = Golygu eich gwybodaeth +settings-tab-label-edit-info = Golygu eich manylion settings-tab-label-notifications = Gosod hysbysiadau settings-tab-label-manage-account = Rheoli cyfrif settings-tab-subtitle-manage-account = Rheoli eich cyfrif { -product-name }. settings-tab-notifications-marketing-title = Cyfathrebu marchnata -settings-tab-notifications-marketing-text = Diweddariadau cyfnodol am { -brand-monitor }, { -brand-mozilla }, a'n cynhyrchion diogelwch eraill. +settings-tab-notifications-marketing-text = Diweddariadau o bryd i'w gilydd am { -brand-monitor }, { -brand-mozilla }, a'n cynnyrch diogelwch eraill. settings-tab-notifications-marketing-link-label = Ewch i osodiadau e-bost { -brand-mozilla } diff --git a/locales/en-CA/exposure-card.ftl b/locales/en-CA/exposure-card.ftl index 99a0f9c40d9..ab343a19c19 100644 --- a/locales/en-CA/exposure-card.ftl +++ b/locales/en-CA/exposure-card.ftl @@ -28,9 +28,10 @@ exposure-card-other = Other exposure-card-description-data-breach-action-needed = Your information was exposed in the { $data_breach_company } data breach on { $data_breach_date }. We’ll walk you through the steps to fix it. exposure-card-description-data-breach-fixed = You’ve taken the steps needed to fix this breach. We’ll continually monitor for data breaches and alert you of any new exposures. exposure-card-your-exposed-info = Your exposed info: +exposure-card-found-the-following-data = { -brand-monitor } found the following exposed data: exposure-card-exposure-type-data-broker = Info for sale exposure-card-exposure-type-data-breach = Data breach -exposure-card-cta = Fix all exposures +exposure-card-resolve-exposures-cta = Resolve exposures exposure-card-label-company-logo = Company logo exposure-card-label-company = Company # Status of the exposure card, could be In Progress, Fixed or Action Needed @@ -39,3 +40,4 @@ exposure-card-label-status = Status # $category_label is the data breach exposure type that was leaked. Eg. Email, IP Address. # $count is the number of times that the data type was leaked. exposure-card-label-and-count = { $category_label }: { $count } +exposure-card-manual-resolution-praise = Great job! You resolved this exposure. diff --git a/locales/en-CA/settings.ftl b/locales/en-CA/settings.ftl index a8951868c97..fbeec680485 100644 --- a/locales/en-CA/settings.ftl +++ b/locales/en-CA/settings.ftl @@ -15,8 +15,6 @@ settings-alert-preferences-allow-breach-alerts-title = Instant breach alerts settings-alert-preferences-allow-breach-alerts-subtitle = These alerts are sent immediately once a data breach is detected settings-alert-preferences-option-one = Send breach alerts to the affected email address settings-alert-preferences-option-two = Send all breach alerts to the primary email address -settings-alert-preferences-allow-monthly-monitor-report-title = Monthly { -brand-monitor } report -settings-alert-preferences-allow-monthly-monitor-report-subtitle = A monthly update of new exposures, what’s been fixed, and what needs your attention. ## Monitored email addresses @@ -47,12 +45,6 @@ settings-email-number-of-breaches-info = *[other] Appears in { $breachCount } known breaches. } -## Deactivate account - -settings-deactivate-account-title = Deactivate account -settings-deactivate-account-info-2 = You can deactivate { -product-short-name } by deleting your { -brand-mozilla-account }. -settings-fxa-link-label-3 = Go to { -brand-mozilla-account } settings - ## Delete Monitor account settings-delete-monitor-free-account-title = Delete { -brand-monitor } account @@ -64,3 +56,18 @@ settings-delete-monitor-free-account-dialog-cta-label = Delete account settings-delete-monitor-free-account-dialog-cancel-button-label = Never mind, take me back settings-delete-monitor-account-confirmation-toast-label-2 = Your { -brand-monitor } account is now deleted. settings-delete-monitor-account-confirmation-toast-dismiss-label = Dismiss + +## Monthly Monitor Report + +settings-alert-preferences-allow-monthly-monitor-report-title = Monthly { -brand-monitor } report +settings-alert-preferences-allow-monthly-monitor-report-subtitle = A monthly update of new exposures, what’s been fixed, and what needs your attention. + +## Settings page redesign + +settings-tab-label-edit-info = Edit your info +settings-tab-label-notifications = Set notifications +settings-tab-label-manage-account = Manage account +settings-tab-subtitle-manage-account = Manage your { -product-name } account. +settings-tab-notifications-marketing-title = Marketing communications +settings-tab-notifications-marketing-text = Periodic updates about { -brand-monitor }, { -brand-mozilla }, and our other security products. +settings-tab-notifications-marketing-link-label = Go to { -brand-mozilla } email settings diff --git a/locales/fy-NL/settings.ftl b/locales/fy-NL/settings.ftl index 8e9a303bd52..2c7874df244 100644 --- a/locales/fy-NL/settings.ftl +++ b/locales/fy-NL/settings.ftl @@ -15,8 +15,6 @@ settings-alert-preferences-allow-breach-alerts-title = Daliks warskôgingen oer settings-alert-preferences-allow-breach-alerts-subtitle = Dizze warskôgingen wurde fuortendaliks ferstjoerd, sa gau as in datalek detektearre wurdt settings-alert-preferences-option-one = Warskôgingen oer datalekken nei it troffen e-mailadres stjoere settings-alert-preferences-option-two = Alle warskôgingen oer datalekken nei it primêre e-mailadres stjoere -settings-alert-preferences-allow-monthly-monitor-report-title = Moanliks { -brand-monitor }-rapport -settings-alert-preferences-allow-monthly-monitor-report-subtitle = In moanlikse update fan nije lekken, wat is oplost en wat jo oandacht nedich hat. ## Monitored email addresses @@ -47,12 +45,6 @@ settings-email-number-of-breaches-info = *[other] Komt foar yn { $breachCount } bekende datalekken. } -## Deactivate account - -settings-deactivate-account-title = Account de-aktivearje -settings-deactivate-account-info-2 = Jo kinne { -product-short-name } de-aktivearje troch jo { -brand-mozilla-account } fuort te smiten. -settings-fxa-link-label-3 = Nei { -brand-mozilla-account }-ynstellingen - ## Delete Monitor account settings-delete-monitor-free-account-title = { -brand-monitor }-account fuortsmite @@ -64,3 +56,18 @@ settings-delete-monitor-free-account-dialog-cta-label = Account fuortsmite settings-delete-monitor-free-account-dialog-cancel-button-label = Lit mar, bring my werom settings-delete-monitor-account-confirmation-toast-label-2 = Jo { -brand-monitor }-account is no fuortsmiten settings-delete-monitor-account-confirmation-toast-dismiss-label = Slute + +## Monthly Monitor Report + +settings-alert-preferences-allow-monthly-monitor-report-title = Moanliks { -brand-monitor }-rapport +settings-alert-preferences-allow-monthly-monitor-report-subtitle = In moanlikse update fan nije lekken, wat is oplost en wat jo oandacht nedich hat. + +## Settings page redesign + +settings-tab-label-edit-info = Jo gegevens bewurkje +settings-tab-label-notifications = Notifikaasjes ynstelle +settings-tab-label-manage-account = Account beheare +settings-tab-subtitle-manage-account = Jo { -product-name }-account beheare. +settings-tab-notifications-marketing-title = Marketingkommunikaasje +settings-tab-notifications-marketing-text = Periodike updates oer { -brand-monitor }, { -brand-mozilla }, en ús oare befeiligingsprodukten. +settings-tab-notifications-marketing-link-label = Nei de e-mailynstellingen fan { -brand-mozilla } diff --git a/locales/he/data-classes.ftl b/locales/he/data-classes.ftl index f19c08c60c1..69d8d1fa5f7 100644 --- a/locales/he/data-classes.ftl +++ b/locales/he/data-classes.ftl @@ -2,7 +2,8 @@ # 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/. -## Breach Data Classes + +# Breach Data Classes account-balances = יתרות בחשבון address-book-contacts = פרטי אנשי קשר @@ -16,7 +17,7 @@ avatars = אווטארים bank-account-numbers = מספרי חשבון בנק beauty-ratings = דירוגי יופי biometric-data = נתונים ביומטריים -# This string is the shortened version of "Biographies", and +# This string is the shortened version of "Biographies", and # refers to biographical data about a user. bios = פרטים ביוגרפיים browser-user-agent-details = פרטי סוכן משתמש בדפדפן diff --git a/locales/he/email-strings.ftl b/locales/he/email-strings.ftl index 3550db12efc..26dc1d13ac0 100644 --- a/locales/he/email-strings.ftl +++ b/locales/he/email-strings.ftl @@ -2,52 +2,39 @@ # 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/. -# Firefox Monitor is a product name and should not be translated. --product-name = Firefox Monitor -# Firefox is a brand name and should not be translated. --brand-name = Firefox + +## Email headers + + +## Email footers # Firefox Relay is a product name and should not be translated. -product-name-relay = Firefox Relay - # A link to legal information about mozilla products. legal = מידע משפטי - -# Unsubscribe link in email. -email-unsub-link = ביטול מינוי - # Button text verify-email-cta = אימות דוא״ל - # Headline of verification email email-link-expires = קישור זה יפוג תוך 24 שעות -## Variables: -## $userEmail (string) - User email address - ## # Subject line of email email-subject-no-breaches = { -product-name } לא מצא דליפות נתונים מוכרות - # Subject line of email email-subject-verify = אימות הדוא״ל שלך עבור { -product-name } ## 2022 email template. HTML tags should not be translated, e.g. `` -## Monthly email for unresolved breaches. HTML tags should not be translated, e.g. `
` -## Variables: -## $email-address (string) - Email address - - ## Verification email ## Breach report -## Variables: -## $email-address (string) - Email address ## Breach alert + +## Redesigned breach alert email + diff --git a/locales/sk/exposure-card.ftl b/locales/sk/exposure-card.ftl index f665a689315..3b5189b0f18 100644 --- a/locales/sk/exposure-card.ftl +++ b/locales/sk/exposure-card.ftl @@ -3,7 +3,6 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. chevron-alt = Podrobnosti o úniku - exposure-card-exposure-type = Typ odhalenia exposure-card-date-found = Dátum odhalenia @@ -29,9 +28,10 @@ exposure-card-other = Iné exposure-card-description-data-breach-action-needed = Vaše informácie boli odhalené pri úniku údajov spoločnosti { $data_breach_company } dňa { $data_breach_date }. Prevedieme vás krokmi na nápravu. exposure-card-description-data-breach-fixed = Vykonali ste kroky potrebné na vyriešenie tohto úniku. Neustále budeme monitorovať nové úniky údajov a upozorníme vás na akékoľvek nové odhalenia. exposure-card-your-exposed-info = Vaše odhalené informácie: +exposure-card-found-the-following-data = { -brand-monitor } našiel nasledujúce uniknuté údaje: exposure-card-exposure-type-data-broker = Informácie na predaj exposure-card-exposure-type-data-breach = Únik údajov -exposure-card-cta = Vyriešiť všetky odhalenia +exposure-card-resolve-exposures-cta = Vyriešiť úniky exposure-card-label-company-logo = Logo spoločnosti exposure-card-label-company = Spoločnosť # Status of the exposure card, could be In Progress, Fixed or Action Needed @@ -40,3 +40,4 @@ exposure-card-label-status = Stav # $category_label is the data breach exposure type that was leaked. Eg. Email, IP Address. # $count is the number of times that the data type was leaked. exposure-card-label-and-count = { $category_label }: { $count } +exposure-card-manual-resolution-praise = Skvelá práca! Tento únik ste vyriešili. diff --git a/locales/sk/settings.ftl b/locales/sk/settings.ftl index c1a6f9baafe..eb083854dec 100644 --- a/locales/sk/settings.ftl +++ b/locales/sk/settings.ftl @@ -15,8 +15,6 @@ settings-alert-preferences-allow-breach-alerts-title = Okamžité upozornenia na settings-alert-preferences-allow-breach-alerts-subtitle = Tieto upozornenia sa odosielajú okamžite po zistení úniku údajov settings-alert-preferences-option-one = Upozornenia na únik údajov posielať na dotknutú e‑mailovú adresu settings-alert-preferences-option-two = Všetky upozornenia na únik údajov posielať na hlavnú e‑mailovú adresu -settings-alert-preferences-allow-monthly-monitor-report-title = Mesačný prehľad { -brand-monitor(case: "gen") } -settings-alert-preferences-allow-monthly-monitor-report-subtitle = Mesačný prehľad nových únikov, toho, čo bolo opravené a čo si vyžaduje vašu pozornosť. ## Monitored email addresses @@ -51,12 +49,6 @@ settings-email-number-of-breaches-info = *[other] Vyskytuje sa v { $breachCount } známych únikoch. } -## Deactivate account - -settings-deactivate-account-title = Deaktivovať účet -settings-deactivate-account-info-2 = { -product-short-name } môžete deaktivovať odstránením svojho { -brand-mozilla-account(case: "gen", capitalization: "lower") }. -settings-fxa-link-label-3 = Prejsť do Nastavení { -brand-mozilla-account(case: "gen", capitalization: "lowe") } - ## Delete Monitor account settings-delete-monitor-free-account-title = Odstrániť účet služby { -brand-monitor } @@ -68,3 +60,18 @@ settings-delete-monitor-free-account-dialog-cta-label = Odstrániť účet settings-delete-monitor-free-account-dialog-cancel-button-label = Rozmyslel som si to settings-delete-monitor-account-confirmation-toast-label-2 = Váš účet služby { -brand-monitor } je teraz odstránený. settings-delete-monitor-account-confirmation-toast-dismiss-label = Zavrieť + +## Monthly Monitor Report + +settings-alert-preferences-allow-monthly-monitor-report-title = Mesačný prehľad { -brand-monitor(case: "gen") } +settings-alert-preferences-allow-monthly-monitor-report-subtitle = Mesačný prehľad nových únikov, toho, čo bolo opravené a čo si vyžaduje vašu pozornosť. + +## Settings page redesign + +settings-tab-label-edit-info = Upravte svoje informácie +settings-tab-label-notifications = Nastaviť upozornenia +settings-tab-label-manage-account = Spravovať účet +settings-tab-subtitle-manage-account = Spravujte svoj účet služby { -product-name }. +settings-tab-notifications-marketing-title = Marketingová komunikácia +settings-tab-notifications-marketing-text = Pravidelné aktualizácie o službe { -brand-monitor }, { -brand-mozilla(case: "loc") } a našich ďalších bezpečnostných produktoch. +settings-tab-notifications-marketing-link-label = Prejsť na nastavení e‑mailov od { -brand-mozilla(case: "gen") } diff --git a/locales/tr/settings.ftl b/locales/tr/settings.ftl index 1b40c5aa34e..ecb198dc72f 100644 --- a/locales/tr/settings.ftl +++ b/locales/tr/settings.ftl @@ -15,8 +15,6 @@ settings-alert-preferences-allow-breach-alerts-title = Anlık ihlal uyarıları settings-alert-preferences-allow-breach-alerts-subtitle = Bu uyarılar bir veri ihlali algılandığı anda hemen gönderilir. settings-alert-preferences-option-one = İhlal uyarılarını etkilenen e-posta adresine gönder settings-alert-preferences-option-two = Tüm ihlal uyarılarını birinci e-posta adresine gönder -settings-alert-preferences-allow-monthly-monitor-report-title = Aylık { -brand-monitor } raporu -settings-alert-preferences-allow-monthly-monitor-report-subtitle = Yeni riskler, çözülen sorunlar ve ilgilenmeniz gereken sorunlara dair aylık bir rapor. ## Monitored email addresses @@ -47,12 +45,6 @@ settings-email-number-of-breaches-info = *[other] Bilinen { $breachCount } ihlalde yer alıyor. } -## Deactivate account - -settings-deactivate-account-title = Hesabı devre dışı bırak -settings-deactivate-account-info-2 = { -product-short-name }’ü { -brand-mozilla-account }nızı silerek devre dışı bırakabilirsiniz. -settings-fxa-link-label-3 = { -brand-mozilla-account } ayarlarına gidin - ## Delete Monitor account settings-delete-monitor-free-account-title = { -brand-monitor } hesabını sil @@ -64,3 +56,12 @@ settings-delete-monitor-free-account-dialog-cta-label = Hesabı sil settings-delete-monitor-free-account-dialog-cancel-button-label = Vazgeçtim, geri dön settings-delete-monitor-account-confirmation-toast-label-2 = { -brand-monitor } hesabınız silindi. settings-delete-monitor-account-confirmation-toast-dismiss-label = Kapat + +## Monthly Monitor Report + +settings-alert-preferences-allow-monthly-monitor-report-title = Aylık { -brand-monitor } raporu +settings-alert-preferences-allow-monthly-monitor-report-subtitle = Yeni riskler, çözülen sorunlar ve ilgilenmeniz gereken sorunlara dair aylık bir rapor. + +## Settings page redesign + +settings-tab-notifications-marketing-title = Pazarlama iletişimi diff --git a/locales/vi/exposure-card.ftl b/locales/vi/exposure-card.ftl index c459db65886..b3da86d1578 100644 --- a/locales/vi/exposure-card.ftl +++ b/locales/vi/exposure-card.ftl @@ -3,7 +3,6 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. chevron-alt = Chi tiết dữ liệu bị lộ - exposure-card-exposure-type = Loại vụ rò rỉ exposure-card-date-found = Ngày vụ rò rỉ @@ -29,9 +28,10 @@ exposure-card-other = Khác exposure-card-description-data-breach-action-needed = Thông tin của bạn đã bị lộ trong vụ rò rỉ dữ liệu { $data_breach_company } vào { $data_breach_date }. Chúng tôi sẽ hướng dẫn bạn các bước để khắc phục sự cố. exposure-card-description-data-breach-fixed = Bạn đã thực hiện các bước cần thiết để khắc phục rò rỉ này. Chúng tôi sẽ liên tục theo dõi các hành vi rò rỉ dữ liệu và cảnh báo cho bạn về bất kỳ hành vi rò rỉ dữ liệu mới nào. exposure-card-your-exposed-info = Thông tin bị lộ của bạn: +exposure-card-found-the-following-data = { -brand-monitor } tìm thấy dữ liệu bị lộ sau đây: exposure-card-exposure-type-data-broker = Thông tin để bán exposure-card-exposure-type-data-breach = Vụ rò rỉ -exposure-card-cta = Giải quyết tất cả vụ rò rỉ +exposure-card-resolve-exposures-cta = Giải quyết dữ liệu bị lộ exposure-card-label-company-logo = Logo công ty exposure-card-label-company = Công ty # Status of the exposure card, could be In Progress, Fixed or Action Needed @@ -40,3 +40,4 @@ exposure-card-label-status = Trạng thái # $category_label is the data breach exposure type that was leaked. Eg. Email, IP Address. # $count is the number of times that the data type was leaked. exposure-card-label-and-count = { $category_label }: { $count } +exposure-card-manual-resolution-praise = Làm tốt lắm! Bạn đã giải quyết vấn đề này. diff --git a/locales/vi/settings.ftl b/locales/vi/settings.ftl index 68f1b18f8cb..4210b053808 100644 --- a/locales/vi/settings.ftl +++ b/locales/vi/settings.ftl @@ -15,8 +15,6 @@ settings-alert-preferences-allow-breach-alerts-title = Cảnh báo rò rỉ tứ settings-alert-preferences-allow-breach-alerts-subtitle = Những cảnh báo này được gửi ngay lập tức khi phát hiện rò rỉ dữ liệu settings-alert-preferences-option-one = Gửi thông báo rò rỉ dữ liệu đến địa chỉ email bị ảnh hưởng settings-alert-preferences-option-two = Gửi tất cả cảnh báo rò rỉ dữ liệu đến địa chỉ email chính -settings-alert-preferences-allow-monthly-monitor-report-title = Báo cáo hàng tháng { -brand-monitor } -settings-alert-preferences-allow-monthly-monitor-report-subtitle = Bản cập nhật hàng tháng về số lần lộ dữ liệu mới, những gì đã được sửa và những gì bạn cần chú ý. ## Monitored email addresses @@ -39,12 +37,6 @@ settings-remove-email-button-tooltip = Dừng giám sát { $emailAddress } # $breachCount (number) - Number of breaches settings-email-number-of-breaches-info = Xuất hiện trong { $breachCount } rò rỉ dữ liệu đã biết. -## Deactivate account - -settings-deactivate-account-title = Hủy kích hoạt tài khoản -settings-deactivate-account-info-2 = Bạn có thể vô hiệu hóa { -product-short-name } bằng cách xoá { -brand-mozilla-account } của bạn. -settings-fxa-link-label-3 = Đi đến cài đặt { -brand-mozilla-account } - ## Delete Monitor account settings-delete-monitor-free-account-title = Xoá tài khoản { -brand-monitor } @@ -56,3 +48,18 @@ settings-delete-monitor-free-account-dialog-cta-label = Xóa tài khoản settings-delete-monitor-free-account-dialog-cancel-button-label = Nghĩ lại rồi, đưa tôi quay lại settings-delete-monitor-account-confirmation-toast-label-2 = Tài khoản { -brand-monitor } của bạn đã bị xóa. settings-delete-monitor-account-confirmation-toast-dismiss-label = Bỏ qua + +## Monthly Monitor Report + +settings-alert-preferences-allow-monthly-monitor-report-title = Báo cáo hàng tháng { -brand-monitor } +settings-alert-preferences-allow-monthly-monitor-report-subtitle = Bản cập nhật hàng tháng về số lần lộ dữ liệu mới, những gì đã được sửa và những gì bạn cần chú ý. + +## Settings page redesign + +settings-tab-label-edit-info = Chỉnh sửa thông tin của bạn +settings-tab-label-notifications = Đặt thông báo +settings-tab-label-manage-account = Quản lý tài khoản +settings-tab-subtitle-manage-account = Quản lý tài khoản { -product-name } của bạn. +settings-tab-notifications-marketing-title = Truyền thông tiếp thị +settings-tab-notifications-marketing-text = Cập nhật định kỳ về { -brand-monitor }, { -brand-mozilla }, và các sản phẩm bảo mật khác của chúng tôi. +settings-tab-notifications-marketing-link-label = Đi đến cài đặt email { -brand-mozilla } diff --git a/package-lock.json b/package-lock.json index 32389ef36a7..3b2ee1ded57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "1.0.0", "license": "MPL-2.0", "dependencies": { - "@aws-sdk/client-s3": "^3.703.0", - "@aws-sdk/lib-storage": "^3.703.0", + "@aws-sdk/client-s3": "^3.705.0", + "@aws-sdk/lib-storage": "^3.705.0", "@fluent/bundle": "^0.18.0", "@fluent/langneg": "^0.7.0", "@fluent/react": "^0.15.2", @@ -53,7 +53,7 @@ "winston": "^3.15.0" }, "devDependencies": { - "@faker-js/faker": "^9.1.0", + "@faker-js/faker": "^9.3.0", "@playwright/test": "^1.49.0", "@storybook/addon-a11y": "^8.4.6", "@storybook/addon-actions": "^8.4.6", @@ -63,9 +63,9 @@ "@storybook/nextjs": "^8.4.6", "@storybook/react": "^8.4.6", "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.0.1", + "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", - "@types/adm-zip": "^0.5.6", + "@types/adm-zip": "^0.5.7", "@types/canvas-confetti": "^1.6.4", "@types/jest-axe": "^3.5.9", "@types/jsonwebtoken": "^9.0.7", @@ -86,7 +86,7 @@ "eslint-plugin-jest": "^27.9.0", "eslint-plugin-jsdoc": "^48.2.2", "fast-check": "^3.23.1", - "husky": "^9.1.6", + "husky": "^9.1.7", "ioredis-mock": "^8.9.0", "jest": "^29.7.0", "jest-axe": "^9.0.0", @@ -95,7 +95,7 @@ "jest-fail-on-console": "^3.3.1", "lint-staged": "^15.2.10", "mjml-browser": "^4.15.3", - "prettier": "3.3.3", + "prettier": "3.4.2", "sass": "^1.81.0", "storybook": "^8.4.6", "stylelint": "^16.11.0", @@ -324,10 +324,9 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.703.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.703.0.tgz", - "integrity": "sha512-4TSrIamzASTeRPKXrTLcEwo+viPTuOSGcbXh4HC1R0m/rXwK0BHJ4btJ0Q34nZNF+WzvM+FiemXVjNc8qTAxog==", - "license": "Apache-2.0", + "version": "3.705.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.705.0.tgz", + "integrity": "sha512-Fm0Cbc4zr0yG0DnNycz7ywlL5tQFdLSb7xCIPfzrxJb3YQiTXWxH5eu61SSsP/Z6RBNRolmRPvst/iNgX0fWvA==", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", @@ -698,10 +697,9 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.703.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.703.0.tgz", - "integrity": "sha512-0ejct/fmx/gF7aTcH5RUWiP9IodGWZY0tAfU8tYct0V41hPd9i9t55NbSk/jnzZcRN31NdSHfVxqGdISGAe0qg==", - "license": "Apache-2.0", + "version": "3.705.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.705.0.tgz", + "integrity": "sha512-jucPgdO5RCQAki8+CcEi3ZQxBUpq6iVcurtMkLS1xGbe/VOhxzNOt44V/4WqjUu7ra3on8DD0DOqd9523BqOzA==", "dependencies": { "@smithy/abort-controller": "^3.1.7", "@smithy/middleware-endpoint": "^3.2.3", @@ -715,7 +713,7 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.703.0" + "@aws-sdk/client-s3": "^3.705.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -3556,9 +3554,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.1.0.tgz", - "integrity": "sha512-GJvX9iM9PBtKScJVlXQ0tWpihK3i0pha/XAhzQa1hPK/ILLa1Wq3I63Ij7lRtqTwmdTxRCyrUhLC5Sly9SLbug==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.3.0.tgz", + "integrity": "sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==", "dev": true, "funding": [ { @@ -10643,9 +10641,9 @@ "dev": true }, "node_modules/@testing-library/react": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.1.tgz", - "integrity": "sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.1.0.tgz", + "integrity": "sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5" @@ -10655,10 +10653,10 @@ }, "peerDependencies": { "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -10723,9 +10721,9 @@ "peer": true }, "node_modules/@types/adm-zip": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.6.tgz", - "integrity": "sha512-lRlcSLg5Yoo7C2H2AUiAoYlvifWoCx/se7iUNiCBTfEVVYFVn+Tr9ZGed4K73tYgLe9O4PjdJvbxlkdAOx/qiw==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", "dev": true, "dependencies": { "@types/node": "*" @@ -16816,9 +16814,9 @@ } }, "node_modules/husky": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", - "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "bin": { "husky": "bin.js" @@ -22481,9 +22479,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/package.json b/package.json index 67dcdad6d8a..a18caee6f2f 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,8 @@ "npm": "10.1.0" }, "dependencies": { - "@aws-sdk/client-s3": "^3.703.0", - "@aws-sdk/lib-storage": "^3.703.0", + "@aws-sdk/client-s3": "^3.705.0", + "@aws-sdk/lib-storage": "^3.705.0", "@fluent/bundle": "^0.18.0", "@fluent/langneg": "^0.7.0", "@fluent/react": "^0.15.2", @@ -114,7 +114,7 @@ "winston": "^3.15.0" }, "devDependencies": { - "@faker-js/faker": "^9.1.0", + "@faker-js/faker": "^9.3.0", "@playwright/test": "^1.49.0", "@storybook/addon-a11y": "^8.4.6", "@storybook/addon-actions": "^8.4.6", @@ -124,9 +124,9 @@ "@storybook/nextjs": "^8.4.6", "@storybook/react": "^8.4.6", "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.0.1", + "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", - "@types/adm-zip": "^0.5.6", + "@types/adm-zip": "^0.5.7", "@types/canvas-confetti": "^1.6.4", "@types/jest-axe": "^3.5.9", "@types/jsonwebtoken": "^9.0.7", @@ -147,7 +147,7 @@ "eslint-plugin-jest": "^27.9.0", "eslint-plugin-jsdoc": "^48.2.2", "fast-check": "^3.23.1", - "husky": "^9.1.6", + "husky": "^9.1.7", "ioredis-mock": "^8.9.0", "jest": "^29.7.0", "jest-axe": "^9.0.0", @@ -156,7 +156,7 @@ "jest-fail-on-console": "^3.3.1", "lint-staged": "^15.2.10", "mjml-browser": "^4.15.3", - "prettier": "3.3.3", + "prettier": "3.4.2", "sass": "^1.81.0", "storybook": "^8.4.6", "stylelint": "^16.11.0", diff --git a/src/apiMocks/mockData.ts b/src/apiMocks/mockData.ts index a7ac1cb408e..156d6fa308a 100644 --- a/src/apiMocks/mockData.ts +++ b/src/apiMocks/mockData.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { faker } from "@faker-js/faker"; -import { OnerepScanResultRow } from "knex/types/tables"; +import { OnerepScanResultDataBrokerRow } from "knex/types/tables"; import { RemovalStatus, RemovalStatusMap, @@ -20,6 +20,7 @@ import { import { Session } from "next-auth"; import { HibpLikeDbBreach } from "../utils/hibp"; import { SerializedSubscriber } from "../next-auth"; +import { DataBrokerRemovalStatus } from "../app/functions/universal/dataBroker"; // Setting this to a constant value produces the same result when the same methods // with the same version of faker are called. @@ -40,6 +41,7 @@ export type RandomScanResultOptions = Partial<{ fakerSeed: number; status: RemovalStatus; manually_resolved: boolean; + broker_status: DataBrokerRemovalStatus; }>; /** @@ -50,7 +52,7 @@ export type RandomScanResultOptions = Partial<{ */ export function createRandomScanResult( options: RandomScanResultOptions = {}, -): OnerepScanResultRow { +): OnerepScanResultDataBrokerRow { faker.seed(options.fakerSeed); const optout_attempts = options.status === "waiting_for_verification" @@ -85,6 +87,11 @@ export function createRandomScanResult( created_at: options.createdDate ?? faker.date.recent({ days: 2 }), updated_at: faker.date.recent({ days: 1 }), optout_attempts, + broker_status: options.broker_status ?? "active", + scan_result_status: faker.helpers.arrayElement( + Object.values(RemovalStatusMap), + ) as RemovalStatus, + url: faker.internet.url(), }; } diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/admin/emails/actions.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/admin/emails/actions.tsx index 8a99e0f7417..5e7eb3423f9 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/admin/emails/actions.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/admin/emails/actions.tsx @@ -20,7 +20,6 @@ import { getDashboardSummary } from "../../../../../functions/server/dashboard"; import { getSubscriberBreaches } from "../../../../../functions/server/getSubscriberBreaches"; import { getCountryCode } from "../../../../../functions/server/getCountryCode"; import { headers } from "next/headers"; -import { getLatestOnerepScanResults } from "../../../../../../db/tables/onerep_scans"; import { FirstDataBrokerRemovalFixed } from "../../../../../../emails/templates/firstDataBrokerRemovalFixed/FirstDataBrokerRemovalFixed"; import { createRandomHibpListing, @@ -40,6 +39,7 @@ import { hasPremium } from "../../../../../functions/universal/user"; import { isEligibleForPremium } from "../../../../../functions/universal/premium"; import { MonthlyActivityFreeEmail } from "../../../../../../emails/templates/monthlyActivityFree/MonthlyActivityFreeEmail"; import { getMonthlyActivityFreeUnsubscribeLink } from "../../../../../../app/functions/cronjobs/unsubscribeLinks"; +import { getScanResultsWithBroker } from "../../../../../../db/tables/onerep_scans"; async function getAdminSubscriber(): Promise { const session = await getServerSession(); @@ -140,8 +140,9 @@ export async function triggerMonthlyActivityFree(emailAddress: string) { if (typeof subscriber.onerep_profile_id === "number") { await refreshStoredScanResults(subscriber.onerep_profile_id); } - const latestScan = await getLatestOnerepScanResults( + const latestScan = await getScanResultsWithBroker( subscriber.onerep_profile_id, + hasPremium(session.user), ); const data = getDashboardSummary( latestScan.results, @@ -178,8 +179,9 @@ export async function triggerMonthlyActivityPlus(emailAddress: string) { if (typeof subscriber.onerep_profile_id === "number") { await refreshStoredScanResults(subscriber.onerep_profile_id); } - const latestScan = await getLatestOnerepScanResults( + const latestScan = await getScanResultsWithBroker( subscriber.onerep_profile_id, + hasPremium(session.user), ); const data = getDashboardSummary( latestScan.results, @@ -217,8 +219,9 @@ export async function triggerBreachAlert( if (typeof subscriber.onerep_profile_id === "number") { await refreshStoredScanResults(subscriber.onerep_profile_id); } - const scanData = await getLatestOnerepScanResults( + const scanData = await getScanResultsWithBroker( subscriber.onerep_profile_id, + hasPremium(session.user), ); const allSubscriberBreaches = await getSubscriberBreaches({ fxaUid: subscriber.fxa_uid, diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/onerepConfig.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/onerepConfig.tsx index 606f50f2e35..cb819a39dec 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/onerepConfig.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/onerepConfig.tsx @@ -380,9 +380,6 @@ const OnerepConfigPage = (props: Props) => { - diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/Dashboard.stories.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/Dashboard.stories.tsx index cdc1d8d401b..075053dc810 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/Dashboard.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/Dashboard.stories.tsx @@ -4,7 +4,10 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { OnerepScanResultRow, OnerepScanRow } from "knex/types/tables"; +import { + OnerepScanResultDataBrokerRow, + OnerepScanRow, +} from "knex/types/tables"; import { faker } from "@faker-js/faker"; import { View as DashboardEl, TabType } from "./View"; import { Shell } from "../../../../Shell"; @@ -91,6 +94,8 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { }); let breaches: SubscriberBreach[] = []; + const scanData: LatestOnerepScanData = { scan: null, results: [] }; + if (props.breaches === "resolved") { breaches = [mockedResolvedBreach]; } @@ -113,7 +118,7 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { onerep_scan_status: "in_progress", }; - const mockedInProgressScanResults: OnerepScanResultRow[] = [ + const mockedInProgressScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "removed", manually_resolved: false }), createRandomScanResult({ status: "waiting_for_verification", @@ -125,19 +130,19 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { }), ]; - const mockedAllResolvedScanResults: OnerepScanResultRow[] = [ + const mockedAllResolvedScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "removed", manually_resolved: false }), createRandomScanResult({ status: "removed", manually_resolved: false }), ]; - const mockedUnresolvedScanResults: OnerepScanResultRow[] = [ + const mockedUnresolvedScanResults: OnerepScanResultDataBrokerRow[] = [ ...mockedInProgressScanResults, createRandomScanResult({ status: "new", manually_resolved: false }), createRandomScanResult({ status: "new", manually_resolved: false }), createRandomScanResult({ status: "new", manually_resolved: true }), ]; - const mockedManuallyResolvedScanResults: OnerepScanResultRow[] = [ + const mockedManuallyResolvedScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "new", manually_resolved: true }), createRandomScanResult({ status: "waiting_for_verification", @@ -150,7 +155,6 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { createRandomScanResult({ status: "removed", manually_resolved: true }), ]; - const scanData: LatestOnerepScanData = { scan: null, results: [] }; let scanCount = 0; if (props.countryCode === "us") { @@ -283,321 +287,6 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const DashboardNonUsNoBreaches: Story = { - name: "Non-US user, with 0 breaches", - args: { - countryCode: "nl", - breaches: "empty", - }, -}; - -export const DashboardNonUsUnresolvedBreaches: Story = { - name: "Non-US user, with unresolved breaches", - args: { - countryCode: "nl", - breaches: "unresolved", - }, -}; - -export const DashboardNonUsResolvedBreaches: Story = { - name: "Non-US user, with all breaches resolved", - args: { - countryCode: "nl", - breaches: "resolved", - }, -}; - -export const DashboardUsNoPremiumNoScanNoBreaches: Story = { - name: "US user, without Premium, without scan, with 0 breaches", - args: { - countryCode: "us", - premium: false, - breaches: "empty", - brokers: "no-scan", - }, -}; - -export const DashboardUsNoPremiumNoScanUnresolvedBreaches: Story = { - name: "US user, without Premium, without scan, with unresolved breaches", - args: { - countryCode: "us", - premium: false, - breaches: "unresolved", - brokers: "no-scan", - }, -}; - -export const DashboardUsNoPremiumNoScanResolvedBreaches: Story = { - name: "US user, without Premium, without scan, with all breaches resolved", - args: { - countryCode: "us", - premium: false, - breaches: "resolved", - brokers: "no-scan", - }, -}; - -export const DashboardUsNoPremiumNoScanNoBreachesScanLimitReached: Story = { - name: "US user, without Premium, without scan, with 0 breaches, Scan limit reached", - args: { - countryCode: "us", - premium: false, - breaches: "empty", - brokers: "no-scan", - totalNumberOfPerformedScans: 280000, - }, -}; - -export const DashboardUsNoPremiumEmptyScanNoBreaches: Story = { - name: "US user, without Premium, with 0 scan results, with 0 breaches", - args: { - countryCode: "us", - premium: false, - breaches: "empty", - brokers: "empty", - }, -}; - -export const DashboardUsNoPremiumEmptyScanUnresolvedBreaches: Story = { - name: "US user, without Premium, with 0 scan results, with unresolved breaches", - args: { - countryCode: "us", - premium: false, - breaches: "unresolved", - brokers: "empty", - }, -}; - -export const DashboardUsNoPremiumEmptyScanResolvedBreaches: Story = { - name: "US user, without Premium, with 0 scan results, with all breaches resolved", - args: { - countryCode: "us", - premium: false, - breaches: "resolved", - brokers: "empty", - }, -}; - -export const DashboardUsNoPremiumUnresolvedScanNoBreaches: Story = { - name: "US user, without Premium, with unresolved scan results, with 0 breaches", - args: { - countryCode: "us", - premium: false, - breaches: "empty", - brokers: "unresolved", - }, -}; - -export const DashboardUsNoPremiumUnresolvedScanUnresolvedBreaches: Story = { - name: "US user, without Premium, with unresolved scan results, with unresolved breaches", - args: { - countryCode: "us", - premium: false, - breaches: "unresolved", - brokers: "unresolved", - }, -}; - -export const DashboardUsNoPremiumUnresolvedScanResolvedBreaches: Story = { - name: "US user, without Premium, with unresolved scan results, with all breaches resolved", - args: { - countryCode: "us", - premium: false, - breaches: "resolved", - brokers: "unresolved", - }, -}; - -export const DashboardUsNoPremiumResolvedScanNoBreaches: Story = { - name: "US user, without Premium, with all scan results resolved, with 0 breaches", - args: { - countryCode: "us", - premium: false, - breaches: "empty", - brokers: "resolved", - }, -}; - -export const DashboardUsNoPremiumResolvedScanUnresolvedBreaches: Story = { - name: "US user, without Premium, with all scan results resolved, with unresolved breaches", - args: { - countryCode: "us", - premium: false, - breaches: "unresolved", - brokers: "resolved", - }, -}; - -export const DashboardUsNoPremiumResolvedScanResolvedBreaches: Story = { - name: "US user, without Premium, with all scan results resolved, with all breaches resolved", - args: { - countryCode: "us", - premium: false, - breaches: "resolved", - brokers: "resolved", - }, -}; - -export const DashboardUsPremiumEmptyScanNoBreaches: Story = { - name: "US user, with Premium, with 0 scan results, with 0 breaches", - args: { - countryCode: "us", - premium: true, - breaches: "empty", - brokers: "empty", - }, -}; - -export const DashboardUsPremiumEmptyScanUnresolvedBreaches: Story = { - name: "US user, with Premium, with 0 scan results, with unresolved breaches", - args: { - countryCode: "us", - premium: true, - breaches: "unresolved", - brokers: "empty", - }, -}; - -export const DashboardUsPremiumEmptyScanResolvedBreaches: Story = { - name: "US user, with Premium, with 0 scan results, with all breaches resolved", - args: { - countryCode: "us", - premium: true, - breaches: "resolved", - brokers: "empty", - }, -}; - -export const DashboardUsPremiumUnresolvedScanNoBreaches: Story = { - name: "US user, with Premium, with unresolved scan results, with 0 breaches", - args: { - countryCode: "us", - premium: true, - breaches: "empty", - brokers: "unresolved", - }, -}; - -export const DashboardUsPremiumUnresolvedScanUnresolvedBreaches: Story = { - name: "US user, with Premium, with unresolved scan results, with unresolved breaches", - args: { - countryCode: "us", - premium: true, - breaches: "unresolved", - brokers: "unresolved", - }, -}; - -export const DashboardUsPremiumUnresolvedScanResolvedBreaches: Story = { - name: "US user, with Premium, with unresolved scan results, with all breaches resolved", - args: { - countryCode: "us", - premium: true, - breaches: "resolved", - brokers: "unresolved", - }, -}; - -export const DashboardUsPremiumResolvedScanNoBreaches: Story = { - name: "US user, with Premium, with all scan results resolved, with 0 breaches", - args: { - countryCode: "us", - premium: true, - breaches: "empty", - brokers: "resolved", - }, -}; - -export const DashboardUsPremiumResolvedScanUnresolvedBreaches: Story = { - name: "US user, with Premium, with all scan results resolved, with unresolved breaches", - args: { - countryCode: "us", - premium: true, - breaches: "unresolved", - brokers: "resolved", - }, -}; - -export const DashboardUsPremiumResolvedScanResolvedBreaches: Story = { - name: "US user, with Premium, with all scan results resolved, with all breaches resolved", - args: { - countryCode: "us", - premium: true, - breaches: "resolved", - brokers: "resolved", - }, -}; - -export const DashboardUsNoPremiumScanInProgressNoBreaches: Story = { - name: "US user, without Premium, scan in progress, with no breaches", - args: { - countryCode: "us", - premium: false, - breaches: "empty", - brokers: "scan-in-progress", - }, -}; - -export const DashboardUsNoPremiumScanInProgressUnresolvedBreaches: Story = { - name: "US user, without Premium, scan in progress, with unresolved breaches", - args: { - countryCode: "us", - premium: false, - breaches: "unresolved", - brokers: "scan-in-progress", - }, -}; - -export const DashboardUsNoPremiumScanInProgressResolvedBreaches: Story = { - name: "US user, without Premium, scan in progress, with resolved breaches", - args: { - countryCode: "us", - premium: false, - breaches: "resolved", - brokers: "scan-in-progress", - }, -}; - -export const DashboardUsPremiumScanInProgressNoBreaches: Story = { - name: "US user, with Premium, scan in progress, with no breaches", - args: { - countryCode: "us", - premium: true, - breaches: "empty", - brokers: "scan-in-progress", - }, -}; - -export const DashboardUsPremiumManuallyResolvedScansNoBreaches: Story = { - name: "US user, with Premium, scan manually resolved, with no breaches", - args: { - countryCode: "us", - premium: true, - breaches: "empty", - brokers: "manually-resolved", - }, -}; - -export const DashboardUsPremiumScanInProgressUnresolvedBreaches: Story = { - name: "US user, with Premium, scan in progress, with unresolved breaches", - args: { - countryCode: "us", - premium: true, - breaches: "unresolved", - brokers: "scan-in-progress", - }, -}; - -export const DashboardUsPremiumScanInProgressResolvedBreaches: Story = { - name: "US user, with Premium, scan in progress, with resolved breaches", - args: { - countryCode: "us", - premium: true, - breaches: "resolved", - brokers: "scan-in-progress", - }, -}; - export const DashboardInvalidPremiumUserNoScanResolvedBreaches: Story = { name: "Invalid state: US user, with Premium, with no scan, with resolved breaches", args: { diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardNonUSUsers.stories.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardNonUSUsers.stories.tsx index 46b3b1302e8..fce5a13f713 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardNonUSUsers.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardNonUSUsers.stories.tsx @@ -4,7 +4,10 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { OnerepScanResultRow, OnerepScanRow } from "knex/types/tables"; +import { + OnerepScanResultDataBrokerRow, + OnerepScanRow, +} from "knex/types/tables"; import { faker } from "@faker-js/faker"; import { View as DashboardEl } from "./View"; import { Shell } from "../../../../Shell"; @@ -58,6 +61,8 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { }); let breaches: SubscriberBreach[] = []; + const scanData: LatestOnerepScanData = { scan: null, results: [] }; + if (props.breaches === "resolved") { breaches = [mockedResolvedBreach]; } @@ -80,7 +85,7 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { onerep_scan_status: "in_progress", }; - const mockedInProgressScanResults: OnerepScanResultRow[] = [ + const mockedInProgressScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "removed", manually_resolved: false }), createRandomScanResult({ status: "waiting_for_verification", @@ -92,19 +97,19 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { }), ]; - const mockedAllResolvedScanResults: OnerepScanResultRow[] = [ + const mockedAllResolvedScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "removed", manually_resolved: false }), createRandomScanResult({ status: "removed", manually_resolved: false }), ]; - const mockedUnresolvedScanResults: OnerepScanResultRow[] = [ + const mockedUnresolvedScanResults: OnerepScanResultDataBrokerRow[] = [ ...mockedInProgressScanResults, createRandomScanResult({ status: "new", manually_resolved: false }), createRandomScanResult({ status: "new", manually_resolved: false }), createRandomScanResult({ status: "new", manually_resolved: true }), ]; - const mockedManuallyResolvedScanResults: OnerepScanResultRow[] = [ + const mockedManuallyResolvedScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "new", manually_resolved: true }), createRandomScanResult({ status: "waiting_for_verification", @@ -117,7 +122,6 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { createRandomScanResult({ status: "removed", manually_resolved: true }), ]; - const scanData: LatestOnerepScanData = { scan: null, results: [] }; let scanCount = 0; if (props.countryCode === "us") { diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardPlusUsers.stories.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardPlusUsers.stories.tsx index bd6237bc57a..7623cb7ef49 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardPlusUsers.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardPlusUsers.stories.tsx @@ -4,7 +4,10 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { OnerepScanResultRow, OnerepScanRow } from "knex/types/tables"; +import { + OnerepScanResultDataBrokerRow, + OnerepScanRow, +} from "knex/types/tables"; import { faker } from "@faker-js/faker"; import { View as DashboardEl } from "./View"; import { Shell } from "../../../../Shell"; @@ -58,6 +61,8 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { }); let breaches: SubscriberBreach[] = []; + const scanData: LatestOnerepScanData = { scan: null, results: [] }; + if (props.breaches === "resolved") { breaches = [mockedResolvedBreach]; } @@ -80,7 +85,7 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { onerep_scan_status: "in_progress", }; - const mockedInProgressScanResults: OnerepScanResultRow[] = [ + const mockedInProgressScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "removed", manually_resolved: false }), createRandomScanResult({ status: "waiting_for_verification", @@ -92,19 +97,19 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { }), ]; - const mockedAllResolvedScanResults: OnerepScanResultRow[] = [ + const mockedAllResolvedScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "removed", manually_resolved: false }), createRandomScanResult({ status: "removed", manually_resolved: false }), ]; - const mockedUnresolvedScanResults: OnerepScanResultRow[] = [ + const mockedUnresolvedScanResults: OnerepScanResultDataBrokerRow[] = [ ...mockedInProgressScanResults, createRandomScanResult({ status: "new", manually_resolved: false }), createRandomScanResult({ status: "new", manually_resolved: false }), createRandomScanResult({ status: "new", manually_resolved: true }), ]; - const mockedManuallyResolvedScanResults: OnerepScanResultRow[] = [ + const mockedManuallyResolvedScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "new", manually_resolved: true }), createRandomScanResult({ status: "waiting_for_verification", @@ -116,8 +121,6 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { }), createRandomScanResult({ status: "removed", manually_resolved: true }), ]; - - const scanData: LatestOnerepScanData = { scan: null, results: [] }; let scanCount = 0; if (props.countryCode === "us") { diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardUSUsers.stories.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardUSUsers.stories.tsx index 588f3307096..58f7df97490 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardUSUsers.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/DashboardUSUsers.stories.tsx @@ -4,7 +4,10 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { OnerepScanResultRow, OnerepScanRow } from "knex/types/tables"; +import { + OnerepScanResultDataBrokerRow, + OnerepScanRow, +} from "knex/types/tables"; import { faker } from "@faker-js/faker"; import { View as DashboardEl } from "./View"; import { Shell } from "../../../../Shell"; @@ -58,6 +61,8 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { }); let breaches: SubscriberBreach[] = []; + const scanData: LatestOnerepScanData = { scan: null, results: [] }; + if (props.breaches === "resolved") { breaches = [mockedResolvedBreach]; } @@ -80,7 +85,7 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { onerep_scan_status: "in_progress", }; - const mockedInProgressScanResults: OnerepScanResultRow[] = [ + const mockedInProgressScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "removed", manually_resolved: false }), createRandomScanResult({ status: "waiting_for_verification", @@ -92,19 +97,19 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { }), ]; - const mockedAllResolvedScanResults: OnerepScanResultRow[] = [ + const mockedAllResolvedScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "removed", manually_resolved: false }), createRandomScanResult({ status: "removed", manually_resolved: false }), ]; - const mockedUnresolvedScanResults: OnerepScanResultRow[] = [ + const mockedUnresolvedScanResults: OnerepScanResultDataBrokerRow[] = [ ...mockedInProgressScanResults, createRandomScanResult({ status: "new", manually_resolved: false }), createRandomScanResult({ status: "new", manually_resolved: false }), createRandomScanResult({ status: "new", manually_resolved: true }), ]; - const mockedManuallyResolvedScanResults: OnerepScanResultRow[] = [ + const mockedManuallyResolvedScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "new", manually_resolved: true }), createRandomScanResult({ status: "waiting_for_verification", @@ -117,7 +122,6 @@ const DashboardWrapper = (props: DashboardWrapperProps) => { createRandomScanResult({ status: "removed", manually_resolved: true }), ]; - const scanData: LatestOnerepScanData = { scan: null, results: [] }; let scanCount = 0; if (props.countryCode === "us") { diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View.tsx index b8f58595d96..daac164e8cb 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View.tsx @@ -8,7 +8,10 @@ import { useContext, useEffect, useRef, useState } from "react"; import { usePathname } from "next/navigation"; import Image from "next/image"; import { Session } from "next-auth"; -import { OnerepScanResultRow } from "knex/types/tables"; +import { + OnerepScanResultDataBrokerRow, + OnerepScanResultRow, +} from "knex/types/tables"; import styles from "./View.module.scss"; import { Toolbar } from "../../../../../../components/client/toolbar/Toolbar"; import { DashboardTopBanner } from "./DashboardTopBanner"; @@ -120,7 +123,7 @@ export const View = (props: Props) => { return { ...scanResult, status: "optout_in_progress", - } as OnerepScanResultRow; + } as OnerepScanResultDataBrokerRow; } return scanResult; }); @@ -176,7 +179,9 @@ export const View = (props: Props) => { const exposureStatus = getExposureStatus( exposure, props.enabledFeatureFlags.includes("AdditionalRemovalStatuses"), + isDataBrokerUnderMaintenance(exposure), ); + return ( (tabKey === "action-needed" && exposureStatus === "actionNeeded") || (tabKey === "fixed" && exposureStatus !== "actionNeeded") @@ -555,3 +560,12 @@ export const View = (props: Props) => { ); }; + +export function isDataBrokerUnderMaintenance( + exposure: Exposure | OnerepScanResultDataBrokerRow, +): boolean { + return ( + isScanResult(exposure) && + exposure.broker_status === "removal_under_maintenance" + ); +} diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/[[...slug]]/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/[[...slug]]/page.tsx index 886e74bdbb7..398d533014b 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/[[...slug]]/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/[[...slug]]/page.tsx @@ -13,8 +13,8 @@ import { hasPremium, } from "../../../../../../../functions/universal/user"; import { - getLatestOnerepScanResults, getLatestScanForProfileByReason, + getScanResultsWithBroker, getScansCountForProfile, } from "../../../../../../../../db/tables/onerep_scans"; import { @@ -102,7 +102,11 @@ export default async function DashboardPage({ params, searchParams }: Props) { return redirect("/user/welcome"); } - const latestScan = await getLatestOnerepScanResults(profileId); + const latestScan = await getScanResultsWithBroker( + profileId, + hasPremium(session.user), + ); + const scanCount = typeof profileId === "number" ? await getScansCountForProfile(profileId) diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/automatic-remove/AutomaticRemove.stories.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/automatic-remove/AutomaticRemove.stories.tsx index f5053c449a2..dff489aea61 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/automatic-remove/AutomaticRemove.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/automatic-remove/AutomaticRemove.stories.tsx @@ -40,14 +40,14 @@ const mockedSession = { }; const meta: Meta = { - title: "Pages/Logged in/Guided resolution/1d. Automatically resolve brokers", + title: "Pages/Logged in/Guided resolution/1e. Automatically resolve brokers", component: AutomaticRemoveView, }; export default meta; type Story = StoryObj; export const AutomaticRemoveViewStory: Story = { - name: "1d. Automatically resolve brokers", + name: "1e. Automatically resolve brokers", render: () => { return ( ); const resolveButtonsBeforeResolving = screen.getAllByRole("button", { - name: "Mark as fixed", + name: "Resolve exposures", }); await user.click(resolveButtonsBeforeResolving[0]); const resolveButtonsAfterResolving = screen.getAllByRole("button", { - name: "Mark as fixed", + name: "Resolve exposures", }); expect(resolveButtonsAfterResolving.length).toBeLessThan( resolveButtonsBeforeResolving.length, @@ -71,7 +71,7 @@ it("refreshes the client-side router cache after resolving a profile", async () expect(mockedRouterRefresh).not.toHaveBeenCalled(); const resolveButtonsBeforeResolving = screen.getAllByRole("button", { - name: "Mark as fixed", + name: "Resolve exposures", }); await user.click(resolveButtonsBeforeResolving[0]); @@ -87,13 +87,13 @@ it("keeps the manual resolution button if resolving a profile failed", async () render(); const resolveButtonsBeforeResolving = screen.getAllByRole("button", { - name: "Mark as fixed", + name: "Resolve exposures", }); await user.click(resolveButtonsBeforeResolving[0]); const resolveButtonsAfterResolving = screen.getAllByRole("button", { - name: "Mark as fixed", + name: "Resolve exposures", }); expect(resolveButtonsAfterResolving.length).toBe( resolveButtonsBeforeResolving.length, diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/manual-remove/RemovalCard.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/manual-remove/RemovalCard.tsx index 0676e5eb676..eec552d7345 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/manual-remove/RemovalCard.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/manual-remove/RemovalCard.tsx @@ -5,7 +5,7 @@ "use client"; import { useRouter } from "next/navigation"; -import { OnerepScanResultRow } from "knex/types/tables"; +import { OnerepScanResultDataBrokerRow } from "knex/types/tables"; import { Button } from "../../../../../../../../../components/client/Button"; import { useL10n } from "../../../../../../../../../hooks/l10n"; import { useState } from "react"; @@ -14,7 +14,7 @@ import { useTelemetry } from "../../../../../../../../../hooks/useTelemetry"; import { ScanResultCard } from "../../../../../../../../../components/client/exposure_card/ScanResultCard"; export type Props = { - scanResult: OnerepScanResultRow; + scanResult: OnerepScanResultDataBrokerRow; isPremiumUser: boolean; isEligibleForPremium: boolean; isExpanded: boolean; @@ -68,9 +68,7 @@ export const RemovalCard = (props: Props) => { }); }} > - {l10n.getString( - "fix-flow-data-broker-profiles-manual-remove-button-mark-fixed", - )} + {l10n.getString("exposure-card-resolve-exposures-cta")} ) : null } diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/manual-remove/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/manual-remove/page.tsx index d96b432f18f..b64125cf22d 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/manual-remove/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/manual-remove/page.tsx @@ -5,7 +5,7 @@ import { redirect } from "next/navigation"; import { headers } from "next/headers"; import { getServerSession } from "../../../../../../../../../functions/server/getServerSession"; -import { getLatestOnerepScanResults } from "../../../../../../../../../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../../../../../../../../../db/tables/onerep_scans"; import { getOnerepProfileId } from "../../../../../../../../../../db/tables/subscribers"; import { getSubscriberBreaches } from "../../../../../../../../../functions/server/getSubscriberBreaches"; import { ManualRemoveView } from "./ManualRemoveView"; @@ -23,7 +23,10 @@ export default async function ManualRemovePage() { const countryCode = getCountryCode(headers()); const profileId = await getOnerepProfileId(session.user.subscriber.id); - const scanData = await getLatestOnerepScanResults(profileId); + const scanData = await getScanResultsWithBroker( + profileId, + hasPremium(session.user), + ); const subBreaches = await getSubscriberBreaches({ fxaUid: session.user.subscriber.fxa_uid, countryCode, diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.stories.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.stories.tsx new file mode 100644 index 00000000000..0833d1bbee2 --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.stories.tsx @@ -0,0 +1,71 @@ +/* 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 type { Meta, StoryObj } from "@storybook/react"; +import { OnerepScanRow } from "knex/types/tables"; +import { + createRandomBreach, + createRandomScanResult, + createUserWithPremiumSubscription, +} from "../../../../../../../../../../apiMocks/mockData"; +import { Shell } from "../../../../../../../Shell"; +import { getL10n } from "../../../../../../../../../functions/l10n/storybookAndJest"; +import { LatestOnerepScanData } from "../../../../../../../../../../db/tables/onerep_scans"; +import { RemovalUnderMaintenanceView } from "./RemovalUnderMaintenanceView"; + +const meta: Meta = { + title: "Pages/Logged in/Guided resolution/1d. Removal Under Maintenance", + component: RemovalUnderMaintenanceView, +}; +export default meta; +type Story = StoryObj; + +const user = createUserWithPremiumSubscription(); + +const mockedSession = { + expires: new Date().toISOString(), + user: user, +}; +const mockedScan: OnerepScanRow = { + created_at: new Date(1998, 2, 31), + updated_at: new Date(1998, 2, 31), + id: 0, + onerep_profile_id: 0, + onerep_scan_id: 0, + onerep_scan_reason: "initial", + onerep_scan_status: "finished", +}; + +const mockedScanData: LatestOnerepScanData = { + scan: mockedScan, + results: [...Array(5)].map(() => + createRandomScanResult({ status: "new", manually_resolved: false }), + ), +}; +const mockedBreaches = [...Array(5)].map(() => createRandomBreach()); + +export const RemovalUnderMaintenanceViewStory: Story = { + render: () => { + return ( + + + + ); + }, +}; diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.test.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.test.tsx new file mode 100644 index 00000000000..8b8e2d32d86 --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.test.tsx @@ -0,0 +1,124 @@ +/* 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 { it, expect } from "@jest/globals"; +import { render, screen } from "@testing-library/react"; +import { userEvent } from "@testing-library/user-event"; +import { composeStory } from "@storybook/react"; +import { axe } from "jest-axe"; +import Meta, { + RemovalUnderMaintenanceViewStory, +} from "./RemovalUnderMaintenanceView.stories"; + +const mockedRouterPush = jest.fn(); + +jest.mock("../../../../../../../../../hooks/useTelemetry"); +jest.mock("next/navigation", () => ({ + useRouter: () => ({ + push: mockedRouterPush, + }), + usePathname: jest.fn(), + useSearchParams: () => ({ + get: jest.fn(), + }), +})); + +describe("Removal under maintenance", () => { + it("passes the axe accessibility test suite", async () => { + const RemovalUnderMaintenanceView = composeStory( + RemovalUnderMaintenanceViewStory, + Meta, + ); + const { container } = render(); + expect(await axe(container)).toHaveNoViolations(); + }); + + it("shows removal instructions", async () => { + const user = userEvent.setup(); + const RemovalUnderMaintenanceView = composeStory( + RemovalUnderMaintenanceViewStory, + Meta, + ); + render(); + const viewRemovalInstructionsButton = screen.getByRole("button", { + name: "View removal instructions", + }); + await user.click(viewRemovalInstructionsButton); + + const headerRemovalGuide = screen.getByText( + "Removal guide for data broker websites", + ); + expect(headerRemovalGuide).toBeInTheDocument(); + }); + + it("closes removal instructions using the “back arrow”", async () => { + const user = userEvent.setup(); + const RemovalUnderMaintenanceView = composeStory( + RemovalUnderMaintenanceViewStory, + Meta, + ); + render(); + const viewRemovalInstructionsButton = screen.getByRole("button", { + name: "View removal instructions", + }); + await user.click(viewRemovalInstructionsButton); + + const headerRemovalGuideOne = screen.getByText( + "Removal guide for data broker websites", + ); + expect(headerRemovalGuideOne).toBeInTheDocument(); + const arrowBackButton = screen.getAllByRole("button", { + name: "Back to exposures", + })[0]; + await user.click(arrowBackButton); + const headerRemovalGuideTwo = screen.queryByText( + "Removal guide for data broker websites", + ); + expect(headerRemovalGuideTwo).not.toBeInTheDocument(); + }); + + it("closes removal instructions using the “Back to exposures” button", async () => { + const user = userEvent.setup(); + const RemovalUnderMaintenanceView = composeStory( + RemovalUnderMaintenanceViewStory, + Meta, + ); + render(); + const viewRemovalInstructionsButton = screen.getByRole("button", { + name: "View removal instructions", + }); + await user.click(viewRemovalInstructionsButton); + + const headerRemovalGuideOne = screen.getByText( + "Removal guide for data broker websites", + ); + expect(headerRemovalGuideOne).toBeInTheDocument(); + const closeButton = screen.getAllByRole("button", { + name: "Back to exposures", + })[1]; + await user.click(closeButton); + const headerRemovalGuideTwo = screen.queryByText( + "Removal guide for data broker websites", + ); + expect(headerRemovalGuideTwo).not.toBeInTheDocument(); + }); + + it("clicks the “Marks exposure resolved” button and shows a loader while resolving the exposure", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(), + }); + const user = userEvent.setup(); + const RemovalUnderMaintenanceView = composeStory( + RemovalUnderMaintenanceViewStory, + Meta, + ); + render(); + const resolveButton = screen.getByRole("button", { + name: "Mark exposure resolved", + }); + await user.click(resolveButton); + expect(resolveButton).toHaveTextContent("Loading"); + }); +}); diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.tsx index b0860e125f9..83bf42546c6 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/RemovalUnderMaintenanceView.tsx @@ -46,6 +46,8 @@ export const RemovalUnderMaintenanceView = (props: Props) => { "DataBrokerManualRemoval", ); + // Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy + /* c8 ignore start */ async function handleManualRemovalChange() { setIsLoadingNextDataBroker(true); try { @@ -87,6 +89,7 @@ export const RemovalUnderMaintenanceView = (props: Props) => { setIsLoadingNextDataBroker(false); } } + /* c8 ignore stop */ const exposureCategoriesArray: React.ReactElement[] = []; if (firstScanResultNotResolved.relatives.length > 0) { @@ -144,13 +147,14 @@ export const RemovalUnderMaintenanceView = (props: Props) => { aria-label={firstScanResultNotResolved.data_broker} >
{exposureCategoriesArray.map((item) => ( {item} ))}
-
{
{dataBrokerCard}
-
- {l10n.getString( - "data-broker-removal-maintenance-steps-to-remove-header", - )} -
-
-
    -
  1. - {l10n.getString( - "data-broker-removal-maintenance-steps-to-remove-header-step-one", - )} -
  2. -
  3. - {l10n.getString( - "data-broker-removal-maintenance-steps-to-remove-header-step-two", - )} -
  4. -
-
+
+
+ {l10n.getString( + "data-broker-removal-maintenance-steps-to-remove-header", + )} +
+
+
    +
  1. + {l10n.getString( + "data-broker-removal-maintenance-steps-to-remove-header-step-one", + )} +
  2. +
  3. + {l10n.getString( + "data-broker-removal-maintenance-steps-to-remove-header-step-two", + )} +
  4. +
+
+
setDetailedRemovalGuide(true)} @@ -255,29 +261,31 @@ export const RemovalUnderMaintenanceView = (props: Props) => {
-
- {l10n.getString( - "data-broker-removal-maintenance-steps-to-remove-header", - )} -
-
- {l10n.getFragment( - "data-broker-removal-maintenance-rationale-answer", - { - elems: { - learn_about_data_exposure_link: ( - - ), +
+
+ {l10n.getString( + "data-broker-removal-maintenance-steps-to-remove-header", + )} +
+
+ {l10n.getFragment( + "data-broker-removal-maintenance-rationale-answer", + { + elems: { + learn_about_data_exposure_link: ( + + ), + }, }, - }, - )} -
+ )} +
+
@@ -291,7 +299,13 @@ export const RemovalUnderMaintenanceView = (props: Props) => { }} className={styles.backArrow} > - +

{l10n.getString("data-broker-removal-guide-header")} @@ -301,55 +315,66 @@ export const RemovalUnderMaintenanceView = (props: Props) => {

{l10n.getString("data-broker-removal-guide-top-section-para-2")}

-
- {l10n.getString("data-broker-removal-guide-step-1-header")} -
-
{l10n.getString("data-broker-removal-guide-step-1-body")}
-
    -
  • - {l10n.getString("data-broker-removal-guide-step-1-list-item-1")} -
  • -
  • - {l10n.getString("data-broker-removal-guide-step-1-list-item-2")} -
  • -
  • - {l10n.getString("data-broker-removal-guide-step-1-list-item-3")} -
  • -
  • - {l10n.getString("data-broker-removal-guide-step-1-list-item-4")} -
  • -
+
+
+ {l10n.getString("data-broker-removal-guide-step-1-header")} +
+
+ {l10n.getString("data-broker-removal-guide-step-1-body")} + +
    +
  • + {l10n.getString("data-broker-removal-guide-step-1-list-item-1")} +
  • +
  • + {l10n.getString("data-broker-removal-guide-step-1-list-item-2")} +
  • +
  • + {l10n.getString("data-broker-removal-guide-step-1-list-item-3")} +
  • +
  • + {l10n.getString("data-broker-removal-guide-step-1-list-item-4")} +
  • +
+
+
-
- {l10n.getString("data-broker-removal-guide-step-2-header")} -
-
- {l10n.getString("data-broker-removal-guide-step-2-body-para-1")} -
-
- {l10n.getString("data-broker-removal-guide-step-2-body-para-2")} -
+
+
+ {l10n.getString("data-broker-removal-guide-step-2-header")} +
+
+ {l10n.getString("data-broker-removal-guide-step-2-body-para-1")} +
+
+ {l10n.getString("data-broker-removal-guide-step-2-body-para-2")} +
+
-
- {l10n.getString("data-broker-removal-guide-step-3-header")} -
-
- {l10n.getString("data-broker-removal-guide-step-3-body-para-1")} -
-
- {l10n.getString("data-broker-removal-guide-step-3-body-para-2")} -
+
+
+ {l10n.getString("data-broker-removal-guide-step-3-header")} +
+
+ {l10n.getString("data-broker-removal-guide-step-3-body-para-1")} +
+
+ {l10n.getString("data-broker-removal-guide-step-3-body-para-2")} +
+
-
- {l10n.getString("data-broker-removal-guide-step-4-header")} -
-
{l10n.getString("data-broker-removal-guide-step-4-body")}
+
+
+ {l10n.getString("data-broker-removal-guide-step-4-header")} +
+
{l10n.getString("data-broker-removal-guide-step-4-body")}
+
diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/page.tsx index 74b6ca922a7..cb9c437eb68 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/removal-under-maintenance/page.tsx @@ -11,7 +11,7 @@ import { import { getCountryCode } from "../../../../../../../../../functions/server/getCountryCode"; import { headers } from "next/headers"; import { - getLatestOnerepScanResults, + getScanResultsWithBroker, getScanResultsWithBrokerUnderMaintenance, } from "../../../../../../../../../../db/tables/onerep_scans"; import { getOnerepProfileId } from "../../../../../../../../../../db/tables/subscribers"; @@ -27,7 +27,13 @@ export default async function RemovalUnderMaintenance() { redirect("/user/dashboard"); } const profileId = await getOnerepProfileId(session.user.subscriber.id); - const latestScan = await getLatestOnerepScanResults(profileId); + const latestScan = await getScanResultsWithBroker( + profileId, + hasPremium(session.user), + ); + const scansWithRemovalUnderMaintenance = + (await getScanResultsWithBrokerUnderMaintenance(profileId)) ?? null; + const data: StepDeterminationData = { countryCode, user: session.user, @@ -37,8 +43,7 @@ export default async function RemovalUnderMaintenance() { countryCode, }), }; - const scansWithRemovalUnderMaintenance = - (await getScanResultsWithBrokerUnderMaintenance(profileId)) ?? null; + const getNextStep = getNextGuidedStep(data, "DataBrokerManualRemoval"); if ( diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/start-free-scan/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/start-free-scan/page.tsx index c26f5204c04..6f88b3ddbc8 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/start-free-scan/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/start-free-scan/page.tsx @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { headers } from "next/headers"; -import { getLatestOnerepScanResults } from "../../../../../../../../../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../../../../../../../../../db/tables/onerep_scans"; import { redirect } from "next/navigation"; import { getServerSession } from "../../../../../../../../../functions/server/getServerSession"; import { getOnerepProfileId } from "../../../../../../../../../../db/tables/subscribers"; @@ -12,6 +12,7 @@ import { StepDeterminationData } from "../../../../../../../../../functions/serv import { getSubscriberBreaches } from "../../../../../../../../../functions/server/getSubscriberBreaches"; import { getSubscriberEmails } from "../../../../../../../../../functions/server/getSubscriberEmails"; import { StartFreeScanView } from "./StartFreeScanView"; +import { hasPremium } from "../../../../../../../../../functions/universal/user"; export default async function StartFreeScanPage() { const countryCode = getCountryCode(headers()); @@ -28,7 +29,10 @@ export default async function StartFreeScanPage() { const latestScanData = typeof onerepProfileId === "number" - ? await getLatestOnerepScanResults(onerepProfileId) + ? await getScanResultsWithBroker( + onerepProfileId, + hasPremium(session.user), + ) : undefined; if (latestScanData?.scan) { // If the user already has done a scan, let them view their results: diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/view-data-brokers/ViewDataBrokers.stories.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/view-data-brokers/ViewDataBrokers.stories.tsx index 8b61d63b782..36b0a7b6b5c 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/view-data-brokers/ViewDataBrokers.stories.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/view-data-brokers/ViewDataBrokers.stories.tsx @@ -3,7 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type { Meta, StoryObj } from "@storybook/react"; -import { OnerepScanResultRow, OnerepScanRow } from "knex/types/tables"; +import { + OnerepScanResultDataBrokerRow, + OnerepScanRow, +} from "knex/types/tables"; import { ViewDataBrokersView } from "./View"; import { createRandomScanResult, @@ -41,19 +44,19 @@ const ViewWrapper = (props: ViewWrapperProps) => { onerep_scan_status: "in_progress", }; - const mockedResolvedScanResults: OnerepScanResultRow[] = [ + const mockedResolvedScanResults: OnerepScanResultDataBrokerRow[] = [ createRandomScanResult({ status: "removed" }), createRandomScanResult({ status: "waiting_for_verification" }), createRandomScanResult({ status: "optout_in_progress" }), ]; - const mockedFewUnresolvedScanResults: OnerepScanResultRow[] = [ + const mockedFewUnresolvedScanResults: OnerepScanResultDataBrokerRow[] = [ ...mockedResolvedScanResults, createRandomScanResult({ status: "new", manually_resolved: false }), createRandomScanResult({ status: "new", manually_resolved: true }), ]; - const mockedManyUnresolvedScanResults: OnerepScanResultRow[] = [ + const mockedManyUnresolvedScanResults: OnerepScanResultDataBrokerRow[] = [ ...Array(42), ].map(() => createRandomScanResult({ status: "new", manually_resolved: false }), diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/view-data-brokers/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/view-data-brokers/page.tsx index c0ea42f7656..cc48c39edb6 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/view-data-brokers/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/view-data-brokers/page.tsx @@ -5,7 +5,7 @@ import { redirect } from "next/navigation"; import { headers } from "next/headers"; import { getServerSession } from "../../../../../../../../../functions/server/getServerSession"; -import { getLatestOnerepScanResults } from "../../../../../../../../../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../../../../../../../../../db/tables/onerep_scans"; import { getOnerepProfileId } from "../../../../../../../../../../db/tables/subscribers"; import { ViewDataBrokersView } from "./View"; import { StepDeterminationData } from "../../../../../../../../../functions/server/getRelevantGuidedSteps"; @@ -13,6 +13,7 @@ import { getCountryCode } from "../../../../../../../../../functions/server/getC import { getSubscriberBreaches } from "../../../../../../../../../functions/server/getSubscriberBreaches"; import { getSubscriberEmails } from "../../../../../../../../../functions/server/getSubscriberEmails"; import { getL10n } from "../../../../../../../../../functions/l10n/serverComponents"; +import { hasPremium } from "../../../../../../../../../functions/universal/user"; export default async function ViewDataBrokers() { const session = await getServerSession(); @@ -23,7 +24,11 @@ export default async function ViewDataBrokers() { const countryCode = getCountryCode(headers()); const profileId = await getOnerepProfileId(session.user.subscriber.id); - const latestScan = await getLatestOnerepScanResults(profileId); + const latestScan = await getScanResultsWithBroker( + profileId, + hasPremium(session.user), + ); + const data: StepDeterminationData = { countryCode, user: session.user, diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/welcome-to-plus/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/welcome-to-plus/page.tsx index 029c77c31ce..c7a6c3bd349 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/welcome-to-plus/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/welcome-to-plus/page.tsx @@ -2,7 +2,7 @@ * 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 { getLatestOnerepScanResults } from "../../../../../../../../../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../../../../../../../../../db/tables/onerep_scans"; import { headers } from "next/headers"; import { getServerSession } from "../../../../../../../../../functions/server/getServerSession"; import { getOnerepProfileId } from "../../../../../../../../../../db/tables/subscribers"; @@ -17,6 +17,7 @@ import { logger } from "../../../../../../../../../functions/server/logging"; import { getL10n } from "../../../../../../../../../functions/l10n/serverComponents"; import { refreshStoredScanResults } from "../../../../../../../../../functions/server/refreshStoredScanResults"; import { checkSession } from "../../../../../../../../../functions/server/checkSession"; +import { hasPremium } from "../../../../../../../../../functions/universal/user"; export default async function WelcomeToPlusPage() { const session = await getServerSession(); @@ -39,7 +40,10 @@ export default async function WelcomeToPlusPage() { redirect("/user/welcome"); } - const scanData = await getLatestOnerepScanResults(profileId); + const scanData = await getScanResultsWithBroker( + profileId, + hasPremium(session.user), + ); const countryCode = getCountryCode(headers()); const subBreaches = await getSubscriberBreaches({ fxaUid: session.user.subscriber.fxa_uid, diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/high-risk-data-breaches/[type]/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/high-risk-data-breaches/[type]/page.tsx index f3f765cd313..65f3de9863b 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/high-risk-data-breaches/[type]/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/high-risk-data-breaches/[type]/page.tsx @@ -13,9 +13,10 @@ import { highRiskBreachTypes, } from "../highRiskBreachData"; import { getCountryCode } from "../../../../../../../../../functions/server/getCountryCode"; -import { getLatestOnerepScanResults } from "../../../../../../../../../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../../../../../../../../../db/tables/onerep_scans"; import { getOnerepProfileId } from "../../../../../../../../../../db/tables/subscribers"; import { isEligibleForPremium } from "../../../../../../../../../functions/universal/premium"; +import { hasPremium } from "../../../../../../../../../functions/universal/user"; interface SecurityRecommendationsProps { params: { @@ -43,7 +44,10 @@ export default async function SecurityRecommendations({ } const profileId = await getOnerepProfileId(session.user.subscriber.id); - const scanData = await getLatestOnerepScanResults(profileId); + const scanData = await getScanResultsWithBroker( + profileId, + hasPremium(session.user), + ); return ( diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/leaked-passwords/[type]/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/leaked-passwords/[type]/page.tsx index 4142bd4b19d..3c60d6d4b94 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/leaked-passwords/[type]/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/leaked-passwords/[type]/page.tsx @@ -14,9 +14,10 @@ import { import { getSubscriberEmails } from "../../../../../../../../../functions/server/getSubscriberEmails"; import { getCountryCode } from "../../../../../../../../../functions/server/getCountryCode"; import { getOnerepProfileId } from "../../../../../../../../../../db/tables/subscribers"; -import { getLatestOnerepScanResults } from "../../../../../../../../../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../../../../../../../../../db/tables/onerep_scans"; import { isEligibleForPremium } from "../../../../../../../../../functions/universal/premium"; import { logger } from "../../../../../../../../../functions/server/logging"; +import { hasPremium } from "../../../../../../../../../functions/universal/user"; interface LeakedPasswordsProps { params: { @@ -47,7 +48,10 @@ export default async function LeakedPasswords({ } const profileId = await getOnerepProfileId(session.user.subscriber.id); - const scanData = await getLatestOnerepScanResults(profileId); + const scanData = await getScanResultsWithBroker( + profileId, + hasPremium(session.user), + ); return (

{l10n.getString("settings-tab-notifications-marketing-title")}

{l10n.getString("settings-tab-notifications-marketing-text")}

- + ); diff --git a/src/app/api/v1/user/welcome-scan/progress/route.ts b/src/app/api/v1/user/welcome-scan/progress/route.ts index 5e0e2ee3428..fab84f32994 100644 --- a/src/app/api/v1/user/welcome-scan/progress/route.ts +++ b/src/app/api/v1/user/welcome-scan/progress/route.ts @@ -13,8 +13,8 @@ import { } from "../../../../../../db/tables/subscribers"; import { - getLatestOnerepScanResults, addOnerepScanResults, + getScanResultsWithBroker, } from "../../../../../../db/tables/onerep_scans"; import { ListScanResultsResponse, @@ -22,6 +22,7 @@ import { getScanDetails, getAllScanResults, } from "../../../../../functions/server/onerep"; +import { hasPremium } from "../../../../../functions/universal/user"; export interface ScanProgressBody { success: boolean; @@ -46,7 +47,10 @@ export async function GET( } const profileId = await getOnerepProfileId(subscriber.id); - const latestScan = await getLatestOnerepScanResults(profileId); + const latestScan = await getScanResultsWithBroker( + profileId, + hasPremium(session.user), + ); const latestScanId = latestScan.scan?.onerep_scan_id; if ( diff --git a/src/app/api/v1/user/welcome-scan/result/route.ts b/src/app/api/v1/user/welcome-scan/result/route.ts index 2c0d469129d..2c9ee411ebe 100644 --- a/src/app/api/v1/user/welcome-scan/result/route.ts +++ b/src/app/api/v1/user/welcome-scan/result/route.ts @@ -13,7 +13,8 @@ import { getSubscriberByFxaUid, } from "../../../../../../db/tables/subscribers"; -import { getLatestOnerepScanResults } from "../../../../../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../../../../../db/tables/onerep_scans"; +import { hasPremium } from "../../../../../functions/universal/user"; export type WelcomeScanResultResponse = | { @@ -34,7 +35,10 @@ export async function GET() { } const profileId = await getOnerepProfileId(subscriber.id); - const scanResults = await getLatestOnerepScanResults(profileId); + const scanResults = await getScanResultsWithBroker( + profileId, + hasPremium(session.user), + ); return NextResponse.json( { success: true, scan_results: scanResults }, { status: 200 }, diff --git a/src/app/components/client/Button.module.scss b/src/app/components/client/Button.module.scss index 02c93e1d75b..54b92667c2d 100644 --- a/src/app/components/client/Button.module.scss +++ b/src/app/components/client/Button.module.scss @@ -33,8 +33,8 @@ } &.secondary { - border: 4px solid $color-blue-50; - border-color: $color-blue-50 transparent transparent transparent; + border: 4px solid $color-purple-70; + border-color: $color-purple-70 transparent transparent transparent; } } .ldsRing div:nth-child(1) { diff --git a/src/app/components/client/ExposuresFilterExplainer.tsx b/src/app/components/client/ExposuresFilterExplainer.tsx index d7a5a5bec37..d20b7bea072 100644 --- a/src/app/components/client/ExposuresFilterExplainer.tsx +++ b/src/app/components/client/ExposuresFilterExplainer.tsx @@ -12,7 +12,7 @@ import { ModalOverlay } from "./dialog/ModalOverlay"; import { Dialog } from "./dialog/Dialog"; import { Button } from "../client/Button"; import { CONST_ONEREP_DATA_BROKER_COUNT } from "../../../constants"; -import { StatusPill } from "../server/StatusPill"; +import { StatusPill, StatusPillTypeMap } from "../server/StatusPill"; import { FeatureFlagName } from "../../../db/tables/featureFlags"; type ExposuresFilterTypeExplainerProps = { @@ -138,28 +138,28 @@ export const ExposuresFilterStatusExplainer = ( "AdditionalRemovalStatuses", ) && (
  • - + {l10n.getString( "modal-exposure-indicator-requested-removal", )}
  • )}
  • - + {l10n.getString("modal-exposure-indicator-in-progress")}
  • - + {l10n.getString("modal-exposure-indicator-removed")}
  • )}
  • - + {l10n.getString("modal-exposure-indicator-fixed")}
  • - + {l10n.getString("modal-exposure-indicator-action-needed")}
  • diff --git a/src/app/components/client/exposure_card/ExposureCard.stories.tsx b/src/app/components/client/exposure_card/ExposureCard.stories.tsx index 34c6923fd20..8d5fae0c8a4 100644 --- a/src/app/components/client/exposure_card/ExposureCard.stories.tsx +++ b/src/app/components/client/exposure_card/ExposureCard.stories.tsx @@ -51,12 +51,15 @@ const ScanMockItemInProgress = createRandomScanResult({ manually_resolved: false, }); const ScanMockItemRemovalUnderMaintenance = createRandomScanResult({ - status: "removal_under_maintenance", + status: "optout_in_progress", manually_resolved: false, + broker_status: "removal_under_maintenance", }); + const ScanMockItemRemovalUnderMaintenanceFixed = createRandomScanResult({ - status: "removal_under_maintenance", + status: "optout_in_progress", manually_resolved: true, + broker_status: "removal_under_maintenance", }); const BreachMockItemRemoved = createRandomBreach({ isResolved: true, diff --git a/src/app/components/client/exposure_card/ExposureCard.tsx b/src/app/components/client/exposure_card/ExposureCard.tsx index 24e82ff7422..6965120122a 100644 --- a/src/app/components/client/exposure_card/ExposureCard.tsx +++ b/src/app/components/client/exposure_card/ExposureCard.tsx @@ -5,7 +5,10 @@ "use client"; import React, { ReactNode } from "react"; -import { OnerepScanResultRow } from "knex/types/tables"; +import { + OnerepScanResultDataBrokerRow, + OnerepScanResultRow, +} from "knex/types/tables"; import { StaticImageData } from "next/image"; import { SubscriberBreach } from "../../../../utils/subscriberBreaches"; import { ScanResultCard } from "./ScanResultCard"; @@ -13,10 +16,12 @@ import { SubscriberBreachCard } from "./SubscriberBreachCard"; import { FeatureFlagName } from "../../../../db/tables/featureFlags"; import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; -export type Exposure = OnerepScanResultRow | SubscriberBreach; +export type Exposure = OnerepScanResultDataBrokerRow | SubscriberBreach; // Typeguard function -export function isScanResult(obj: Exposure): obj is OnerepScanResultRow { +export function isScanResult( + obj: Exposure, +): obj is OnerepScanResultDataBrokerRow { return (obj as OnerepScanResultRow).data_broker !== undefined; // only ScanResult has an instance of data_broker } diff --git a/src/app/components/client/exposure_card/ScanResultCard.tsx b/src/app/components/client/exposure_card/ScanResultCard.tsx index f3aad3abdc5..e9ea6a01caa 100644 --- a/src/app/components/client/exposure_card/ScanResultCard.tsx +++ b/src/app/components/client/exposure_card/ScanResultCard.tsx @@ -6,7 +6,7 @@ import React, { ReactNode, useId } from "react"; import Image from "next/image"; -import { OnerepScanResultRow } from "knex/types/tables"; +import { OnerepScanResultDataBrokerRow } from "knex/types/tables"; import styles from "./ExposureCard.module.scss"; import { StatusPill } from "../../server/StatusPill"; import { ChevronDown } from "../../server/Icons"; @@ -19,7 +19,7 @@ import { ExperimentData } from "../../../../telemetry/generated/nimbus/experimen import SparkleImage from "../assets/sparkle.png"; export type ScanResultCardProps = { - scanResult: OnerepScanResultRow; + scanResult: OnerepScanResultDataBrokerRow; locale: string; resolutionCta: ReactNode; isPremiumUser: boolean; @@ -118,19 +118,27 @@ export const ScanResultCard = (props: ScanResultCardProps) => { const dataBrokerDescription = () => { // Data broker cards manually resolved do not change their status to "removed"; // instead, we track them using the "manually_resolved" property. - if (scanResult.manually_resolved) { - switch (scanResult.status) { - case "removal_under_maintenance": - return l10n.getFragment( - "exposure-card-description-info-for-sale-fixed-removal-under-maintenance-manually-fixed", - { elems: { data_broker_profile: dataBrokerProfileLink } }, - ); - default: - return l10n.getFragment( - "exposure-card-description-info-for-sale-fixed-manually-fixed", - { elems: { data_broker_profile: dataBrokerProfileLink } }, - ); + if (scanResult.broker_status === "removal_under_maintenance") { + if (scanResult.manually_resolved) { + return l10n.getFragment( + "exposure-card-description-info-for-sale-fixed-removal-under-maintenance-manually-fixed", + { elems: { data_broker_profile: dataBrokerProfileLink } }, + ); } + return l10n.getFragment( + "exposure-card-description-info-for-sale-manual-removal-needed", + { + elems: { + b: , + }, + }, + ); + } + if (scanResult.manually_resolved) { + return l10n.getFragment( + "exposure-card-description-info-for-sale-fixed-manually-fixed", + { elems: { data_broker_profile: dataBrokerProfileLink } }, + ); } // if a data broker is not manually resolved switch (scanResult.status) { @@ -194,15 +202,6 @@ export const ScanResultCard = (props: ScanResultCardProps) => { }, }, ); - case "removal_under_maintenance": - return l10n.getFragment( - "exposure-card-description-info-for-sale-manual-removal-needed", - { - elems: { - b: , - }, - }, - ); } }; @@ -237,24 +236,25 @@ export const ScanResultCard = (props: ScanResultCardProps) => { const resolveExposuresCta = (() => { if (props.scanResult.manually_resolved) { return ( - props.scanResult.status === "removal_under_maintenance" && ( -
    - - - {l10n.getFragment("exposure-card-manual-resolution-praise", { - elems: { - b: , - }, - })} - -
    - ) +
    + + + {l10n.getFragment("exposure-card-manual-resolution-praise", { + elems: { + b: , + }, + })} + +
    ); } + if (props.scanResult.broker_status === "removal_under_maintenance") { + return {props.resolutionCta}; + } + switch (props.scanResult.status) { case "new": - case "removal_under_maintenance": return {props.resolutionCta}; default: return null; @@ -323,6 +323,9 @@ export const ScanResultCard = (props: ScanResultCardProps) => {
    = { type DirectTypeProps = { type: StatusPillType }; type ExposureProps = { exposure: Exposure }; + export type Props = (DirectTypeProps | ExposureProps) & { enabledFeatureFlags?: FeatureFlagName[]; note?: string; + isRemovalUnderMaintenance?: boolean; // Optional for ExposureProps }; // This component just renders HTML without business logic: -/* c8 ignore start */ export const StatusPill = (props: Props) => { const l10n = useL10n(); + const pillType = hasDirectType(props) ? props.type : getExposureStatus( props.exposure, props.enabledFeatureFlags?.includes("AdditionalRemovalStatuses") ?? false, + props.isRemovalUnderMaintenance || false, // Pass maintenance flag ); return (
    - {getStatusLabel({ pillType, l10n })} + {!hasDirectType(props) + ? getStatusLabel({ + exposure: props.exposure, + pillType, + l10n, + isDataBrokerUnderMaintenance: props.isRemovalUnderMaintenance, + }) + : getStatusLabel({ + pillType, + l10n, + })}
    {props.note}
    @@ -56,39 +69,54 @@ export const StatusPill = (props: Props) => { function hasDirectType(props: Props): props is DirectTypeProps { return typeof (props as DirectTypeProps).type === "string"; } -/* c8 ignore stop */ -const getStatusLabel = ({ - pillType, - l10n, -}: { +type StatusLabelProps = { + exposure?: Exposure; pillType: string; l10n: ExtendedReactLocalization; -}): string => { - switch (pillType) { + isDataBrokerUnderMaintenance?: boolean; +}; + +const getStatusLabel = (props: StatusLabelProps): string => { + const manuallyRemoved = + props.exposure && + isScanResult(props.exposure) && + props.exposure.manually_resolved; + if (manuallyRemoved) { + return props.l10n.getString("status-pill-removed"); + } + if (props.isDataBrokerUnderMaintenance) { + return props.l10n.getString("status-pill-action-needed"); + } + switch (props.pillType) { case StatusPillTypeMap.RequestedRemoval: - return l10n.getString("status-pill-requested-removal"); + return props.l10n.getString("status-pill-requested-removal"); case StatusPillTypeMap.InProgress: - return l10n.getString("status-pill-progress"); + return props.l10n.getString("status-pill-progress"); case StatusPillTypeMap.Removed: - return l10n.getString("status-pill-removed"); + return props.l10n.getString("status-pill-removed"); case StatusPillTypeMap.Fixed: - return l10n.getString("status-pill-fixed"); + return props.l10n.getString("status-pill-fixed"); case StatusPillTypeMap.ActionNeeded: default: - return l10n.getString("status-pill-action-needed"); + return props.l10n.getString("status-pill-action-needed"); } }; export const getExposureStatus = ( exposure: Exposure, additionalRemovalStatusesEnabled: boolean, + isRemovalUnderMaintenance: boolean, ): StatusPillType => { if (isScanResult(exposure)) { if (exposure.manually_resolved) { return "fixed"; } + if (isRemovalUnderMaintenance) { + return "actionNeeded"; + } + switch (exposure.status) { case "removed": return "removed"; @@ -98,8 +126,6 @@ export const getExposureStatus = ( : "inProgress"; case "optout_in_progress": return "inProgress"; - case "removal_under_maintenance": - return "actionNeeded"; case "new": default: return "actionNeeded"; diff --git a/src/app/functions/server/dashboard.test.ts b/src/app/functions/server/dashboard.test.ts index e8f7116780f..02b5509d0d2 100644 --- a/src/app/functions/server/dashboard.test.ts +++ b/src/app/functions/server/dashboard.test.ts @@ -2,7 +2,7 @@ * 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 { OnerepScanResultRow } from "knex/types/tables"; +import { OnerepScanResultDataBrokerRow } from "knex/types/tables"; import * as fc from "fast-check"; import { DashboardSummary, @@ -13,6 +13,7 @@ import { } from "./dashboard"; import { SubscriberBreach } from "../../../utils/subscriberBreaches"; import { RemovalStatus, RemovalStatusMap } from "../universal/scanResult"; +import { faker } from "@faker-js/faker"; const unresolvedBreaches: SubscriberBreach[] = [ { @@ -104,7 +105,7 @@ const unresolvedBreaches: SubscriberBreach[] = [ ], }, ]; -const unresolvedScannedResults: OnerepScanResultRow[] = [ +const unresolvedScannedResults: OnerepScanResultDataBrokerRow[] = [ { id: 1, onerep_scan_result_id: 11238, @@ -146,9 +147,12 @@ const unresolvedScannedResults: OnerepScanResultRow[] = [ created_at: new Date("2023-09-26T16:59:04.046Z"), updated_at: new Date("2023-09-26T16:59:04.046Z"), manually_resolved: false, + broker_status: "active", + scan_result_status: "optout_in_progress", + url: faker.internet.url(), }, ]; -const inProgressScannedResults: OnerepScanResultRow[] = [ +const inProgressScannedResults: OnerepScanResultDataBrokerRow[] = [ { id: 3, onerep_scan_result_id: 11236, @@ -190,9 +194,12 @@ const inProgressScannedResults: OnerepScanResultRow[] = [ created_at: new Date("2023-09-26T16:59:04.046Z"), updated_at: new Date("2023-09-26T16:59:04.046Z"), manually_resolved: false, + broker_status: "active", + scan_result_status: "optout_in_progress", + url: faker.internet.url(), }, ]; -const manuallyResolvedScannedResults: OnerepScanResultRow[] = [ +const manuallyResolvedScannedResults: OnerepScanResultDataBrokerRow[] = [ { id: 3, onerep_scan_result_id: 11236, @@ -234,9 +241,12 @@ const manuallyResolvedScannedResults: OnerepScanResultRow[] = [ created_at: new Date("2023-09-26T16:59:04.046Z"), updated_at: new Date("2023-09-26T16:59:04.046Z"), manually_resolved: true, + broker_status: "active", + scan_result_status: "optout_in_progress", + url: faker.internet.url(), }, ]; -const allResolvedScannedResults: OnerepScanResultRow[] = [ +const allResolvedScannedResults: OnerepScanResultDataBrokerRow[] = [ { id: 1, onerep_scan_result_id: 11238, @@ -278,6 +288,9 @@ const allResolvedScannedResults: OnerepScanResultRow[] = [ created_at: new Date("2023-09-26T16:59:04.046Z"), updated_at: new Date("2023-09-26T16:59:04.046Z"), manually_resolved: false, + broker_status: "active", + scan_result_status: "optout_in_progress", + url: faker.internet.url(), }, { id: 2, @@ -320,6 +333,9 @@ const allResolvedScannedResults: OnerepScanResultRow[] = [ created_at: new Date("2023-09-26T16:59:04.046Z"), updated_at: new Date("2023-09-26T16:59:04.046Z"), manually_resolved: false, + broker_status: "active", + scan_result_status: "optout_in_progress", + url: faker.internet.url(), }, { id: 3, @@ -362,6 +378,9 @@ const allResolvedScannedResults: OnerepScanResultRow[] = [ created_at: new Date("2023-09-26T16:59:04.046Z"), updated_at: new Date("2023-09-26T16:59:04.046Z"), manually_resolved: false, + broker_status: "active", + scan_result_status: "optout_in_progress", + url: faker.internet.url(), }, ]; const allResolvedBreaches: SubscriberBreach[] = [ @@ -496,6 +515,7 @@ describe("getExposureReduction", () => { dataBreachUnresolvedNum: 0, dataBreachResolvedNum: 0, dataBrokerManuallyResolvedNum: 0, + dataBrokerRemovalUnderMaintenance: 0, }; const exposureReduction = getDataPointReduction(testSummary); @@ -542,6 +562,7 @@ describe("getExposureReduction", () => { dataBreachUnresolvedNum: 0, dataBreachResolvedNum: 0, dataBrokerManuallyResolvedNum: 0, + dataBrokerRemovalUnderMaintenance: 0, }; const exposureReduction = getDataPointReduction(testSummary); @@ -839,7 +860,7 @@ describe("getDashboardSummary", () => { | "addresses" | "familyMembers", count: number, - ): OnerepScanResultRow { + ): OnerepScanResultDataBrokerRow { return { addresses: dataPoint === "addresses" @@ -868,6 +889,9 @@ describe("getDashboardSummary", () => { ? "optout_in_progress" : "new", updated_at: new Date(), + broker_status: "active", + scan_result_status: "optout_in_progress", + url: faker.internet.url(), }; } @@ -1093,6 +1117,7 @@ describe("getDashboardSummary", () => { dataBrokerManuallyResolvedDataPointsNum: dataBrokerManuallyResolvedDataPointsNum, totalDataPointsNum: totalDataPointsNum, + dataBrokerRemovalUnderMaintenance: 0, // TODO: Figure out what these should be and, when we do, replace // `toMatchObject` by `toStrictEqual`: // unresolvedSanitizedDataPoints: [], @@ -1142,7 +1167,7 @@ describe("getDashboardSummary", () => { function getScanResultsForCounts( dataPointCounts: DataPoints, resolution: Parameters[0], - ): OnerepScanResultRow[] { + ): OnerepScanResultDataBrokerRow[] { return ( [ "emailAddresses", @@ -1262,6 +1287,7 @@ describe("getDashboardSummary", () => { inProgressDataPoints: emptyDataPoints, fixedDataPoints: emptyDataPoints, manuallyResolvedDataBrokerDataPoints: emptyDataPoints, + dataBrokerRemovalUnderMaintenance: 0, }; } diff --git a/src/app/functions/server/dashboard.ts b/src/app/functions/server/dashboard.ts index 4cfe10b3ec7..52c2bb89551 100644 --- a/src/app/functions/server/dashboard.ts +++ b/src/app/functions/server/dashboard.ts @@ -2,10 +2,11 @@ * 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 { OnerepScanResultRow } from "knex/types/tables"; +import { OnerepScanResultDataBrokerRow } from "knex/types/tables"; import { BreachDataTypes } from "../universal/breach"; import { RemovalStatusMap } from "../universal/scanResult"; import { SubscriberBreach } from "../../../utils/subscriberBreaches"; +import { DataBrokerRemovalStatusMap } from "../universal/dataBroker"; export type DataPoints = { // shared @@ -67,7 +68,8 @@ export interface DashboardSummary { fixedDataPoints: DataPoints; /** manually resolved data broker data points separated by data classes */ manuallyResolvedDataBrokerDataPoints: DataPoints; - + /** total number of data brokers with removal under maintenance broker status */ + dataBrokerRemovalUnderMaintenance: number; /** sanitized all data points for frontend display */ unresolvedSanitizedDataPoints: SanitizedDataPoints; /** sanitized resolved and removed data points for frontend display */ @@ -93,7 +95,7 @@ export const dataClassKeyMap: Record = { }; export function getDashboardSummary( - scannedResults: OnerepScanResultRow[], + scannedResults: OnerepScanResultDataBrokerRow[], subscriberBreaches: SubscriberBreach[], ): DashboardSummary { const summary: DashboardSummary = { @@ -105,6 +107,7 @@ export function getDashboardSummary( dataBrokerTotalNum: scannedResults.length, dataBrokerTotalDataPointsNum: 0, dataBrokerAutoFixedNum: 0, + dataBrokerRemovalUnderMaintenance: 0, dataBrokerAutoFixedDataPointsNum: 0, dataBrokerInProgressNum: 0, dataBrokerInProgressDataPointsNum: 0, @@ -201,8 +204,12 @@ export function getDashboardSummary( (r.status === RemovalStatusMap.OptOutInProgress || r.status === RemovalStatusMap.WaitingForVerification) && !isManuallyResolved; + const isRemovalUnderMaintenance = + r.broker_status === DataBrokerRemovalStatusMap.RemovalUnderMaintenance; if (isInProgress) { - summary.dataBrokerInProgressNum++; + if (!isRemovalUnderMaintenance) { + summary.dataBrokerInProgressNum++; + } } else if (isAutoFixed) { summary.dataBrokerAutoFixedNum++; } else if (isManuallyResolved) { @@ -224,11 +231,13 @@ export function getDashboardSummary( summary.allDataPoints.familyMembers += r.relatives.length; if (isInProgress) { - summary.inProgressDataPoints.emailAddresses += r.emails.length; - summary.inProgressDataPoints.phoneNumbers += r.phones.length; - summary.inProgressDataPoints.addresses += r.addresses.length; - summary.inProgressDataPoints.familyMembers += r.relatives.length; - summary.dataBrokerInProgressDataPointsNum += dataPointsIncrement; + if (!isRemovalUnderMaintenance) { + summary.inProgressDataPoints.emailAddresses += r.emails.length; + summary.inProgressDataPoints.phoneNumbers += r.phones.length; + summary.inProgressDataPoints.addresses += r.addresses.length; + summary.inProgressDataPoints.familyMembers += r.relatives.length; + summary.dataBrokerInProgressDataPointsNum += dataPointsIncrement; + } } // for fixed data points: email, phones, addresses, relatives, full name (1) diff --git a/src/app/functions/server/getRelevantGuidedSteps.test.ts b/src/app/functions/server/getRelevantGuidedSteps.test.ts index 19369aada03..692bb0c8f39 100644 --- a/src/app/functions/server/getRelevantGuidedSteps.test.ts +++ b/src/app/functions/server/getRelevantGuidedSteps.test.ts @@ -419,6 +419,7 @@ describe("getNextGuidedStep", () => { createRandomScanResult({ status: "new", manually_resolved: false, + broker_status: "active", }), ], }, @@ -560,6 +561,36 @@ describe("getNextGuidedStep", () => { ).toBe("Done"); }); + it("links to the removal under maintenance step if a user has scan resutls with a data broker that has a removal under maintenance status", () => { + expect( + getNextGuidedStep({ + countryCode: "us", + latestScanData: { + scan: { + ...completedScan.scan!, + onerep_scan_status: "finished", + }, + results: [ + createRandomScanResult({ + status: "optout_in_progress", + manually_resolved: false, + broker_status: "removal_under_maintenance", + }), + ], + }, + subscriberBreaches: [], + user: { + email: "arbitrary@example.com", + }, + }), + ).toStrictEqual({ + href: "/user/dashboard/fix/data-broker-profiles/removal-under-maintenance", + id: "DataBrokerManualRemoval", + completed: false, + eligible: true, + }); + }); + it("links to the Credit Card step if the user's credit card has been breached", () => { expect( getNextGuidedStep({ diff --git a/src/app/functions/server/getRelevantGuidedSteps.ts b/src/app/functions/server/getRelevantGuidedSteps.ts index a5259d81a99..bd7d3dd7db2 100644 --- a/src/app/functions/server/getRelevantGuidedSteps.ts +++ b/src/app/functions/server/getRelevantGuidedSteps.ts @@ -6,7 +6,6 @@ import { Session } from "next-auth"; import { LatestOnerepScanData } from "../../../db/tables/onerep_scans"; import { SubscriberBreach } from "../../../utils/subscriberBreaches"; import { BreachDataTypes, HighRiskDataTypes } from "../universal/breach"; -import { hasPremium } from "../universal/user"; export type StepDeterminationData = { user: Session["user"]; @@ -138,10 +137,12 @@ export function isEligibleForStep( ): boolean { // Only premium users can see the manual data broker removal flow, once they have run a scan if (stepId === "DataBrokerManualRemoval") { - return ( - hasPremium(data.user) && - typeof data.user.subscriber?.onerep_profile_id === "number" - ); + const dataBrokersRequireManualRemoval = + data.latestScanData?.results?.some((result) => { + return result.broker_status === "removal_under_maintenance"; + }) ?? false; + + return dataBrokersRequireManualRemoval; } if (stepId === "Scan") { @@ -244,13 +245,13 @@ export function hasCompletedStep( stepId: StepLink["id"], ): boolean { if (stepId === "DataBrokerManualRemoval") { - const hasResolvedAllScanResults = - data.latestScanData?.results - ?.filter( - (scanResult) => scanResult.status === "removal_under_maintenance", - ) - .every((scanResult) => scanResult.manually_resolved) ?? false; - return hasResolvedAllScanResults; + return ( + data.latestScanData?.results.every( + (result) => + result.broker_status === "removal_under_maintenance" && + result.manually_resolved, + ) ?? false + ); } if (stepId === "Scan") { diff --git a/src/app/functions/server/onerep.ts b/src/app/functions/server/onerep.ts index da3b2ec445b..b2d34aae8b5 100644 --- a/src/app/functions/server/onerep.ts +++ b/src/app/functions/server/onerep.ts @@ -11,12 +11,13 @@ import { import { StateAbbr } from "../../../utils/states.js"; import { getEmailForProfile, - getLatestOnerepScanResults, getLatestScanForProfileByReason, + getScanResultsWithBroker, } from "../../../db/tables/onerep_scans"; import { RemovalStatus } from "../universal/scanResult.js"; import { logger } from "./logging"; import { isUsingMockONEREPEndpoint } from "../universal/mock.ts"; +import { hasPremium } from "../universal/user.ts"; export const monthlyScansQuota = parseInt( (process.env.MONTHLY_SCANS_QUOTA as string) ?? "0", @@ -385,7 +386,10 @@ export async function isEligibleForFreeScan( } const profileId = await getOnerepProfileId(user.subscriber.id); - const scanResult = await getLatestOnerepScanResults(profileId); + const scanResult = await getScanResultsWithBroker( + profileId, + hasPremium(user), + ); if (scanResult.scan) { logger.warn("User has already used free scan"); diff --git a/src/app/functions/universal/scanResult.ts b/src/app/functions/universal/scanResult.ts index ab245cf5cf6..1488156c8bb 100644 --- a/src/app/functions/universal/scanResult.ts +++ b/src/app/functions/universal/scanResult.ts @@ -4,17 +4,14 @@ // TODO: Move pure functions that operate on scan results to this file -// TODO: removal_under_maintenance does not belong here export type RemovalStatus = | "new" | "optout_in_progress" | "waiting_for_verification" - | "removed" - | "removal_under_maintenance"; + | "removed"; export const RemovalStatusMap = { New: "new", OptOutInProgress: "optout_in_progress", WaitingForVerification: "waiting_for_verification", Removed: "removed", - RemovalUnderMaintenance: "removal_under_maintenance", }; diff --git a/src/constants.ts b/src/constants.ts index 7b5a9ba48f4..1a6e1c088fa 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -28,6 +28,7 @@ export const CONST_URL_TERMS = "https://www.mozilla.org/about/legal/terms/subscription-services/"; export const CONST_URL_PRIVACY_POLICY = "https://www.mozilla.org/privacy/subscription-services/"; +export const CONST_URL_MOZILLA_BASKET = "https://basket.mozilla.org"; export const CONST_URL_DATA_PRIVACY_PETITION_BANNER = "https://foundation.mozilla.org/campaigns/demand-privacy-for-all-support-a-us-data-privacy-law-v1/?utm_source=monitor&utm_medium=in-product&utm_campaign=24-pfa"; export const CONST_URL_MONITOR_GITHUB = diff --git a/src/db/tables/featureFlags.ts b/src/db/tables/featureFlags.ts index 8a13f2d09f7..381b634aff5 100644 --- a/src/db/tables/featureFlags.ts +++ b/src/db/tables/featureFlags.ts @@ -5,6 +5,7 @@ import createDbConnection from "../connect"; import { logger } from "../../app/functions/server/logging"; import { FeatureFlagRow } from "knex/types/tables"; +import { headers } from "next/headers"; const knex = createDbConnection(); @@ -77,11 +78,26 @@ export async function getEnabledFeatureFlags( return void subQuery; }); - const enabledFlagNames = await query; - - return enabledFlagNames.map( + const enabledFlagNames = (await query).map( (row: { name: string }) => row.name as FeatureFlagName, ); + + // Force feature flags for E2E tests via URL query params + if (process.env.E2E_TEST_ENV === "local") { + const forcedFeatureFlags = headers().get("x-forced-feature-flags"); + if (forcedFeatureFlags) { + const forcedFeatureFlagsFiltered = forcedFeatureFlags + .split(",") + .filter((forcedFeatureFlag) => + featureFlagNames.includes(forcedFeatureFlag as FeatureFlagName), + ); + return [ + ...new Set([...enabledFlagNames, ...forcedFeatureFlagsFiltered]), + ] as FeatureFlagName[]; + } + } + + return enabledFlagNames; } /** diff --git a/src/db/tables/onerep_scans.ts b/src/db/tables/onerep_scans.ts index 6a16af53cbe..ce21da5c482 100644 --- a/src/db/tables/onerep_scans.ts +++ b/src/db/tables/onerep_scans.ts @@ -17,14 +17,20 @@ import { OnerepScanResultDataBrokerRow, } from "knex/types/tables"; import { RemovalStatus } from "../../app/functions/universal/scanResult.js"; -import { getQaCustomBrokers, getQaToggleRow } from "./qa_customs.ts"; import { CONST_DAY_MILLISECONDS } from "../../constants.ts"; +import { getQaCustomBrokers, getQaToggleRow } from "./qa_customs.ts"; const knex = createDbConnection(); export interface LatestOnerepScanData { scan: OnerepScanRow | null; - results: OnerepScanResultRow[] | OnerepScanResultDataBrokerRow[]; + results: OnerepScanResultDataBrokerRow[]; +} + +/** @deprecated This has been replaced by the LatestOnerepScanData type which only recognizes OnerepScanResultDataBrokerRow as the result type */ +export interface LatestOnerepScanDataOld { + scan: OnerepScanRow | null; + results: OnerepScanResultRow[]; } async function getAllScansForProfile( @@ -145,9 +151,13 @@ async function getLatestOnerepScan( /* Note: please, don't write the results of this function back to the database! */ +/** + * @param onerepProfileId + * @deprecated This has been replaced by getScanResultsWithBroker + */ async function getLatestOnerepScanResults( onerepProfileId: number | null, -): Promise { +): Promise { const scan = await getLatestOnerepScan(onerepProfileId); let results: OnerepScanResultRow[] = []; @@ -407,12 +417,14 @@ async function getEmailForProfile(onerepProfileId: number) { async function getScanResultsWithBrokerUnderMaintenance( onerepProfileId: number | null, -) { +): Promise { if (onerepProfileId === null) { - return null; + return { results: [], scan: null }; } - let scanResults = await knex("onerep_scan_results as sr") + let scanResults: OnerepScanResultDataBrokerRow[] = await knex( + "onerep_scan_results as sr", + ) .select( "sr.*", "s.*", @@ -436,13 +448,45 @@ async function getScanResultsWithBrokerUnderMaintenance( return { results: scanResults } as LatestOnerepScanData; } +async function getScanResultsWithBroker( + onerepProfileId: number | null, + hasPremium: boolean | null, +): Promise { + if (onerepProfileId === null) { + return { + scan: null, + results: [], + } as LatestOnerepScanData; + } + + const scan = await getLatestOnerepScan(onerepProfileId); + let scanResults: OnerepScanResultDataBrokerRow[] | OnerepScanResultRow[] = []; + + if (hasPremium) { + scanResults = await knex("onerep_scan_results as sr") + .select( + "sr.*", + "s.*", + "sr.status as scan_result_status", // rename to avoid collision + "db.status as broker_status", // rename to avoid collision + ) + .innerJoin("onerep_scans as s", "sr.onerep_scan_id", "s.onerep_scan_id") + .where("s.onerep_profile_id", onerepProfileId) + .join("onerep_data_brokers as db", "sr.data_broker", "db.data_broker") + .orderBy("sr.onerep_scan_result_id"); + } else { + scanResults = (await getLatestOnerepScanResults(onerepProfileId)).results; + } + + return { scan: scan ?? null, results: scanResults } as LatestOnerepScanData; +} + export { getAllScansForProfile, getLatestScanForProfileByReason, getScanResults, getLatestOnerepScan, getAllScanResults, - getLatestOnerepScanResults, setOnerepProfileId, setOnerepScan, addOnerepScanResults, @@ -455,4 +499,7 @@ export { deleteSomeScansForProfile, getEmailForProfile, getScanResultsWithBrokerUnderMaintenance, + getScanResultsWithBroker, + /** @deprecated This has been replaced by getScanResultsWithBroker */ + getLatestOnerepScanResults, }; diff --git a/src/e2e/pages/dashBoardPage.ts b/src/e2e/pages/dashBoardPage.ts index 6bb09a7d66c..8018f37d46e 100644 --- a/src/e2e/pages/dashBoardPage.ts +++ b/src/e2e/pages/dashBoardPage.ts @@ -101,7 +101,7 @@ export class DashboardPage { this.reviewAndRemoveProfiles = page.getByText( "Review & remove your profiles", ); - this.markAsFixed = page.getByRole("button", { name: "Mark as fixed" }); + this.markAsFixed = page.getByRole("button", { name: "Resolve exposures" }); this.skipExposureRemoval = page.getByRole("link", { name: "Skip for now" }); this.continuousProtectionButton = page.getByRole("button", { name: "Get continuous protection", diff --git a/src/e2e/pages/landingPage.ts b/src/e2e/pages/landingPage.ts index eab1e01be9c..9152024d368 100644 --- a/src/e2e/pages/landingPage.ts +++ b/src/e2e/pages/landingPage.ts @@ -85,15 +85,17 @@ export class LandingPage { constructor(page: Page) { this.page = page; this.freeMonitoringTooltipTrigger = page - .getByRole("gridcell", { name: "Manual removal Open tooltip" }) + .getByRole("gridcell", { name: "Manual removal" }) .getByLabel("Open tooltip"); this.freeMonitoringTooltipText = page.getByText( - "We’ll let you know which data", + "We’ll let you know which data brokers are selling your info so you can contact them to request removal.", ); this.monitorPlusTooltipTrigger = page - .getByRole("gridcell", { name: "Automatic removal Open tooltip" }) + .getByRole("gridcell", { name: "Automatic removal" }) .getByLabel("Open tooltip"); - this.monitorPlusTooltipText = page.getByText("We’ll automatically request"); + this.monitorPlusTooltipText = page.getByText( + "We’ll automatically request removal of your private info across more than ⁨190⁩ data broker sites.", + ); this.closeTooltips = page.locator( '//div[starts-with(@class, "PlansTable_popoverUnderlay")]', ); @@ -262,7 +264,8 @@ export class LandingPage { } async goToSignIn() { - await this.signInButton.click(); + await this.page.waitForTimeout(500); // sign in button may not be loaded at this point. + await this.signInButton.click({ force: true }); // FxA can take a while to load on stage: await this.page.waitForURL("**/oauth/**", { timeout: 60_000 }); } diff --git a/src/e2e/pages/settingsPage.ts b/src/e2e/pages/settingsPage.ts index 15f5fe4876c..c4c27182695 100644 --- a/src/e2e/pages/settingsPage.ts +++ b/src/e2e/pages/settingsPage.ts @@ -9,6 +9,7 @@ export class SettingsPage { readonly settingsHeader: Locator; readonly emailPrefHeader: Locator; readonly emailHeader: Locator; + readonly tabNotifications: Locator; readonly deleteHeader: Locator; readonly deactivateHeader: Locator; readonly addEmailButton: Locator; @@ -25,6 +26,9 @@ export class SettingsPage { this.settingsHeader = page.locator("h2").first(); this.emailPrefHeader = page.getByText(/Email preferences/); this.emailHeader = page.getByText(/Monitored email addresses/); + this.tabNotifications = page.getByRole("tab", { + name: "Set notifications", + }); this.deleteHeader = page.getByText(/Delete ⁨Monitor⁩ account/); this.deactivateHeader = page.getByText(/Deactivate account/); this.addEmailButton = page.getByText(/Add email address/); @@ -47,6 +51,9 @@ export class SettingsPage { } async open() { + await this.page.setExtraHTTPHeaders({ + "x-forced-feature-flags": "SettingsPageRedesign", + }); await this.page.goto("/user/settings"); } } diff --git a/src/e2e/specs/dashboard/dashboard-navigation.spec.ts b/src/e2e/specs/dashboard/dashboard-navigation.spec.ts index fc8193345bd..ce16798441f 100644 --- a/src/e2e/specs/dashboard/dashboard-navigation.spec.ts +++ b/src/e2e/specs/dashboard/dashboard-navigation.spec.ts @@ -40,7 +40,7 @@ test.describe(`${process.env.E2E_TEST_ENV} - Breaches Dashboard - Navigation @sm //testrail's step 1 await dashboardPage.goToDashboard(); await goToHrefOf(dashboardPage.settingsPageLink); - await expect(page).toHaveURL(/\/settings$/); + await expect(page).toHaveURL(/.*\/settings.*/); //testrail's step 2 await goToHrefOf(dashboardPage.dashboardPageLink); diff --git a/src/e2e/specs/landing/landing-content.spec.ts b/src/e2e/specs/landing/landing-content.spec.ts index d053b41f401..d30558b0e36 100644 --- a/src/e2e/specs/landing/landing-content.spec.ts +++ b/src/e2e/specs/landing/landing-content.spec.ts @@ -226,10 +226,11 @@ test.describe(`${process.env.E2E_TEST_ENV} - Verify the Landing Page content`, ( description: "https://testrail.stage.mozaws.net/index.php?/cases/view/2463504", }); - await landingPage.freeMonitoringTooltipTrigger.click(); + await expect(landingPage.freeMonitoringTooltipTrigger).toBeVisible(); + await landingPage.freeMonitoringTooltipTrigger.click({ force: true }); await expect(landingPage.freeMonitoringTooltipText).toBeVisible(); - await landingPage.closeTooltips.click(); - await landingPage.monitorPlusTooltipTrigger.click(); + await landingPage.closeTooltips.click({ force: true }); + await landingPage.monitorPlusTooltipTrigger.click({ force: true }); await expect(landingPage.monitorPlusTooltipText).toBeVisible(); }); }); diff --git a/src/e2e/specs/settings.spec.ts b/src/e2e/specs/settings.spec.ts index e86390f5616..1eae43aa43f 100644 --- a/src/e2e/specs/settings.spec.ts +++ b/src/e2e/specs/settings.spec.ts @@ -10,12 +10,10 @@ test.describe(`${process.env.E2E_TEST_ENV} Settings Page`, () => { test("Verify settings page loads", async ({ settingsPage }) => { // should go directly to data breach page await settingsPage.open(); - await expect(settingsPage.emailPrefHeader).toBeVisible(); - await expect(settingsPage.emailHeader).toBeVisible(); - (await settingsPage.deleteHeader.isVisible()) - ? await expect(settingsPage.deleteHeader).toBeVisible() - : await expect(settingsPage.deactivateHeader).toBeVisible(); + + await expect(settingsPage.settingsHeader).toBeVisible(); await expect(settingsPage.addEmailButton).toBeVisible(); + await expect(settingsPage.emailHeader).toBeVisible(); }); test("Verify that a user can select between the Breach alert preferences", async ({ @@ -29,6 +27,7 @@ test.describe(`${process.env.E2E_TEST_ENV} Settings Page`, () => { // go to monitor settings page await settingsPage.open(); + await settingsPage.tabNotifications.click(); await expect(async () => { // select "send breach alerts to the affected email address" option diff --git a/src/emails/templates/monthlyActivityFree/MonthlyActivityFreeEmail.stories.tsx b/src/emails/templates/monthlyActivityFree/MonthlyActivityFreeEmail.stories.tsx index a034e9dff19..6c4028f805e 100644 --- a/src/emails/templates/monthlyActivityFree/MonthlyActivityFreeEmail.stories.tsx +++ b/src/emails/templates/monthlyActivityFree/MonthlyActivityFreeEmail.stories.tsx @@ -69,6 +69,7 @@ const mockedDataSummary = { manuallyResolvedDataBrokerDataPoints: mockedDataPoints, unresolvedSanitizedDataPoints: [], fixedSanitizedDataPoints: [], + dataBrokerRemovalUnderMaintenance: 0, }; export const MonthlyReportFreeUserNoScanWithBreachesNothingResolved: Story = { diff --git a/src/scripts/cronjobs/emailBreachAlerts.test.ts b/src/scripts/cronjobs/emailBreachAlerts.test.ts index 24f4f5a8034..866fc082e64 100644 --- a/src/scripts/cronjobs/emailBreachAlerts.test.ts +++ b/src/scripts/cronjobs/emailBreachAlerts.test.ts @@ -55,7 +55,7 @@ jest.mock("../../db/tables/featureFlags", () => { jest.mock("../../db/tables/onerep_scans", () => { return { - getLatestOnerepScanResults: jest.fn(() => + getScanResultsWithBroker: jest.fn(() => Promise.resolve({ scan: null, results: [] }), ), }; diff --git a/src/scripts/cronjobs/emailBreachAlerts.tsx b/src/scripts/cronjobs/emailBreachAlerts.tsx index 15b239a5047..f917ff9140e 100644 --- a/src/scripts/cronjobs/emailBreachAlerts.tsx +++ b/src/scripts/cronjobs/emailBreachAlerts.tsx @@ -38,7 +38,6 @@ import { import { getCronjobL10n } from "../../app/functions/l10n/cronjobs"; import { sanitizeSubscriberRow } from "../../app/functions/server/sanitize"; import { getEnabledFeatureFlags } from "../../db/tables/featureFlags"; -import { getLatestOnerepScanResults } from "../../db/tables/onerep_scans"; import { getSubscriberBreaches } from "../../app/functions/server/getSubscriberBreaches"; import { getSignupLocaleCountry } from "../../emails/functions/getSignupLocaleCountry"; import { refreshStoredScanResults } from "../../app/functions/server/refreshStoredScanResults"; @@ -48,6 +47,7 @@ import { DashboardSummary, getDashboardSummary, } from "../../app/functions/server/dashboard"; +import { getScanResultsWithBroker } from "../../db/tables/onerep_scans"; const SENTRY_SLUG = "cron-breach-alerts"; @@ -282,8 +282,9 @@ export async function poll( !hasPremium(recipient) && typeof recipient.onerep_profile_id === "number" ) { - const scanData = await getLatestOnerepScanResults( + const scanData = await getScanResultsWithBroker( recipient.onerep_profile_id, + hasPremium(recipient), ); const allSubscriberBreaches = await getSubscriberBreaches({ fxaUid: recipient.fxa_uid, diff --git a/src/scripts/cronjobs/firstDataBrokerRemovalFixed.tsx b/src/scripts/cronjobs/firstDataBrokerRemovalFixed.tsx index 2971b13ce61..881d4888960 100644 --- a/src/scripts/cronjobs/firstDataBrokerRemovalFixed.tsx +++ b/src/scripts/cronjobs/firstDataBrokerRemovalFixed.tsx @@ -2,7 +2,11 @@ * 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 { OnerepScanResultRow, SubscriberRow } from "knex/types/tables"; +import { + OnerepScanResultDataBrokerRow, + OnerepScanResultRow, + SubscriberRow, +} from "knex/types/tables"; import { getPotentialSubscribersWaitingForFirstDataBrokerRemovalFixedEmail, markFirstDataBrokerRemovalFixedEmailAsJustSent, @@ -13,11 +17,12 @@ import { FirstDataBrokerRemovalFixed } from "../../emails/templates/firstDataBro import { getCronjobL10n } from "../../app/functions/l10n/cronjobs"; import { sanitizeSubscriberRow } from "../../app/functions/server/sanitize"; import { refreshStoredScanResults } from "../../app/functions/server/refreshStoredScanResults"; -import { getLatestOnerepScanResults } from "../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../db/tables/onerep_scans"; +import { hasPremium } from "../../app/functions/universal/user"; type SubscriberFirstRemovedScanResult = { subscriber: SubscriberRow; - firstRemovedScanResult: OnerepScanResultRow; + firstRemovedScanResult: OnerepScanResultDataBrokerRow; }; function isFulfilledResult( @@ -54,8 +59,9 @@ async function run() { if (subscriber.onerep_profile_id !== null) { await refreshStoredScanResults(subscriber.onerep_profile_id); } - const latestScan = await getLatestOnerepScanResults( + const latestScan = await getScanResultsWithBroker( subscriber.onerep_profile_id, + hasPremium(subscriber), ); let firstRemovedScanResult = null; diff --git a/src/scripts/cronjobs/monthlyActivityFree.tsx b/src/scripts/cronjobs/monthlyActivityFree.tsx index 634314acac2..d682c491063 100644 --- a/src/scripts/cronjobs/monthlyActivityFree.tsx +++ b/src/scripts/cronjobs/monthlyActivityFree.tsx @@ -4,7 +4,7 @@ import { SubscriberRow } from "knex/types/tables"; import { getFreeSubscribersWaitingForMonthlyEmail } from "../../db/tables/subscribers"; -import { getLatestOnerepScanResults } from "../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../db/tables/onerep_scans"; import { updateEmailPreferenceForSubscriber } from "../../db/tables/subscriber_email_preferences"; import { initEmail, sendEmail, closeEmailPool } from "../../utils/email"; import { renderEmail } from "../../emails/renderEmail"; @@ -18,6 +18,7 @@ import { getSignupLocaleCountry } from "../../emails/functions/getSignupLocaleCo import createDbConnection from "../../db/connect"; import { logger } from "../../app/functions/server/logging"; import { getMonthlyActivityFreeUnsubscribeLink } from "../../app/functions/cronjobs/unsubscribeLinks"; +import { hasPremium } from "../../app/functions/universal/user"; await run(); await createDbConnection().destroy(); @@ -32,6 +33,8 @@ async function run() { `Could not send monthly activity emails, because the env var MONTHLY_ACTIVITY_FREE_EMAIL_BATCH_SIZE has a non-numeric value: [${process.env.MONTHLY_ACTIVITY_EMAIL_BATCH_SIZE}].`, ); } + + logger.info(`Getting free subscribers with batch size: ${batchSize}`); const subscribersToEmail = (await getFreeSubscribersWaitingForMonthlyEmail()) .filter((subscriber) => { const assumedCountryCode = getSignupLocaleCountry(subscriber); @@ -40,11 +43,19 @@ async function run() { .slice(0, batchSize); await initEmail(); - await Promise.allSettled( - subscribersToEmail.map((subscriber) => - sendMonthlyActivityEmail(subscriber), - ), - ); + for (const subscriber of subscribersToEmail) { + try { + await sendMonthlyActivityEmail(subscriber); + logger.info("send_monthly_activity_email_free_success", { + subscriberId: subscriber.id, + }); + } catch (error) { + logger.error("send_monthly_activity_email_free_error", { + subscriberId: subscriber.id, + error, + }); + } + } closeEmailPool(); console.log( @@ -69,8 +80,9 @@ async function sendMonthlyActivityEmail(subscriber: SubscriberRow) { await refreshStoredScanResults(subscriber.onerep_profile_id); } - const latestScan = await getLatestOnerepScanResults( + const latestScan = await getScanResultsWithBroker( subscriber.onerep_profile_id, + hasPremium(subscriber), ); const subscriberBreaches = await getSubscriberBreaches({ fxaUid: subscriber.fxa_uid, diff --git a/src/scripts/cronjobs/monthlyActivityPlus.tsx b/src/scripts/cronjobs/monthlyActivityPlus.tsx index f05c4ef6bf7..5cfb4f6d7dc 100644 --- a/src/scripts/cronjobs/monthlyActivityPlus.tsx +++ b/src/scripts/cronjobs/monthlyActivityPlus.tsx @@ -13,10 +13,11 @@ import { MonthlyActivityPlusEmail } from "../../emails/templates/monthlyActivity import { getCronjobL10n } from "../../app/functions/l10n/cronjobs"; import { sanitizeSubscriberRow } from "../../app/functions/server/sanitize"; import { getDashboardSummary } from "../../app/functions/server/dashboard"; -import { getLatestOnerepScanResults } from "../../db/tables/onerep_scans"; +import { getScanResultsWithBroker } from "../../db/tables/onerep_scans"; import { getSubscriberBreaches } from "../../app/functions/server/getSubscriberBreaches"; import { refreshStoredScanResults } from "../../app/functions/server/refreshStoredScanResults"; import { getSignupLocaleCountry } from "../../emails/functions/getSignupLocaleCountry"; +import { hasPremium } from "../../app/functions/universal/user"; void run(); @@ -64,8 +65,9 @@ async function sendMonthlyActivityEmail(subscriber: SubscriberRow) { await refreshStoredScanResults(subscriber.onerep_profile_id); } - const latestScan = await getLatestOnerepScanResults( + const latestScan = await getScanResultsWithBroker( subscriber.onerep_profile_id, + hasPremium(subscriber), ); const subscriberBreaches = await getSubscriberBreaches({ fxaUid: subscriber.fxa_uid, diff --git a/src/utils/fetchWithDelay.test.ts b/src/utils/fetchWithDelay.test.ts new file mode 100644 index 00000000000..eb0babb11f1 --- /dev/null +++ b/src/utils/fetchWithDelay.test.ts @@ -0,0 +1,29 @@ +/* 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 { describe, it, expect } from "@jest/globals"; +import { faker } from "@faker-js/faker/."; +import fetchWithDelay from "./fetchWithDelay"; + +describe("fetchWithDelay", () => { + it("resolves after the delay", async () => { + global.fetch = jest.fn().mockResolvedValueOnce({ + json: () => Promise.resolve({ success: true }), + }); + const response = await fetchWithDelay(`${faker.internet.url()}/api/test`, { + delay: 750, + }); + const data = await response.json(); + expect(data).toStrictEqual({ success: true }); + }); + + it("throws an error", async () => { + global.fetch = jest.fn().mockRejectedValueOnce(new Error("fetch failed")); + await expect(async () => { + await fetchWithDelay(`${faker.internet.url()}/api/test`, { + delay: 750, + }); + }).rejects.toThrow("fetch failed"); + }); +});