Merge branch 'main' into mntor-2700-send-email
This commit is contained in:
Коммит
5708f3328a
|
@ -119,6 +119,12 @@ E2E_TEST_BASE_URL=
|
|||
E2E_TEST_ACCOUNT_EMAIL=
|
||||
E2E_TEST_ACCOUNT_PASSWORD=
|
||||
|
||||
E2E_TEST_ACCOUNT_EMAIL_ZERO_BREACHES=
|
||||
E2E_TEST_ACCOUNT_EMAIL_EXPOSURES_STARTED=
|
||||
|
||||
E2E_TEST_PAYPAL_LOGIN =
|
||||
E2E_TEST_PAYPAL_PASSWORD =
|
||||
|
||||
# Monitor Premium features
|
||||
# Link to start user on the subscription process. PREMIUM_ENABLED must be set to `true`.
|
||||
FXA_SUBSCRIPTIONS_URL=https://accounts.stage.mozaws.net/subscriptions
|
||||
|
|
|
@ -55,6 +55,10 @@ jobs:
|
|||
E2E_TEST_BASE_URL: ${{ secrets.E2E_TEST_BASE_URL }}
|
||||
E2E_TEST_ACCOUNT_EMAIL: ${{ secrets.E2E_TEST_ACCOUNT_EMAIL }}
|
||||
E2E_TEST_ACCOUNT_PASSWORD: ${{ secrets.E2E_TEST_ACCOUNT_PASSWORD }}
|
||||
E2E_TEST_ACCOUNT_EMAIL_ZERO_BREACHES: ${{ secrets.E2E_TEST_ACCOUNT_EMAIL_ZERO_BREACHES }}
|
||||
E2E_TEST_ACCOUNT_EMAIL_EXPOSURES_STARTED: ${{ secrets.E2E_TEST_ACCOUNT_EMAIL_EXPOSURES_STARTED }}
|
||||
E2E_TEST_PAYPAL_LOGIN: ${{ secrets.E2E_TEST_PAYPAL_LOGIN }}
|
||||
E2E_TEST_PAYPAL_PASSWORD: ${{ secrets.E2E_TEST_PAYPAL_PASSWORD }}
|
||||
ADMINS: ${{ secrets.ADMINS }}
|
||||
FXA_ENABLED: true
|
||||
OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }}
|
||||
|
|
|
@ -70,7 +70,11 @@ jobs:
|
|||
E2E_TEST_ENV: ${{ inputs.environment != null && inputs.environment || 'local' }}
|
||||
E2E_TEST_BASE_URL: ${{ secrets.E2E_TEST_BASE_URL }}
|
||||
E2E_TEST_ACCOUNT_EMAIL: ${{ secrets.E2E_TEST_ACCOUNT_EMAIL }}
|
||||
E2E_TEST_ACCOUNT_EMAIL_ZERO_BREACHES: ${{ secrets.E2E_TEST_ACCOUNT_EMAIL_ZERO_BREACHES }}
|
||||
E2E_TEST_ACCOUNT_EMAIL_EXPOSURES_STARTED: ${{ secrets.E2E_TEST_ACCOUNT_EMAIL_EXPOSURES_STARTED }}
|
||||
E2E_TEST_ACCOUNT_PASSWORD: ${{ secrets.E2E_TEST_ACCOUNT_PASSWORD }}
|
||||
E2E_TEST_PAYPAL_LOGIN: ${{ secrets.E2E_TEST_PAYPAL_LOGIN }}
|
||||
E2E_TEST_PAYPAL_PASSWORD: ${{ secrets.E2E_TEST_PAYPAL_PASSWORD }}
|
||||
ADMINS: ${{ secrets.ADMINS }}
|
||||
FXA_ENABLED: true
|
||||
OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }}
|
||||
|
|
|
@ -21,6 +21,34 @@ features:
|
|||
value: { "enabled": false }
|
||||
welcome-scan-optional-info:
|
||||
description: Show additional optional inputs to provide middle name and name suffix for broker scan
|
||||
variables:
|
||||
enabled:
|
||||
description: If the feature is enabled
|
||||
type: Boolean
|
||||
default: false
|
||||
defaults:
|
||||
- channel: local
|
||||
value: { "enabled": true }
|
||||
- channel: staging
|
||||
value: { "enabled": false }
|
||||
- channel: production
|
||||
value: { "enabled": false }
|
||||
automatic-removal-csat-survey:
|
||||
description: Show the CSAT survey for Plus users that have automatically removed broker results
|
||||
variables:
|
||||
enabled:
|
||||
description: If the feature is enabled
|
||||
type: Boolean
|
||||
default: false
|
||||
defaults:
|
||||
- channel: local
|
||||
value: { "enabled": true }
|
||||
- channel: staging
|
||||
value: { "enabled": true }
|
||||
- channel: production
|
||||
value: { "enabled": false }
|
||||
last-scan-date-csat-survey:
|
||||
description: Show the CSAT survey for users that are enrolled in the `last-scan-date` experiment
|
||||
variables:
|
||||
enabled:
|
||||
description: If the feature is enabled
|
||||
|
|
|
@ -47,19 +47,13 @@ settings-email-number-of-breaches-info =
|
|||
*[other] Optræder i { $breachCount } kendte datalæk
|
||||
}
|
||||
|
||||
## Deactivate account
|
||||
|
||||
settings-deactivate-account-title = Deaktiver konto
|
||||
settings-deactivate-account-info-2 = Du kan deaktivere { -product-short-name } ved at slette din { -brand-mozilla-account }.
|
||||
settings-fxa-link-label-3 = Gå til indstillingerne for { -brand-mozilla-account }
|
||||
|
||||
## Delete Monitor account
|
||||
|
||||
settings-delete-monitor-free-account-title = Slet { -brand-monitor }-konto
|
||||
settings-delete-monitor-free-account-description = Dette vil permanent slette din { -brand-monitor }-konto og slå alle advarsler fra.
|
||||
settings-delete-monitor-free-account-cta-label = Slet konto
|
||||
settings-delete-monitor-free-account-dialog-title = Din { -brand-monitor }-konto vil blive slettet permanent
|
||||
settings-delete-monitor-free-account-dialog-lead = Alle dine kontooplysninger for { -brand-monitor } vil blive slettet, og vi holder ikke længere øje med nye datalæk. Denne handling sletter ikke din { -brand-mozilla }-konto.
|
||||
settings-delete-monitor-free-account-dialog-lead-v2 = Alle dine kontooplysninger for { -brand-monitor } vil blive slettet, og vi holder ikke længere øje med nye datalæk. Denne handling sletter ikke din { -brand-mozilla-account }.
|
||||
settings-delete-monitor-free-account-dialog-cta-label = Slet konto
|
||||
settings-delete-monitor-free-account-dialog-cancel-button-label = Jag har skiftet mening - gå tilbage
|
||||
settings-delete-monitor-account-confirmation-toast-label-2 = Din { -brand-monitor }-konto er nu slettet.
|
||||
|
|
|
@ -146,7 +146,7 @@ security-recommendation-email-summary =
|
|||
[one] Ne ñanduti veve kundaharape ojehecha { $num_breaches } mba’ekuaarã ñemboguápe:
|
||||
*[other] Ne ñanduti veve kundaharape ojehecha { $num_breaches } mba’ekuaarã ñemboguápe:
|
||||
}
|
||||
security-recommendation-email-description = Rombyasyeterei, ndaikatumo’ãi emyatyrõ ko apañuãi. Hákatu eku’ekuaa eñemo’ã hag̃ua.
|
||||
security-recommendation-email-description = Rombyasyeterei, ndaikatumo’ãi emyatyrõ ko apañuái. Hákatu eku’ekuaa eñemo’ã hag̃ua.
|
||||
security-recommendation-email-step-one = Ani eikutu juajuha ñanduti veve eikua’ỹva omboúvagui; eimo’ãramo ouha ejeroviahágui, ehenói eikuaa porã hag̃ua
|
||||
security-recommendation-email-step-two = Ema’ẽke <link_to_info>phishing jehode</link_to_info> rehe
|
||||
security-recommendation-email-step-three = Emongurusu ñanduti veve ikatúva spam ha emboyke imbouhára
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
# 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/.
|
||||
|
@ -11,7 +10,6 @@ rec-ssn =
|
|||
Eguereko mbohapy marandu’i ñemurã reigua arýpe léipe he’iháicha.
|
||||
ejerure ha ehechajey nombyaíri ne ñemurã.
|
||||
Eheka mba’ete, virujeporu térã kuatia’atã ñemurã eikuaa’ỹva.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-pw-1-subhead = Emoambue ne ñe’ẽñemi
|
||||
# Link title
|
||||
|
@ -20,7 +18,6 @@ rec-pw-1-2 =
|
|||
Ejapo ko ñe’ẽñemígui ha’eño ha iñambuéva oimeraẽva umi eiporúvagui.
|
||||
Mba’e iporãva ha’e emojopyru mokõi térã hetave ñe’ẽ ojuehegua’ỹva
|
||||
emoheñói hag̃ua peteĩ ñe’ẽsyry ha emoinge papapy ha ta’ãnga’i.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-pw-2-subhead = Embohekopyahu ambue tembiapo ñepyrũ ko ñe’ẽñemímente
|
||||
# Link title
|
||||
|
@ -28,7 +25,6 @@ rec-pw-2-cta-fx = Ehecha tembiapo ñepyrũ { -brand-name }-pe
|
|||
rec-pw-2 =
|
||||
Eiporujeývo ñe’ẽñemi ombohetakuaa mba’ekuaarã ñembyai. Kóva oikóvo
|
||||
ñe’ẽñemi eiporukuaa, umi mba’evai apoha ikatu oiporu oike hag̃ua ambue mba’etépe.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-pw-3-subhead = Eiporu ñe’ẽñemi ñangarekoha eraha hag̃ua ne ñe’ẽñemi opa hendápe
|
||||
# Link title
|
||||
|
@ -39,7 +35,6 @@ rec-pw-3-fx =
|
|||
rec-pw-3-non-fx =
|
||||
Eiporu { -brand-lockwise } ehapykueho hag̃ua opaite ne
|
||||
ñe’ẽñemi ha eike tekorosãme ne pumbyry térã tableta rupive.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-pw-4-subhead = Emboheko pe ñemoneĩ mokõi factor rehegua (2FA)
|
||||
# Link title
|
||||
|
@ -48,19 +43,16 @@ rec-pw-4 =
|
|||
Heta ñanduti renda ome’ẽse 2FA tekorosãverãramo. Kóva oikotevẽ
|
||||
ambue marandu omoñepyrũ hag̃ua tembiapo ne mba’etépe, peteĩ
|
||||
ayvu ha’eñóvaramo og̃uahẽ ñe’ẽmondo haipyre rupi.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-bank-acc-subhead = Ehechameme banco pegua kuatia
|
||||
rec-bank-acc =
|
||||
Ehecha banco pegua kuatia tembiapo vai térã oikomeme’ỹva
|
||||
Emombe’u ne báncope ehecháramo mba’e eikuaaporã’ỹva
|
||||
|
||||
# Recommendation subhead
|
||||
rec-cc-subhead = Ehechameme kuatia’atã ñemuha kuatia
|
||||
rec-cc =
|
||||
Ema’ẽ tapiáke nde kuatia’atã ñemurã. Ikatu hína
|
||||
ejerure kuatia’atã ñemurã pyahu ipapapy pyahúva me’ẽhárape.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-email-mask-subhead = Eiporu peteĩ ñanduti veve rovara’ãnga
|
||||
rec-email-cta = Eiporu { -brand-relay }
|
||||
|
@ -68,58 +60,48 @@ rec-email =
|
|||
Eme’ẽvo ne ñanduti veve ha’etéva remoĩma umi mba’evaiapoha ha tapykuehoha
|
||||
ojuhúvo ne ñe’ẽñemi térã ndejuhúvo ñandutípe. Ko { -brand-relay } rembiapo
|
||||
ha’e omokañývo ne ñanduti veve emondojey aja ñandutiveve ne g̃uahẽhame.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-ip-subhead-2 = Eiporu VPN emoñemi hag̃ua nde IP kundaharape
|
||||
|
||||
# Recommendation subhead
|
||||
rec-moz-vpn-cta = Eñe’ẽ { -brand-mozilla-vpn }
|
||||
rec-moz-vpn-update-2 =
|
||||
Ne kundaharape ñandutigua (IP kundaharape) ohechauka ne rendaite
|
||||
ha ñanduti me’ẽha. Pe mba’eporu { -brand-mozilla-vpn } rehegua
|
||||
omoypytũ nde IP kundaharape oñomi hag̃ua ne rendaite.
|
||||
|
||||
rec-hist-pw-subhead = Aníke eiporujo’a ñe’ẽñemi
|
||||
# Link title
|
||||
rec-hist-pw-cta-fx = Ehecha tembiapo ñepyrũ { -brand-name }
|
||||
rec-hist-pw = Eiporu ñe’ẽñemi ha’ete ha hekorosãva peteĩteĩva mba’etépe g̃uarã. Pe ñe’ẽñemi oreko ijehe mba’ekuaarã ñembyai, tekotevẽ embohekopyahu tembiapo ñepyrũ año.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-sec-qa-subhead = Emoheñói mbohovái porandu hekorosãva rehegua
|
||||
rec-sec-qa =
|
||||
Heta ñanduti renda oporandu peteĩchaite. Peteĩ mbohovái ojehecháramo, pe
|
||||
marandu ojekuaa. Emoheñói mbohovái ipuku ha jereguáva ha eñongatu tenda hekorosãvape.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-phone-num-subhead = Aníke emoherakuã ne pumbyry papapy
|
||||
rec-phone-num =
|
||||
Ani eme’ẽ ne pumbyry papapy eñemboheraguapývo mba’ete
|
||||
ipyahúvape térã mba’eporurãme. Nereikotevẽiramo pumbyry papapy, anínte emoinge.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-dob-subhead = Aníke eiporu nde jehegua PINs-pe
|
||||
rec-dob =
|
||||
Ndahasýi rupi nereñoiha ára ijuhu,
|
||||
ani eiporu ñe’ẽñemi ha PINs. Umi
|
||||
oikuaáva ne aramboty arange ikatu avei oike ne PIN-pe.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-pins-subhead = Emohekorosãvéke ne PINs
|
||||
rec-pins = Peteĩ PIN hekorosãva ndorekói marandu ndejehegua, ne arareñói térã kundaharape. Orekova’erã papapy nde añoite eikuaáva ha ndaikatuiva ojekuaarei.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-address-subhead = Aníke eiporu kundaharape ñe’ẽñemíme
|
||||
rec-address =
|
||||
Eiporúrõ kundaharape térã tape eiko hague omokangy ne
|
||||
ñe’ẽñemi. Mba’ekuaarã ndahasýiva ijuhu oimehápe, péva rupi ko’ã
|
||||
ñe’ẽñemi ndahasýi ijekuaa.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-gen-1-subhead = Eiporu ñe’ẽñemi ha’eño ha hekorosãva peteĩteĩva mba’etépe g̃uarã
|
||||
# Link title
|
||||
rec-gen-1-cta = Mba’éicha emoheñóita ñe’ẽñemi hekorosãva
|
||||
rec-gen-1 = Ñe’ẽñemi eiporujeýrõ ombyaikuaa opaite ne mba’ete. Kóva he’ise pe ñe’ẽñemi ojehechakuaa, umi mba’evaiapoha oreko mba’eñemi heta mba’ete rehegua.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-gen-2-subhead = Embyaty ñe’ẽñemi tenda hekorosãvape
|
||||
# Link title
|
||||
|
@ -127,7 +109,6 @@ rec-gen-2-cta = Mombe’ugua’u ñe’ẽñemi ñangarekohára rehegua
|
|||
rec-gen-2 =
|
||||
Eñongatu ne mba’ekuaarã rembiapo ñepyrũ tenda hekorosãvape eikekuaahápe neañomi, peteĩ
|
||||
ñe’ẽñemi ñangarekoha. Kóva nepytyvõta ehapykueho hag̃ua ne ñe’ẽñeminguéra.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-gen-3-subhead = Ehecháke mávapepa ikatu embohasa marandu ndejehegua
|
||||
# Link title
|
||||
|
@ -135,9 +116,8 @@ rec-gen-3-cta = Emoñe’ẽve tekorosã rehegua
|
|||
rec-gen-3 =
|
||||
Ani eme’ẽ mba’ekuaarã ndejehegua natekotevẽirõ. Ojejerurérõ
|
||||
emoingévo térã eme’ẽ ñanduti veve kundaharape, código postal térã pumbyry papapy, ikatu embotove.
|
||||
|
||||
# Recommendation subhead
|
||||
rec-gen-4-subhead = Embohekopyahu meme software ha tembiporu’i
|
||||
rec-gen-4 =
|
||||
Ñembohekopyahu tembiporu’i, kundahára ha apopyvusu oku’éva ne pumbyry ha’evéva ojapo
|
||||
mba’e’okágui hekorosãva. Ko’ã ñembohekopyahu omoiporã jejavy, software ñembyaikuaa ha tekorosã apañuãi.
|
||||
mba’e’okágui hekorosãva. Ko’ã ñembohekopyahu omoiporã jejavy, software ñembyaikuaa ha tekorosã apañuái.
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
### Dialog window that allows a user to add a new email address to be monitored
|
||||
|
||||
add-email-add-another-heading = Legg til ei ny e-postadresse
|
||||
|
||||
add-email-address-input-label = E-postadresse
|
||||
add-email-send-verification-button = Send stadfestingslenke
|
||||
|
||||
# $email is the newly added email address. $settings-href is the URL for the Settings page. HTML tags should not be translated, e.g. `<a>`
|
||||
# This string will be deprecated when the new Plus plan is live.
|
||||
add-email-verify-the-link = Stadfest lenka som vart sendt til { $email } for å leggje henne til i { -brand-fx-monitor }. Handsam alle e-postadresser i <a { $settings-href }>Innstillingar</a>.
|
||||
# Variables:
|
||||
# $email (string) - An email address submitted by the user for monitoring, e.g. `example@example.com`
|
||||
add-email-verify-the-link-2 = Stadfest lenka som vart sendt til <b>{ $email }</b> for å leggje henne til i { -brand-mozilla-monitor }.
|
||||
|
|
|
@ -24,8 +24,9 @@ exposure-chart-legend-heading-nr = Antal
|
|||
exposure-chart-legend-value-nr = { $nr }×
|
||||
exposure-chart-returning-user-upgrade-prompt-cta = Start ei gratis skanning
|
||||
modal-cta-ok = OK
|
||||
modal-open-alt = Opne
|
||||
modal-close-alt = Lat att
|
||||
open-modal-alt = Opne modal
|
||||
close-modal-alt = Lat att modal
|
||||
open-tooltip-alt = Opne verktøytips
|
||||
progress-card-heres-what-we-fixed-headline-all = Du har løyst følgjande
|
||||
progress-card-manually-fixed-headline = Manuelt løyst
|
||||
dashboard-tab-label-action-needed = Handling påkravd
|
||||
|
|
|
@ -5,12 +5,15 @@
|
|||
public-nav-name = { -brand-mozilla-monitor }
|
||||
landing-all-hero-title = Finn ut kvar din informasjon er eksponert — og ta han tilbake
|
||||
landing-all-hero-emailform-input-placeholder = dittnamn@doeme.no
|
||||
landing-all-hero-emailform-input-label = Skriv inn e-postadressa di for å søkje etter datalekkasje-eksponeringar
|
||||
landing-all-hero-emailform-submit-label = Få gratis skanning
|
||||
# This is a label underneath a big number "14" - it's an image that demos Monitor.
|
||||
landing-all-hero-image-chart-label = eksponeringar
|
||||
|
||||
# Value Proposition
|
||||
|
||||
landing-all-value-prop-fix-exposures = Vi hjelper deg med å løyse eksponeringane dine
|
||||
landing-all-value-prop-info-at-risk = Kva for type informasjon kan vere i fare?
|
||||
|
||||
# Quote
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ settings-page-title = { -product-short-name }-innstillingar
|
|||
|
||||
## Breach alert preferences
|
||||
|
||||
settings-alert-email-preferences-title = E-postinnstillingar
|
||||
settings-alert-email-preferences-subtitle = Fortel oss kva for e-postar du ønskjer å få.
|
||||
settings-alert-preferences-option-one = Send alle åtvaringar om datalekkasjar til den ramma e-postadressa
|
||||
settings-alert-preferences-option-two = Send alle åtvaringar om datalekkasjar til den primære e-postadressa
|
||||
settings-alert-preferences-allow-monthly-monitor-report-subtitle = Ei månadleg oppdatering av nye eksponeringar, kva som er fiksa og kva som krev di merksemd.
|
||||
|
@ -26,15 +28,11 @@ settings-remove-email-button-label = Fjern
|
|||
# $emailAddress (string) - The email address to remove, e.g. `billnye@example.com`
|
||||
settings-remove-email-button-tooltip = Slutt å overvake { $emailAddress }
|
||||
|
||||
## Deactivate account
|
||||
|
||||
settings-deactivate-account-title = Deaktiver kontoen
|
||||
settings-fxa-link-label-3 = Gå til { -brand-mozilla-account }-innstillingane
|
||||
|
||||
## Delete Monitor account
|
||||
|
||||
settings-delete-monitor-free-account-title = Slett { -brand-monitor }-kontoen
|
||||
settings-delete-monitor-free-account-cta-label = Slett kontoen
|
||||
settings-delete-monitor-free-account-dialog-cta-label = Slett kontoen
|
||||
settings-delete-monitor-free-account-dialog-cancel-button-label = Gløym det, ta meg tillbake
|
||||
settings-delete-monitor-account-confirmation-toast-label-2 = { -brand-monitor }-kontoen din er no sletta.
|
||||
settings-delete-monitor-account-confirmation-toast-dismiss-label = Ignorer
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
26
package.json
26
package.json
|
@ -51,8 +51,8 @@
|
|||
"npm": "10.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.577.0",
|
||||
"@aws-sdk/lib-storage": "^3.578.0",
|
||||
"@aws-sdk/client-s3": "^3.588.0",
|
||||
"@aws-sdk/lib-storage": "^3.588.0",
|
||||
"@fluent/bundle": "^0.18.0",
|
||||
"@fluent/langneg": "^0.7.0",
|
||||
"@fluent/react": "^0.15.2",
|
||||
|
@ -65,7 +65,7 @@
|
|||
"@sentry/nextjs": "^8.2.1",
|
||||
"@sentry/node": "^8.0.0",
|
||||
"@sentry/utils": "^8.0.0",
|
||||
"@stripe/stripe-js": "^3.0.6",
|
||||
"@stripe/stripe-js": "^3.4.1",
|
||||
"@types/jsdom": "^21.1.5",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/react": "^18.2.79",
|
||||
|
@ -94,13 +94,13 @@
|
|||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@storybook/addon-a11y": "^8.1.3",
|
||||
"@storybook/addon-actions": "^8.1.3",
|
||||
"@storybook/addon-essentials": "^8.1.3",
|
||||
"@storybook/addon-interactions": "^8.1.3",
|
||||
"@storybook/addon-links": "^8.1.3",
|
||||
"@storybook/addon-a11y": "^8.1.5",
|
||||
"@storybook/addon-actions": "^8.1.5",
|
||||
"@storybook/addon-essentials": "^8.1.5",
|
||||
"@storybook/addon-interactions": "^8.1.5",
|
||||
"@storybook/addon-links": "^8.1.5",
|
||||
"@storybook/blocks": "^8.0.0",
|
||||
"@storybook/nextjs": "^8.1.3",
|
||||
"@storybook/nextjs": "^8.1.5",
|
||||
"@storybook/react": "^8.0.0",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
|
@ -133,16 +133,16 @@
|
|||
"jest-canvas-mock": "^2.5.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fail-on-console": "^3.3.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"lint-staged": "^15.2.5",
|
||||
"mjml-browser": "^4.15.3",
|
||||
"prettier": "3.2.5",
|
||||
"react-intersection-observer": "^9.10.2",
|
||||
"sass": "^1.77.2",
|
||||
"storybook": "^8.1.3",
|
||||
"sass": "^1.77.4",
|
||||
"storybook": "^8.1.5",
|
||||
"stylelint": "^16.6.1",
|
||||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-scss": "^6.3.0",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-jest": "^29.1.4",
|
||||
"tsx": "^4.10.5",
|
||||
"typescript": "^5.4.5",
|
||||
"yaml": "^2.4.2"
|
||||
|
|
|
@ -17,7 +17,11 @@ import { SubscriberBreach } from "../../../../../../../utils/subscriberBreaches"
|
|||
import { LatestOnerepScanData } from "../../../../../../../db/tables/onerep_scans";
|
||||
import { CountryCodeProvider } from "../../../../../../../contextProviders/country-code";
|
||||
import { SessionProvider } from "../../../../../../../contextProviders/session";
|
||||
import { defaultExperimentData } from "../../../../../../../telemetry/generated/nimbus/experiments";
|
||||
import {
|
||||
ExperimentData,
|
||||
defaultExperimentData,
|
||||
} from "../../../../../../../telemetry/generated/nimbus/experiments";
|
||||
import { FeatureFlagName } from "../../../../../../../db/tables/featureFlags";
|
||||
|
||||
const brokerOptions = {
|
||||
"no-scan": "No scan started",
|
||||
|
@ -47,6 +51,8 @@ type DashboardWrapperProps = (
|
|||
elapsedTimeInDaysSinceInitialScan?: number;
|
||||
totalNumberOfPerformedScans?: number;
|
||||
activeTab?: TabType;
|
||||
enabledFeatureFlags?: FeatureFlagName[];
|
||||
experimentData?: ExperimentData;
|
||||
};
|
||||
const DashboardWrapper = (props: DashboardWrapperProps) => {
|
||||
const mockedResolvedBreach: SubscriberBreach = createRandomBreach({
|
||||
|
@ -184,7 +190,6 @@ const DashboardWrapper = (props: DashboardWrapperProps) => {
|
|||
userScanData={scanData}
|
||||
isEligibleForPremium={props.countryCode === "us"}
|
||||
isEligibleForFreeScan={props.countryCode === "us" && !scanData.scan}
|
||||
enabledFeatureFlags={["CsatSurvey"]}
|
||||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
fxaSettingsUrl=""
|
||||
|
@ -199,12 +204,15 @@ const DashboardWrapper = (props: DashboardWrapperProps) => {
|
|||
elapsedTimeInDaysSinceInitialScan={
|
||||
props.elapsedTimeInDaysSinceInitialScan
|
||||
}
|
||||
experimentData={{
|
||||
...defaultExperimentData,
|
||||
"last-scan-date": {
|
||||
enabled: true,
|
||||
},
|
||||
}}
|
||||
enabledFeatureFlags={props.enabledFeatureFlags ?? []}
|
||||
experimentData={
|
||||
props.experimentData ?? {
|
||||
...defaultExperimentData,
|
||||
"last-scan-date": {
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
activeTab={props.activeTab ?? "action-needed"}
|
||||
/>
|
||||
</Shell>
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { userEvent } from "@testing-library/user-event";
|
||||
import { composeStory } from "@storybook/react";
|
||||
import { axe } from "jest-axe";
|
||||
import { Cookies } from "react-cookie";
|
||||
import Meta, {
|
||||
DashboardNonUsNoBreaches,
|
||||
DashboardNonUsUnresolvedBreaches,
|
||||
|
@ -51,6 +52,8 @@ import Meta, {
|
|||
DashboardUsPremiumManuallyResolvedScansNoBreaches,
|
||||
} from "./Dashboard.stories";
|
||||
import { useTelemetry } from "../../../../../../hooks/useTelemetry";
|
||||
import { deleteAllCookies } from "../../../../../../functions/client/deleteAllCookies";
|
||||
import { defaultExperimentData } from "../../../../../../../telemetry/generated/nimbus/experiments";
|
||||
|
||||
jest.mock("next/navigation", () => ({
|
||||
useRouter: jest.fn(),
|
||||
|
@ -72,6 +75,11 @@ jest.mock(
|
|||
},
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
// Make the CSAT banner show up again.
|
||||
deleteAllCookies();
|
||||
});
|
||||
|
||||
describe("axe accessibility test suite", () => {
|
||||
it("passes the axe accessibility test suite for DashboardNonUsNoBreaches", async () => {
|
||||
const ComposedDashboard = composeStory(DashboardNonUsNoBreaches, Meta);
|
||||
|
@ -3348,12 +3356,22 @@ it("send telemetry when users click on exposure chart free scan", async () => {
|
|||
});
|
||||
|
||||
describe("CSAT survey banner", () => {
|
||||
it("does not display the CSAT survey banner on the dashboard tab “action needed” to Plus users", () => {
|
||||
it("does not display the “automatic removal” CSAT survey banner on the dashboard tab “action needed” to Plus users", () => {
|
||||
const ComposedDashboard = composeStory(
|
||||
DashboardUsPremiumResolvedScanNoBreaches,
|
||||
Meta,
|
||||
);
|
||||
render(<ComposedDashboard elapsedTimeInDaysSinceInitialScan={1} />);
|
||||
render(
|
||||
<ComposedDashboard
|
||||
elapsedTimeInDaysSinceInitialScan={1}
|
||||
experimentData={{
|
||||
...defaultExperimentData,
|
||||
"automatic-removal-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const answerButton = screen.queryByRole("button", {
|
||||
name: "Neutral",
|
||||
|
@ -3361,13 +3379,23 @@ describe("CSAT survey banner", () => {
|
|||
expect(answerButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not display the CSAT survey banner to users who do not have automatic data removal enabled", async () => {
|
||||
it("does not display the “automatic removal” CSAT survey banner to users who do not have automatic data removal enabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedDashboard = composeStory(
|
||||
DashboardUsPremiumEmptyScanNoBreaches,
|
||||
Meta,
|
||||
);
|
||||
render(<ComposedDashboard elapsedTimeInDaysSinceInitialScan={1} />);
|
||||
render(
|
||||
<ComposedDashboard
|
||||
elapsedTimeInDaysSinceInitialScan={1}
|
||||
experimentData={{
|
||||
...defaultExperimentData,
|
||||
"automatic-removal-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fixedTab = screen.getByText("Fixed");
|
||||
await user.click(fixedTab);
|
||||
|
@ -3378,13 +3406,23 @@ describe("CSAT survey banner", () => {
|
|||
expect(answerButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the CSAT survey banner to Plus users, after more than 90 days since their initial scan", async () => {
|
||||
it("displays the “automatic removal” CSAT survey banner to Plus users, after more than 90 days since their initial scan", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedDashboard = composeStory(
|
||||
DashboardUsPremiumResolvedScanNoBreaches,
|
||||
Meta,
|
||||
);
|
||||
render(<ComposedDashboard elapsedTimeInDaysSinceInitialScan={91} />);
|
||||
render(
|
||||
<ComposedDashboard
|
||||
elapsedTimeInDaysSinceInitialScan={91}
|
||||
experimentData={{
|
||||
...defaultExperimentData,
|
||||
"automatic-removal-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fixedTab = screen.getByText("Fixed");
|
||||
await user.click(fixedTab);
|
||||
|
@ -3395,13 +3433,23 @@ describe("CSAT survey banner", () => {
|
|||
expect(answerButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the initial CSAT survey banner only to Plus users with automatically fixed data brokers", async () => {
|
||||
it("displays the initial “automatic removal” CSAT survey banner only to Plus users with automatically fixed data brokers", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedDashboard = composeStory(
|
||||
DashboardUsPremiumResolvedScanNoBreaches,
|
||||
Meta,
|
||||
);
|
||||
render(<ComposedDashboard elapsedTimeInDaysSinceInitialScan={1} />);
|
||||
render(
|
||||
<ComposedDashboard
|
||||
elapsedTimeInDaysSinceInitialScan={1}
|
||||
experimentData={{
|
||||
...defaultExperimentData,
|
||||
"automatic-removal-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const answerButtonOne = screen.queryByRole("button", {
|
||||
name: "Neutral",
|
||||
|
@ -3417,13 +3465,23 @@ describe("CSAT survey banner", () => {
|
|||
expect(answerButtonTwo).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the 6-months CSAT survey banner on the dashboard tab “fixed” only", async () => {
|
||||
it("displays the 6-months “automatic removal” CSAT survey banner on the dashboard tab “fixed” only", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedDashboard = composeStory(
|
||||
DashboardUsPremiumResolvedScanNoBreaches,
|
||||
Meta,
|
||||
);
|
||||
render(<ComposedDashboard elapsedTimeInDaysSinceInitialScan={180} />);
|
||||
render(
|
||||
<ComposedDashboard
|
||||
elapsedTimeInDaysSinceInitialScan={180}
|
||||
experimentData={{
|
||||
...defaultExperimentData,
|
||||
"automatic-removal-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const answerButtonOne = screen.queryByRole("button", {
|
||||
name: "Satisfied",
|
||||
|
@ -3439,13 +3497,23 @@ describe("CSAT survey banner", () => {
|
|||
expect(answerButtonTwo).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the follow-up CSAT survey banner link", async () => {
|
||||
it("displays the follow-up “automatic removal” CSAT survey banner link", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedDashboard = composeStory(
|
||||
DashboardUsPremiumResolvedScanNoBreaches,
|
||||
Meta,
|
||||
);
|
||||
render(<ComposedDashboard elapsedTimeInDaysSinceInitialScan={185} />);
|
||||
render(
|
||||
<ComposedDashboard
|
||||
elapsedTimeInDaysSinceInitialScan={185}
|
||||
experimentData={{
|
||||
...defaultExperimentData,
|
||||
"automatic-removal-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fixedTab = screen.getByText("Fixed");
|
||||
await user.click(fixedTab);
|
||||
|
@ -3461,4 +3529,74 @@ describe("CSAT survey banner", () => {
|
|||
);
|
||||
expect(feedbackLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the “latest scan date” CSAT survey banner over the “automatic removal” CSAT survey if a user is targeted by both", async () => {
|
||||
const mockedRecord = useTelemetry();
|
||||
const user = userEvent.setup();
|
||||
const ComposedDashboard = composeStory(
|
||||
DashboardUsPremiumResolvedScanNoBreaches,
|
||||
Meta,
|
||||
);
|
||||
render(
|
||||
<ComposedDashboard
|
||||
elapsedTimeInDaysSinceInitialScan={1}
|
||||
experimentData={{
|
||||
...defaultExperimentData,
|
||||
"automatic-removal-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
"last-scan-date": {
|
||||
enabled: true,
|
||||
},
|
||||
"last-scan-date-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fixedTab = screen.getByText("Fixed");
|
||||
await user.click(fixedTab);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Very satisfied",
|
||||
});
|
||||
await user.click(answerButton);
|
||||
|
||||
expect(mockedRecord).toHaveBeenLastCalledWith(
|
||||
"button",
|
||||
"click",
|
||||
expect.objectContaining({
|
||||
button_id: "csat_survey_latest_scan_date_plus-user_very-satisfied",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("confirms that the “automatic removal” CSAT survey banner has been skipped in favour of the “latest scan date” CSAT survey if a user is targeted by both", () => {
|
||||
const ComposedDashboard = composeStory(
|
||||
DashboardUsPremiumResolvedScanNoBreaches,
|
||||
Meta,
|
||||
);
|
||||
render(
|
||||
<ComposedDashboard
|
||||
activeTab="fixed"
|
||||
elapsedTimeInDaysSinceInitialScan={1}
|
||||
experimentData={{
|
||||
...defaultExperimentData,
|
||||
"automatic-removal-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
"last-scan-date": {
|
||||
enabled: true,
|
||||
},
|
||||
"last-scan-date-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const cookies = new Cookies(null, { path: "/" });
|
||||
expect(cookies.get("csat_survey_initial_dismissed")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,7 +41,7 @@ import ScanProgressIllustration from "./images/scan-illustration.svg";
|
|||
import { CountryCodeContext } from "../../../../../../../contextProviders/country-code";
|
||||
import { FeatureFlagName } from "../../../../../../../db/tables/featureFlags";
|
||||
import { getNextGuidedStep } from "../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { CsatSurvey } from "../../../../../../components/client/CsatSurvey";
|
||||
import { CsatSurvey } from "../../../../../../components/client/csat_survey/CsatSurvey";
|
||||
import { WaitlistDialog } from "../../../../../../components/client/SubscriberWaitlistDialog";
|
||||
import { useOverlayTriggerState } from "react-stately";
|
||||
import { useOverlayTrigger } from "react-aria";
|
||||
|
@ -415,12 +415,6 @@ export const View = (props: Props) => {
|
|||
);
|
||||
};
|
||||
|
||||
const showCsatSurvey =
|
||||
hasPremium(props.user) &&
|
||||
props.enabledFeatureFlags.includes("CsatSurvey") &&
|
||||
activeTab === "fixed" &&
|
||||
typeof props.elapsedTimeInDaysSinceInitialScan !== "undefined";
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Toolbar
|
||||
|
@ -446,17 +440,18 @@ export const View = (props: Props) => {
|
|||
selectedKey={activeTab}
|
||||
/>
|
||||
</Toolbar>
|
||||
{showCsatSurvey &&
|
||||
typeof props.elapsedTimeInDaysSinceInitialScan !== "undefined" && (
|
||||
<CsatSurvey
|
||||
elapsedTimeInDaysSinceInitialScan={
|
||||
props.elapsedTimeInDaysSinceInitialScan
|
||||
}
|
||||
hasAutoFixedDataBrokers={
|
||||
dataSummary.dataBrokerAutoFixedDataPointsNum > 0
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<CsatSurvey
|
||||
activeTab={activeTab}
|
||||
elapsedTimeInDaysSinceInitialScan={
|
||||
props.elapsedTimeInDaysSinceInitialScan
|
||||
}
|
||||
experimentData={props.experimentData}
|
||||
hasAutoFixedDataBrokers={
|
||||
dataSummary.dataBrokerAutoFixedDataPointsNum > 0
|
||||
}
|
||||
user={props.user}
|
||||
/>
|
||||
|
||||
<div className={styles.dashboardContent}>
|
||||
<DashboardTopBanner
|
||||
tabType={activeTab}
|
||||
|
|
|
@ -290,7 +290,7 @@ const Plans = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={styles.plans}>
|
||||
<div id="pricing" className={styles.plans}>
|
||||
<h2 id={headingId} className={styles.planName}>
|
||||
{props.l10n.getString("landing-premium-plans-heading")}
|
||||
</h2>
|
||||
|
|
|
@ -114,7 +114,7 @@ export const BreachDetailsView = (props: Props) => {
|
|||
{typeof breach.Domain === "string" && breach.Domain.length > 0 ? (
|
||||
<p>
|
||||
<TelemetryLink
|
||||
href={breach.Domain}
|
||||
href={`https://${breach.Domain}`}
|
||||
eventData={{ link_id: breach.Domain }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "./Button";
|
||||
import { CloseBtn, OpenInNew } from "../server/Icons";
|
||||
import { useL10n } from "../../hooks/l10n";
|
||||
import { useLocalDismissal } from "../../hooks/useLocalDismissal";
|
||||
import { useHasRenderedClientSide } from "../../hooks/useHasRenderedClientSide";
|
||||
import { useTelemetry } from "../../hooks/useTelemetry";
|
||||
import styles from "./CsatSurvey.module.scss";
|
||||
|
||||
const surveyResponses = [
|
||||
"very-dissatisfied",
|
||||
"dissatisfied",
|
||||
"neutral",
|
||||
"satisfied",
|
||||
"very-satisfied",
|
||||
] as const;
|
||||
|
||||
type SurveyResponse = (typeof surveyResponses)[number];
|
||||
|
||||
type SurveyLinks = Record<SurveyResponse, string>;
|
||||
|
||||
type SurveyTypes = "initial" | "3-months" | "6-months" | "12-months";
|
||||
|
||||
type Survey = {
|
||||
id: SurveyTypes;
|
||||
daysThreshold: number;
|
||||
options: SurveyLinks;
|
||||
};
|
||||
|
||||
const surveys: Survey[] = [
|
||||
{
|
||||
id: "initial",
|
||||
daysThreshold: 0,
|
||||
options: {
|
||||
"very-dissatisfied":
|
||||
"https://survey.alchemer.com/s3/7714663/69d629a9e8e6",
|
||||
dissatisfied: "https://survey.alchemer.com/s3/7714663/481c08f43d81",
|
||||
neutral: "https://survey.alchemer.com/s3/7714663/6a94888b165c",
|
||||
satisfied: "https://survey.alchemer.com/s3/7714663/996da64e2fbb",
|
||||
"very-satisfied": "https://survey.alchemer.com/s3/7714663/668f6cb4d250",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "3-months",
|
||||
daysThreshold: 90,
|
||||
options: {
|
||||
"very-dissatisfied":
|
||||
"https://survey.alchemer.com/s3/7718223/9bf87045f7fb",
|
||||
dissatisfied: "https://survey.alchemer.com/s3/7718223/4ebd39f49be3",
|
||||
neutral: "https://survey.alchemer.com/s3/7718223/fe77a597f97a",
|
||||
satisfied: "https://survey.alchemer.com/s3/7718223/fbbb597a762a",
|
||||
"very-satisfied": "https://survey.alchemer.com/s3/7718223/8f7abc102a9a",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "6-months",
|
||||
daysThreshold: 180,
|
||||
options: {
|
||||
"very-dissatisfied":
|
||||
"https://survey.alchemer.com/s3/7718561/1354e1186d33",
|
||||
dissatisfied: "https://survey.alchemer.com/s3/7718561/6dfb2e8b6d68",
|
||||
neutral: "https://survey.alchemer.com/s3/7718561/2ff6ff90e603",
|
||||
satisfied: "https://survey.alchemer.com/s3/7718561/9393c233103e",
|
||||
"very-satisfied": "https://survey.alchemer.com/s3/7718561/a443cc84b78a",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "12-months",
|
||||
daysThreshold: 351,
|
||||
options: {
|
||||
"very-dissatisfied":
|
||||
"https://survey.alchemer.com/s3/7718562/c254fe9e3c33",
|
||||
dissatisfied: "https://survey.alchemer.com/s3/7718562/8d2a7f93852f",
|
||||
neutral: "https://survey.alchemer.com/s3/7718562/76e17004efd6",
|
||||
satisfied: "https://survey.alchemer.com/s3/7718562/92b30b6aa491",
|
||||
"very-satisfied": "https://survey.alchemer.com/s3/7718562/002e20b6b82f",
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
type Props = {
|
||||
elapsedTimeInDaysSinceInitialScan: number;
|
||||
hasAutoFixedDataBrokers: boolean;
|
||||
};
|
||||
|
||||
const getRelevantSurvey = ({
|
||||
elapsedTimeInDaysSinceInitialScan,
|
||||
hasAutoFixedDataBrokers,
|
||||
}: Props): Survey | undefined => {
|
||||
const relevantSurvey = surveys.findLast(
|
||||
(survey) => elapsedTimeInDaysSinceInitialScan >= survey.daysThreshold,
|
||||
);
|
||||
|
||||
// Show the initial survey only to users who have automatically fixed
|
||||
// data broker results.
|
||||
if (relevantSurvey?.id === "initial" && !hasAutoFixedDataBrokers) {
|
||||
return;
|
||||
}
|
||||
|
||||
return relevantSurvey;
|
||||
};
|
||||
|
||||
export const CsatSurvey = (props: Props) => {
|
||||
const l10n = useL10n();
|
||||
const [answer, setAnswer] = useState<keyof SurveyLinks>();
|
||||
|
||||
const recordTelemetry = useTelemetry();
|
||||
const hasRenderedClientSide = useHasRenderedClientSide();
|
||||
const survey = getRelevantSurvey(props);
|
||||
const localDismissal = useLocalDismissal(`survey-csat_${survey?.id}`);
|
||||
|
||||
if (
|
||||
!hasRenderedClientSide ||
|
||||
typeof survey === "undefined" ||
|
||||
localDismissal.isDismissed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { dismiss } = localDismissal;
|
||||
|
||||
const submit = (satisfaction: SurveyResponse) => {
|
||||
setAnswer(satisfaction);
|
||||
dismiss({ soft: true });
|
||||
recordTelemetry("button", "click", {
|
||||
button_id: `csat_survey_${survey.id}_${satisfaction}`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className={styles.wrapper}>
|
||||
{typeof answer !== "undefined" ? (
|
||||
<div className={styles.prompt}>
|
||||
<a
|
||||
href={survey.options[answer]}
|
||||
onClick={() => dismiss()}
|
||||
target="_blank"
|
||||
rel="noopen noreferrer"
|
||||
>
|
||||
{l10n.getString("survey-csat-follow-up-link-label")}
|
||||
<OpenInNew
|
||||
alt={l10n.getString("open-in-new-tab-alt")}
|
||||
width="13"
|
||||
height="13"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.prompt}>
|
||||
{l10n.getString("survey-csat-question")}
|
||||
</div>
|
||||
<ol className={`${styles.answers} noList`}>
|
||||
{surveyResponses.map((response) => (
|
||||
<li key={response}>
|
||||
<Button
|
||||
className={styles.answer}
|
||||
variant="primary"
|
||||
small
|
||||
onPress={() => submit(response)}
|
||||
>
|
||||
{l10n.getString(`survey-csat-answer-${response}`)}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
<button className={styles.closeButton} onClick={() => dismiss()}>
|
||||
<CloseBtn
|
||||
alt={l10n.getString("survey-csat-survey-dismiss-label")}
|
||||
width="14"
|
||||
height="14"
|
||||
/>
|
||||
</button>
|
||||
</aside>
|
||||
);
|
||||
};
|
|
@ -6,27 +6,31 @@ import { composeStory } from "@storybook/react";
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { axe } from "jest-axe";
|
||||
import Meta, { CsatSurveyBanner } from "./stories/CsatBanner.stories";
|
||||
import { useTelemetry } from "../../hooks/useTelemetry";
|
||||
import { deleteAllCookies } from "../../functions/client/deleteAllCookies";
|
||||
import Meta, {
|
||||
CsatSurveyAutomaticRemoval,
|
||||
CsatSurveyLatestScanDate,
|
||||
} from "../stories/CsatSurvey.stories";
|
||||
import { useTelemetry } from "../../../hooks/useTelemetry";
|
||||
import { deleteAllCookies } from "../../../functions/client/deleteAllCookies";
|
||||
import { createUserWithPremiumSubscription } from "../../../../apiMocks/mockData";
|
||||
|
||||
jest.mock("../../hooks/useTelemetry");
|
||||
jest.mock("../../../hooks/useTelemetry");
|
||||
|
||||
afterEach(() => {
|
||||
// Make the CSAT banner show up again.
|
||||
deleteAllCookies();
|
||||
});
|
||||
|
||||
describe("CSAT survey", () => {
|
||||
describe("CSAT survey banner: Automatic Removal", () => {
|
||||
it("passes the axe accessibility test suite for CsatSurveyBanner", async () => {
|
||||
const ComposedTextComboBox = composeStory(CsatSurveyBanner, Meta);
|
||||
const { container } = render(<ComposedTextComboBox />);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
const { container } = render(<ComposedCsatSurvey />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("displays the survey to users with automatic data removal enabled for less than 90 days", () => {
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
render(<ComposedCsatSurveyBanner elapsedTimeInDaysSinceInitialScan={89} />);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(<ComposedCsatSurvey elapsedTimeInDaysSinceInitialScan={89} />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Dissatisfied",
|
||||
|
@ -37,11 +41,9 @@ describe("CSAT survey", () => {
|
|||
it.each([90, 180, 351])(
|
||||
"displays the survey to users with automatic data removal enabled for at least n days",
|
||||
(dayCount) => {
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(
|
||||
<ComposedCsatSurveyBanner
|
||||
elapsedTimeInDaysSinceInitialScan={dayCount}
|
||||
/>,
|
||||
<ComposedCsatSurvey elapsedTimeInDaysSinceInitialScan={dayCount} />,
|
||||
);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
|
@ -53,8 +55,8 @@ describe("CSAT survey", () => {
|
|||
|
||||
it("shows the correct follow-up feedback link for response “Very dissatisfied”", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
render(<ComposedCsatSurveyBanner elapsedTimeInDaysSinceInitialScan={91} />);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(<ComposedCsatSurvey elapsedTimeInDaysSinceInitialScan={91} />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Very dissatisfied",
|
||||
|
@ -73,10 +75,8 @@ describe("CSAT survey", () => {
|
|||
|
||||
it("shows the correct follow-up feedback link for response “Dissatisfied”", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
render(
|
||||
<ComposedCsatSurveyBanner elapsedTimeInDaysSinceInitialScan={180} />,
|
||||
);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(<ComposedCsatSurvey elapsedTimeInDaysSinceInitialScan={180} />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Dissatisfied",
|
||||
|
@ -94,10 +94,8 @@ describe("CSAT survey", () => {
|
|||
|
||||
it("shows the correct follow-up feedback link for response “Neutral”", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
render(
|
||||
<ComposedCsatSurveyBanner elapsedTimeInDaysSinceInitialScan={351} />,
|
||||
);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(<ComposedCsatSurvey elapsedTimeInDaysSinceInitialScan={351} />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Neutral",
|
||||
|
@ -115,8 +113,8 @@ describe("CSAT survey", () => {
|
|||
|
||||
it("shows the correct follow-up feedback link for response “Satisfied”", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
render(<ComposedCsatSurveyBanner elapsedTimeInDaysSinceInitialScan={91} />);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(<ComposedCsatSurvey elapsedTimeInDaysSinceInitialScan={91} />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Satisfied",
|
||||
|
@ -135,10 +133,8 @@ describe("CSAT survey", () => {
|
|||
|
||||
it("shows the correct follow-up feedback link for response “Very satisfied”", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
render(
|
||||
<ComposedCsatSurveyBanner elapsedTimeInDaysSinceInitialScan={180} />,
|
||||
);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(<ComposedCsatSurvey elapsedTimeInDaysSinceInitialScan={180} />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Very satisfied",
|
||||
|
@ -157,10 +153,8 @@ describe("CSAT survey", () => {
|
|||
it("records telemetry when submitting the survey", async () => {
|
||||
const mockedRecord = useTelemetry();
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
render(
|
||||
<ComposedCsatSurveyBanner elapsedTimeInDaysSinceInitialScan={180} />,
|
||||
);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(<ComposedCsatSurvey elapsedTimeInDaysSinceInitialScan={180} />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Very satisfied",
|
||||
|
@ -178,8 +172,8 @@ describe("CSAT survey", () => {
|
|||
|
||||
it("dismisses the survey by clicking the “close” button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
render(<ComposedCsatSurveyBanner />);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(<ComposedCsatSurvey />);
|
||||
|
||||
const dismissButton = screen.getByRole("button", {
|
||||
name: "Dismiss",
|
||||
|
@ -194,10 +188,8 @@ describe("CSAT survey", () => {
|
|||
|
||||
it("dismisses the survey by clicking the follow-up link", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurveyBanner = composeStory(CsatSurveyBanner, Meta);
|
||||
render(
|
||||
<ComposedCsatSurveyBanner elapsedTimeInDaysSinceInitialScan={180} />,
|
||||
);
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyAutomaticRemoval, Meta);
|
||||
render(<ComposedCsatSurvey elapsedTimeInDaysSinceInitialScan={180} />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Very satisfied",
|
||||
|
@ -215,3 +207,87 @@ describe("CSAT survey", () => {
|
|||
expect(feedbackLinkTwo).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSAT survey banner: Latest scan date", () => {
|
||||
it("passes the axe accessibility test suite for CsatSurveyBanner", async () => {
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyLatestScanDate, Meta);
|
||||
const { container } = render(<ComposedCsatSurvey />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("displays the survey to free users on the “action needed” tab", () => {
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyLatestScanDate, Meta);
|
||||
const user = createUserWithPremiumSubscription();
|
||||
if (user.fxa) {
|
||||
user.fxa.subscriptions = [];
|
||||
}
|
||||
|
||||
render(<ComposedCsatSurvey activeTab="action-needed" user={user} />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Satisfied",
|
||||
});
|
||||
expect(answerButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the survey to Plus users on the “fixed” tab", () => {
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyLatestScanDate, Meta);
|
||||
render(<ComposedCsatSurvey />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Dissatisfied",
|
||||
});
|
||||
expect(answerButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("dismisses the survey by clicking the “close” button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyLatestScanDate, Meta);
|
||||
render(<ComposedCsatSurvey />);
|
||||
|
||||
const dismissButton = screen.getByRole("button", {
|
||||
name: "Dismiss",
|
||||
});
|
||||
await user.click(dismissButton);
|
||||
|
||||
const answerButton = screen.queryByRole("button", {
|
||||
name: "Neutral",
|
||||
});
|
||||
expect(answerButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("records telemetry when submitting the survey", async () => {
|
||||
const mockedRecord = useTelemetry();
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyLatestScanDate, Meta);
|
||||
render(<ComposedCsatSurvey />);
|
||||
|
||||
const answerButton = screen.getByRole("button", {
|
||||
name: "Very satisfied",
|
||||
});
|
||||
await user.click(answerButton);
|
||||
|
||||
expect(mockedRecord).toHaveBeenCalledWith(
|
||||
"button",
|
||||
"click",
|
||||
expect.objectContaining({
|
||||
button_id: "csat_survey_latest_scan_date_plus-user_very-satisfied",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show a follow-up survey after submitting the survery", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ComposedCsatSurvey = composeStory(CsatSurveyLatestScanDate, Meta);
|
||||
render(<ComposedCsatSurvey />);
|
||||
|
||||
const answerButtonOne = screen.getByRole("button", {
|
||||
name: "Very satisfied",
|
||||
});
|
||||
await user.click(answerButtonOne);
|
||||
const feedbackLink = screen.queryByText(
|
||||
/Your feedback is helpful to us! How can we improve Monitor for you\?/i,
|
||||
);
|
||||
expect(feedbackLink).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
/* 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/. */
|
||||
|
||||
"use client";
|
||||
|
||||
import { Session } from "next-auth";
|
||||
import { Cookies } from "react-cookie";
|
||||
import { CsatSurveyBanner } from "./CsatSurveyBanner";
|
||||
import { TabType } from "../../../(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View";
|
||||
import { getAutomaticRemovalCsatSurvey } from "./surveys/automaticRemovalCsatSurvey";
|
||||
import { getLatestScanDateCsatSurvey } from "./surveys/latestScanDateCsatSurvey";
|
||||
import { COOKIE_DISMISSAL_MAX_AGE_IN_SECONDS } from "../../../hooks/useLocalDismissal";
|
||||
import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments";
|
||||
|
||||
export type CsatSurveyProps = {
|
||||
activeTab: TabType;
|
||||
user: Session["user"];
|
||||
experimentData: ExperimentData;
|
||||
hasAutoFixedDataBrokers: boolean;
|
||||
elapsedTimeInDaysSinceInitialScan?: number;
|
||||
};
|
||||
|
||||
export const CsatSurvey = (props: CsatSurveyProps) => {
|
||||
const surveyOptions = {
|
||||
activeTab: props.activeTab,
|
||||
experimentData: props.experimentData,
|
||||
user: props.user,
|
||||
};
|
||||
// The order of the surveys here matter: If there are multiple matching
|
||||
// surveys for the user we dismiss all surveys, but the last one in the list.
|
||||
const surveys = [
|
||||
getAutomaticRemovalCsatSurvey({
|
||||
...surveyOptions,
|
||||
elapsedTimeInDaysSinceInitialScan:
|
||||
props.elapsedTimeInDaysSinceInitialScan,
|
||||
hasAutoFixedDataBrokers: props.hasAutoFixedDataBrokers,
|
||||
}),
|
||||
getLatestScanDateCsatSurvey(surveyOptions),
|
||||
];
|
||||
|
||||
// Filters out previously dismissed surveys to make sure `currentSurvey` will
|
||||
// always be relevant to show for the user.
|
||||
const cookies = new Cookies(null, { path: "/" });
|
||||
const filteredSurveys = surveys.filter((survey) => {
|
||||
if (!survey) {
|
||||
return;
|
||||
}
|
||||
const cookieDismissalId = `${survey.localDismissalId}_dismissed`;
|
||||
const surveyIsDismissed = cookies.get(cookieDismissalId);
|
||||
return !surveyIsDismissed;
|
||||
});
|
||||
const currentSurvey =
|
||||
filteredSurveys.length > 0 && filteredSurveys.slice(-1)[0];
|
||||
if (!currentSurvey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark all surveys except the current one as automatically dismissed.
|
||||
filteredSurveys.forEach((survey) => {
|
||||
if (
|
||||
survey &&
|
||||
survey?.localDismissalId !== currentSurvey?.localDismissalId
|
||||
) {
|
||||
const cookieDismissalId = `${survey.localDismissalId}_dismissed`;
|
||||
cookies.set(cookieDismissalId, Date.now().toString(), {
|
||||
maxAge: COOKIE_DISMISSAL_MAX_AGE_IN_SECONDS,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
currentSurvey && (
|
||||
<CsatSurveyBanner
|
||||
key={currentSurvey.localDismissalId}
|
||||
localDismissalId={currentSurvey.localDismissalId}
|
||||
survey={currentSurvey.survey}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
@import "../../tokens";
|
||||
@import "../../../tokens";
|
||||
|
||||
.wrapper {
|
||||
align-items: center;
|
|
@ -0,0 +1,111 @@
|
|||
/* 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/. */
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "../Button";
|
||||
import { CloseBtn, OpenInNew } from "../../server/Icons";
|
||||
import { useL10n } from "../../../hooks/l10n";
|
||||
import { useLocalDismissal } from "../../../hooks/useLocalDismissal";
|
||||
import { useHasRenderedClientSide } from "../../../hooks/useHasRenderedClientSide";
|
||||
import { useTelemetry } from "../../../hooks/useTelemetry";
|
||||
import styles from "./CsatSurveyBanner.module.scss";
|
||||
import { Survey } from "./surveys/csatSurvey";
|
||||
|
||||
const surveyResponses = [
|
||||
"very-dissatisfied",
|
||||
"dissatisfied",
|
||||
"neutral",
|
||||
"satisfied",
|
||||
"very-satisfied",
|
||||
] as const;
|
||||
|
||||
type SurveyResponse = (typeof surveyResponses)[number];
|
||||
|
||||
type SurveyLinks = Record<SurveyResponse, string>;
|
||||
|
||||
type Props = {
|
||||
localDismissalId: string;
|
||||
survey: Survey;
|
||||
};
|
||||
|
||||
export const CsatSurveyBanner = ({ localDismissalId, survey }: Props) => {
|
||||
const l10n = useL10n();
|
||||
const [answer, setAnswer] = useState<keyof SurveyLinks>();
|
||||
|
||||
const recordTelemetry = useTelemetry();
|
||||
const hasRenderedClientSide = useHasRenderedClientSide();
|
||||
const localDismissal = useLocalDismissal(localDismissalId);
|
||||
|
||||
if (
|
||||
!hasRenderedClientSide ||
|
||||
typeof survey === "undefined" ||
|
||||
localDismissal.isDismissed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { dismiss } = localDismissal;
|
||||
const hasFollowUpSurveyOptions =
|
||||
"followUpSurveyOptions" in survey &&
|
||||
typeof survey.followUpSurveyOptions !== "undefined";
|
||||
|
||||
const submit = (satisfaction: SurveyResponse) => {
|
||||
setAnswer(satisfaction);
|
||||
dismiss({ soft: hasFollowUpSurveyOptions });
|
||||
recordTelemetry("button", "click", {
|
||||
button_id: `${localDismissalId}_${satisfaction}`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className={styles.wrapper}>
|
||||
{typeof answer !== "undefined" && hasFollowUpSurveyOptions ? (
|
||||
<div className={styles.prompt}>
|
||||
<a
|
||||
href={(survey.followUpSurveyOptions as SurveyLinks)[answer]}
|
||||
onClick={() => dismiss()}
|
||||
target="_blank"
|
||||
rel="noopen noreferrer"
|
||||
>
|
||||
{l10n.getString("survey-csat-follow-up-link-label")}
|
||||
<OpenInNew
|
||||
alt={l10n.getString("open-in-new-tab-alt")}
|
||||
width="13"
|
||||
height="13"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.prompt}>
|
||||
{l10n.getString("survey-csat-question")}
|
||||
</div>
|
||||
<ol className={`${styles.answers} noList`}>
|
||||
{surveyResponses.map((response) => (
|
||||
<li key={response}>
|
||||
<Button
|
||||
className={styles.answer}
|
||||
variant="primary"
|
||||
small
|
||||
onPress={() => submit(response)}
|
||||
>
|
||||
{l10n.getString(`survey-csat-answer-${response}`)}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
<button className={styles.closeButton} onClick={() => dismiss()}>
|
||||
<CloseBtn
|
||||
alt={l10n.getString("survey-csat-survey-dismiss-label")}
|
||||
width="14"
|
||||
height="14"
|
||||
/>
|
||||
</button>
|
||||
</aside>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,113 @@
|
|||
/* 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 { TabType } from "../../../../(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View";
|
||||
import {
|
||||
CsatSurveyProps,
|
||||
RelevantSurvey,
|
||||
SurveyData,
|
||||
SurveyLinks,
|
||||
UserType,
|
||||
getRelevantSurveys,
|
||||
} from "./csatSurvey";
|
||||
|
||||
export type AutomaticRemovalVariation = {
|
||||
id: "initial" | "3-months" | "6-months" | "12-months";
|
||||
showForUser: UserType[];
|
||||
showOnTab: TabType[];
|
||||
daysThreshold: number;
|
||||
followUpSurveyOptions: SurveyLinks;
|
||||
};
|
||||
|
||||
const surveyData: SurveyData = {
|
||||
id: "csat_survey",
|
||||
requiredExperimentIds: ["automatic-removal-csat-survey"],
|
||||
variations: [
|
||||
{
|
||||
id: "initial",
|
||||
showForUser: ["plus-user"],
|
||||
showOnTab: ["fixed"],
|
||||
daysThreshold: 0,
|
||||
followUpSurveyOptions: {
|
||||
"very-dissatisfied":
|
||||
"https://survey.alchemer.com/s3/7714663/69d629a9e8e6",
|
||||
dissatisfied: "https://survey.alchemer.com/s3/7714663/481c08f43d81",
|
||||
neutral: "https://survey.alchemer.com/s3/7714663/6a94888b165c",
|
||||
satisfied: "https://survey.alchemer.com/s3/7714663/996da64e2fbb",
|
||||
"very-satisfied": "https://survey.alchemer.com/s3/7714663/668f6cb4d250",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "3-months",
|
||||
showForUser: ["plus-user"],
|
||||
showOnTab: ["fixed"],
|
||||
daysThreshold: 90,
|
||||
followUpSurveyOptions: {
|
||||
"very-dissatisfied":
|
||||
"https://survey.alchemer.com/s3/7718223/9bf87045f7fb",
|
||||
dissatisfied: "https://survey.alchemer.com/s3/7718223/4ebd39f49be3",
|
||||
neutral: "https://survey.alchemer.com/s3/7718223/fe77a597f97a",
|
||||
satisfied: "https://survey.alchemer.com/s3/7718223/fbbb597a762a",
|
||||
"very-satisfied": "https://survey.alchemer.com/s3/7718223/8f7abc102a9a",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "6-months",
|
||||
showForUser: ["plus-user"],
|
||||
showOnTab: ["fixed"],
|
||||
daysThreshold: 180,
|
||||
followUpSurveyOptions: {
|
||||
"very-dissatisfied":
|
||||
"https://survey.alchemer.com/s3/7718561/1354e1186d33",
|
||||
dissatisfied: "https://survey.alchemer.com/s3/7718561/6dfb2e8b6d68",
|
||||
neutral: "https://survey.alchemer.com/s3/7718561/2ff6ff90e603",
|
||||
satisfied: "https://survey.alchemer.com/s3/7718561/9393c233103e",
|
||||
"very-satisfied": "https://survey.alchemer.com/s3/7718561/a443cc84b78a",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "12-months",
|
||||
showForUser: ["plus-user"],
|
||||
showOnTab: ["fixed"],
|
||||
daysThreshold: 351,
|
||||
followUpSurveyOptions: {
|
||||
"very-dissatisfied":
|
||||
"https://survey.alchemer.com/s3/7718562/c254fe9e3c33",
|
||||
dissatisfied: "https://survey.alchemer.com/s3/7718562/8d2a7f93852f",
|
||||
neutral: "https://survey.alchemer.com/s3/7718562/76e17004efd6",
|
||||
satisfied: "https://survey.alchemer.com/s3/7718562/92b30b6aa491",
|
||||
"very-satisfied": "https://survey.alchemer.com/s3/7718562/002e20b6b82f",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const getAutomaticRemovalCsatSurvey = (
|
||||
props: CsatSurveyProps & {
|
||||
elapsedTimeInDaysSinceInitialScan: number | undefined;
|
||||
hasAutoFixedDataBrokers: boolean;
|
||||
},
|
||||
): RelevantSurvey | null => {
|
||||
const surveys = getRelevantSurveys({ ...surveyData, ...props });
|
||||
// Find the last survey variation that matches the time since the users
|
||||
// automatic removal.
|
||||
const relevantSurvey =
|
||||
surveys &&
|
||||
surveys.findLast((surveyVariation) => {
|
||||
const survey = surveyVariation.survey as AutomaticRemovalVariation;
|
||||
// Show the initial survey only to users who have automatically fixed
|
||||
// data broker results.
|
||||
if (survey?.id === "initial" && !props.hasAutoFixedDataBrokers) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
typeof props.elapsedTimeInDaysSinceInitialScan !== "undefined" &&
|
||||
props.elapsedTimeInDaysSinceInitialScan >= survey.daysThreshold
|
||||
);
|
||||
});
|
||||
return relevantSurvey ?? null;
|
||||
};
|
||||
|
||||
export { getAutomaticRemovalCsatSurvey };
|
|
@ -0,0 +1,80 @@
|
|||
/* 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 { Session } from "next-auth";
|
||||
import { TabType } from "../../../../(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/View";
|
||||
import { hasPremium } from "../../../../functions/universal/user";
|
||||
import { AutomaticRemovalVariation } from "./automaticRemovalCsatSurvey";
|
||||
import { ExperimentData } from "../../../../../telemetry/generated/nimbus/experiments";
|
||||
|
||||
const surveyResponses = [
|
||||
"very-dissatisfied",
|
||||
"dissatisfied",
|
||||
"neutral",
|
||||
"satisfied",
|
||||
"very-satisfied",
|
||||
] as const;
|
||||
|
||||
export type SurveyResponse = (typeof surveyResponses)[number];
|
||||
|
||||
export type SurveyType = "csat_survey" | "csat_survey_latest_scan_date";
|
||||
|
||||
export type UserType = "free-user" | "plus-user";
|
||||
|
||||
export type SurveyLinks = Record<SurveyResponse, string>;
|
||||
|
||||
export type SurveyData = {
|
||||
id: SurveyType;
|
||||
requiredExperimentIds: (keyof ExperimentData)[];
|
||||
variations: Survey[];
|
||||
};
|
||||
|
||||
export type LatestScanDateVariation = {
|
||||
id: UserType;
|
||||
showForUser: UserType[];
|
||||
showOnTab: TabType[];
|
||||
};
|
||||
|
||||
export type Survey = AutomaticRemovalVariation | LatestScanDateVariation;
|
||||
|
||||
export type CsatSurveyProps = {
|
||||
activeTab: TabType;
|
||||
experimentData: ExperimentData;
|
||||
user: Session["user"];
|
||||
};
|
||||
|
||||
export type RelevantSurvey = {
|
||||
localDismissalId: string;
|
||||
survey: Survey;
|
||||
};
|
||||
|
||||
export function getRelevantSurveys({
|
||||
id,
|
||||
requiredExperimentIds,
|
||||
variations,
|
||||
activeTab,
|
||||
experimentData,
|
||||
user,
|
||||
}: SurveyData & CsatSurveyProps): RelevantSurvey[] | null {
|
||||
if (
|
||||
!requiredExperimentIds.every(
|
||||
(experimentId) => experimentData[experimentId].enabled,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filteredSurveys = variations.filter((surveyVariation) => {
|
||||
const isRelevantUser = surveyVariation.showForUser.includes(
|
||||
hasPremium(user) ? "plus-user" : "free-user",
|
||||
);
|
||||
const isRelevantTab = surveyVariation.showOnTab.includes(activeTab);
|
||||
return isRelevantUser && isRelevantTab;
|
||||
});
|
||||
|
||||
return filteredSurveys.map((survey) => ({
|
||||
localDismissalId: `${id}_${survey.id}`,
|
||||
survey,
|
||||
}));
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/* 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 {
|
||||
CsatSurveyProps,
|
||||
RelevantSurvey,
|
||||
SurveyData,
|
||||
getRelevantSurveys,
|
||||
} from "./csatSurvey";
|
||||
|
||||
const surveyData: SurveyData = {
|
||||
id: "csat_survey_latest_scan_date",
|
||||
requiredExperimentIds: ["last-scan-date", "last-scan-date-csat-survey"],
|
||||
variations: [
|
||||
{
|
||||
id: "free-user",
|
||||
showForUser: ["free-user"],
|
||||
showOnTab: ["action-needed"],
|
||||
},
|
||||
{
|
||||
id: "plus-user",
|
||||
showForUser: ["plus-user"],
|
||||
showOnTab: ["fixed"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const getLatestScanDateCsatSurvey = (
|
||||
props: CsatSurveyProps,
|
||||
): RelevantSurvey | null => {
|
||||
const surveys = getRelevantSurveys({ ...surveyData, ...props });
|
||||
// In case there are multiple matching survey variations for the current user:
|
||||
// Return the first one.
|
||||
return surveys && surveys.length > 0 ? surveys[0] : null;
|
||||
};
|
||||
|
||||
export { getLatestScanDateCsatSurvey };
|
|
@ -1,22 +0,0 @@
|
|||
/* 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 { CsatSurvey } from "../CsatSurvey";
|
||||
|
||||
const meta: Meta<typeof CsatSurvey> = {
|
||||
title: "CsatSurvey",
|
||||
component: CsatSurvey,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CsatSurvey>;
|
||||
|
||||
export const CsatSurveyBanner: Story = {
|
||||
name: "CsatSurvey",
|
||||
args: {
|
||||
elapsedTimeInDaysSinceInitialScan: 0,
|
||||
hasAutoFixedDataBrokers: true,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/* 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 { CsatSurvey } from "../csat_survey/CsatSurvey";
|
||||
import { createUserWithPremiumSubscription } from "../../../../apiMocks/mockData";
|
||||
import { defaultExperimentData } from "../../../../telemetry/generated/nimbus/experiments";
|
||||
|
||||
const meta: Meta<typeof CsatSurvey> = {
|
||||
title: "CsatSurvey",
|
||||
component: CsatSurvey,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CsatSurvey>;
|
||||
|
||||
export const CsatSurveyAutomaticRemoval: Story = {
|
||||
name: "AutomaticRemoval",
|
||||
args: {
|
||||
activeTab: "fixed",
|
||||
user: createUserWithPremiumSubscription(),
|
||||
experimentData: {
|
||||
...defaultExperimentData,
|
||||
"automatic-removal-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
hasAutoFixedDataBrokers: true,
|
||||
elapsedTimeInDaysSinceInitialScan: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const CsatSurveyLatestScanDate: Story = {
|
||||
name: "LatestScanDate",
|
||||
args: {
|
||||
activeTab: "fixed",
|
||||
user: createUserWithPremiumSubscription(),
|
||||
experimentData: {
|
||||
...defaultExperimentData,
|
||||
"last-scan-date": {
|
||||
enabled: true,
|
||||
},
|
||||
"last-scan-date-csat-survey": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -45,7 +45,7 @@ export async function getExperiments(params: {
|
|||
context: {
|
||||
// Nimbus takes a language, rather than a locale, hence the .split:
|
||||
language: params.locale.split("-")[0],
|
||||
region: params.countryCode,
|
||||
region: params.countryCode.toUpperCase(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { useLocalDismissal } from "./useLocalDismissal";
|
||||
import {
|
||||
COOKIE_DISMISSAL_MAX_AGE_IN_SECONDS,
|
||||
useLocalDismissal,
|
||||
} from "./useLocalDismissal";
|
||||
import { useCookies } from "react-cookie";
|
||||
|
||||
jest.mock("react-cookie", () => {
|
||||
|
@ -71,7 +74,7 @@ describe("useLocalDismissal", () => {
|
|||
expect(mockedSetCookie).toHaveBeenCalledWith(
|
||||
"some_key_dismissed",
|
||||
expect.any(String),
|
||||
{ maxAge: 100 * 365 * 24 * 60 * 60 },
|
||||
{ maxAge: COOKIE_DISMISSAL_MAX_AGE_IN_SECONDS },
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -117,7 +120,7 @@ describe("useLocalDismissal", () => {
|
|||
expect(mockedSetCookie).toHaveBeenCalledWith(
|
||||
"some_key_dismissed",
|
||||
expect.any(String),
|
||||
{ maxAge: 100 * 365 * 24 * 60 * 60 },
|
||||
{ maxAge: COOKIE_DISMISSAL_MAX_AGE_IN_SECONDS },
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
import { useState } from "react";
|
||||
import { useCookies } from "react-cookie";
|
||||
|
||||
export const COOKIE_DISMISSAL_MAX_AGE_IN_SECONDS = 100 * 365 * 24 * 60 * 60;
|
||||
|
||||
export type DismissOptions = {
|
||||
/** If true, the dismissal won't take effect right away, but the cookie to store the dismissal _will_ be set. */
|
||||
soft?: boolean;
|
||||
|
@ -47,7 +49,7 @@ export function useLocalDismissal(
|
|||
// tests.
|
||||
typeof options.duration === "number"
|
||||
? options.duration
|
||||
: 100 * 365 * 24 * 60 * 60;
|
||||
: COOKIE_DISMISSAL_MAX_AGE_IN_SECONDS;
|
||||
setCookie(cookieId, Date.now().toString(), {
|
||||
maxAge: maxAgeInSeconds,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/* 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/. */
|
||||
|
||||
export async function up(knex) {
|
||||
return knex.schema
|
||||
.createTable("subscriber_coupons", table => {
|
||||
table.increments('id').primary()
|
||||
table.integer("subscriber_id").references("subscribers.id").notNullable().onDelete("CASCADE").onUpdate("CASCADE");
|
||||
table.string("coupon_code").notNullable()
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now())
|
||||
table.unique(["subscriber_id", "coupon_code"])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
export async function down(knex) {
|
||||
return knex.schema
|
||||
.dropTableIfExists("subscriber_coupons")
|
||||
}
|
|
@ -42,7 +42,6 @@ export type FeatureFlagName =
|
|||
| "RedesignedEmails"
|
||||
| "UpdatedEmailPreferencesOption"
|
||||
| "MonthlyActivityEmail"
|
||||
| "CsatSurvey"
|
||||
| "CancellationFlow"
|
||||
| "ConfirmCancellation"
|
||||
| "FirstDataBrokerRemovalFixedEmail";
|
||||
|
|
|
@ -88,7 +88,9 @@ async function getLatestOnerepScanResults(
|
|||
"onerep_scans",
|
||||
"onerep_scan_results.onerep_scan_id",
|
||||
"onerep_scans.onerep_scan_id",
|
||||
)) as OnerepScanResultRow[]);
|
||||
)
|
||||
.orderBy("link")
|
||||
.orderBy("onerep_scan_result_id", "desc")) as OnerepScanResultRow[]);
|
||||
|
||||
return {
|
||||
scan: scan ?? null,
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/* 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 createDbConnection from "../connect.js";
|
||||
import { logger } from "../../app/functions/server/logging";
|
||||
import { SubscriberCouponRow } from "knex/types/tables";
|
||||
|
||||
const knex = createDbConnection();
|
||||
|
||||
async function checkCouponForSubscriber(
|
||||
subscriberId: number,
|
||||
couponCode: string,
|
||||
) {
|
||||
logger.info("checkCouponForSubscriber", subscriberId);
|
||||
return !!(await knex("subscriber_coupons")
|
||||
.where({
|
||||
subscriber_id: subscriberId,
|
||||
coupon_code: couponCode,
|
||||
})
|
||||
.first());
|
||||
}
|
||||
|
||||
async function addCouponForSubscriber(
|
||||
subscriberId: number,
|
||||
couponCode: string,
|
||||
) {
|
||||
logger.info("addCouponForSubscriber", { subscriberId, couponCode });
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await knex("subscribers_coupon")
|
||||
.insert({
|
||||
subscriber_id: subscriberId,
|
||||
coupon_code: couponCode,
|
||||
})
|
||||
.returning("*");
|
||||
} catch (e) {
|
||||
if ((e as Error).message.includes("violates unique constraint")) {
|
||||
logger.error("could_not_add_coupon", {
|
||||
subscriberId,
|
||||
error: (e as Error).message,
|
||||
});
|
||||
} else {
|
||||
logger.error("could_not_add_coupon", { error: JSON.stringify(e) });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return res?.[0] as SubscriberCouponRow;
|
||||
}
|
||||
|
||||
export { checkCouponForSubscriber, addCouponForSubscriber };
|
|
@ -13,6 +13,7 @@ export class AuthPage {
|
|||
readonly continueButton: Locator;
|
||||
readonly ageInputField: Locator;
|
||||
readonly verifyCodeInputField: Locator;
|
||||
readonly useDifferentEmailButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
@ -22,6 +23,7 @@ export class AuthPage {
|
|||
this.ageInputField = page.getByLabel("How old are you?");
|
||||
this.continueButton = page.locator('[type="submit"]').first();
|
||||
this.verifyCodeInputField = page.locator("div.card input");
|
||||
this.useDifferentEmailButton = page.locator("#use-different");
|
||||
}
|
||||
|
||||
async continue({ waitForURL = "**/*" }) {
|
||||
|
@ -39,16 +41,16 @@ export class AuthPage {
|
|||
await this.continue({ waitForURL: "**/oauth/**" });
|
||||
}
|
||||
|
||||
async enterPassword() {
|
||||
await this.passwordInputField.fill(
|
||||
process.env.E2E_TEST_ACCOUNT_PASSWORD as string,
|
||||
);
|
||||
async enterPassword(optionalPassword?: string) {
|
||||
const password =
|
||||
optionalPassword || (process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
|
||||
await this.passwordInputField.fill(password);
|
||||
await this.continue({ waitForURL: "**/user/**" });
|
||||
}
|
||||
|
||||
async signIn(email: string) {
|
||||
async signIn(email: string, optionalPassword?: string) {
|
||||
await this.enterEmail(email);
|
||||
await this.enterPassword();
|
||||
await this.enterPassword(optionalPassword);
|
||||
}
|
||||
|
||||
async signUp(email: string, page: Page) {
|
||||
|
|
|
@ -52,7 +52,11 @@ export class DashboardPage {
|
|||
readonly servicesPocket: Locator;
|
||||
readonly servicesFirefoxDesktop: Locator;
|
||||
readonly servicesFirefoxMobile: Locator;
|
||||
readonly servicesMozilla: Locator;
|
||||
readonly appsAndServicesMenu: Locator;
|
||||
|
||||
readonly profileSettings: Locator;
|
||||
readonly profileSignOut: Locator;
|
||||
readonly profileEmail: Locator;
|
||||
readonly manageProfile: Locator;
|
||||
readonly helpAndSupport: Locator;
|
||||
|
@ -75,6 +79,7 @@ export class DashboardPage {
|
|||
readonly privacyNoticeFooter: Locator;
|
||||
readonly githubFooter: Locator;
|
||||
|
||||
readonly overviewCard: Locator;
|
||||
readonly overviewCardSummary: Locator;
|
||||
readonly overviewCardFindings: Locator;
|
||||
|
||||
|
@ -135,6 +140,13 @@ export class DashboardPage {
|
|||
this.actionNeededTab = page.getByRole("tab", { name: "Action needed" });
|
||||
this.fixedTab = page.getByRole("tab", { name: "Fixed" });
|
||||
this.profileButton = page.getByTitle("Profile").nth(1);
|
||||
|
||||
this.profileSettings = page.locator(
|
||||
'a[title*="Configure"][title*="Mozilla Monitor"]',
|
||||
);
|
||||
this.profileSignOut = page.locator(
|
||||
'button[title*="Sign out of"][title*="Mozilla Monitor"]',
|
||||
);
|
||||
this.profileEmail = page
|
||||
.locator('//li[starts-with(@class, "UserMenu_menuItemWrapper")]/b')
|
||||
.first();
|
||||
|
@ -146,6 +158,7 @@ export class DashboardPage {
|
|||
this.appsAndServices = page.getByRole("button", {
|
||||
name: "Mozilla apps and services",
|
||||
});
|
||||
this.appsAndServicesMenu = page.locator("div[class*='AppPicker_popup']");
|
||||
this.servicesVpn = page.getByRole("link", { name: "Mozilla VPN" });
|
||||
this.servicesRelay = page.getByRole("link", { name: "Firefox Relay" });
|
||||
this.servicesPocket = page.getByRole("link", { name: "Pocket" });
|
||||
|
@ -155,6 +168,7 @@ export class DashboardPage {
|
|||
this.servicesFirefoxMobile = page.getByRole("link", {
|
||||
name: "Firefox for Mobile",
|
||||
});
|
||||
this.servicesMozilla = page.locator('[data-key="mozilla"] > a');
|
||||
this.closeAppsAndServices = page.locator(
|
||||
'//div[starts-with(@class, "Popover_underlay")]',
|
||||
);
|
||||
|
@ -228,6 +242,7 @@ export class DashboardPage {
|
|||
|
||||
//upsell button
|
||||
this.upsellScreenButton = page.getByText(/Let’s (keep going|fix it)/);
|
||||
this.overviewCard = page.locator("[class*='DashboardTopBanner_container']");
|
||||
this.overviewCardSummary = page.locator(
|
||||
"[aria-label='Dashboard summary'] > div > p",
|
||||
);
|
||||
|
|
|
@ -8,11 +8,13 @@ export class DataBrokersPage {
|
|||
readonly page: Page;
|
||||
readonly removeThemForMeButton: Locator;
|
||||
readonly xButton: Locator;
|
||||
readonly forwardArrowButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.removeThemForMeButton = page.getByText("Remove them for me");
|
||||
this.xButton = page.getByLabel("Return to dashboard");
|
||||
this.forwardArrowButton = page.getByLabel("Go to next step");
|
||||
}
|
||||
|
||||
async open() {
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
* 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 { Locator, Page, expect } from "@playwright/test";
|
||||
import { BrowserContext, Locator, Page, expect } from "@playwright/test";
|
||||
import { removeUnicodeChars } from "../utils/helpers";
|
||||
import { DashboardPage } from "./dashBoardPage";
|
||||
|
||||
export class PurchasePage {
|
||||
readonly page: Page;
|
||||
|
@ -17,6 +18,7 @@ export class PurchasePage {
|
|||
readonly returnToDashboardButton: Locator;
|
||||
readonly goToNextStep: Locator;
|
||||
readonly planDetails: Locator;
|
||||
readonly paypalButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
@ -37,6 +39,7 @@ export class PurchasePage {
|
|||
this.returnToDashboardButton = page.getByLabel("Return to dashboard");
|
||||
this.goToNextStep = page.getByLabel("Go to next step");
|
||||
this.planDetails = page.locator(".plan-details-description");
|
||||
this.paypalButton = page.getByTitle("PayPal").nth(1);
|
||||
}
|
||||
|
||||
async fillOutStripeCardInfo() {
|
||||
|
@ -68,6 +71,42 @@ export class PurchasePage {
|
|||
await frame.fill(".InputElement[name=postal]", "77777");
|
||||
}
|
||||
|
||||
async fillOutPaypalInfo(context: BrowserContext) {
|
||||
const emailToUse = process.env.E2E_TEST_PAYPAL_LOGIN as string;
|
||||
const pwdToUse = process.env.E2E_TEST_PAYPAL_PASSWORD as string;
|
||||
expect(emailToUse).not.toBeUndefined();
|
||||
expect(pwdToUse).not.toBeUndefined();
|
||||
|
||||
//away from mozilla
|
||||
const pagePromise = context.waitForEvent("page");
|
||||
await this.paypalButton.click();
|
||||
const newPage = await pagePromise;
|
||||
await newPage.waitForLoadState();
|
||||
await newPage.waitForURL(/.*paypal\.com.*\/checkout.*/);
|
||||
|
||||
const emailPrompt = newPage.locator("#email");
|
||||
await expect(emailPrompt).toBeVisible();
|
||||
|
||||
await emailPrompt.fill(emailToUse);
|
||||
const nextButton = newPage.locator("#btnNext");
|
||||
if (await nextButton.isVisible()) {
|
||||
await newPage.locator("#btnNext").click();
|
||||
await newPage.waitForSelector("#password", { state: "visible" });
|
||||
}
|
||||
const pwdInput = newPage.locator("#password");
|
||||
await expect(pwdInput).toBeVisible();
|
||||
await pwdInput.fill(pwdToUse);
|
||||
|
||||
await newPage.locator("#btnLogin").click();
|
||||
await newPage.waitForURL(/^(?!.*checkoutnow).*/);
|
||||
|
||||
const saveAndContinue = newPage.locator("#consentButton");
|
||||
await expect(saveAndContinue).toBeVisible();
|
||||
await saveAndContinue.click();
|
||||
|
||||
//back to mozilla
|
||||
}
|
||||
|
||||
async verifyMonthlyPlanDetails() {
|
||||
await this.subscriptionHeader.waitFor();
|
||||
const planDetails = removeUnicodeChars(
|
||||
|
@ -86,4 +125,48 @@ export class PurchasePage {
|
|||
`${process.env.E2E_TEST_ENV === "prod" ? "yearly" : "every 2 months"}`,
|
||||
);
|
||||
}
|
||||
|
||||
async gotoPurchaseFromDashboard(
|
||||
dashboardPage: DashboardPage,
|
||||
yearly: boolean,
|
||||
) {
|
||||
// navigate to subscription
|
||||
await dashboardPage.open();
|
||||
await dashboardPage.subscribeButton.click();
|
||||
|
||||
// verify user purchase choices
|
||||
await expect(dashboardPage.subscribeDialogCloseButton).toBeVisible();
|
||||
await expect(dashboardPage.yearlyMonthlyTablist).toBeVisible();
|
||||
await dashboardPage.yearlyTab.click();
|
||||
await expect(
|
||||
dashboardPage.subscribeDialogSelectYearlyPlanLink,
|
||||
).toBeVisible();
|
||||
|
||||
await dashboardPage.monthlyTab.click();
|
||||
await expect(
|
||||
dashboardPage.subscribeDialogSelectMonthlyPlanLink,
|
||||
).toBeVisible();
|
||||
|
||||
if (yearly) {
|
||||
await dashboardPage.yearlyTab.click();
|
||||
await dashboardPage.subscribeDialogSelectYearlyPlanLink.click();
|
||||
} else {
|
||||
await dashboardPage.monthlyTab.click();
|
||||
await dashboardPage.subscribeDialogSelectMonthlyPlanLink.click();
|
||||
}
|
||||
await this.subscriptionHeader.waitFor();
|
||||
}
|
||||
|
||||
async postPaymentPageCheck(dashboardPage: DashboardPage) {
|
||||
await this.page.getByText("Subscription confirmation").waitFor();
|
||||
// navigate to confirmation
|
||||
await this.getStartedButton.click();
|
||||
await this.goToNextStep.click();
|
||||
// confirm successful payment
|
||||
await dashboardPage.plusSubscription.waitFor({
|
||||
state: "attached",
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(dashboardPage.plusSubscription).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,13 @@
|
|||
import { Locator } from "@playwright/test";
|
||||
import { test, expect } from "../fixtures/basePage.js";
|
||||
import { DashboardPage } from "../pages/dashBoardPage.js";
|
||||
import { checkAuthState, removeUnicodeChars } from "../utils/helpers.js";
|
||||
import {
|
||||
checkAuthState,
|
||||
removeUnicodeChars,
|
||||
clickOnATagCheckDomain,
|
||||
escapeRegExp,
|
||||
forceLoginAs,
|
||||
} from "../utils/helpers.js";
|
||||
|
||||
// bypass login
|
||||
test.use({ storageState: "./e2e/storageState.json" });
|
||||
|
@ -127,6 +133,94 @@ test.describe(`${process.env.E2E_TEST_ENV} - Breaches Dashboard - Headers`, () =
|
|||
await dashboardPage.popupCloseButton.click();
|
||||
await expect(dashboardPage.aboutFixedExposuresPopup).toBeHidden();
|
||||
});
|
||||
|
||||
test("Verify that the Apps and Services header options work correctly.", async ({
|
||||
dashboardPage,
|
||||
page,
|
||||
}) => {
|
||||
// link to testrail
|
||||
test.info().annotations.push({
|
||||
type: "testrail",
|
||||
description:
|
||||
"https://testrail.stage.mozaws.net/index.php?/cases/view/2463569",
|
||||
});
|
||||
|
||||
await dashboardPage.fixedTab.click();
|
||||
expect(page.url()).toMatch(/.*dashboard\/fixed\/?/);
|
||||
await dashboardPage.actionNeededTab.click();
|
||||
expect(page.url()).toMatch(/.*dashboard\/action-needed\/?/);
|
||||
|
||||
//apps and services button check
|
||||
const clickOnLinkAndGoBack = async (
|
||||
aTag: Locator,
|
||||
host: string | RegExp = /.*/,
|
||||
path: string | RegExp = /.*/,
|
||||
) => {
|
||||
await expect(dashboardPage.appsAndServices).toBeVisible();
|
||||
await dashboardPage.appsAndServices.click();
|
||||
await expect(dashboardPage.appsAndServicesMenu).toBeVisible();
|
||||
await clickOnATagCheckDomain(aTag, host, path, page);
|
||||
};
|
||||
|
||||
await clickOnLinkAndGoBack(
|
||||
dashboardPage.servicesVpn,
|
||||
"www.mozilla.org",
|
||||
/.*\/products\/vpn\/?.*/,
|
||||
);
|
||||
await clickOnLinkAndGoBack(
|
||||
dashboardPage.servicesRelay,
|
||||
"relay.firefox.com",
|
||||
);
|
||||
await clickOnLinkAndGoBack(
|
||||
dashboardPage.servicesPocket,
|
||||
/getpocket\.com|apps\.apple\.com|app\.adjust\.com/,
|
||||
/.*(\/pocket-and-firefox\/?).*|.*about.*|.*pocket-stay-informed.*/,
|
||||
);
|
||||
await clickOnLinkAndGoBack(
|
||||
dashboardPage.servicesFirefoxDesktop,
|
||||
"www.mozilla.org",
|
||||
/.*\/firefox\/new\/?.*/,
|
||||
);
|
||||
await clickOnLinkAndGoBack(
|
||||
dashboardPage.servicesFirefoxMobile,
|
||||
"www.mozilla.org",
|
||||
/.*\/browsers\/mobile\/?.*/,
|
||||
);
|
||||
await clickOnLinkAndGoBack(
|
||||
dashboardPage.servicesMozilla,
|
||||
"www.mozilla.org",
|
||||
);
|
||||
|
||||
const openProfileMenuItem = async (
|
||||
what: Locator,
|
||||
whatUrl: string | RegExp,
|
||||
) => {
|
||||
await dashboardPage.open();
|
||||
await dashboardPage.profileButton.click();
|
||||
await expect(what).toBeVisible();
|
||||
if (await what.evaluate((e) => e.hasAttribute("href"))) {
|
||||
const href = await what.getAttribute("href");
|
||||
expect(href).not.toBeNull();
|
||||
await page.goto(href!);
|
||||
} else {
|
||||
await what.click();
|
||||
}
|
||||
await page.waitForURL(whatUrl);
|
||||
};
|
||||
|
||||
await openProfileMenuItem(
|
||||
dashboardPage.manageProfile,
|
||||
/.*accounts.*settings.*/,
|
||||
);
|
||||
await openProfileMenuItem(
|
||||
dashboardPage.profileSettings,
|
||||
/.*\/user\/settings.*/,
|
||||
);
|
||||
|
||||
const base_url = process.env["E2E_TEST_BASE_URL"];
|
||||
expect(base_url).toBeTruthy();
|
||||
await openProfileMenuItem(dashboardPage.profileSignOut, base_url!);
|
||||
});
|
||||
});
|
||||
|
||||
// fix coming - playwright does not currently have access to the aws headers, skipping for now
|
||||
|
@ -450,10 +544,6 @@ test.describe(`${process.env.E2E_TEST_ENV} - Breaches Dashboard - Overview Card`
|
|||
await automaticRemovePage.open();
|
||||
await expect(automaticRemovePage.forwardArrowButton).toBeVisible();
|
||||
|
||||
const escapeRegExp = (str: string): string => {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
};
|
||||
|
||||
const breachString0 = "high-risk-data-breaches";
|
||||
const breachString1 = "leaked-passwords";
|
||||
const breachString2 = "security-recommendations";
|
||||
|
@ -535,26 +625,6 @@ test.describe(`${process.env.E2E_TEST_ENV} - Breaches Dashboard - Footer`, () =>
|
|||
"https://testrail.stage.mozaws.net/index.php?/cases/view/2463570",
|
||||
});
|
||||
|
||||
const clickOnATagCheckDomain = async (
|
||||
aTag: Locator,
|
||||
host: string,
|
||||
path: string | RegExp = /.*/,
|
||||
) => {
|
||||
if (typeof path === "string") path = new RegExp(".*" + path + ".*");
|
||||
host = host.replace(/^(https?:\/\/)/, "");
|
||||
|
||||
const href = await aTag.getAttribute("href");
|
||||
if (href === null) return false;
|
||||
|
||||
await page.goto(href);
|
||||
const currentUrl = new URL(page.url());
|
||||
const perceivedHost = currentUrl.hostname;
|
||||
const perceivedPath = currentUrl.pathname;
|
||||
expect(perceivedHost).toBe(host);
|
||||
expect(path.test(perceivedPath)).toBeTruthy();
|
||||
await page.goBack();
|
||||
};
|
||||
|
||||
expect(process.env["E2E_TEST_BASE_URL"]).toBeTruthy();
|
||||
const baseUrl = process.env["E2E_TEST_BASE_URL"]!;
|
||||
await dashboardPage.goToDashboard();
|
||||
|
@ -563,31 +633,37 @@ test.describe(`${process.env.E2E_TEST_ENV} - Breaches Dashboard - Footer`, () =>
|
|||
dashboardPage.mozillaLogoFooter,
|
||||
"www.mozilla.org",
|
||||
/^(\/en-US\/)?$/,
|
||||
page,
|
||||
);
|
||||
await clickOnATagCheckDomain(
|
||||
dashboardPage.allBreachesFooter,
|
||||
baseUrl,
|
||||
"/breaches",
|
||||
page,
|
||||
);
|
||||
await clickOnATagCheckDomain(
|
||||
dashboardPage.faqsFooter,
|
||||
"support.mozilla.org",
|
||||
/.*\/kb.*\/mozilla-monitor-faq.*/,
|
||||
page,
|
||||
);
|
||||
await clickOnATagCheckDomain(
|
||||
dashboardPage.termsOfServiceFooter,
|
||||
"www.mozilla.org",
|
||||
"/about/legal/terms/subscription-services/",
|
||||
page,
|
||||
);
|
||||
await clickOnATagCheckDomain(
|
||||
dashboardPage.privacyNoticeFooter,
|
||||
"www.mozilla.org",
|
||||
"/privacy/subscription-services/",
|
||||
page,
|
||||
);
|
||||
await clickOnATagCheckDomain(
|
||||
dashboardPage.githubFooter,
|
||||
"github.com",
|
||||
"/mozilla/blurts-server",
|
||||
page,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -642,3 +718,41 @@ test.describe(`${process.env.E2E_TEST_ENV} - Breaches Dashboard - Navigation`, (
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(`${process.env.E2E_TEST_ENV} - Breaches Dashboard - Data Breaches`, () => {
|
||||
test.beforeEach(async ({ landingPage, page, authPage }) => {
|
||||
const emailToUse = process.env
|
||||
.E2E_TEST_ACCOUNT_EMAIL_EXPOSURES_STARTED as string;
|
||||
const pwdToUse = process.env.E2E_TEST_ACCOUNT_PASSWORD as string;
|
||||
expect(emailToUse).not.toBeUndefined();
|
||||
expect(pwdToUse).not.toBeUndefined();
|
||||
await forceLoginAs(emailToUse, pwdToUse, page, landingPage, authPage);
|
||||
});
|
||||
|
||||
test("Verify that the High risk data breaches step is displayed correctly", async ({
|
||||
dashboardPage,
|
||||
dataBrokersPage,
|
||||
page,
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: "testrail",
|
||||
description:
|
||||
"https://testrail.stage.mozaws.net/index.php?/cases/view/2463592",
|
||||
});
|
||||
|
||||
await expect(dashboardPage.upsellScreenButton).toBeVisible();
|
||||
await dashboardPage.upsellScreenButton.click();
|
||||
await page.waitForURL(/.*\/data-broker-profiles\/view-data-brokers\/?/);
|
||||
await expect(dataBrokersPage.forwardArrowButton).toBeVisible();
|
||||
await dataBrokersPage.forwardArrowButton.click();
|
||||
await page.waitForURL(/.*\/high-risk-data-breaches.*/);
|
||||
const highRiskDataBreachLi = page.locator(
|
||||
'li:has(div:has-text("High risk data breaches"))',
|
||||
);
|
||||
await expect(highRiskDataBreachLi).toBeVisible();
|
||||
await expect(highRiskDataBreachLi).toHaveAttribute("aria-current", "step");
|
||||
await expect(
|
||||
highRiskDataBreachLi.locator("div").getByText("High risk data breaches"),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -154,4 +154,50 @@ test.describe(`${process.env.E2E_TEST_ENV} - Breach Scan, Monitor Plus Purchase
|
|||
});
|
||||
await expect(dashboardPage.plusSubscription).toBeVisible();
|
||||
});
|
||||
|
||||
test("Verify that the user can purchase the plus subscription with a PayPal account - yearly", async ({
|
||||
purchasePage,
|
||||
dashboardPage,
|
||||
context,
|
||||
}) => {
|
||||
test.skip(
|
||||
process.env.E2E_TEST_ENV === "production",
|
||||
"payment method test not available in production",
|
||||
);
|
||||
// link to testrail case
|
||||
test.info().annotations.push({
|
||||
type: "testrail",
|
||||
description:
|
||||
"https://testrail.stage.mozaws.net/index.php?/cases/view/2463628",
|
||||
});
|
||||
|
||||
await purchasePage.gotoPurchaseFromDashboard(dashboardPage, true);
|
||||
// fill out subscription payment
|
||||
await purchasePage.authorizationCheckbox.check();
|
||||
await purchasePage.fillOutPaypalInfo(context);
|
||||
await purchasePage.postPaymentPageCheck(dashboardPage);
|
||||
});
|
||||
|
||||
test("Verify that the user can purchase the plus subscription with a PayPal account - monthly", async ({
|
||||
purchasePage,
|
||||
dashboardPage,
|
||||
context,
|
||||
}) => {
|
||||
test.skip(
|
||||
process.env.E2E_TEST_ENV === "production",
|
||||
"payment method test not available in production",
|
||||
);
|
||||
// link to testrail case
|
||||
test.info().annotations.push({
|
||||
type: "testrail",
|
||||
description:
|
||||
"https://testrail.stage.mozaws.net/index.php?/cases/view/2463628",
|
||||
});
|
||||
|
||||
await purchasePage.gotoPurchaseFromDashboard(dashboardPage, false);
|
||||
// fill out subscription payment
|
||||
await purchasePage.authorizationCheckbox.check();
|
||||
await purchasePage.fillOutPaypalInfo(context);
|
||||
await purchasePage.postPaymentPageCheck(dashboardPage);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
* 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 { request, Page } from "@playwright/test";
|
||||
import { expect, request, Page, Locator, test } from "@playwright/test";
|
||||
import { InternalServerError } from "../../utils/error.js";
|
||||
import { LandingPage } from "../pages/landingPage.js";
|
||||
import { AuthPage } from "../pages/authPage.js";
|
||||
|
||||
enum ENV {
|
||||
local = "local",
|
||||
|
@ -160,3 +162,63 @@ export function removeUnicodeChars(text: string): string {
|
|||
// eslint-disable-next-line no-control-regex
|
||||
return text.replace(/[^\x00-\x7F]/g, "");
|
||||
}
|
||||
|
||||
export const clickOnATagCheckDomain = async (
|
||||
aTag: Locator,
|
||||
host: string | RegExp = /.*/,
|
||||
path: string | RegExp = /.*/,
|
||||
page: Page,
|
||||
) => {
|
||||
if (typeof host === "string")
|
||||
host = new RegExp(escapeRegExp(host.replace(/^(https?:\/\/)/, "")));
|
||||
if (typeof path === "string") path = new RegExp(".*" + path + ".*");
|
||||
|
||||
const href = await aTag.getAttribute("href");
|
||||
if (href === null) return false;
|
||||
|
||||
await page.goto(href);
|
||||
const currentUrl = new URL(page.url());
|
||||
const perceivedHost = currentUrl.hostname;
|
||||
const perceivedPath = currentUrl.pathname;
|
||||
expect(host.test(perceivedHost)).toBeTruthy();
|
||||
expect(path.test(perceivedPath)).toBeTruthy();
|
||||
await page.goBack();
|
||||
};
|
||||
|
||||
export const escapeRegExp = (str: string): string => {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
};
|
||||
|
||||
export const forceLoginAs = async (
|
||||
email: string,
|
||||
password: string,
|
||||
page: Page,
|
||||
landingPage: LandingPage,
|
||||
authPage: AuthPage,
|
||||
) => {
|
||||
test.slow(
|
||||
true,
|
||||
"this test runs through the welcome scan flow, increasing timeout to address it",
|
||||
);
|
||||
|
||||
// speed up test by ignoring non necessary requests
|
||||
await page.route(/(analytics)/, async (route) => {
|
||||
await route.abort();
|
||||
});
|
||||
await page.context().clearCookies();
|
||||
await landingPage.open();
|
||||
await landingPage.goToSignIn();
|
||||
let visible = true;
|
||||
try {
|
||||
await expect(authPage.useDifferentEmailButton).toBeVisible();
|
||||
} catch {
|
||||
visible = false;
|
||||
}
|
||||
if (visible) {
|
||||
await authPage.useDifferentEmailButton.click();
|
||||
await page.waitForURL(/^(?!.*signin).*/);
|
||||
}
|
||||
await authPage.signIn(email, password);
|
||||
await page.waitForURL("**/user/dashboard");
|
||||
await expect(page).toHaveURL(/.*\/user\/dashboard.*/);
|
||||
};
|
||||
|
|
|
@ -176,6 +176,17 @@ declare module "knex/types/tables" {
|
|||
"id" | "created_at" | "updated_at"
|
||||
>;
|
||||
|
||||
interface SubscriberCouponRow {
|
||||
id: number;
|
||||
subscriber_id: number;
|
||||
coupon_code: string;
|
||||
created_at: Date;
|
||||
}
|
||||
type SubscriberCouponAutoInsertedColumns = Extract<
|
||||
keyof SubscriberCouponRow,
|
||||
"id" | "subscriber_id" | "created_at"
|
||||
>;
|
||||
|
||||
interface BreachRow {
|
||||
id: number;
|
||||
name: string;
|
||||
|
@ -339,6 +350,14 @@ declare module "knex/types/tables" {
|
|||
Pick<SubscriberRow, "updated_at">
|
||||
>;
|
||||
|
||||
subscriber_coupons: Knex.CompositeTableType<
|
||||
SubscriberCouponRow,
|
||||
// On updates, auto-generated columns cannot be set, and nullable columns are optional:
|
||||
Omit<SubscriberCouponRow, SubscriberAutoInsertedColumns>,
|
||||
// On updates, don't allow updating the ID; all other fields are optional:
|
||||
Partial<Omit<SubscriberCouponRow, "id">>
|
||||
>;
|
||||
|
||||
email_addresses: Knex.CompositeTableType<
|
||||
EmailAddressRow,
|
||||
// On updates, auto-generated columns cannot be set, and nullable columns are optional:
|
||||
|
|
Загрузка…
Ссылка в новой задаче